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
+44 -19
View File
@@ -20,24 +20,28 @@ import (
type addOIDCApp struct {
AddApp
Version domain.OIDCVersion
RedirectUris []string
ResponseTypes []domain.OIDCResponseType
GrantTypes []domain.OIDCGrantType
ApplicationType domain.OIDCApplicationType
AuthMethodType domain.OIDCAuthMethodType
PostLogoutRedirectUris []string
DevMode bool
AccessTokenType domain.OIDCTokenType
AccessTokenRoleAssertion bool
IDTokenRoleAssertion bool
IDTokenUserinfoAssertion bool
ClockSkew time.Duration
AdditionalOrigins []string
SkipSuccessPageForNativeApp bool
BackChannelLogoutURI string
LoginVersion domain.LoginVersion
LoginBaseURI string
Version domain.OIDCVersion
RedirectUris []string
ResponseTypes []domain.OIDCResponseType
GrantTypes []domain.OIDCGrantType
ApplicationType domain.OIDCApplicationType
AuthMethodType domain.OIDCAuthMethodType
PostLogoutRedirectUris []string
DevMode bool
AccessTokenType domain.OIDCTokenType
AccessTokenRoleAssertion bool
IDTokenRoleAssertion bool
IDTokenUserinfoAssertion bool
ClockSkew time.Duration
AdditionalOrigins []string
SkipSuccessPageForNativeApp bool
BackChannelLogoutURI string
LoginVersion domain.LoginVersion
LoginBaseURI string
IOSTeamID string
IOSBundleID string
AndroidPackageName string
AndroidSHA256CertFingerprints []string
ClientID string
ClientSecret string
@@ -118,6 +122,10 @@ func (c *Commands) AddOIDCAppCommand(app *addOIDCApp) preparation.Validation {
app.BackChannelLogoutURI,
app.LoginVersion,
app.LoginBaseURI,
app.IOSTeamID,
app.IOSBundleID,
app.AndroidPackageName,
app.AndroidSHA256CertFingerprints,
),
}, nil
}, nil
@@ -251,6 +259,10 @@ func (c *Commands) pushOIDCApplication(ctx context.Context, addedApplication *OI
backchannelLogoutURI,
gu.Value(oidcApp.LoginVersion),
strings.TrimSpace(gu.Value(oidcApp.LoginBaseURI)),
strings.TrimSpace(gu.Value(oidcApp.IOSTeamID)),
strings.TrimSpace(gu.Value(oidcApp.IOSBundleID)),
strings.TrimSpace(gu.Value(oidcApp.AndroidPackageName)),
trimStringSliceWhiteSpaces(oidcApp.AndroidSHA256CertFingerprints),
))
events = append(events, extraEvents...)
@@ -340,7 +352,7 @@ func (c *Commands) UpdateOIDCApplication(ctx context.Context, oidc *domain.OIDCA
// UpdateDynamicOIDCClient). It reports whether anything actually changed.
func (c *Commands) oidcApplicationChangeEvent(ctx context.Context, existingOIDC *OIDCApplicationWriteModel, oidc *domain.OIDCApp) (*project_repo.OIDCConfigChangedEvent, bool, error) {
projectAgg := ProjectAggregateFromWriteModelWithCTX(ctx, &existingOIDC.WriteModel)
var backChannelLogout, loginBaseURI *string
var backChannelLogout, loginBaseURI, iosTeamID, iosBundleID, androidPackageName *string
if oidc.BackChannelLogoutURI != nil {
bcl, err := c.validateBackchannelLogoutURI(oidc)
if err != nil {
@@ -352,6 +364,15 @@ func (c *Commands) oidcApplicationChangeEvent(ctx context.Context, existingOIDC
if oidc.LoginBaseURI != nil {
loginBaseURI = gu.Ptr(strings.TrimSpace(*oidc.LoginBaseURI))
}
if oidc.IOSTeamID != nil {
iosTeamID = gu.Ptr(strings.TrimSpace(*oidc.IOSTeamID))
}
if oidc.IOSBundleID != nil {
iosBundleID = gu.Ptr(strings.TrimSpace(*oidc.IOSBundleID))
}
if oidc.AndroidPackageName != nil {
androidPackageName = gu.Ptr(strings.TrimSpace(*oidc.AndroidPackageName))
}
return existingOIDC.NewChangedEvent(
ctx,
@@ -375,6 +396,10 @@ func (c *Commands) oidcApplicationChangeEvent(ctx context.Context, existingOIDC
backChannelLogout,
oidc.LoginVersion,
loginBaseURI,
iosTeamID,
iosBundleID,
androidPackageName,
trimStringSliceWhiteSpaces(oidc.AndroidSHA256CertFingerprints),
)
}