Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,104 @@ const login = await fetch("/api/v1/auth/passkeys/authenticate/complete", {
// → {access_token, refresh_token, ...}
```

## Native mobile apps (iOS / Android)

Passkeys are cross-platform by design. One RP ID, one passkey per user, usable on web and native apps — if you set up the domain-association files correctly. A user who enrolls a passkey on their phone can sign in on the web, and vice versa.

### RP ID choice matters more on mobile

Public-suffix domains (`*.vercel.app`, `*.netlify.app`, `*.github.io`, `*.herokuapp.com`) can host a web-only passkey setup, but they're a poor fit for native apps:

- Apple and Google want a stable, verifiable domain serving association JSON. Vercel preview URLs change per deployment.
- Public Suffix List (PSL) rules prevent using the parent (e.g. `vercel.app` itself as RP ID) — browsers reject it outright per the WebAuthn spec, and mobile SDKs follow the same rule.
- Some enterprise device-management policies block passkey enrollment on public-suffix domains.

For anything shipping to the App Store or Play Store: **use a custom domain you control.** This is also the only way to extend later with multiple frontends (`app.example.com`, `admin.example.com`) sharing one passkey via RP ID `example.com`.

### Association files

Two static JSON files, served over HTTPS from the RP ID root, prove your app is authorised to use credentials scoped to that domain.

**iOS — `https://<rp-id>/.well-known/apple-app-site-association`**

```json
{
"webcredentials": {
"apps": ["<TEAMID>.<bundle-id>"]
}
}
```

`TEAMID` is your Apple Developer team ID (10 chars). `bundle-id` must match the iOS app bundle identifier. Content-Type `application/json`, no `.json` extension on the URL path. Apple caches this aggressively — use a rollout plan for changes.

**Android — `https://<rp-id>/.well-known/assetlinks.json`**

```json
[{
"relation": ["delegate_permission/common.get_login_creds"],
"target": {
"namespace": "android_app",
"package_name": "<package-id>",
"sha256_cert_fingerprints": ["<SHA-256 fingerprint of signing cert>"]
}
}]
```

The fingerprint must come from the signing certificate that Play Store uses — for apps enrolled in **Play App Signing** that's the app-signing-key fingerprint from the Play Console, **not** the upload keystore. Getting this wrong is the most common reason Android passkey enrolment silently fails.

### `PASSKEY_ORIGINS` entries for native apps

Beyond the web origin, add per-platform entries:

```bash
export FULLAUTH_PASSKEY_RP_ID=app.example.com
export FULLAUTH_PASSKEY_ORIGINS='[
"https://app.example.com",
"android:apk-key-hash:AbCdEf1234..."
]'
```

- **iOS** sends `origin: https://<rp-id>` in the clientDataJSON — same as a browser. The web origin entry covers iOS ceremonies; no extra entry needed.
- **Android** sends `origin: android:apk-key-hash:<base64url(SHA-256 of signing cert)>`. This must be in `PASSKEY_ORIGINS` or the assertion is rejected in origin validation.

Get the Android hash:

```bash
# From the signing keystore (upload or Play App Signing):
keytool -list -v -keystore release.jks -alias <alias> | grep SHA256
# Then base64url-encode the SHA-256 (strip colons, hex → bytes → b64url).
```

Or pull it straight from the Play Console under **Release → Setup → App integrity → App signing**.

### Flutter client setup

**iOS (Flutter iOS target):**
- Add the `com.apple.developer.associated-domains` entitlement in `ios/Runner/Runner.entitlements`:
```xml
<key>com.apple.developer.associated-domains</key>
<array>
<string>webcredentials:app.example.com</string>
</array>
```
- Use a Flutter passkey plugin (e.g. [`passkeys`](https://pub.dev/packages/passkeys)) that bridges to `ASAuthorizationPlatformPublicKeyCredentialProvider` on iOS 16+.

**Android (Flutter Android target):**
- Minimum API 28 (Android 9) for Credential Manager with back-compat via `androidx.credentials:credentials`.
- No manifest changes needed — the `assetlinks.json` + signing fingerprint is the binding.
- The same Flutter plugin will dispatch to Credential Manager on Android.

### End-to-end flow is identical

The library doesn't care whether the ceremony came from a browser, iOS, or Android — once the origin validates and the attestation/assertion verifies, the stored `PasskeyRecord` is platform-agnostic. A user can register on mobile and authenticate on web using the same passkey (synced via iCloud Keychain or Google Password Manager) without any special server-side handling.

### Common pitfalls

- **Android fingerprint mismatch** — you put the upload-keystore fingerprint in `assetlinks.json` but Play App Signing re-signs. Always use the Play-console-displayed fingerprint.
- **AASA served with wrong content-type** — some static hosts serve `.well-known/apple-app-site-association` as `text/html`. Apple silently rejects it. Force `application/json`.
- **RP ID case mismatch** — `Example.com` in config, `example.com` in association file. DNS is case-insensitive but these string comparisons aren't. Keep everything lowercase.
- **Preview deployment URLs** — Vercel preview (`pr-123-myapp.vercel.app`) won't match RP ID `myapp.com`. Either exclude previews from passkey flows or route `app.example.com` → production only.

## User verification (UV) is required by default

`PASSKEY_REQUIRE_USER_VERIFICATION=True` (default). Both `register/begin` and `authenticate/begin` request `UserVerificationRequirement.REQUIRED` from the authenticator, and both `register/complete` and `authenticate/complete` pass `require_user_verification=True` into the webauthn library's verify call.
Expand Down
Loading