A community based topic aggregation platform built on atproto
1package oauth 2 3import ( 4 "Coves/internal/atproto/oauth" 5 "context" 6 "fmt" 7 "time" 8 9 "github.com/lestrrat-go/jwx/v2/jwk" 10) 11 12// AuthService handles authentication-related business logic 13// Extracted from middleware to maintain clean architecture 14type AuthService struct { 15 sessionStore SessionStore 16 oauthClient *oauth.Client 17} 18 19// NewAuthService creates a new authentication service 20func NewAuthService(sessionStore SessionStore, oauthClient *oauth.Client) *AuthService { 21 return &AuthService{ 22 sessionStore: sessionStore, 23 oauthClient: oauthClient, 24 } 25} 26 27// ValidateSession retrieves and validates a user's OAuth session 28// Returns the session if valid, error if not found or expired 29func (s *AuthService) ValidateSession(ctx context.Context, did string) (*OAuthSession, error) { 30 session, err := s.sessionStore.GetSession(did) 31 if err != nil { 32 return nil, fmt.Errorf("session not found: %w", err) 33 } 34 return session, nil 35} 36 37// RefreshTokenIfNeeded checks if token needs refresh and refreshes if necessary 38// Returns updated session if refreshed, original session otherwise 39func (s *AuthService) RefreshTokenIfNeeded(ctx context.Context, session *OAuthSession, threshold time.Duration) (*OAuthSession, error) { 40 // Check if token needs refresh 41 if time.Until(session.ExpiresAt) >= threshold { 42 // Token is still valid, no refresh needed 43 return session, nil 44 } 45 46 // Parse DPoP key 47 dpopKey, err := oauth.ParseJWKFromJSON([]byte(session.DPoPPrivateJWK)) 48 if err != nil { 49 return nil, fmt.Errorf("failed to parse DPoP key: %w", err) 50 } 51 52 // Refresh token 53 tokenResp, err := s.oauthClient.RefreshTokenRequest( 54 ctx, 55 session.RefreshToken, 56 session.AuthServerIss, 57 session.DPoPAuthServerNonce, 58 dpopKey, 59 ) 60 if err != nil { 61 return nil, fmt.Errorf("failed to refresh token: %w", err) 62 } 63 64 // Update session with new tokens 65 expiresAt := time.Now().Add(time.Duration(tokenResp.ExpiresIn) * time.Second) 66 if err := s.sessionStore.RefreshSession(session.DID, tokenResp.AccessToken, tokenResp.RefreshToken, expiresAt); err != nil { 67 return nil, fmt.Errorf("failed to update session: %w", err) 68 } 69 70 // Update nonce if provided (best effort - non-critical) 71 if tokenResp.DpopAuthserverNonce != "" { 72 session.DPoPAuthServerNonce = tokenResp.DpopAuthserverNonce 73 if updateErr := s.sessionStore.UpdateAuthServerNonce(session.DID, tokenResp.DpopAuthserverNonce); updateErr != nil { 74 // Log but don't fail - nonce will be updated on next request 75 // (We ignore the error here intentionally as nonce updates are non-critical) 76 _ = updateErr 77 } 78 } 79 80 // Return updated session 81 session.AccessToken = tokenResp.AccessToken 82 session.RefreshToken = tokenResp.RefreshToken 83 session.ExpiresAt = expiresAt 84 85 return session, nil 86} 87 88// CreateDPoPKey generates a new DPoP key for a session 89func (s *AuthService) CreateDPoPKey() (jwk.Key, error) { 90 return oauth.GenerateDPoPKey() 91}