mirror of
https://github.com/andwati/zitadel.git
synced 2026-11-09 23:55:12 +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:
@@ -38,30 +38,34 @@ const (
|
||||
AppAPIConfigColumnClientSecret = "client_secret"
|
||||
AppAPIConfigColumnAuthMethod = "auth_method"
|
||||
|
||||
appOIDCTableSuffix = "oidc_configs"
|
||||
AppOIDCConfigColumnAppID = "app_id"
|
||||
AppOIDCConfigColumnInstanceID = "instance_id"
|
||||
AppOIDCConfigColumnVersion = "version"
|
||||
AppOIDCConfigColumnClientID = "client_id"
|
||||
AppOIDCConfigColumnClientSecret = "client_secret"
|
||||
AppOIDCConfigColumnRedirectUris = "redirect_uris"
|
||||
AppOIDCConfigColumnResponseTypes = "response_types"
|
||||
AppOIDCConfigColumnGrantTypes = "grant_types"
|
||||
AppOIDCConfigColumnApplicationType = "application_type"
|
||||
AppOIDCConfigColumnAuthMethodType = "auth_method_type"
|
||||
AppOIDCConfigColumnPostLogoutRedirectUris = "post_logout_redirect_uris"
|
||||
AppOIDCConfigColumnDevMode = "is_dev_mode"
|
||||
AppOIDCConfigColumnAccessTokenType = "access_token_type"
|
||||
AppOIDCConfigColumnAccessTokenRoleAssertion = "access_token_role_assertion"
|
||||
AppOIDCConfigColumnIDTokenRoleAssertion = "id_token_role_assertion"
|
||||
AppOIDCConfigColumnIDTokenUserinfoAssertion = "id_token_userinfo_assertion"
|
||||
AppOIDCConfigColumnClockSkew = "clock_skew"
|
||||
AppOIDCConfigColumnAdditionalOrigins = "additional_origins"
|
||||
AppOIDCConfigColumnSkipNativeAppSuccessPage = "skip_native_app_success_page"
|
||||
AppOIDCConfigColumnBackChannelLogoutURI = "back_channel_logout_uri"
|
||||
AppOIDCConfigColumnLoginVersion = "login_version"
|
||||
AppOIDCConfigColumnLoginBaseURI = "login_base_uri"
|
||||
AppOIDCConfigColumnRegistrationToken = "registration_token"
|
||||
appOIDCTableSuffix = "oidc_configs"
|
||||
AppOIDCConfigColumnAppID = "app_id"
|
||||
AppOIDCConfigColumnInstanceID = "instance_id"
|
||||
AppOIDCConfigColumnVersion = "version"
|
||||
AppOIDCConfigColumnClientID = "client_id"
|
||||
AppOIDCConfigColumnClientSecret = "client_secret"
|
||||
AppOIDCConfigColumnRedirectUris = "redirect_uris"
|
||||
AppOIDCConfigColumnResponseTypes = "response_types"
|
||||
AppOIDCConfigColumnGrantTypes = "grant_types"
|
||||
AppOIDCConfigColumnApplicationType = "application_type"
|
||||
AppOIDCConfigColumnAuthMethodType = "auth_method_type"
|
||||
AppOIDCConfigColumnPostLogoutRedirectUris = "post_logout_redirect_uris"
|
||||
AppOIDCConfigColumnDevMode = "is_dev_mode"
|
||||
AppOIDCConfigColumnAccessTokenType = "access_token_type"
|
||||
AppOIDCConfigColumnAccessTokenRoleAssertion = "access_token_role_assertion"
|
||||
AppOIDCConfigColumnIDTokenRoleAssertion = "id_token_role_assertion"
|
||||
AppOIDCConfigColumnIDTokenUserinfoAssertion = "id_token_userinfo_assertion"
|
||||
AppOIDCConfigColumnClockSkew = "clock_skew"
|
||||
AppOIDCConfigColumnAdditionalOrigins = "additional_origins"
|
||||
AppOIDCConfigColumnSkipNativeAppSuccessPage = "skip_native_app_success_page"
|
||||
AppOIDCConfigColumnBackChannelLogoutURI = "back_channel_logout_uri"
|
||||
AppOIDCConfigColumnLoginVersion = "login_version"
|
||||
AppOIDCConfigColumnLoginBaseURI = "login_base_uri"
|
||||
AppOIDCConfigColumnIOSTeamID = "ios_team_id"
|
||||
AppOIDCConfigColumnIOSBundleID = "ios_bundle_id"
|
||||
AppOIDCConfigColumnAndroidPackageName = "android_package_name"
|
||||
AppOIDCConfigColumnAndroidSHA256CertFingerprints = "android_sha256_cert_fingerprints"
|
||||
AppOIDCConfigColumnRegistrationToken = "registration_token"
|
||||
|
||||
appSAMLTableSuffix = "saml_configs"
|
||||
AppSAMLConfigColumnAppID = "app_id"
|
||||
@@ -134,6 +138,10 @@ func (*appProjection) Init() *old_handler.Check {
|
||||
handler.NewColumn(AppOIDCConfigColumnBackChannelLogoutURI, handler.ColumnTypeText, handler.Nullable()),
|
||||
handler.NewColumn(AppOIDCConfigColumnLoginVersion, handler.ColumnTypeEnum, handler.Nullable()),
|
||||
handler.NewColumn(AppOIDCConfigColumnLoginBaseURI, handler.ColumnTypeText, handler.Nullable()),
|
||||
handler.NewColumn(AppOIDCConfigColumnIOSTeamID, handler.ColumnTypeText, handler.Nullable()),
|
||||
handler.NewColumn(AppOIDCConfigColumnIOSBundleID, handler.ColumnTypeText, handler.Nullable()),
|
||||
handler.NewColumn(AppOIDCConfigColumnAndroidPackageName, handler.ColumnTypeText, handler.Nullable()),
|
||||
handler.NewColumn(AppOIDCConfigColumnAndroidSHA256CertFingerprints, handler.ColumnTypeTextArray, handler.Nullable()),
|
||||
handler.NewColumn(AppOIDCConfigColumnRegistrationToken, handler.ColumnTypeText, handler.Nullable()),
|
||||
},
|
||||
handler.NewPrimaryKey(AppOIDCConfigColumnInstanceID, AppOIDCConfigColumnAppID),
|
||||
@@ -519,6 +527,10 @@ func (p *appProjection) reduceOIDCConfigAdded(event eventstore.Event) (*handler.
|
||||
handler.NewCol(AppOIDCConfigColumnBackChannelLogoutURI, e.BackChannelLogoutURI),
|
||||
handler.NewCol(AppOIDCConfigColumnLoginVersion, e.LoginVersion),
|
||||
handler.NewCol(AppOIDCConfigColumnLoginBaseURI, e.LoginBaseURI),
|
||||
handler.NewCol(AppOIDCConfigColumnIOSTeamID, e.IOSTeamID),
|
||||
handler.NewCol(AppOIDCConfigColumnIOSBundleID, e.IOSBundleID),
|
||||
handler.NewCol(AppOIDCConfigColumnAndroidPackageName, e.AndroidPackageName),
|
||||
handler.NewCol(AppOIDCConfigColumnAndroidSHA256CertFingerprints, database.TextArray[string](e.AndroidSHA256CertFingerprints)),
|
||||
},
|
||||
handler.WithTableSuffix(appOIDCTableSuffix),
|
||||
),
|
||||
@@ -596,6 +608,18 @@ func (p *appProjection) reduceOIDCConfigChanged(event eventstore.Event) (*handle
|
||||
if e.LoginBaseURI != nil {
|
||||
cols = append(cols, handler.NewCol(AppOIDCConfigColumnLoginBaseURI, *e.LoginBaseURI))
|
||||
}
|
||||
if e.IOSTeamID != nil {
|
||||
cols = append(cols, handler.NewCol(AppOIDCConfigColumnIOSTeamID, *e.IOSTeamID))
|
||||
}
|
||||
if e.IOSBundleID != nil {
|
||||
cols = append(cols, handler.NewCol(AppOIDCConfigColumnIOSBundleID, *e.IOSBundleID))
|
||||
}
|
||||
if e.AndroidPackageName != nil {
|
||||
cols = append(cols, handler.NewCol(AppOIDCConfigColumnAndroidPackageName, *e.AndroidPackageName))
|
||||
}
|
||||
if e.AndroidSHA256CertFingerprints != nil {
|
||||
cols = append(cols, handler.NewCol(AppOIDCConfigColumnAndroidSHA256CertFingerprints, database.TextArray[string](*e.AndroidSHA256CertFingerprints)))
|
||||
}
|
||||
|
||||
if len(cols) == 0 {
|
||||
return handler.NewNoOpStatement(e), nil
|
||||
|
||||
@@ -572,7 +572,7 @@ func TestAppProjection_reduces(t *testing.T) {
|
||||
executer: &testExecuter{
|
||||
executions: []execution{
|
||||
{
|
||||
expectedStmt: "INSERT INTO projections.apps7_oidc_configs (app_id, instance_id, version, client_id, client_secret, redirect_uris, response_types, grant_types, application_type, auth_method_type, post_logout_redirect_uris, is_dev_mode, access_token_type, access_token_role_assertion, id_token_role_assertion, id_token_userinfo_assertion, clock_skew, additional_origins, skip_native_app_success_page, back_channel_logout_uri, login_version, login_base_uri) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22)",
|
||||
expectedStmt: "INSERT INTO projections.apps7_oidc_configs (app_id, instance_id, version, client_id, client_secret, redirect_uris, response_types, grant_types, application_type, auth_method_type, post_logout_redirect_uris, is_dev_mode, access_token_type, access_token_role_assertion, id_token_role_assertion, id_token_userinfo_assertion, clock_skew, additional_origins, skip_native_app_success_page, back_channel_logout_uri, login_version, login_base_uri, ios_team_id, ios_bundle_id, android_package_name, android_sha256_cert_fingerprints) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26)",
|
||||
expectedArgs: []interface{}{
|
||||
"app-id",
|
||||
"instance-id",
|
||||
@@ -596,6 +596,10 @@ func TestAppProjection_reduces(t *testing.T) {
|
||||
"back.channel.one.ch",
|
||||
domain.LoginVersion2,
|
||||
"https://login.ch/",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
database.TextArray[string](nil),
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -650,7 +654,7 @@ func TestAppProjection_reduces(t *testing.T) {
|
||||
executer: &testExecuter{
|
||||
executions: []execution{
|
||||
{
|
||||
expectedStmt: "INSERT INTO projections.apps7_oidc_configs (app_id, instance_id, version, client_id, client_secret, redirect_uris, response_types, grant_types, application_type, auth_method_type, post_logout_redirect_uris, is_dev_mode, access_token_type, access_token_role_assertion, id_token_role_assertion, id_token_userinfo_assertion, clock_skew, additional_origins, skip_native_app_success_page, back_channel_logout_uri, login_version, login_base_uri) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22)",
|
||||
expectedStmt: "INSERT INTO projections.apps7_oidc_configs (app_id, instance_id, version, client_id, client_secret, redirect_uris, response_types, grant_types, application_type, auth_method_type, post_logout_redirect_uris, is_dev_mode, access_token_type, access_token_role_assertion, id_token_role_assertion, id_token_userinfo_assertion, clock_skew, additional_origins, skip_native_app_success_page, back_channel_logout_uri, login_version, login_base_uri, ios_team_id, ios_bundle_id, android_package_name, android_sha256_cert_fingerprints) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26)",
|
||||
expectedArgs: []interface{}{
|
||||
"app-id",
|
||||
"instance-id",
|
||||
@@ -674,6 +678,10 @@ func TestAppProjection_reduces(t *testing.T) {
|
||||
"back.channel.one.ch",
|
||||
domain.LoginVersion2,
|
||||
"https://login.ch/",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
database.TextArray[string](nil),
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -761,6 +769,52 @@ func TestAppProjection_reduces(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "project reduceOIDCConfigChanged app links",
|
||||
args: args{
|
||||
event: getEvent(
|
||||
testEvent(
|
||||
project.OIDCConfigChangedType,
|
||||
project.AggregateType,
|
||||
[]byte(`{
|
||||
"appId": "app-id",
|
||||
"iosTeamId": "TEAMID",
|
||||
"iosBundleId": "com.example.app",
|
||||
"androidPackageName": "com.example.app",
|
||||
"androidSha256CertFingerprints": ["AA:BB:CC"]
|
||||
}`),
|
||||
), project.OIDCConfigChangedEventMapper),
|
||||
},
|
||||
reduce: (&appProjection{}).reduceOIDCConfigChanged,
|
||||
want: wantReduce{
|
||||
aggregateType: eventstore.AggregateType("project"),
|
||||
sequence: 15,
|
||||
executer: &testExecuter{
|
||||
executions: []execution{
|
||||
{
|
||||
expectedStmt: "UPDATE projections.apps7_oidc_configs SET (ios_team_id, ios_bundle_id, android_package_name, android_sha256_cert_fingerprints) = ($1, $2, $3, $4) WHERE (app_id = $5) AND (instance_id = $6)",
|
||||
expectedArgs: []interface{}{
|
||||
"TEAMID",
|
||||
"com.example.app",
|
||||
"com.example.app",
|
||||
database.TextArray[string]{"AA:BB:CC"},
|
||||
"app-id",
|
||||
"instance-id",
|
||||
},
|
||||
},
|
||||
{
|
||||
expectedStmt: "UPDATE projections.apps7 SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)",
|
||||
expectedArgs: []interface{}{
|
||||
anyArg{},
|
||||
uint64(15),
|
||||
"app-id",
|
||||
"instance-id",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "project reduceOIDCConfigChanged noop",
|
||||
args: args{
|
||||
|
||||
Reference in New Issue
Block a user