diff --git a/README.md b/README.md index 8625d69..684338e 100644 --- a/README.md +++ b/README.md @@ -123,7 +123,7 @@ Request body: "code_challenge": "", "code_challenge_method": "S256", "state": "", - "redirect_uri": "yourapp://oauth/callback" + "redirect_uri": "https://yourapp.com/oauth/callback" } ``` @@ -157,6 +157,137 @@ Update your app's `client-metadata.json` to use the proxy: | Session lifetime | 7 days max | Unlimited | | User re-login frequency | Every 1-7 days | Only when user chooses | +## HTTPS Redirect URIs (Required for iOS) + +Bluesky's auth server enforces that `application_type: "native"` clients must use `token_endpoint_auth_method: "none"`. This means **you cannot use the proxy with `application_type: "native"`** — the PAR request will fail with: + +``` +{"error":"invalid_client_metadata","error_description":"Native clients must authenticate using \"none\" method"} +``` + +To use `private_key_jwt`, your client metadata must declare `application_type: "web"`, which requires HTTPS redirect URIs instead of custom URL schemes. + +The AT Protocol OAuth spec explicitly supports this: native clients are allowed to use an HTTPS URL as long as the URL origin matches the `client_id`. For example, if your `client_id` is `https://yourapp.com/oauth/client-metadata.json`, your redirect URI must be `https://yourapp.com/...`. + +Your client metadata should look like: + +```json +{ + "client_id": "https://yourapp.com/oauth/client-metadata.json", + "redirect_uris": ["https://yourapp.com/oauth/callback"], + "application_type": "web", + "token_endpoint_auth_method": "private_key_jwt", + "token_endpoint_auth_signing_alg": "ES256", + "dpop_bound_access_tokens": true, + "jwks_uri": "https://auth.yourapp.com/.well-known/jwks.json" +} +``` + +### iOS Setup + +On iOS, HTTPS OAuth callbacks are handled via `ASWebAuthenticationSession`'s HTTPS callback API (iOS 17.4+). You also need an Apple App Site Association (AASA) file for Universal Links as a safety net. + +#### 1. Apple App Site Association File + +Serve this at `https://yourapp.com/.well-known/apple-app-site-association`: + +```json +{ + "applinks": { + "details": [ + { + "appIDs": ["."], + "components": [ + { + "/": "/oauth/callback", + "comment": "AT Protocol OAuth callback" + } + ] + } + ] + }, + "webcredentials": { + "apps": ["."] + } +} +``` + +Replace `` with your Apple Developer Team ID and `` with your app's bundle identifier. + +Requirements: +- Must be served with `Content-Type: application/json` +- Must **not** redirect — Apple's CDN fetches it directly +- Apple caches the file via its CDN; updates can take hours to propagate +- Verify propagation: `https://app-site-association.cdn-apple.com/a/v1/yourapp.com` + +#### 2. Entitlements + +Add an Associated Domains entitlement to your app (via Xcode: target > Signing & Capabilities > Associated Domains, or via an `.entitlements` file): + +```xml +com.apple.developer.associated-domains + + applinks:yourapp.com + webcredentials:yourapp.com + +``` + +#### 3. ASWebAuthenticationSession + +Use the HTTPS callback initializer instead of the custom-scheme one: + +```swift +// Before (custom scheme — won't work with the proxy) +let session = ASWebAuthenticationSession( + url: authURL, + callbackURLScheme: "yourapp" +) { callbackURL, error in ... } + +// After (HTTPS callback — required for the proxy) +let session = ASWebAuthenticationSession( + url: authURL, + callback: .https(host: "yourapp.com", path: "/oauth/callback") +) { callbackURL, error in ... } +``` + +This requires iOS 17.4+. + +#### 4. Remove Custom URL Scheme + +If you were previously using a custom URL scheme (e.g., `yourapp://oauth/callback`) for OAuth, remove the `CFBundleURLTypes` entry from your `Info.plist`. The custom scheme is no longer needed for OAuth callbacks. + +#### 5. Web Fallback Page + +Add a simple page at `/oauth/callback` on your website. `ASWebAuthenticationSession` intercepts the redirect before it reaches the server, but having a real page there improves the experience for edge cases (e.g., users who land there in a browser): + +```html +

Redirecting to YourApp...

+

If you're not redirected automatically, open the app on your device.

+``` + +### Deployment Sequence + +Order matters — deploy bottom-up so each layer is ready before the one above depends on it: + +1. Deploy the auth proxy and verify it's live (`curl https://auth.yourapp.com/health`) +2. Deploy the AASA file to your website +3. **Wait** for Apple CDN propagation — verify via `https://app-site-association.cdn-apple.com/a/v1/yourapp.com` +4. Deploy updated client metadata (`application_type: "web"`, HTTPS redirect URI) +5. Build and test the iOS app **on a real device** (Universal Links don't work in Simulator) +6. Submit the app update + +> **Warning:** Do not deploy the client metadata update before the AASA file is cached by Apple's CDN. If it isn't, Universal Links won't work and the OAuth callback won't route to your app. + +### Rollback + +If something goes wrong, revert your `client-metadata.json`: +- `application_type`: `"web"` back to `"native"` +- `redirect_uris`: HTTPS URL back to custom scheme +- `token_endpoint_auth_method`: `"private_key_jwt"` back to `"none"` +- Remove `jwks_uri` and `token_endpoint_auth_signing_alg` + +The iOS app also needs a new build to switch back to `callbackURLScheme`. To minimize rollback friction, keep the AASA file deployed permanently — it has no downside even when unused. + ## Key Rotation The proxy supports zero-downtime key rotation. During rotation, both old and new public keys are published in the JWKS so existing sessions bound to the old key continue to work.