A community based topic aggregation platform built on atproto
1package oauth 2 3import ( 4 "crypto/rand" 5 "crypto/sha256" 6 "encoding/base64" 7 "log/slog" 8 "net/http" 9 "net/url" 10) 11 12// allowedMobileRedirectURIs contains the EXACT allowed redirect URIs for mobile apps. 13// 14// Per atproto OAuth spec (https://atproto.com/specs/oauth#mobile-clients): 15// - Custom URL schemes are allowed for native mobile apps 16// - The scheme must match the client_id hostname in REVERSE-DOMAIN order 17// - For client_id https://coves.social/..., the scheme is "social.coves" 18// 19// We support two redirect URI types: 20// 1. Custom scheme: social.coves:/callback (per atproto spec, simpler for mobile) 21// 2. Universal Links: https://coves.social/app/oauth/callback (cryptographically bound) 22// 23// Universal Links provide stronger security guarantees but require: 24// - iOS: Verified via /.well-known/apple-app-site-association 25// - Android: Verified via /.well-known/assetlinks.json 26var allowedMobileRedirectURIs = map[string]bool{ 27 // Custom scheme per atproto spec (reverse-domain of coves.social) 28 "social.coves:/callback": true, 29 "social.coves://callback": true, // Some platforms add double slash 30 "social.coves:/oauth/callback": true, // Alternative path 31 "social.coves://oauth/callback": true, 32 // Universal Links - cryptographically bound to app (preferred for security) 33 "https://coves.social/app/oauth/callback": true, 34} 35 36// isAllowedMobileRedirectURI validates that the redirect URI is in the exact allowlist. 37// SECURITY: Exact URI matching prevents token theft by rogue apps. 38// 39// Per atproto OAuth spec, custom schemes must match the client_id hostname 40// in reverse-domain order (social.coves for coves.social), which provides 41// some protection as malicious apps would need to know the specific scheme. 42// 43// Universal Links (https://) provide stronger security as they're cryptographically 44// bound to the app via .well-known verification files. 45func isAllowedMobileRedirectURI(redirectURI string) bool { 46 // Normalize and check exact match 47 return allowedMobileRedirectURIs[redirectURI] 48} 49 50// extractScheme extracts the scheme from a URI for logging purposes 51func extractScheme(uri string) string { 52 if u, err := url.Parse(uri); err == nil && u.Scheme != "" { 53 return u.Scheme 54 } 55 return "invalid" 56} 57 58// generateCSRFToken generates a cryptographically secure CSRF token 59func generateCSRFToken() (string, error) { 60 csrfToken := make([]byte, 32) 61 if _, err := rand.Read(csrfToken); err != nil { 62 slog.Error("failed to generate CSRF token", "error", err) 63 return "", err 64 } 65 return base64.URLEncoding.EncodeToString(csrfToken), nil 66} 67 68// generateMobileRedirectBinding generates a cryptographically secure binding token 69// that ties the CSRF token and mobile redirect URI to this specific OAuth flow. 70// SECURITY: This prevents multiple attack vectors: 71// 1. Session fixation: attacker plants mobile_redirect_uri cookie, user does web login 72// 2. CSRF bypass: attacker manipulates cookies without knowing the CSRF token 73// 3. Cookie replay: binding validates both CSRF and redirect URI together 74// 75// The binding is hash(csrfToken + "|" + mobileRedirectURI) which ensures: 76// - CSRF token value is verified (not just presence) 77// - Redirect URI is tied to the specific CSRF token that started the flow 78// - Cannot forge binding without knowing both values 79func generateMobileRedirectBinding(csrfToken, mobileRedirectURI string) string { 80 // Combine CSRF token and redirect URI with separator to prevent length extension 81 combined := csrfToken + "|" + mobileRedirectURI 82 hash := sha256.Sum256([]byte(combined)) 83 // Use first 16 bytes (128 bits) for the binding - sufficient for this purpose 84 return base64.URLEncoding.EncodeToString(hash[:16]) 85} 86 87// validateMobileRedirectBinding validates that the CSRF token and mobile redirect URI 88// together match the binding token, preventing CSRF attacks and cross-flow token theft. 89// This implements a proper double-submit cookie pattern where the CSRF token value 90// (not just presence) is cryptographically verified. 91func validateMobileRedirectBinding(csrfToken, mobileRedirectURI, binding string) bool { 92 expectedBinding := generateMobileRedirectBinding(csrfToken, mobileRedirectURI) 93 // Constant-time comparison to prevent timing attacks 94 return constantTimeCompare(expectedBinding, binding) 95} 96 97// constantTimeCompare performs a constant-time string comparison to prevent timing attacks 98func constantTimeCompare(a, b string) bool { 99 if len(a) != len(b) { 100 return false 101 } 102 var result byte 103 for i := 0; i < len(a); i++ { 104 result |= a[i] ^ b[i] 105 } 106 return result == 0 107} 108 109// clearMobileCookies clears all mobile-related cookies to prevent reuse 110func clearMobileCookies(w http.ResponseWriter) { 111 http.SetCookie(w, &http.Cookie{ 112 Name: "mobile_redirect_uri", 113 Value: "", 114 Path: "/oauth", 115 MaxAge: -1, 116 }) 117 http.SetCookie(w, &http.Cookie{ 118 Name: "mobile_redirect_binding", 119 Value: "", 120 Path: "/oauth", 121 MaxAge: -1, 122 }) 123 http.SetCookie(w, &http.Cookie{ 124 Name: "oauth_csrf", 125 Value: "", 126 Path: "/oauth", 127 MaxAge: -1, 128 }) 129}