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}