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// SECURITY: Only Universal Links (HTTPS) are allowed - cryptographically bound to app. 14// 15// Universal Links provide strong security guarantees: 16// - iOS: Verified via /.well-known/apple-app-site-association 17// - Android: Verified via /.well-known/assetlinks.json 18// - System verifies domain ownership before routing to app 19// - Prevents malicious apps from intercepting OAuth callbacks 20// 21// Custom URL schemes (coves-app://, coves://) are NOT allowed because: 22// - Any app can register the same scheme and intercept tokens 23// - No cryptographic binding to app identity 24// - Token theft is trivial for malicious apps 25// 26// See: https://atproto.com/specs/oauth#mobile-clients 27var allowedMobileRedirectURIs = map[string]bool{ 28 // Universal Links only - cryptographically bound to app 29 "https://coves.social/app/oauth/callback": true, 30} 31 32// isAllowedMobileRedirectURI validates that the redirect URI is in the exact allowlist. 33// SECURITY: Exact URI matching prevents token theft by rogue apps that register the same scheme. 34// 35// Custom URL schemes are NOT cryptographically bound to apps: 36// - Any app on the device can register "coves-app://" or "coves://" 37// - A malicious app can intercept deep links intended for Coves 38// - Without exact URI matching, the attacker receives the sealed token 39// 40// This function performs EXACT matching (not scheme-only) as a security measure. 41// For production, migrate to Universal Links (iOS) or App Links (Android). 42func isAllowedMobileRedirectURI(redirectURI string) bool { 43 // Normalize and check exact match 44 return allowedMobileRedirectURIs[redirectURI] 45} 46 47// extractScheme extracts the scheme from a URI for logging purposes 48func extractScheme(uri string) string { 49 if u, err := url.Parse(uri); err == nil && u.Scheme != "" { 50 return u.Scheme 51 } 52 return "invalid" 53} 54 55// generateCSRFToken generates a cryptographically secure CSRF token 56func generateCSRFToken() (string, error) { 57 csrfToken := make([]byte, 32) 58 if _, err := rand.Read(csrfToken); err != nil { 59 slog.Error("failed to generate CSRF token", "error", err) 60 return "", err 61 } 62 return base64.URLEncoding.EncodeToString(csrfToken), nil 63} 64 65// generateMobileRedirectBinding generates a cryptographically secure binding token 66// that ties the CSRF token and mobile redirect URI to this specific OAuth flow. 67// SECURITY: This prevents multiple attack vectors: 68// 1. Session fixation: attacker plants mobile_redirect_uri cookie, user does web login 69// 2. CSRF bypass: attacker manipulates cookies without knowing the CSRF token 70// 3. Cookie replay: binding validates both CSRF and redirect URI together 71// 72// The binding is hash(csrfToken + "|" + mobileRedirectURI) which ensures: 73// - CSRF token value is verified (not just presence) 74// - Redirect URI is tied to the specific CSRF token that started the flow 75// - Cannot forge binding without knowing both values 76func generateMobileRedirectBinding(csrfToken, mobileRedirectURI string) string { 77 // Combine CSRF token and redirect URI with separator to prevent length extension 78 combined := csrfToken + "|" + mobileRedirectURI 79 hash := sha256.Sum256([]byte(combined)) 80 // Use first 16 bytes (128 bits) for the binding - sufficient for this purpose 81 return base64.URLEncoding.EncodeToString(hash[:16]) 82} 83 84// validateMobileRedirectBinding validates that the CSRF token and mobile redirect URI 85// together match the binding token, preventing CSRF attacks and cross-flow token theft. 86// This implements a proper double-submit cookie pattern where the CSRF token value 87// (not just presence) is cryptographically verified. 88func validateMobileRedirectBinding(csrfToken, mobileRedirectURI, binding string) bool { 89 expectedBinding := generateMobileRedirectBinding(csrfToken, mobileRedirectURI) 90 // Constant-time comparison to prevent timing attacks 91 return constantTimeCompare(expectedBinding, binding) 92} 93 94// constantTimeCompare performs a constant-time string comparison to prevent timing attacks 95func constantTimeCompare(a, b string) bool { 96 if len(a) != len(b) { 97 return false 98 } 99 var result byte 100 for i := 0; i < len(a); i++ { 101 result |= a[i] ^ b[i] 102 } 103 return result == 0 104} 105 106// clearMobileCookies clears all mobile-related cookies to prevent reuse 107func clearMobileCookies(w http.ResponseWriter) { 108 http.SetCookie(w, &http.Cookie{ 109 Name: "mobile_redirect_uri", 110 Value: "", 111 Path: "/oauth", 112 MaxAge: -1, 113 }) 114 http.SetCookie(w, &http.Cookie{ 115 Name: "mobile_redirect_binding", 116 Value: "", 117 Path: "/oauth", 118 MaxAge: -1, 119 }) 120 http.SetCookie(w, &http.Cookie{ 121 Name: "oauth_csrf", 122 Value: "", 123 Path: "/oauth", 124 MaxAge: -1, 125 }) 126}