···
"Coves/internal/api/middleware"
"Coves/internal/api/routes"
6
-
"Coves/internal/atproto/auth"
"Coves/internal/atproto/identity"
"Coves/internal/atproto/jetstream"
8
+
"Coves/internal/atproto/oauth"
"Coves/internal/core/aggregators"
"Coves/internal/core/blobs"
"Coves/internal/core/comments"
···
"Coves/internal/core/users"
···
commentsAPI "Coves/internal/api/handlers/comments"
postgresRepo "Coves/internal/db/postgres"
42
-
indigoIdentity "github.com/bluesky-social/indigo/atproto/identity"
···
identityResolver := identity.NewResolver(db, identityConfig)
140
-
// Initialize atProto auth middleware for JWT validation
141
-
// Phase 1: Set skipVerify=true to test JWT parsing only
142
-
// Phase 2: Set skipVerify=false to enable full signature verification
143
-
skipVerify := os.Getenv("AUTH_SKIP_VERIFY") == "true"
145
-
log.Println("⚠️ WARNING: JWT signature verification is DISABLED (Phase 1 testing)")
146
-
log.Println(" Set AUTH_SKIP_VERIFY=false for production")
149
-
// Initialize Indigo directory for DID resolution (used by auth)
140
+
// Get PLC URL for OAuth and other services
plcURL := os.Getenv("PLC_DIRECTORY_URL")
plcURL = "https://plc.directory"
154
-
indigoDir := &indigoIdentity.BaseDirectory{
156
-
HTTPClient: http.Client{Timeout: 10 * time.Second},
146
+
// Initialize OAuth client for sealed session tokens
147
+
// Mobile apps authenticate via OAuth flow and receive sealed session tokens
148
+
// These tokens are encrypted references to OAuth sessions stored in the database
149
+
oauthSealSecret := os.Getenv("OAUTH_SEAL_SECRET")
150
+
if oauthSealSecret == "" {
151
+
if os.Getenv("IS_DEV_ENV") != "true" {
152
+
log.Fatal("OAUTH_SEAL_SECRET is required in production mode")
154
+
// Generate RANDOM secret for dev mode
155
+
randomBytes := make([]byte, 32)
156
+
if _, err := rand.Read(randomBytes); err != nil {
157
+
log.Fatal("Failed to generate random seal secret: ", err)
159
+
oauthSealSecret = base64.StdEncoding.EncodeToString(randomBytes)
160
+
log.Println("⚠️ DEV MODE: Generated random OAuth seal secret (won't persist across restarts)")
159
-
// Initialize JWT config early to cache HS256_ISSUERS and PDS_JWT_SECRET
160
-
// This avoids reading env vars on every request
161
-
auth.InitJWTConfig()
163
+
isDevMode := os.Getenv("IS_DEV_ENV") == "true"
164
+
oauthConfig := &oauth.OAuthConfig{
165
+
PublicURL: os.Getenv("APPVIEW_PUBLIC_URL"),
166
+
SealSecret: oauthSealSecret,
167
+
Scopes: []string{"atproto", "transition:generic"},
168
+
DevMode: isDevMode,
169
+
AllowPrivateIPs: isDevMode, // Allow private IPs only in dev mode
171
+
// SessionTTL and SealedTokenTTL will use defaults if not set (7 days and 14 days)
163
-
// Create combined key fetcher for both DID and URL issuers
164
-
// - DID issuers (did:plc:, did:web:) → resolved via DID document keys (ES256)
165
-
// - URL issuers → JWKS endpoint (fallback for legacy tokens)
166
-
jwksCacheTTL := 1 * time.Hour
167
-
jwksFetcher := auth.NewCachedJWKSFetcher(jwksCacheTTL)
168
-
keyFetcher := auth.NewCombinedKeyFetcher(indigoDir, jwksFetcher)
174
+
// Create PostgreSQL-backed OAuth session store (using default 7-day TTL)
175
+
baseOAuthStore := oauth.NewPostgresOAuthStore(db, 0)
176
+
// Wrap with MobileAwareStoreWrapper to capture OAuth state for mobile CSRF validation.
177
+
// This intercepts SaveAuthRequestInfo to save mobile CSRF data when present in context.
178
+
oauthStore := oauth.NewMobileAwareStoreWrapper(baseOAuthStore)
180
+
if oauthConfig.PublicURL == "" {
181
+
oauthConfig.PublicURL = "http://localhost:8080"
182
+
oauthConfig.DevMode = true // Force dev mode for localhost
185
+
// Optional: confidential client secret for production
186
+
oauthConfig.ClientSecret = os.Getenv("OAUTH_CLIENT_SECRET")
187
+
oauthConfig.ClientKID = os.Getenv("OAUTH_CLIENT_KID")
170
-
authMiddleware := middleware.NewAtProtoAuthMiddleware(keyFetcher, skipVerify)
171
-
log.Println("✅ atProto auth middleware initialized (DID + JWKS key resolution)")
189
+
oauthClient, err := oauth.NewOAuthClient(oauthConfig, oauthStore)
191
+
log.Fatalf("Failed to initialize OAuth client: %v", err)
194
+
// Create OAuth handler for HTTP endpoints
195
+
oauthHandler := oauth.NewOAuthHandler(oauthClient, oauthStore)
197
+
// Create OAuth auth middleware
198
+
// Validates sealed session tokens and loads OAuth sessions from database
199
+
authMiddleware := middleware.NewOAuthAuthMiddleware(oauthClient, oauthStore)
200
+
log.Println("✅ OAuth auth middleware initialized (sealed session tokens)")
// Initialize repositories and services
userRepo := postgresRepo.NewUserRepository(db)
···
log.Println(" - Indexing: social.coves.community.profile (community profiles)")
log.Println(" - Indexing: social.coves.community.subscription (user subscriptions)")
306
-
// Start JWKS cache cleanup background job
335
+
// Start OAuth session cleanup background job with cancellable context
336
+
cleanupCtx, cleanupCancel := context.WithCancel(context.Background())
ticker := time.NewTicker(1 * time.Hour)
310
-
for range ticker.C {
311
-
jwksFetcher.CleanupExpiredCache()
312
-
log.Println("JWKS cache cleanup completed")
342
+
case <-cleanupCtx.Done():
343
+
log.Println("OAuth cleanup job stopped")
346
+
// Check if store implements cleanup methods
347
+
// Use UnwrapPostgresStore to get the underlying store from the wrapper
348
+
if cleanupStore := oauthStore.UnwrapPostgresStore(); cleanupStore != nil {
349
+
sessions, sessErr := cleanupStore.CleanupExpiredSessions(cleanupCtx)
350
+
if sessErr != nil {
351
+
log.Printf("Error cleaning up expired OAuth sessions: %v", sessErr)
353
+
requests, reqErr := cleanupStore.CleanupExpiredAuthRequests(cleanupCtx)
355
+
log.Printf("Error cleaning up expired OAuth auth requests: %v", reqErr)
357
+
if sessions > 0 || requests > 0 {
358
+
log.Printf("OAuth cleanup: removed %d expired sessions, %d expired auth requests", sessions, requests)
316
-
log.Println("Started JWKS cache cleanup background job (runs hourly)")
365
+
log.Println("Started OAuth session cleanup background job (runs hourly)")
// Initialize aggregator service
aggregatorRepo := postgresRepo.NewAggregatorRepository(db)
···
log.Println("✅ Comment query API registered (20 req/min rate limit)")
log.Println(" - GET /xrpc/social.coves.community.comment.getComments")
546
+
// Configure allowed CORS origins for OAuth callback
547
+
// SECURITY: Never use wildcard "*" with credentials - only allow specific origins
548
+
var oauthAllowedOrigins []string
549
+
appviewPublicURL := os.Getenv("APPVIEW_PUBLIC_URL")
550
+
if appviewPublicURL == "" {
551
+
appviewPublicURL = "http://localhost:8080"
553
+
oauthAllowedOrigins = append(oauthAllowedOrigins, appviewPublicURL)
555
+
// In dev mode, also allow common localhost origins for testing
556
+
if oauthConfig.DevMode {
557
+
oauthAllowedOrigins = append(oauthAllowedOrigins,
558
+
"http://localhost:3000",
559
+
"http://localhost:3001",
560
+
"http://localhost:5173",
561
+
"http://127.0.0.1:8080",
562
+
"http://127.0.0.1:3000",
563
+
"http://127.0.0.1:3001",
564
+
"http://127.0.0.1:5173",
566
+
log.Printf("🧪 DEV MODE: OAuth CORS allows localhost origins for testing")
568
+
log.Printf("OAuth CORS allowed origins: %v", oauthAllowedOrigins)
570
+
// Register OAuth routes for authentication flow
571
+
routes.RegisterOAuthRoutes(r, oauthHandler, oauthAllowedOrigins)
572
+
log.Println("✅ OAuth endpoints registered")
573
+
log.Println(" - GET /oauth/client-metadata.json")
574
+
log.Println(" - GET /oauth/jwks.json")
575
+
log.Println(" - GET /oauth/login")
576
+
log.Println(" - GET /oauth/mobile/login")
577
+
log.Println(" - GET /oauth/callback")
578
+
log.Println(" - POST /oauth/logout")
579
+
log.Println(" - POST /oauth/refresh")
581
+
// Register well-known routes for mobile app deep linking
582
+
routes.RegisterWellKnownRoutes(r)
583
+
log.Println("✅ Well-known endpoints registered (mobile Universal Links & App Links)")
584
+
log.Println(" - GET /.well-known/apple-app-site-association (iOS Universal Links)")
585
+
log.Println(" - GET /.well-known/assetlinks.json (Android App Links)")
// Health check endpoints
healthHandler := func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
···
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
543
-
// Stop auth middleware background goroutines (DPoP replay cache cleanup)
544
-
authMiddleware.Stop()
545
-
log.Println("Auth middleware stopped")
633
+
// Stop OAuth cleanup background job
if err := server.Shutdown(ctx); err != nil {
log.Fatalf("Server shutdown error: %v", err)