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