diff --git a/internal/api/handlers/wellknown/universal_links.go b/internal/api/handlers/wellknown/universal_links.go new file mode 100644 index 0000000..b6f5739 --- /dev/null +++ b/internal/api/handlers/wellknown/universal_links.go @@ -0,0 +1,137 @@ +package wellknown + +import ( + "encoding/json" + "log/slog" + "net/http" + "os" +) + +// HandleAppleAppSiteAssociation serves the iOS Universal Links configuration +// GET /.well-known/apple-app-site-association +// +// Universal Links provide cryptographic binding between the app and domain: +// - Requires apple-app-site-association file served over HTTPS +// - App must have Associated Domains capability configured +// - System verifies domain ownership before routing deep links +// - Prevents malicious apps from intercepting deep links +// +// Spec: https://developer.apple.com/documentation/xcode/supporting-universal-links-in-your-app +func HandleAppleAppSiteAssociation(w http.ResponseWriter, r *http.Request) { + // Get Apple App ID from environment (format: .) + // Example: "ABCD1234.social.coves.app" + // Find Team ID in Apple Developer Portal -> Membership + // Bundle ID is configured in Xcode project + appleAppID := os.Getenv("APPLE_APP_ID") + if appleAppID == "" { + // Development fallback - allows testing without real Team ID + // IMPORTANT: This MUST be set in production for Universal Links to work + appleAppID = "DEVELOPMENT.social.coves.app" + slog.Warn("APPLE_APP_ID not set, using development placeholder", + "app_id", appleAppID, + "note", "Set APPLE_APP_ID env var for production Universal Links") + } + + // Apple requires application/json content type (no charset) + w.Header().Set("Content-Type", "application/json") + + // Construct the response per Apple's spec + // See: https://developer.apple.com/documentation/bundleresources/applinks + response := map[string]interface{}{ + "applinks": map[string]interface{}{ + "apps": []string{}, // Must be empty array per Apple spec + "details": []map[string]interface{}{ + { + "appID": appleAppID, + // Paths that trigger Universal Links when opened in Safari/other apps + // These URLs will open the app instead of the browser + "paths": []string{ + "/app/oauth/callback", // Primary Universal Link OAuth callback + "/app/oauth/callback/*", // Catch-all for query params + }, + }, + }, + }, + } + + if err := json.NewEncoder(w).Encode(response); err != nil { + slog.Error("failed to encode apple-app-site-association", "error", err) + http.Error(w, "internal server error", http.StatusInternalServerError) + return + } + + slog.Debug("served apple-app-site-association", "app_id", appleAppID) +} + +// HandleAssetLinks serves the Android App Links configuration +// GET /.well-known/assetlinks.json +// +// App Links provide cryptographic binding between the app and domain: +// - Requires assetlinks.json file served over HTTPS +// - App must have intent-filter with android:autoVerify="true" +// - System verifies domain ownership via SHA-256 certificate fingerprint +// - Prevents malicious apps from intercepting deep links +// +// Spec: https://developer.android.com/training/app-links/verify-android-applinks +func HandleAssetLinks(w http.ResponseWriter, r *http.Request) { + // Get Android package name from environment + // Example: "social.coves.app" + androidPackage := os.Getenv("ANDROID_PACKAGE_NAME") + if androidPackage == "" { + androidPackage = "social.coves.app" // Default for development + slog.Warn("ANDROID_PACKAGE_NAME not set, using default", + "package", androidPackage, + "note", "Set ANDROID_PACKAGE_NAME env var for production App Links") + } + + // Get SHA-256 fingerprint from environment + // This is the SHA-256 fingerprint of the app's signing certificate + // + // To get the fingerprint: + // Production: keytool -list -v -keystore release.jks -alias release + // Debug: keytool -list -v -keystore ~/.android/debug.keystore -alias androiddebugkey -storepass android -keypass android + // + // Look for "SHA256:" in the output + // Format: AA:BB:CC:DD:...:FF (64 hex characters separated by colons) + androidFingerprint := os.Getenv("ANDROID_SHA256_FINGERPRINT") + if androidFingerprint == "" { + // Development fallback - this won't work for real App Links verification + // IMPORTANT: This MUST be set in production for App Links to work + androidFingerprint = "00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00" + slog.Warn("ANDROID_SHA256_FINGERPRINT not set, using development placeholder", + "fingerprint", androidFingerprint, + "note", "Set ANDROID_SHA256_FINGERPRINT env var for production App Links") + } + + w.Header().Set("Content-Type", "application/json") + + // Construct the response per Google's Digital Asset Links spec + // See: https://developers.google.com/digital-asset-links/v1/getting-started + response := []map[string]interface{}{ + { + // delegate_permission/common.handle_all_urls grants the app permission + // to handle URLs for this domain + "relation": []string{"delegate_permission/common.handle_all_urls"}, + "target": map[string]interface{}{ + "namespace": "android_app", + "package_name": androidPackage, + // List of certificate fingerprints that can sign the app + // Multiple fingerprints can be provided for different signing keys + // (e.g., debug + release) + "sha256_cert_fingerprints": []string{ + androidFingerprint, + }, + }, + }, + } + + if err := json.NewEncoder(w).Encode(response); err != nil { + slog.Error("failed to encode assetlinks.json", "error", err) + http.Error(w, "internal server error", http.StatusInternalServerError) + return + } + + slog.Debug("served assetlinks.json", + "package", androidPackage, + "fingerprint", androidFingerprint) +} diff --git a/internal/api/routes/wellknown.go b/internal/api/routes/wellknown.go new file mode 100644 index 0000000..8605698 --- /dev/null +++ b/internal/api/routes/wellknown.go @@ -0,0 +1,25 @@ +package routes + +import ( + "Coves/internal/api/handlers/wellknown" + + "github.com/go-chi/chi/v5" +) + +// RegisterWellKnownRoutes registers RFC 8615 well-known URI endpoints +// These endpoints are used for service discovery and mobile app deep linking +// +// Spec: https://www.rfc-editor.org/rfc/rfc8615.html +func RegisterWellKnownRoutes(r chi.Router) { + // iOS Universal Links configuration + // Required for cryptographically-bound deep linking on iOS + // Must be served at exact path /.well-known/apple-app-site-association + // Content-Type: application/json (no redirects allowed) + r.Get("/.well-known/apple-app-site-association", wellknown.HandleAppleAppSiteAssociation) + + // Android App Links configuration + // Required for cryptographically-bound deep linking on Android + // Must be served at exact path /.well-known/assetlinks.json + // Content-Type: application/json (no redirects allowed) + r.Get("/.well-known/assetlinks.json", wellknown.HandleAssetLinks) +}