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}