mirror of
https://github.com/andwati/zitadel.git
synced 2026-10-18 05:52:51 +00:00
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:
@@ -179,6 +179,57 @@ message OIDCConfig {
|
||||
description: "Specify the preferred login UI, where the user is redirected to for authentication. If unset, the login UI is chosen by the instance default.";
|
||||
}
|
||||
];
|
||||
IOSAppLinkConfig ios = 23 [
|
||||
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||
description: "iOS Associated Domains / passkey trust config. Served in /.well-known/apple-app-site-association as webcredentials.apps entry \"{team_id}.{bundle_id}\" (Apple's full App ID). That response may be HTTP-cached (Cache-Control), and platform verifiers may cache longer; changes can take time to take effect.";
|
||||
}
|
||||
];
|
||||
AndroidAppLinkConfig android = 24 [
|
||||
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||
description: "Android Digital Asset Links / passkey trust config. Served in /.well-known/assetlinks.json for delegate_permission/common.get_login_creds. That response may be HTTP-cached (Cache-Control), and platform verifiers may cache longer; changes can take time to take effect.";
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
message IOSAppLinkConfig {
|
||||
string team_id = 1 [
|
||||
(validate.rules).string = {ignore_empty: true, len: 10, pattern: "^[A-Z0-9]+$"},
|
||||
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||
example: "\"ABCDE12345\"";
|
||||
description: "Apple Team ID (App ID prefix), exactly 10 alphanumeric characters, e.g. ABCDE12345. Combined with bundle_id as TEAMID.BUNDLEID in AASA webcredentials.apps. Not the full Apple App ID by itself. Empty clears the value on update.";
|
||||
}
|
||||
];
|
||||
string bundle_id = 2 [
|
||||
(validate.rules).string = {ignore_empty: true, min_len: 1, max_len: 200},
|
||||
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||
example: "\"com.example.app\"";
|
||||
description: "iOS Bundle ID (CFBundleIdentifier), e.g. com.example.app. Combined with team_id for AASA webcredentials.apps. Do not paste TEAMID.bundle.id here. Empty clears the value on update.";
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
message AndroidAppLinkConfig {
|
||||
string package_name = 1 [
|
||||
(validate.rules).string = {ignore_empty: true, min_len: 1, max_len: 200},
|
||||
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||
example: "\"com.example.app\"";
|
||||
description: "Android package name (applicationId). Used in assetlinks.json target.package_name. Empty clears the value on update.";
|
||||
}
|
||||
];
|
||||
repeated string sha256_cert_fingerprints = 2 [
|
||||
(validate.rules).repeated = {
|
||||
max_items: 20,
|
||||
items: {
|
||||
string: {
|
||||
pattern: "^([0-9A-Fa-f]{2}(:[0-9A-Fa-f]{2}){31}|[0-9A-Fa-f]{64})$";
|
||||
}
|
||||
}
|
||||
},
|
||||
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||
example: "[\"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\"]";
|
||||
description: "SHA-256 signing certificate fingerprints for Digital Asset Links (passkey / get_login_creds). 64 hex chars, optionally colon-separated pairs. At most 20 entries. Include debug and release as needed.";
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
enum OIDCResponseType {
|
||||
|
||||
@@ -341,6 +341,19 @@ message CreateOIDCApplicationRequest {
|
||||
// hosted on any other domain.
|
||||
// If unset, the login UI is chosen by the instance default.
|
||||
LoginVersion login_version = 17;
|
||||
|
||||
// IOS is iOS Associated Domains / passkey trust config.
|
||||
// Served in /.well-known/apple-app-site-association as webcredentials.apps entry
|
||||
// "{team_id}.{bundle_id}" (Apple's full App ID = Team ID prefix + Bundle ID).
|
||||
// That response may be HTTP-cached (Cache-Control), and platform verifiers may cache longer;
|
||||
// changes can take time to take effect.
|
||||
IOSAppLinkConfig ios = 18;
|
||||
|
||||
// Android is Android Digital Asset Links / passkey trust config.
|
||||
// Served in /.well-known/assetlinks.json for delegate_permission/common.get_login_creds.
|
||||
// That response may be HTTP-cached (Cache-Control), and platform verifiers may cache longer;
|
||||
// changes can take time to take effect.
|
||||
AndroidAppLinkConfig android = 19;
|
||||
}
|
||||
|
||||
message CreateOIDCApplicationResponse {
|
||||
@@ -578,6 +591,21 @@ message UpdateOIDCApplicationConfigurationRequest {
|
||||
// hosted on any other domain.
|
||||
// If unset, the login UI is chosen by the instance default.
|
||||
optional LoginVersion login_version = 17;
|
||||
|
||||
// IOS is iOS Associated Domains / passkey trust config.
|
||||
// Served in /.well-known/apple-app-site-association as webcredentials.apps entry
|
||||
// "{team_id}.{bundle_id}" (Apple's full App ID = Team ID prefix + Bundle ID).
|
||||
// That response may be HTTP-cached (Cache-Control), and platform verifiers may cache longer;
|
||||
// changes can take time to take effect.
|
||||
// If not set, the iOS config will not be changed.
|
||||
optional IOSAppLinkConfig ios = 18;
|
||||
|
||||
// Android is Android Digital Asset Links / passkey trust config.
|
||||
// Served in /.well-known/assetlinks.json for delegate_permission/common.get_login_creds.
|
||||
// That response may be HTTP-cached (Cache-Control), and platform verifiers may cache longer;
|
||||
// changes can take time to take effect.
|
||||
// If not set, the Android config will not be changed.
|
||||
optional AndroidAppLinkConfig android = 19;
|
||||
}
|
||||
|
||||
message UpdateAPIApplicationConfigurationRequest {
|
||||
|
||||
@@ -4,6 +4,7 @@ package zitadel.application.v2;
|
||||
|
||||
import "google/protobuf/duration.proto";
|
||||
import "protoc-gen-openapiv2/options/annotations.proto";
|
||||
import "validate/validate.proto";
|
||||
import "zitadel/application/v2/login.proto";
|
||||
|
||||
option go_package = "github.com/zitadel/zitadel/pkg/grpc/application/v2;application";
|
||||
@@ -157,4 +158,55 @@ message OIDCConfiguration {
|
||||
// hosted on any other domain.
|
||||
// If unset, the login UI is chosen by the instance default.
|
||||
LoginVersion login_version = 21;
|
||||
|
||||
// IOS is iOS Associated Domains / passkey trust config.
|
||||
// Served in /.well-known/apple-app-site-association as webcredentials.apps entry
|
||||
// "{team_id}.{bundle_id}" (Apple's full App ID = Team ID prefix + Bundle ID).
|
||||
// That response may be HTTP-cached (Cache-Control), and platform verifiers may cache longer;
|
||||
// changes can take time to take effect.
|
||||
IOSAppLinkConfig ios = 22;
|
||||
|
||||
// Android is Android Digital Asset Links / passkey trust config.
|
||||
// Served in /.well-known/assetlinks.json for delegate_permission/common.get_login_creds.
|
||||
// That response may be HTTP-cached (Cache-Control), and platform verifiers may cache longer;
|
||||
// changes can take time to take effect.
|
||||
AndroidAppLinkConfig android = 23;
|
||||
}
|
||||
|
||||
// IOSAppLinkConfig is iOS Associated Domains / passkey trust config.
|
||||
// Served in /.well-known/apple-app-site-association as webcredentials.apps entry
|
||||
// "{team_id}.{bundle_id}" (Apple's full App ID = Team ID prefix + Bundle ID).
|
||||
// That response may be HTTP-cached (Cache-Control), and platform verifiers may cache longer;
|
||||
// changes can take time to take effect.
|
||||
message IOSAppLinkConfig {
|
||||
// TeamID is the Apple Developer Team ID (exactly 10 alphanumeric characters), e.g. ABCDE12345.
|
||||
// From Apple Developer account membership — not the Bundle ID.
|
||||
// Empty clears the value on update.
|
||||
string team_id = 1 [(validate.rules).string = {ignore_empty: true, len: 10, pattern: "^[A-Z0-9]+$"}];
|
||||
|
||||
// BundleID is the Xcode / CFBundleIdentifier, e.g. com.example.app.
|
||||
// Not the full Apple App ID; do not include the Team ID prefix here.
|
||||
// Empty clears the value on update.
|
||||
string bundle_id = 2 [(validate.rules).string = {ignore_empty: true, min_len: 1, max_len: 200}];
|
||||
}
|
||||
|
||||
// AndroidAppLinkConfig is Android Digital Asset Links / passkey trust config.
|
||||
// Served in /.well-known/assetlinks.json for delegate_permission/common.get_login_creds.
|
||||
// That response may be HTTP-cached (Cache-Control), and platform verifiers may cache longer;
|
||||
// changes can take time to take effect.
|
||||
message AndroidAppLinkConfig {
|
||||
// PackageName is the Android applicationId / package name from the app manifest.
|
||||
// Empty clears the value on update.
|
||||
string package_name = 1 [(validate.rules).string = {ignore_empty: true, min_len: 1, max_len: 200}];
|
||||
|
||||
// SHA256CertFingerprints are signing certificate fingerprints (64 hex chars, optional colons).
|
||||
// Include debug and release fingerprints as needed. At most 20 entries.
|
||||
repeated string sha256_cert_fingerprints = 2 [(validate.rules).repeated = {
|
||||
max_items: 20,
|
||||
items: {
|
||||
string: {
|
||||
pattern: "^([0-9A-Fa-f]{2}(:[0-9A-Fa-f]{2}){31}|[0-9A-Fa-f]{64})$";
|
||||
}
|
||||
}
|
||||
}];
|
||||
}
|
||||
|
||||
@@ -10382,6 +10382,16 @@ message AddOIDCAppRequest {
|
||||
description: "Specify the preferred login UI, where the user is redirected to for authentication. If unset, the login UI is chosen by the instance default.";
|
||||
}
|
||||
];
|
||||
zitadel.app.v1.IOSAppLinkConfig ios = 20 [
|
||||
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||
description: "iOS Associated Domains / passkey trust config. Served in /.well-known/apple-app-site-association as webcredentials.apps entry \"{team_id}.{bundle_id}\" (Apple's full App ID). That response may be HTTP-cached (Cache-Control), and platform verifiers may cache longer; changes can take time to take effect.";
|
||||
}
|
||||
];
|
||||
zitadel.app.v1.AndroidAppLinkConfig android = 21 [
|
||||
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||
description: "Android Digital Asset Links / passkey trust config. Served in /.well-known/assetlinks.json for delegate_permission/common.get_login_creds. That response may be HTTP-cached (Cache-Control), and platform verifiers may cache longer; changes can take time to take effect.";
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
message AddOIDCAppResponse {
|
||||
@@ -10573,6 +10583,16 @@ message UpdateOIDCAppConfigRequest {
|
||||
description: "Specify the preferred login UI, where the user is redirected to for authentication. If unset, the login UI is chosen by the instance default.";
|
||||
}
|
||||
];
|
||||
zitadel.app.v1.IOSAppLinkConfig ios = 19 [
|
||||
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||
description: "iOS Associated Domains / passkey trust config. Served in /.well-known/apple-app-site-association as webcredentials.apps entry \"{team_id}.{bundle_id}\" (Apple's full App ID). That response may be HTTP-cached (Cache-Control), and platform verifiers may cache longer; changes can take time to take effect.";
|
||||
}
|
||||
];
|
||||
zitadel.app.v1.AndroidAppLinkConfig android = 20 [
|
||||
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||
description: "Android Digital Asset Links / passkey trust config. Served in /.well-known/assetlinks.json for delegate_permission/common.get_login_creds. That response may be HTTP-cached (Cache-Control), and platform verifiers may cache longer; changes can take time to take effect.";
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
message UpdateOIDCAppConfigResponse {
|
||||
|
||||
Reference in New Issue
Block a user