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}