Monorepo for Tangled tangled.org

appview: oauth: swap out db store for redis cache

Signed-off-by: Anirudh Oppiliappan <anirudh@tangled.sh>

anirudh.fi c3d04acd 98a3c986

verified
Changed files
+50 -54
appview
cache
session
oauth
state
+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
··· 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
··· 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
··· 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
··· 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)