mirror of
https://github.com/andwati/zitadel.git
synced 2026-11-13 01:25:11 +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:
+111
-59
@@ -43,27 +43,31 @@ type App struct {
|
||||
}
|
||||
|
||||
type OIDCApp struct {
|
||||
RedirectURIs database.TextArray[string]
|
||||
ResponseTypes database.NumberArray[domain.OIDCResponseType]
|
||||
GrantTypes database.NumberArray[domain.OIDCGrantType]
|
||||
AppType domain.OIDCApplicationType
|
||||
ClientID string
|
||||
AuthMethodType domain.OIDCAuthMethodType
|
||||
PostLogoutRedirectURIs database.TextArray[string]
|
||||
Version domain.OIDCVersion
|
||||
ComplianceProblems database.TextArray[string]
|
||||
IsDevMode bool
|
||||
AccessTokenType domain.OIDCTokenType
|
||||
AssertAccessTokenRole bool
|
||||
AssertIDTokenRole bool
|
||||
AssertIDTokenUserinfo bool
|
||||
ClockSkew time.Duration
|
||||
AdditionalOrigins database.TextArray[string]
|
||||
AllowedOrigins database.TextArray[string]
|
||||
SkipNativeAppSuccessPage bool
|
||||
BackChannelLogoutURI string
|
||||
LoginVersion domain.LoginVersion
|
||||
LoginBaseURI *string
|
||||
RedirectURIs database.TextArray[string]
|
||||
ResponseTypes database.NumberArray[domain.OIDCResponseType]
|
||||
GrantTypes database.NumberArray[domain.OIDCGrantType]
|
||||
AppType domain.OIDCApplicationType
|
||||
ClientID string
|
||||
AuthMethodType domain.OIDCAuthMethodType
|
||||
PostLogoutRedirectURIs database.TextArray[string]
|
||||
Version domain.OIDCVersion
|
||||
ComplianceProblems database.TextArray[string]
|
||||
IsDevMode bool
|
||||
AccessTokenType domain.OIDCTokenType
|
||||
AssertAccessTokenRole bool
|
||||
AssertIDTokenRole bool
|
||||
AssertIDTokenUserinfo bool
|
||||
ClockSkew time.Duration
|
||||
AdditionalOrigins database.TextArray[string]
|
||||
AllowedOrigins database.TextArray[string]
|
||||
SkipNativeAppSuccessPage bool
|
||||
BackChannelLogoutURI string
|
||||
LoginVersion domain.LoginVersion
|
||||
LoginBaseURI *string
|
||||
IOSTeamID string
|
||||
IOSBundleID string
|
||||
AndroidPackageName string
|
||||
AndroidSHA256CertFingerprints database.TextArray[string]
|
||||
}
|
||||
|
||||
type SAMLApp struct {
|
||||
@@ -278,6 +282,22 @@ var (
|
||||
name: projection.AppOIDCConfigColumnLoginBaseURI,
|
||||
table: appOIDCConfigsTable,
|
||||
}
|
||||
AppOIDCConfigColumnIOSTeamID = Column{
|
||||
name: projection.AppOIDCConfigColumnIOSTeamID,
|
||||
table: appOIDCConfigsTable,
|
||||
}
|
||||
AppOIDCConfigColumnIOSBundleID = Column{
|
||||
name: projection.AppOIDCConfigColumnIOSBundleID,
|
||||
table: appOIDCConfigsTable,
|
||||
}
|
||||
AppOIDCConfigColumnAndroidPackageName = Column{
|
||||
name: projection.AppOIDCConfigColumnAndroidPackageName,
|
||||
table: appOIDCConfigsTable,
|
||||
}
|
||||
AppOIDCConfigColumnAndroidSHA256CertFingerprints = Column{
|
||||
name: projection.AppOIDCConfigColumnAndroidSHA256CertFingerprints,
|
||||
table: appOIDCConfigsTable,
|
||||
}
|
||||
)
|
||||
|
||||
func (q *Queries) AppByProjectAndAppID(ctx context.Context, shouldTriggerBulk bool, projectID, appID string) (app *App, err error) {
|
||||
@@ -725,6 +745,10 @@ func prepareAppQuery(activeOnly bool) (sq.SelectBuilder, func(*sql.Row) (*App, e
|
||||
AppOIDCConfigColumnBackChannelLogoutURI.identifier(),
|
||||
AppOIDCConfigColumnLoginVersion.identifier(),
|
||||
AppOIDCConfigColumnLoginBaseURI.identifier(),
|
||||
AppOIDCConfigColumnIOSTeamID.identifier(),
|
||||
AppOIDCConfigColumnIOSBundleID.identifier(),
|
||||
AppOIDCConfigColumnAndroidPackageName.identifier(),
|
||||
AppOIDCConfigColumnAndroidSHA256CertFingerprints.identifier(),
|
||||
|
||||
AppSAMLConfigColumnAppID.identifier(),
|
||||
AppSAMLConfigColumnEntityID.identifier(),
|
||||
@@ -794,6 +818,10 @@ func scanApp(row *sql.Row) (*App, error) {
|
||||
&oidcConfig.backChannelLogoutURI,
|
||||
&oidcConfig.loginVersion,
|
||||
&oidcConfig.loginBaseURI,
|
||||
&oidcConfig.iosTeamID,
|
||||
&oidcConfig.iosBundleID,
|
||||
&oidcConfig.androidPackageName,
|
||||
&oidcConfig.androidSHA256CertFingerprints,
|
||||
|
||||
&samlConfig.appID,
|
||||
&samlConfig.entityID,
|
||||
@@ -848,6 +876,10 @@ func prepareOIDCAppQuery() (sq.SelectBuilder, func(*sql.Row) (*App, error)) {
|
||||
AppOIDCConfigColumnBackChannelLogoutURI.identifier(),
|
||||
AppOIDCConfigColumnLoginVersion.identifier(),
|
||||
AppOIDCConfigColumnLoginBaseURI.identifier(),
|
||||
AppOIDCConfigColumnIOSTeamID.identifier(),
|
||||
AppOIDCConfigColumnIOSBundleID.identifier(),
|
||||
AppOIDCConfigColumnAndroidPackageName.identifier(),
|
||||
AppOIDCConfigColumnAndroidSHA256CertFingerprints.identifier(),
|
||||
).From(appsTable.identifier()).
|
||||
Join(join(AppOIDCConfigColumnAppID, AppColumnID)).
|
||||
PlaceholderFormat(sq.Dollar), func(row *sql.Row) (*App, error) {
|
||||
@@ -887,6 +919,10 @@ func prepareOIDCAppQuery() (sq.SelectBuilder, func(*sql.Row) (*App, error)) {
|
||||
&oidcConfig.backChannelLogoutURI,
|
||||
&oidcConfig.loginVersion,
|
||||
&oidcConfig.loginBaseURI,
|
||||
&oidcConfig.iosTeamID,
|
||||
&oidcConfig.iosBundleID,
|
||||
&oidcConfig.androidPackageName,
|
||||
&oidcConfig.androidSHA256CertFingerprints,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
@@ -1004,6 +1040,10 @@ func prepareAppsQuery() (sq.SelectBuilder, func(*sql.Rows) (*Apps, error)) {
|
||||
AppOIDCConfigColumnBackChannelLogoutURI.identifier(),
|
||||
AppOIDCConfigColumnLoginVersion.identifier(),
|
||||
AppOIDCConfigColumnLoginBaseURI.identifier(),
|
||||
AppOIDCConfigColumnIOSTeamID.identifier(),
|
||||
AppOIDCConfigColumnIOSBundleID.identifier(),
|
||||
AppOIDCConfigColumnAndroidPackageName.identifier(),
|
||||
AppOIDCConfigColumnAndroidSHA256CertFingerprints.identifier(),
|
||||
|
||||
AppSAMLConfigColumnAppID.identifier(),
|
||||
AppSAMLConfigColumnEntityID.identifier(),
|
||||
@@ -1061,6 +1101,10 @@ func prepareAppsQuery() (sq.SelectBuilder, func(*sql.Rows) (*Apps, error)) {
|
||||
&oidcConfig.backChannelLogoutURI,
|
||||
&oidcConfig.loginVersion,
|
||||
&oidcConfig.loginBaseURI,
|
||||
&oidcConfig.iosTeamID,
|
||||
&oidcConfig.iosBundleID,
|
||||
&oidcConfig.androidPackageName,
|
||||
&oidcConfig.androidSHA256CertFingerprints,
|
||||
|
||||
&samlConfig.appID,
|
||||
&samlConfig.entityID,
|
||||
@@ -1148,26 +1192,30 @@ func prepareLoginVersionBySAMLAppID() (sq.SelectBuilder, func(*sql.Row) (domain.
|
||||
}
|
||||
|
||||
type sqlOIDCConfig struct {
|
||||
appID sql.NullString
|
||||
version sql.NullInt32
|
||||
clientID sql.NullString
|
||||
redirectUris database.TextArray[string]
|
||||
applicationType sql.NullInt16
|
||||
authMethodType sql.NullInt16
|
||||
postLogoutRedirectUris database.TextArray[string]
|
||||
devMode sql.NullBool
|
||||
accessTokenType sql.NullInt16
|
||||
accessTokenRoleAssertion sql.NullBool
|
||||
iDTokenRoleAssertion sql.NullBool
|
||||
iDTokenUserinfoAssertion sql.NullBool
|
||||
clockSkew sql.NullInt64
|
||||
additionalOrigins database.TextArray[string]
|
||||
responseTypes database.NumberArray[domain.OIDCResponseType]
|
||||
grantTypes database.NumberArray[domain.OIDCGrantType]
|
||||
skipNativeAppSuccessPage sql.NullBool
|
||||
backChannelLogoutURI sql.NullString
|
||||
loginVersion sql.NullInt16
|
||||
loginBaseURI sql.NullString
|
||||
appID sql.NullString
|
||||
version sql.NullInt32
|
||||
clientID sql.NullString
|
||||
redirectUris database.TextArray[string]
|
||||
applicationType sql.NullInt16
|
||||
authMethodType sql.NullInt16
|
||||
postLogoutRedirectUris database.TextArray[string]
|
||||
devMode sql.NullBool
|
||||
accessTokenType sql.NullInt16
|
||||
accessTokenRoleAssertion sql.NullBool
|
||||
iDTokenRoleAssertion sql.NullBool
|
||||
iDTokenUserinfoAssertion sql.NullBool
|
||||
clockSkew sql.NullInt64
|
||||
additionalOrigins database.TextArray[string]
|
||||
responseTypes database.NumberArray[domain.OIDCResponseType]
|
||||
grantTypes database.NumberArray[domain.OIDCGrantType]
|
||||
skipNativeAppSuccessPage sql.NullBool
|
||||
backChannelLogoutURI sql.NullString
|
||||
loginVersion sql.NullInt16
|
||||
loginBaseURI sql.NullString
|
||||
iosTeamID sql.NullString
|
||||
iosBundleID sql.NullString
|
||||
androidPackageName sql.NullString
|
||||
androidSHA256CertFingerprints database.TextArray[string]
|
||||
}
|
||||
|
||||
func (c sqlOIDCConfig) set(app *App) {
|
||||
@@ -1175,24 +1223,28 @@ func (c sqlOIDCConfig) set(app *App) {
|
||||
return
|
||||
}
|
||||
app.OIDCConfig = &OIDCApp{
|
||||
Version: domain.OIDCVersion(c.version.Int32),
|
||||
ClientID: c.clientID.String,
|
||||
RedirectURIs: c.redirectUris,
|
||||
AppType: domain.OIDCApplicationType(c.applicationType.Int16),
|
||||
AuthMethodType: domain.OIDCAuthMethodType(c.authMethodType.Int16),
|
||||
PostLogoutRedirectURIs: c.postLogoutRedirectUris,
|
||||
IsDevMode: c.devMode.Bool,
|
||||
AccessTokenType: domain.OIDCTokenType(c.accessTokenType.Int16),
|
||||
AssertAccessTokenRole: c.accessTokenRoleAssertion.Bool,
|
||||
AssertIDTokenRole: c.iDTokenRoleAssertion.Bool,
|
||||
AssertIDTokenUserinfo: c.iDTokenUserinfoAssertion.Bool,
|
||||
ClockSkew: time.Duration(c.clockSkew.Int64),
|
||||
AdditionalOrigins: c.additionalOrigins,
|
||||
ResponseTypes: c.responseTypes,
|
||||
GrantTypes: c.grantTypes,
|
||||
SkipNativeAppSuccessPage: c.skipNativeAppSuccessPage.Bool,
|
||||
BackChannelLogoutURI: c.backChannelLogoutURI.String,
|
||||
LoginVersion: domain.LoginVersion(c.loginVersion.Int16),
|
||||
Version: domain.OIDCVersion(c.version.Int32),
|
||||
ClientID: c.clientID.String,
|
||||
RedirectURIs: c.redirectUris,
|
||||
AppType: domain.OIDCApplicationType(c.applicationType.Int16),
|
||||
AuthMethodType: domain.OIDCAuthMethodType(c.authMethodType.Int16),
|
||||
PostLogoutRedirectURIs: c.postLogoutRedirectUris,
|
||||
IsDevMode: c.devMode.Bool,
|
||||
AccessTokenType: domain.OIDCTokenType(c.accessTokenType.Int16),
|
||||
AssertAccessTokenRole: c.accessTokenRoleAssertion.Bool,
|
||||
AssertIDTokenRole: c.iDTokenRoleAssertion.Bool,
|
||||
AssertIDTokenUserinfo: c.iDTokenUserinfoAssertion.Bool,
|
||||
ClockSkew: time.Duration(c.clockSkew.Int64),
|
||||
AdditionalOrigins: c.additionalOrigins,
|
||||
ResponseTypes: c.responseTypes,
|
||||
GrantTypes: c.grantTypes,
|
||||
SkipNativeAppSuccessPage: c.skipNativeAppSuccessPage.Bool,
|
||||
BackChannelLogoutURI: c.backChannelLogoutURI.String,
|
||||
LoginVersion: domain.LoginVersion(c.loginVersion.Int16),
|
||||
IOSTeamID: c.iosTeamID.String,
|
||||
IOSBundleID: c.iosBundleID.String,
|
||||
AndroidPackageName: c.androidPackageName.String,
|
||||
AndroidSHA256CertFingerprints: c.androidSHA256CertFingerprints,
|
||||
}
|
||||
if c.loginBaseURI.Valid {
|
||||
app.OIDCConfig.LoginBaseURI = &c.loginBaseURI.String
|
||||
|
||||
Reference in New Issue
Block a user