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}