appview: oauth: swap out db store for redis cache #212

merged
opened by anirudh.fi targeting master from push-ruoqnsmttnxx
Changed files
+50 -54
appview
cache
session
oauth
state
+3 -3
appview/cache/session/store.go
···
}
func (s *SessionStore) GetRequestByState(ctx context.Context, state string) (*OAuthRequest, error) {
-
didKey, err := s.getRequestKey(ctx, state)
if err != nil {
return nil, err
}
···
}
func (s *SessionStore) DeleteRequestByState(ctx context.Context, state string) error {
-
didKey, err := s.getRequestKey(ctx, state)
if err != nil {
return err
}
-
err = s.cache.Del(ctx, fmt.Sprintf(stateKey, "state")).Err()
if err != nil {
return err
}
···
}
func (s *SessionStore) GetRequestByState(ctx context.Context, state string) (*OAuthRequest, error) {
+
didKey, err := s.getRequestKeyFromState(ctx, state)
if err != nil {
return nil, err
}
···
}
func (s *SessionStore) DeleteRequestByState(ctx context.Context, state string) error {
+
didKey, err := s.getRequestKeyFromState(ctx, state)
if err != nil {
return err
}
+
err = s.cache.Del(ctx, fmt.Sprintf(stateKey, state)).Err()
if err != nil {
return err
}
+9 -5
appview/oauth/handler/handler.go
···
"github.com/lestrrat-go/jwx/v2/jwk"
"github.com/posthog/posthog-go"
"tangled.sh/icyphox.sh/atproto-oauth/helpers"
"tangled.sh/tangled.sh/core/appview/config"
"tangled.sh/tangled.sh/core/appview/db"
"tangled.sh/tangled.sh/core/appview/idresolver"
···
config *config.Config
pages *pages.Pages
idResolver *idresolver.Resolver
db *db.DB
store *sessions.CookieStore
oauth *oauth.OAuth
···
pages *pages.Pages,
idResolver *idresolver.Resolver,
db *db.DB,
store *sessions.CookieStore,
oauth *oauth.OAuth,
enforcer *rbac.Enforcer,
···
pages: pages,
idResolver: idResolver,
db: db,
store: store,
oauth: oauth,
enforcer: enforcer,
···
return
}
-
err = db.SaveOAuthRequest(o.db, db.OAuthRequest{
Did: resolved.DID.String(),
PdsUrl: resolved.PDSEndpoint(),
Handle: handle,
···
func (o *OAuthHandler) callback(w http.ResponseWriter, r *http.Request) {
state := r.FormValue("state")
-
oauthRequest, err := db.GetOAuthRequestByState(o.db, state)
if err != nil {
log.Println("failed to get oauth request:", err)
o.pages.Notice(w, "login-msg", "Failed to authenticate. Try again later.")
···
}
defer func() {
-
err := db.DeleteOAuthRequestByState(o.db, state)
if err != nil {
log.Println("failed to delete oauth request for state:", state, err)
}
···
return
}
-
err = o.oauth.SaveSession(w, r, oauthRequest, tokenResp)
if err != nil {
log.Println("failed to save session:", err)
o.pages.Notice(w, "login-msg", "Failed to authenticate. Try again later.")
···
}
log.Println("session cleared successfully")
-
http.Redirect(w, r, "/", http.StatusFound)
}
func pubKeyFromJwk(jwks string) (jwk.Key, error) {
···
"github.com/lestrrat-go/jwx/v2/jwk"
"github.com/posthog/posthog-go"
"tangled.sh/icyphox.sh/atproto-oauth/helpers"
+
sessioncache "tangled.sh/tangled.sh/core/appview/cache/session"
"tangled.sh/tangled.sh/core/appview/config"
"tangled.sh/tangled.sh/core/appview/db"
"tangled.sh/tangled.sh/core/appview/idresolver"
···
config *config.Config
pages *pages.Pages
idResolver *idresolver.Resolver
+
sess *sessioncache.SessionStore
db *db.DB
store *sessions.CookieStore
oauth *oauth.OAuth
···
pages *pages.Pages,
idResolver *idresolver.Resolver,
db *db.DB,
+
sess *sessioncache.SessionStore,
store *sessions.CookieStore,
oauth *oauth.OAuth,
enforcer *rbac.Enforcer,
···
pages: pages,
idResolver: idResolver,
db: db,
+
sess: sess,
store: store,
oauth: oauth,
enforcer: enforcer,
···
return
}
+
err = o.sess.SaveRequest(r.Context(), sessioncache.OAuthRequest{
Did: resolved.DID.String(),
PdsUrl: resolved.PDSEndpoint(),
Handle: handle,
···
func (o *OAuthHandler) callback(w http.ResponseWriter, r *http.Request) {
state := r.FormValue("state")
+
oauthRequest, err := o.sess.GetRequestByState(r.Context(), state)
if err != nil {
log.Println("failed to get oauth request:", err)
o.pages.Notice(w, "login-msg", "Failed to authenticate. Try again later.")
···
}
defer func() {
+
err := o.sess.DeleteRequestByState(r.Context(), state)
if err != nil {
log.Println("failed to delete oauth request for state:", state, err)
}
···
return
}
+
err = o.oauth.SaveSession(w, r, *oauthRequest, tokenResp)
if err != nil {
log.Println("failed to save session:", err)
o.pages.Notice(w, "login-msg", "Failed to authenticate. Try again later.")
···
}
log.Println("session cleared successfully")
+
o.pages.HxRedirect(w, "/login")
}
func pubKeyFromJwk(jwks string) (jwk.Key, error) {
+28 -35
appview/oauth/oauth.go
···
"github.com/gorilla/sessions"
oauth "tangled.sh/icyphox.sh/atproto-oauth"
"tangled.sh/icyphox.sh/atproto-oauth/helpers"
"tangled.sh/tangled.sh/core/appview/config"
-
"tangled.sh/tangled.sh/core/appview/db"
"tangled.sh/tangled.sh/core/appview/oauth/client"
xrpc "tangled.sh/tangled.sh/core/appview/xrpcclient"
)
-
type OAuthRequest struct {
-
ID uint
-
AuthserverIss string
-
State string
-
Did string
-
PdsUrl string
-
PkceVerifier string
-
DpopAuthserverNonce string
-
DpopPrivateJwk string
-
}
-
type OAuth struct {
-
Store *sessions.CookieStore
-
Db *db.DB
-
Config *config.Config
}
-
func NewOAuth(db *db.DB, config *config.Config) *OAuth {
return &OAuth{
-
Store: sessions.NewCookieStore([]byte(config.Core.CookieSecret)),
-
Db: db,
-
Config: config,
}
}
-
func (o *OAuth) SaveSession(w http.ResponseWriter, r *http.Request, oreq db.OAuthRequest, oresp *oauth.TokenResponse) error {
// first we save the did in the user session
-
userSession, err := o.Store.Get(r, SessionName)
if err != nil {
return err
}
···
}
// then save the whole thing in the db
-
session := db.OAuthSession{
Did: oreq.Did,
Handle: oreq.Handle,
PdsUrl: oreq.PdsUrl,
···
Expiry: time.Now().Add(time.Duration(oresp.ExpiresIn) * time.Second).Format(time.RFC3339),
}
-
return db.SaveOAuthSession(o.Db, session)
}
func (o *OAuth) ClearSession(r *http.Request, w http.ResponseWriter) error {
-
userSession, err := o.Store.Get(r, SessionName)
if err != nil || userSession.IsNew {
return fmt.Errorf("error getting user session (or new session?): %w", err)
}
did := userSession.Values[SessionDid].(string)
-
err = db.DeleteOAuthSessionByDid(o.Db, did)
if err != nil {
return fmt.Errorf("error deleting oauth session: %w", err)
}
···
return userSession.Save(r, w)
}
-
func (o *OAuth) GetSession(r *http.Request) (*db.OAuthSession, bool, error) {
-
userSession, err := o.Store.Get(r, SessionName)
if err != nil || userSession.IsNew {
return nil, false, fmt.Errorf("error getting user session (or new session?): %w", err)
}
···
did := userSession.Values[SessionDid].(string)
auth := userSession.Values[SessionAuthenticated].(bool)
-
session, err := db.GetOAuthSessionByDid(o.Db, did)
if err != nil {
return nil, false, fmt.Errorf("error getting oauth session: %w", err)
}
···
oauthClient, err := client.NewClient(
self.ClientID,
-
o.Config.OAuth.Jwks,
self.RedirectURIs[0],
)
···
}
newExpiry := time.Now().Add(time.Duration(resp.ExpiresIn) * time.Second).Format(time.RFC3339)
-
err = db.RefreshOAuthSession(o.Db, did, resp.AccessToken, resp.RefreshToken, newExpiry)
if err != nil {
return nil, false, fmt.Errorf("error refreshing oauth session: %w", err)
}
···
}
func (a *OAuth) GetUser(r *http.Request) *User {
-
clientSession, err := a.Store.Get(r, SessionName)
if err != nil || clientSession.IsNew {
return nil
···
}
func (a *OAuth) GetDid(r *http.Request) string {
-
clientSession, err := a.Store.Get(r, SessionName)
if err != nil || clientSession.IsNew {
return ""
···
client := &oauth.XrpcClient{
OnDpopPdsNonceChanged: func(did, newNonce string) {
-
err := db.UpdateDpopPdsNonce(o.Db, did, newNonce)
if err != nil {
log.Printf("error updating dpop pds nonce: %v", err)
}
···
return []string{fmt.Sprintf("%s/oauth/callback", c)}
}
-
clientURI := o.Config.Core.AppviewHost
clientID := fmt.Sprintf("%s/oauth/client-metadata.json", clientURI)
redirectURIs := makeRedirectURIs(clientURI)
-
if o.Config.Core.Dev {
clientURI = fmt.Sprintf("http://127.0.0.1:3000")
redirectURIs = makeRedirectURIs(clientURI)
···
"github.com/gorilla/sessions"
oauth "tangled.sh/icyphox.sh/atproto-oauth"
"tangled.sh/icyphox.sh/atproto-oauth/helpers"
+
sessioncache "tangled.sh/tangled.sh/core/appview/cache/session"
"tangled.sh/tangled.sh/core/appview/config"
"tangled.sh/tangled.sh/core/appview/oauth/client"
xrpc "tangled.sh/tangled.sh/core/appview/xrpcclient"
)
type OAuth struct {
+
store *sessions.CookieStore
+
config *config.Config
+
sess *sessioncache.SessionStore
}
+
func NewOAuth(config *config.Config, sess *sessioncache.SessionStore) *OAuth {
return &OAuth{
+
store: sessions.NewCookieStore([]byte(config.Core.CookieSecret)),
+
config: config,
+
sess: sess,
}
}
+
func (o *OAuth) Stores() *sessions.CookieStore {
+
return o.store
+
}
+
+
func (o *OAuth) SaveSession(w http.ResponseWriter, r *http.Request, oreq sessioncache.OAuthRequest, oresp *oauth.TokenResponse) error {
// first we save the did in the user session
+
userSession, err := o.store.Get(r, SessionName)
if err != nil {
return err
}
···
}
// then save the whole thing in the db
+
session := sessioncache.OAuthSession{
Did: oreq.Did,
Handle: oreq.Handle,
PdsUrl: oreq.PdsUrl,
···
Expiry: time.Now().Add(time.Duration(oresp.ExpiresIn) * time.Second).Format(time.RFC3339),
}
+
return o.sess.SaveSession(r.Context(), session)
}
func (o *OAuth) ClearSession(r *http.Request, w http.ResponseWriter) error {
+
userSession, err := o.store.Get(r, SessionName)
if err != nil || userSession.IsNew {
return fmt.Errorf("error getting user session (or new session?): %w", err)
}
did := userSession.Values[SessionDid].(string)
+
err = o.sess.DeleteSession(r.Context(), did)
if err != nil {
return fmt.Errorf("error deleting oauth session: %w", err)
}
···
return userSession.Save(r, w)
}
+
func (o *OAuth) GetSession(r *http.Request) (*sessioncache.OAuthSession, bool, error) {
+
userSession, err := o.store.Get(r, SessionName)
if err != nil || userSession.IsNew {
return nil, false, fmt.Errorf("error getting user session (or new session?): %w", err)
}
···
did := userSession.Values[SessionDid].(string)
auth := userSession.Values[SessionAuthenticated].(bool)
+
session, err := o.sess.GetSession(r.Context(), did)
if err != nil {
return nil, false, fmt.Errorf("error getting oauth session: %w", err)
}
···
oauthClient, err := client.NewClient(
self.ClientID,
+
o.config.OAuth.Jwks,
self.RedirectURIs[0],
)
···
}
newExpiry := time.Now().Add(time.Duration(resp.ExpiresIn) * time.Second).Format(time.RFC3339)
+
err = o.sess.RefreshSession(r.Context(), did, resp.AccessToken, resp.RefreshToken, newExpiry)
if err != nil {
return nil, false, fmt.Errorf("error refreshing oauth session: %w", err)
}
···
}
func (a *OAuth) GetUser(r *http.Request) *User {
+
clientSession, err := a.store.Get(r, SessionName)
if err != nil || clientSession.IsNew {
return nil
···
}
func (a *OAuth) GetDid(r *http.Request) string {
+
clientSession, err := a.store.Get(r, SessionName)
if err != nil || clientSession.IsNew {
return ""
···
client := &oauth.XrpcClient{
OnDpopPdsNonceChanged: func(did, newNonce string) {
+
err := o.sess.UpdateNonce(r.Context(), did, newNonce)
if err != nil {
log.Printf("error updating dpop pds nonce: %v", err)
}
···
return []string{fmt.Sprintf("%s/oauth/callback", c)}
}
+
clientURI := o.config.Core.AppviewHost
clientID := fmt.Sprintf("%s/oauth/client-metadata.json", clientURI)
redirectURIs := makeRedirectURIs(clientURI)
+
if o.config.Core.Dev {
clientURI = fmt.Sprintf("http://127.0.0.1:3000")
redirectURIs = makeRedirectURIs(clientURI)
+1 -3
appview/state/router.go
···
r.Get("/", s.Timeline)
-
r.With(middleware.AuthMiddleware(s.oauth)).Post("/logout", s.Logout)
-
r.Route("/knots", func(r chi.Router) {
r.Use(middleware.AuthMiddleware(s.oauth))
r.Get("/", s.Knots)
···
func (s *State) OAuthRouter() http.Handler {
store := sessions.NewCookieStore([]byte(s.config.Core.CookieSecret))
-
oauth := oauthhandler.New(s.config, s.pages, s.idResolver, s.db, store, s.oauth, s.enforcer, s.posthog)
return oauth.Router()
}
···
r.Get("/", s.Timeline)
r.Route("/knots", func(r chi.Router) {
r.Use(middleware.AuthMiddleware(s.oauth))
r.Get("/", s.Knots)
···
func (s *State) OAuthRouter() http.Handler {
store := sessions.NewCookieStore([]byte(s.config.Core.CookieSecret))
+
oauth := oauthhandler.New(s.config, s.pages, s.idResolver, s.db, s.sess, store, s.oauth, s.enforcer, s.posthog)
return oauth.Router()
}
+9 -8
appview/state/state.go
···
"github.com/posthog/posthog-go"
"tangled.sh/tangled.sh/core/api/tangled"
"tangled.sh/tangled.sh/core/appview"
"tangled.sh/tangled.sh/core/appview/config"
"tangled.sh/tangled.sh/core/appview/db"
"tangled.sh/tangled.sh/core/appview/idresolver"
···
enforcer *rbac.Enforcer
tidClock syntax.TIDClock
pages *pages.Pages
idResolver *idresolver.Resolver
posthog posthog.Client
jc *jetstream.JetstreamClient
···
res = idresolver.DefaultResolver()
}
-
oauth := oauth.NewOAuth(d, config)
posthog, err := posthog.NewWithConfig(config.Posthog.ApiKey, posthog.Config{Endpoint: config.Posthog.Endpoint})
if err != nil {
···
enforcer,
clock,
pgs,
res,
posthog,
jc,
···
return c.Next().String()
}
-
func (s *State) Logout(w http.ResponseWriter, r *http.Request) {
-
s.oauth.ClearSession(r, w)
-
w.Header().Set("HX-Redirect", "/login")
-
w.WriteHeader(http.StatusSeeOther)
-
}
-
func (s *State) Timeline(w http.ResponseWriter, r *http.Request) {
user := s.oauth.GetUser(r)
···
return
case http.MethodPost:
-
session, err := s.oauth.Store.Get(r, oauth.SessionName)
if err != nil || session.IsNew {
log.Println("unauthorized attempt to generate registration key")
http.Error(w, "Forbidden", http.StatusUnauthorized)
···
"github.com/posthog/posthog-go"
"tangled.sh/tangled.sh/core/api/tangled"
"tangled.sh/tangled.sh/core/appview"
+
"tangled.sh/tangled.sh/core/appview/cache"
+
"tangled.sh/tangled.sh/core/appview/cache/session"
"tangled.sh/tangled.sh/core/appview/config"
"tangled.sh/tangled.sh/core/appview/db"
"tangled.sh/tangled.sh/core/appview/idresolver"
···
enforcer *rbac.Enforcer
tidClock syntax.TIDClock
pages *pages.Pages
+
sess *session.SessionStore
idResolver *idresolver.Resolver
posthog posthog.Client
jc *jetstream.JetstreamClient
···
res = idresolver.DefaultResolver()
}
+
cache := cache.New(config.Redis.Addr)
+
sess := session.New(cache)
+
+
oauth := oauth.NewOAuth(config, sess)
posthog, err := posthog.NewWithConfig(config.Posthog.ApiKey, posthog.Config{Endpoint: config.Posthog.Endpoint})
if err != nil {
···
enforcer,
clock,
pgs,
+
sess,
res,
posthog,
jc,
···
return c.Next().String()
}
func (s *State) Timeline(w http.ResponseWriter, r *http.Request) {
user := s.oauth.GetUser(r)
···
return
case http.MethodPost:
+
session, err := s.oauth.Stores().Get(r, oauth.SessionName)
if err != nil || session.IsNew {
log.Println("unauthorized attempt to generate registration key")
http.Error(w, "Forbidden", http.StatusUnauthorized)