+3
-3
appview/cache/session/store.go
+3
-3
appview/cache/session/store.go
···
102
}
103
104
func (s *SessionStore) GetRequestByState(ctx context.Context, state string) (*OAuthRequest, error) {
105
-
didKey, err := s.getRequestKey(ctx, state)
106
if err != nil {
107
return nil, err
108
}
···
127
}
128
129
func (s *SessionStore) DeleteRequestByState(ctx context.Context, state string) error {
130
-
didKey, err := s.getRequestKey(ctx, state)
131
if err != nil {
132
return err
133
}
134
135
-
err = s.cache.Del(ctx, fmt.Sprintf(stateKey, "state")).Err()
136
if err != nil {
137
return err
138
}
···
102
}
103
104
func (s *SessionStore) GetRequestByState(ctx context.Context, state string) (*OAuthRequest, error) {
105
+
didKey, err := s.getRequestKeyFromState(ctx, state)
106
if err != nil {
107
return nil, err
108
}
···
127
}
128
129
func (s *SessionStore) DeleteRequestByState(ctx context.Context, state string) error {
130
+
didKey, err := s.getRequestKeyFromState(ctx, state)
131
if err != nil {
132
return err
133
}
134
135
+
err = s.cache.Del(ctx, fmt.Sprintf(stateKey, state)).Err()
136
if err != nil {
137
return err
138
}
+9
-5
appview/oauth/handler/handler.go
+9
-5
appview/oauth/handler/handler.go
···
13
"github.com/lestrrat-go/jwx/v2/jwk"
14
"github.com/posthog/posthog-go"
15
"tangled.sh/icyphox.sh/atproto-oauth/helpers"
16
"tangled.sh/tangled.sh/core/appview/config"
17
"tangled.sh/tangled.sh/core/appview/db"
18
"tangled.sh/tangled.sh/core/appview/idresolver"
···
32
config *config.Config
33
pages *pages.Pages
34
idResolver *idresolver.Resolver
35
db *db.DB
36
store *sessions.CookieStore
37
oauth *oauth.OAuth
···
44
pages *pages.Pages,
45
idResolver *idresolver.Resolver,
46
db *db.DB,
47
store *sessions.CookieStore,
48
oauth *oauth.OAuth,
49
enforcer *rbac.Enforcer,
···
54
pages: pages,
55
idResolver: idResolver,
56
db: db,
57
store: store,
58
oauth: oauth,
59
enforcer: enforcer,
···
158
return
159
}
160
161
-
err = db.SaveOAuthRequest(o.db, db.OAuthRequest{
162
Did: resolved.DID.String(),
163
PdsUrl: resolved.PDSEndpoint(),
164
Handle: handle,
···
186
func (o *OAuthHandler) callback(w http.ResponseWriter, r *http.Request) {
187
state := r.FormValue("state")
188
189
-
oauthRequest, err := db.GetOAuthRequestByState(o.db, state)
190
if err != nil {
191
log.Println("failed to get oauth request:", err)
192
o.pages.Notice(w, "login-msg", "Failed to authenticate. Try again later.")
···
194
}
195
196
defer func() {
197
-
err := db.DeleteOAuthRequestByState(o.db, state)
198
if err != nil {
199
log.Println("failed to delete oauth request for state:", state, err)
200
}
···
263
return
264
}
265
266
-
err = o.oauth.SaveSession(w, r, oauthRequest, tokenResp)
267
if err != nil {
268
log.Println("failed to save session:", err)
269
o.pages.Notice(w, "login-msg", "Failed to authenticate. Try again later.")
···
295
}
296
297
log.Println("session cleared successfully")
298
-
http.Redirect(w, r, "/", http.StatusFound)
299
}
300
301
func pubKeyFromJwk(jwks string) (jwk.Key, error) {
···
13
"github.com/lestrrat-go/jwx/v2/jwk"
14
"github.com/posthog/posthog-go"
15
"tangled.sh/icyphox.sh/atproto-oauth/helpers"
16
+
sessioncache "tangled.sh/tangled.sh/core/appview/cache/session"
17
"tangled.sh/tangled.sh/core/appview/config"
18
"tangled.sh/tangled.sh/core/appview/db"
19
"tangled.sh/tangled.sh/core/appview/idresolver"
···
33
config *config.Config
34
pages *pages.Pages
35
idResolver *idresolver.Resolver
36
+
sess *sessioncache.SessionStore
37
db *db.DB
38
store *sessions.CookieStore
39
oauth *oauth.OAuth
···
46
pages *pages.Pages,
47
idResolver *idresolver.Resolver,
48
db *db.DB,
49
+
sess *sessioncache.SessionStore,
50
store *sessions.CookieStore,
51
oauth *oauth.OAuth,
52
enforcer *rbac.Enforcer,
···
57
pages: pages,
58
idResolver: idResolver,
59
db: db,
60
+
sess: sess,
61
store: store,
62
oauth: oauth,
63
enforcer: enforcer,
···
162
return
163
}
164
165
+
err = o.sess.SaveRequest(r.Context(), sessioncache.OAuthRequest{
166
Did: resolved.DID.String(),
167
PdsUrl: resolved.PDSEndpoint(),
168
Handle: handle,
···
190
func (o *OAuthHandler) callback(w http.ResponseWriter, r *http.Request) {
191
state := r.FormValue("state")
192
193
+
oauthRequest, err := o.sess.GetRequestByState(r.Context(), state)
194
if err != nil {
195
log.Println("failed to get oauth request:", err)
196
o.pages.Notice(w, "login-msg", "Failed to authenticate. Try again later.")
···
198
}
199
200
defer func() {
201
+
err := o.sess.DeleteRequestByState(r.Context(), state)
202
if err != nil {
203
log.Println("failed to delete oauth request for state:", state, err)
204
}
···
267
return
268
}
269
270
+
err = o.oauth.SaveSession(w, r, *oauthRequest, tokenResp)
271
if err != nil {
272
log.Println("failed to save session:", err)
273
o.pages.Notice(w, "login-msg", "Failed to authenticate. Try again later.")
···
299
}
300
301
log.Println("session cleared successfully")
302
+
o.pages.HxRedirect(w, "/login")
303
}
304
305
func pubKeyFromJwk(jwks string) (jwk.Key, error) {
+28
-35
appview/oauth/oauth.go
+28
-35
appview/oauth/oauth.go
···
10
"github.com/gorilla/sessions"
11
oauth "tangled.sh/icyphox.sh/atproto-oauth"
12
"tangled.sh/icyphox.sh/atproto-oauth/helpers"
13
"tangled.sh/tangled.sh/core/appview/config"
14
-
"tangled.sh/tangled.sh/core/appview/db"
15
"tangled.sh/tangled.sh/core/appview/oauth/client"
16
xrpc "tangled.sh/tangled.sh/core/appview/xrpcclient"
17
)
18
19
-
type OAuthRequest struct {
20
-
ID uint
21
-
AuthserverIss string
22
-
State string
23
-
Did string
24
-
PdsUrl string
25
-
PkceVerifier string
26
-
DpopAuthserverNonce string
27
-
DpopPrivateJwk string
28
-
}
29
-
30
type OAuth struct {
31
-
Store *sessions.CookieStore
32
-
Db *db.DB
33
-
Config *config.Config
34
}
35
36
-
func NewOAuth(db *db.DB, config *config.Config) *OAuth {
37
return &OAuth{
38
-
Store: sessions.NewCookieStore([]byte(config.Core.CookieSecret)),
39
-
Db: db,
40
-
Config: config,
41
}
42
}
43
44
-
func (o *OAuth) SaveSession(w http.ResponseWriter, r *http.Request, oreq db.OAuthRequest, oresp *oauth.TokenResponse) error {
45
// first we save the did in the user session
46
-
userSession, err := o.Store.Get(r, SessionName)
47
if err != nil {
48
return err
49
}
···
58
}
59
60
// then save the whole thing in the db
61
-
session := db.OAuthSession{
62
Did: oreq.Did,
63
Handle: oreq.Handle,
64
PdsUrl: oreq.PdsUrl,
···
70
Expiry: time.Now().Add(time.Duration(oresp.ExpiresIn) * time.Second).Format(time.RFC3339),
71
}
72
73
-
return db.SaveOAuthSession(o.Db, session)
74
}
75
76
func (o *OAuth) ClearSession(r *http.Request, w http.ResponseWriter) error {
77
-
userSession, err := o.Store.Get(r, SessionName)
78
if err != nil || userSession.IsNew {
79
return fmt.Errorf("error getting user session (or new session?): %w", err)
80
}
81
82
did := userSession.Values[SessionDid].(string)
83
84
-
err = db.DeleteOAuthSessionByDid(o.Db, did)
85
if err != nil {
86
return fmt.Errorf("error deleting oauth session: %w", err)
87
}
···
91
return userSession.Save(r, w)
92
}
93
94
-
func (o *OAuth) GetSession(r *http.Request) (*db.OAuthSession, bool, error) {
95
-
userSession, err := o.Store.Get(r, SessionName)
96
if err != nil || userSession.IsNew {
97
return nil, false, fmt.Errorf("error getting user session (or new session?): %w", err)
98
}
···
100
did := userSession.Values[SessionDid].(string)
101
auth := userSession.Values[SessionAuthenticated].(bool)
102
103
-
session, err := db.GetOAuthSessionByDid(o.Db, did)
104
if err != nil {
105
return nil, false, fmt.Errorf("error getting oauth session: %w", err)
106
}
···
119
120
oauthClient, err := client.NewClient(
121
self.ClientID,
122
-
o.Config.OAuth.Jwks,
123
self.RedirectURIs[0],
124
)
125
···
133
}
134
135
newExpiry := time.Now().Add(time.Duration(resp.ExpiresIn) * time.Second).Format(time.RFC3339)
136
-
err = db.RefreshOAuthSession(o.Db, did, resp.AccessToken, resp.RefreshToken, newExpiry)
137
if err != nil {
138
return nil, false, fmt.Errorf("error refreshing oauth session: %w", err)
139
}
···
155
}
156
157
func (a *OAuth) GetUser(r *http.Request) *User {
158
-
clientSession, err := a.Store.Get(r, SessionName)
159
160
if err != nil || clientSession.IsNew {
161
return nil
···
169
}
170
171
func (a *OAuth) GetDid(r *http.Request) string {
172
-
clientSession, err := a.Store.Get(r, SessionName)
173
174
if err != nil || clientSession.IsNew {
175
return ""
···
189
190
client := &oauth.XrpcClient{
191
OnDpopPdsNonceChanged: func(did, newNonce string) {
192
-
err := db.UpdateDpopPdsNonce(o.Db, did, newNonce)
193
if err != nil {
194
log.Printf("error updating dpop pds nonce: %v", err)
195
}
···
234
return []string{fmt.Sprintf("%s/oauth/callback", c)}
235
}
236
237
-
clientURI := o.Config.Core.AppviewHost
238
clientID := fmt.Sprintf("%s/oauth/client-metadata.json", clientURI)
239
redirectURIs := makeRedirectURIs(clientURI)
240
241
-
if o.Config.Core.Dev {
242
clientURI = fmt.Sprintf("http://127.0.0.1:3000")
243
redirectURIs = makeRedirectURIs(clientURI)
244
···
10
"github.com/gorilla/sessions"
11
oauth "tangled.sh/icyphox.sh/atproto-oauth"
12
"tangled.sh/icyphox.sh/atproto-oauth/helpers"
13
+
sessioncache "tangled.sh/tangled.sh/core/appview/cache/session"
14
"tangled.sh/tangled.sh/core/appview/config"
15
"tangled.sh/tangled.sh/core/appview/oauth/client"
16
xrpc "tangled.sh/tangled.sh/core/appview/xrpcclient"
17
)
18
19
type OAuth struct {
20
+
store *sessions.CookieStore
21
+
config *config.Config
22
+
sess *sessioncache.SessionStore
23
}
24
25
+
func NewOAuth(config *config.Config, sess *sessioncache.SessionStore) *OAuth {
26
return &OAuth{
27
+
store: sessions.NewCookieStore([]byte(config.Core.CookieSecret)),
28
+
config: config,
29
+
sess: sess,
30
}
31
}
32
33
+
func (o *OAuth) Stores() *sessions.CookieStore {
34
+
return o.store
35
+
}
36
+
37
+
func (o *OAuth) SaveSession(w http.ResponseWriter, r *http.Request, oreq sessioncache.OAuthRequest, oresp *oauth.TokenResponse) error {
38
// first we save the did in the user session
39
+
userSession, err := o.store.Get(r, SessionName)
40
if err != nil {
41
return err
42
}
···
51
}
52
53
// then save the whole thing in the db
54
+
session := sessioncache.OAuthSession{
55
Did: oreq.Did,
56
Handle: oreq.Handle,
57
PdsUrl: oreq.PdsUrl,
···
63
Expiry: time.Now().Add(time.Duration(oresp.ExpiresIn) * time.Second).Format(time.RFC3339),
64
}
65
66
+
return o.sess.SaveSession(r.Context(), session)
67
}
68
69
func (o *OAuth) ClearSession(r *http.Request, w http.ResponseWriter) error {
70
+
userSession, err := o.store.Get(r, SessionName)
71
if err != nil || userSession.IsNew {
72
return fmt.Errorf("error getting user session (or new session?): %w", err)
73
}
74
75
did := userSession.Values[SessionDid].(string)
76
77
+
err = o.sess.DeleteSession(r.Context(), did)
78
if err != nil {
79
return fmt.Errorf("error deleting oauth session: %w", err)
80
}
···
84
return userSession.Save(r, w)
85
}
86
87
+
func (o *OAuth) GetSession(r *http.Request) (*sessioncache.OAuthSession, bool, error) {
88
+
userSession, err := o.store.Get(r, SessionName)
89
if err != nil || userSession.IsNew {
90
return nil, false, fmt.Errorf("error getting user session (or new session?): %w", err)
91
}
···
93
did := userSession.Values[SessionDid].(string)
94
auth := userSession.Values[SessionAuthenticated].(bool)
95
96
+
session, err := o.sess.GetSession(r.Context(), did)
97
if err != nil {
98
return nil, false, fmt.Errorf("error getting oauth session: %w", err)
99
}
···
112
113
oauthClient, err := client.NewClient(
114
self.ClientID,
115
+
o.config.OAuth.Jwks,
116
self.RedirectURIs[0],
117
)
118
···
126
}
127
128
newExpiry := time.Now().Add(time.Duration(resp.ExpiresIn) * time.Second).Format(time.RFC3339)
129
+
err = o.sess.RefreshSession(r.Context(), did, resp.AccessToken, resp.RefreshToken, newExpiry)
130
if err != nil {
131
return nil, false, fmt.Errorf("error refreshing oauth session: %w", err)
132
}
···
148
}
149
150
func (a *OAuth) GetUser(r *http.Request) *User {
151
+
clientSession, err := a.store.Get(r, SessionName)
152
153
if err != nil || clientSession.IsNew {
154
return nil
···
162
}
163
164
func (a *OAuth) GetDid(r *http.Request) string {
165
+
clientSession, err := a.store.Get(r, SessionName)
166
167
if err != nil || clientSession.IsNew {
168
return ""
···
182
183
client := &oauth.XrpcClient{
184
OnDpopPdsNonceChanged: func(did, newNonce string) {
185
+
err := o.sess.UpdateNonce(r.Context(), did, newNonce)
186
if err != nil {
187
log.Printf("error updating dpop pds nonce: %v", err)
188
}
···
227
return []string{fmt.Sprintf("%s/oauth/callback", c)}
228
}
229
230
+
clientURI := o.config.Core.AppviewHost
231
clientID := fmt.Sprintf("%s/oauth/client-metadata.json", clientURI)
232
redirectURIs := makeRedirectURIs(clientURI)
233
234
+
if o.config.Core.Dev {
235
clientURI = fmt.Sprintf("http://127.0.0.1:3000")
236
redirectURIs = makeRedirectURIs(clientURI)
237
+1
-3
appview/state/router.go
+1
-3
appview/state/router.go
···
97
98
r.Get("/", s.Timeline)
99
100
-
r.With(middleware.AuthMiddleware(s.oauth)).Post("/logout", s.Logout)
101
-
102
r.Route("/knots", func(r chi.Router) {
103
r.Use(middleware.AuthMiddleware(s.oauth))
104
r.Get("/", s.Knots)
···
156
157
func (s *State) OAuthRouter() http.Handler {
158
store := sessions.NewCookieStore([]byte(s.config.Core.CookieSecret))
159
-
oauth := oauthhandler.New(s.config, s.pages, s.idResolver, s.db, store, s.oauth, s.enforcer, s.posthog)
160
return oauth.Router()
161
}
162
···
97
98
r.Get("/", s.Timeline)
99
100
r.Route("/knots", func(r chi.Router) {
101
r.Use(middleware.AuthMiddleware(s.oauth))
102
r.Get("/", s.Knots)
···
154
155
func (s *State) OAuthRouter() http.Handler {
156
store := sessions.NewCookieStore([]byte(s.config.Core.CookieSecret))
157
+
oauth := oauthhandler.New(s.config, s.pages, s.idResolver, s.db, s.sess, store, s.oauth, s.enforcer, s.posthog)
158
return oauth.Router()
159
}
160
+9
-8
appview/state/state.go
+9
-8
appview/state/state.go
···
20
"github.com/posthog/posthog-go"
21
"tangled.sh/tangled.sh/core/api/tangled"
22
"tangled.sh/tangled.sh/core/appview"
23
"tangled.sh/tangled.sh/core/appview/config"
24
"tangled.sh/tangled.sh/core/appview/db"
25
"tangled.sh/tangled.sh/core/appview/idresolver"
···
37
enforcer *rbac.Enforcer
38
tidClock syntax.TIDClock
39
pages *pages.Pages
40
idResolver *idresolver.Resolver
41
posthog posthog.Client
42
jc *jetstream.JetstreamClient
···
65
res = idresolver.DefaultResolver()
66
}
67
68
-
oauth := oauth.NewOAuth(d, config)
69
70
posthog, err := posthog.NewWithConfig(config.Posthog.ApiKey, posthog.Config{Endpoint: config.Posthog.Endpoint})
71
if err != nil {
···
104
enforcer,
105
clock,
106
pgs,
107
res,
108
posthog,
109
jc,
···
118
return c.Next().String()
119
}
120
121
-
func (s *State) Logout(w http.ResponseWriter, r *http.Request) {
122
-
s.oauth.ClearSession(r, w)
123
-
w.Header().Set("HX-Redirect", "/login")
124
-
w.WriteHeader(http.StatusSeeOther)
125
-
}
126
-
127
func (s *State) Timeline(w http.ResponseWriter, r *http.Request) {
128
user := s.oauth.GetUser(r)
129
···
176
177
return
178
case http.MethodPost:
179
-
session, err := s.oauth.Store.Get(r, oauth.SessionName)
180
if err != nil || session.IsNew {
181
log.Println("unauthorized attempt to generate registration key")
182
http.Error(w, "Forbidden", http.StatusUnauthorized)
···
20
"github.com/posthog/posthog-go"
21
"tangled.sh/tangled.sh/core/api/tangled"
22
"tangled.sh/tangled.sh/core/appview"
23
+
"tangled.sh/tangled.sh/core/appview/cache"
24
+
"tangled.sh/tangled.sh/core/appview/cache/session"
25
"tangled.sh/tangled.sh/core/appview/config"
26
"tangled.sh/tangled.sh/core/appview/db"
27
"tangled.sh/tangled.sh/core/appview/idresolver"
···
39
enforcer *rbac.Enforcer
40
tidClock syntax.TIDClock
41
pages *pages.Pages
42
+
sess *session.SessionStore
43
idResolver *idresolver.Resolver
44
posthog posthog.Client
45
jc *jetstream.JetstreamClient
···
68
res = idresolver.DefaultResolver()
69
}
70
71
+
cache := cache.New(config.Redis.Addr)
72
+
sess := session.New(cache)
73
+
74
+
oauth := oauth.NewOAuth(config, sess)
75
76
posthog, err := posthog.NewWithConfig(config.Posthog.ApiKey, posthog.Config{Endpoint: config.Posthog.Endpoint})
77
if err != nil {
···
110
enforcer,
111
clock,
112
pgs,
113
+
sess,
114
res,
115
posthog,
116
jc,
···
125
return c.Next().String()
126
}
127
128
func (s *State) Timeline(w http.ResponseWriter, r *http.Request) {
129
user := s.oauth.GetUser(r)
130
···
177
178
return
179
case http.MethodPost:
180
+
session, err := s.oauth.Stores().Get(r, oauth.SessionName)
181
if err != nil || session.IsNew {
182
log.Println("unauthorized attempt to generate registration key")
183
http.Error(w, "Forbidden", http.StatusUnauthorized)