feat: native app links for passkeys (#12580)

# Which Problems Are Solved

- Native iOS/Android passkeys need OS trust files on the ZITADEL domain;
without them, Associated Domains / App Links verification fails.
- Operators could not configure iOS Team ID + Bundle ID or Android
package name + SHA-256 fingerprints on OIDC apps.
- `/.well-known/apple-app-site-association` and
`/.well-known/assetlinks.json` were not served from application config.

# How the Problems Are Solved

- Add iOS/Android app-link fields on OIDC app create/update (Application
API v2 + Management), with validation.
- Persist and project those fields; query active app-link configs
instance-wide.
- Serve AASA (`webcredentials`) and Digital Asset Links
(`get_login_creds`) from well-known paths, with configurable
`Cache-Control` and fingerprint normalization at serve time.
- Console UI to edit the fields, with links to the well-known endpoints.
- Operator docs for configuration, endpoints, caching, and verification.

# Additional Changes

- Document on API fields that well-known responses may be HTTP-cached
and platform verifiers may delay propagation.
- Runtime config: `WellKnown.AppLinksCacheControlMaxAge` (default `5m`;
`0` → `no-store`).

# Additional Context

- Closes #12497
- Implemented and reviewed as stack:
  - #12531 API contract
  - #12532 storage wiring
  - #12536 well-known endpoints
  - #12537 console
  - #12547 docs

---------

Co-authored-by: Cursor <[email protected]>
This commit is contained in:
Livio Spring
2026-08-12 08:04:27 +02:00
committed by GitHub
co-authored by Cursor
parent 2bd42e8fc4
commit 318bfc36d7
68 changed files with 3227 additions and 467 deletions
@@ -15,6 +15,13 @@ This would give us `${CUSTOM_DOMAIN}/.well-known/openid-configuration`.
**Link to spec.** [OpenID Connect Discovery 1.0 incorporating errata set 1](https://openid.net/specs/openid-connect-discovery-1_0.html)
ZITADEL also serves instance-scoped association files for native passkeys (not part of OIDC discovery):
- `${CUSTOM_DOMAIN}/.well-known/apple-app-site-association`
- `${CUSTOM_DOMAIN}/.well-known/assetlinks.json`
See [Native app links for passkeys](/guides/integrate/login/oidc/native-app-links).
## authorization_endpoint
`${CUSTOM_DOMAIN}/oauth/v2/authorize`
@@ -35,5 +35,6 @@ Imagine signing in without passwords! Passkeys, replacing traditional passwords,
* Passkey support is still evolving in browsers and platforms. Check compatibility for your target audience.
* ZITADEL actively develops its passkey features. Stay updated with documentation and releases.
* Passkeys are bound to your domain, thus we recommend configuring a [Custom Domain](/concepts/features/custom-domain) before setting up passkeys.
* Native iOS and Android apps that use platform passkey APIs also need domain association files. Configure them on the OIDC app and let ZITADEL serve the well-known documents — see [Native app links for passkeys](/guides/integrate/login/oidc/native-app-links).
Don't hesitate to ask if you have further questions about integrating passkeys in your ZITADEL application!
@@ -12,6 +12,11 @@ Passkeys are a replacement for passwords that provide faster, easier, and more s
Unlike passwords, passkeys are phishing-resistant and can improve the user experience and security at the same time.
Passkeys and there underlying protocols are a standard defined by the [FIDO Standard](https://fidoalliance.org/) .
<Callout type="info">
This guide covers passkeys in a **custom browser login UI**.
Native iOS/Android apps that use platform passkey APIs also need domain association files served from your issuer — see [Native app links for passkeys](/guides/integrate/login/oidc/native-app-links).
</Callout>
## Register Passkey
### Flow
@@ -21,5 +21,6 @@ import { FileText, Folder, Link as LinkIcon } from 'lucide-react';
<Card title="Grant Types" href="/apis/openidoauth/grant-types" icon={<FileText />} />
<Card title="Authrequest" href="/apis/openidoauth/authrequest" icon={<FileText />} />
<Card title="Webkeys" href="/guides/integrate/login/oidc/webkeys" icon={<FileText />} />
<Card title="Native App Links" href="/guides/integrate/login/oidc/native-app-links" icon={<FileText />} />
<Card title="Token Exchange" href="/guides/integrate/token-exchange" icon={<FileText />} />
</Cards>
@@ -0,0 +1,135 @@
---
title: Native app links for passkeys
description: "Configure iOS and Android association on OIDC apps so ZITADEL can serve apple-app-site-association and assetlinks.json for native passkeys"
sidebar_label: Native App Links
---
Native mobile apps that create or use passkeys with platform WebAuthn APIs need the issuer domain to publish OS trust association files.
ZITADEL serves those files from your instance based on iOS and Android settings on OIDC applications.
## Prerequisites
- An OIDC **Native** application in your project
- Passkeys enabled in your login policy
- A stable issuer / [custom domain](/concepts/features/custom-domain) (passkeys are domain-bound)
- Platform association configured in the mobile app project (Associated Domains on iOS; Digital Asset Links / App Links on Android)
## Configure the OIDC application
Set association fields on each Native OIDC app that should participate in passkey trust.
You can do this in the [Console](/guides/manage/console/applications-overview#native-app-links) under the app's **Native App Links** section, or via the Application API / Management API OIDC app create and update endpoints (`ios` / `android`).
### iOS
| Field | Description |
|-------|-------------|
| **Team ID** | Apple Developer Team ID (exactly 10 alphanumeric characters), for example `ABCDE12345` |
| **Bundle ID** | App bundle identifier (`CFBundleIdentifier`), for example `com.example.app` — do not include the Team ID prefix |
ZITADEL publishes these as a single App ID in the association file: `{team_id}.{bundle_id}`.
### Android
| Field | Description |
|-------|-------------|
| **Package name** | Android `applicationId` / package name from the app manifest |
| **SHA-256 certificate fingerprints** | Signing certificate fingerprints (64 hex characters, optionally colon-separated). Include debug and release fingerprints as needed. |
### Update semantics
On update:
- Omit a field group to leave it unchanged
- Send an empty message / empty values to clear
- Send populated values to replace
## Well-known endpoints
ZITADEL aggregates association data from **active** OIDC apps on the instance and serves:
| Path | Purpose |
|------|---------|
| `${CUSTOM_DOMAIN}/.well-known/apple-app-site-association` | iOS Associated Domains / `webcredentials` |
| `${CUSTOM_DOMAIN}/.well-known/assetlinks.json` | Android Digital Asset Links for `delegate_permission/common.get_login_creds` |
If no apps are configured, both endpoints still return HTTP `200` with empty collections.
### Example: apple-app-site-association
```json
{
"webcredentials": {
"apps": [
"ABCDE12345.com.example.one",
"FGHIJ67890.com.example.two"
]
}
}
```
Duplicate iOS App IDs across apps are deduplicated.
### Example: assetlinks.json
```json
[
{
"relation": ["delegate_permission/common.get_login_creds"],
"target": {
"namespace": "android_app",
"package_name": "com.example.one",
"sha256_cert_fingerprints": [
"AA:BB:CC:DD:EE:FF:00:11:22:33:44:55:66:77:88:99:AA:BB:CC:DD:EE:FF:00:11:22:33:44:55:66:77:88:99"
]
}
}
]
```
Fingerprints are normalized to colon-separated uppercase hex when served.
## Caching
Successful responses include a `Cache-Control` header so intermediate caches may reuse the documents for a short time.
By default (and in ZITADEL Cloud) caching is allowed for 5 minutes:
```
Cache-Control: public, max-age=300
```
Self-hosters can change this with the `ZITADEL_WELLKNOWN_APPLINKSCACHECONTROLMAXAGE` environment variable or in the settings YAML:
```yaml
WellKnown:
AppLinksCacheControlMaxAge: 5m
```
Setting the value to `0` results in `Cache-Control: no-store`.
<Callout type="info">
Platform verifiers may cache association files on their side as well.
After you change app link settings, allow time for HTTP caches and OS verification to refresh before expecting passkeys to work with the new values.
</Callout>
## Verify
```bash
curl -i "${CUSTOM_DOMAIN}/.well-known/apple-app-site-association"
curl -i "${CUSTOM_DOMAIN}/.well-known/assetlinks.json"
```
Confirm:
- HTTP status `200`
- `Content-Type: application/json`
- Expected `Cache-Control` header
- JSON contents match your configured apps
## Related
- [Passkeys concept](/concepts/features/passkeys)
- [Console applications](/guides/manage/console/applications-overview)
- [Custom domain](/concepts/features/custom-domain)
- [Passkeys in a custom login UI](/guides/integrate/login-ui/passkey) (browser / WebAuthn APIs)
@@ -63,6 +63,15 @@ Native Applications are installed on a thin client, such as a smartphone or desk
<AuthType components={props.components} appType="native" />
#### Native app links
For native passkeys, configure OS association on the OIDC app under **Native App Links**:
* **iOS:** Team ID and Bundle ID (published in `/.well-known/apple-app-site-association`)
* **Android:** Package name and SHA-256 certificate fingerprints (published in `/.well-known/assetlinks.json`)
See [Native app links for passkeys](/guides/integrate/login/oidc/native-app-links) for endpoint details, caching, and verification.
### User Agent
User Agent Applications are executed entirely in the web browser (Client-Side).
@@ -219,3 +228,4 @@ Ensure the management of application settings is limited to authorized users onl
## References
- [Recommended OIDC Flows](../../integrate/login/oidc/oauth-recommended-flows)
- [Native app links for passkeys](../../integrate/login/oidc/native-app-links)
+1
View File
@@ -269,6 +269,7 @@ export const guidesSidebar: readonly SidebarItem[] = [
"apis/openidoauth/grant-types",
"apis/openidoauth/authrequest",
"guides/integrate/login/oidc/webkeys",
"guides/integrate/login/oidc/native-app-links",
"guides/integrate/token-exchange",
],
},