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
+111 -59
View File
@@ -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
+100
View File
@@ -0,0 +1,100 @@
package query
import (
"context"
"database/sql"
"strings"
sq "github.com/Masterminds/squirrel"
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/database"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/telemetry/tracing"
"github.com/zitadel/zitadel/internal/zerrors"
)
// OIDCAppLinkConfig holds iOS/Android association fields for one active OIDC app.
type OIDCAppLinkConfig struct {
AppID string
IOSTeamID string
IOSBundleID string
AndroidPackageName string
AndroidSHA256CertFingerprints []string
}
// SearchOIDCAppLinkConfigs returns association fields for active OIDC apps in the
// current instance that have at least one iOS or Android app-link field set.
func (q *Queries) SearchOIDCAppLinkConfigs(ctx context.Context) (configs []*OIDCAppLinkConfig, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
query, scan := prepareOIDCAppLinkConfigsQuery()
iosTeamIDCol := AppOIDCConfigColumnIOSTeamID.identifier()
iosBundleIDCol := AppOIDCConfigColumnIOSBundleID.identifier()
androidPackageCol := AppOIDCConfigColumnAndroidPackageName.identifier()
stmt, args, err := query.Where(sq.And{
sq.Eq{
AppOIDCConfigColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(),
AppColumnState.identifier(): domain.AppStateActive,
},
sq.Or{
sq.Expr("COALESCE(" + iosTeamIDCol + ", '') <> ''"),
sq.Expr("COALESCE(" + iosBundleIDCol + ", '') <> ''"),
sq.Expr("COALESCE(" + androidPackageCol + ", '') <> ''"),
},
}).OrderBy(AppOIDCConfigColumnAppID.identifier()).ToSql()
if err != nil {
return nil, zerrors.ThrowInvalidArgument(err, "QUERY-ApLn1", "Errors.Query.InvalidRequest")
}
err = q.client.QueryContext(ctx, func(rows *sql.Rows) error {
configs, err = scan(rows)
return err
}, stmt, args...)
if err != nil {
return nil, zerrors.ThrowInternal(err, "QUERY-ApLn2", "Errors.Internal")
}
return configs, nil
}
func prepareOIDCAppLinkConfigsQuery() (sq.SelectBuilder, func(*sql.Rows) ([]*OIDCAppLinkConfig, error)) {
return sq.Select(
AppOIDCConfigColumnAppID.identifier(),
AppOIDCConfigColumnIOSTeamID.identifier(),
AppOIDCConfigColumnIOSBundleID.identifier(),
AppOIDCConfigColumnAndroidPackageName.identifier(),
AppOIDCConfigColumnAndroidSHA256CertFingerprints.identifier(),
).From(appOIDCConfigsTable.identifier()).
Join(join(AppColumnID, AppOIDCConfigColumnAppID)).
PlaceholderFormat(sq.Dollar),
func(rows *sql.Rows) ([]*OIDCAppLinkConfig, error) {
configs := make([]*OIDCAppLinkConfig, 0)
for rows.Next() {
var (
appID sql.NullString
iosTeamID sql.NullString
iosBundleID sql.NullString
androidPackage sql.NullString
fingerprints database.TextArray[string]
)
if err := rows.Scan(
&appID,
&iosTeamID,
&iosBundleID,
&androidPackage,
&fingerprints,
); err != nil {
return nil, zerrors.ThrowInternal(err, "QUERY-ApLn3", "Errors.Internal")
}
configs = append(configs, &OIDCAppLinkConfig{
AppID: appID.String,
IOSTeamID: strings.TrimSpace(iosTeamID.String),
IOSBundleID: strings.TrimSpace(iosBundleID.String),
AndroidPackageName: strings.TrimSpace(androidPackage.String),
AndroidSHA256CertFingerprints: fingerprints,
})
}
return configs, nil
}
}
+96
View File
@@ -51,6 +51,10 @@ var (
` projections.apps7_oidc_configs.back_channel_logout_uri,` +
` projections.apps7_oidc_configs.login_version,` +
` projections.apps7_oidc_configs.login_base_uri,` +
` projections.apps7_oidc_configs.ios_team_id,` +
` projections.apps7_oidc_configs.ios_bundle_id,` +
` projections.apps7_oidc_configs.android_package_name,` +
` projections.apps7_oidc_configs.android_sha256_cert_fingerprints,` +
//saml config
` projections.apps7_saml_configs.app_id,` +
` projections.apps7_saml_configs.entity_id,` +
@@ -99,6 +103,10 @@ var (
` projections.apps7_oidc_configs.back_channel_logout_uri,` +
` projections.apps7_oidc_configs.login_version,` +
` projections.apps7_oidc_configs.login_base_uri,` +
` projections.apps7_oidc_configs.ios_team_id,` +
` projections.apps7_oidc_configs.ios_bundle_id,` +
` projections.apps7_oidc_configs.android_package_name,` +
` projections.apps7_oidc_configs.android_sha256_cert_fingerprints,` +
//saml config
` projections.apps7_saml_configs.app_id,` +
` projections.apps7_saml_configs.entity_id,` +
@@ -172,6 +180,10 @@ var (
"back_channel_logout_uri",
"login_version",
"login_base_uri",
"ios_team_id",
"ios_bundle_id",
"android_package_name",
"android_sha256_cert_fingerprints",
//saml config
"app_id",
"entity_id",
@@ -248,6 +260,10 @@ func Test_AppsPrepare(t *testing.T) {
nil,
nil,
nil,
nil,
nil,
nil,
nil,
// saml config
nil,
nil,
@@ -319,6 +335,10 @@ func Test_AppsPrepare(t *testing.T) {
nil,
nil,
nil,
nil,
nil,
nil,
nil,
// saml config
nil,
nil,
@@ -393,6 +413,10 @@ func Test_AppsPrepare(t *testing.T) {
nil,
nil,
nil,
nil,
nil,
nil,
nil,
// saml config
"app-id",
"https://test.com/saml/metadata",
@@ -469,6 +493,10 @@ func Test_AppsPrepare(t *testing.T) {
"back.channel.logout.ch",
domain.LoginVersionUnspecified,
nil,
nil,
nil,
nil,
nil,
// saml config
nil,
nil,
@@ -563,6 +591,10 @@ func Test_AppsPrepare(t *testing.T) {
"back.channel.logout.ch",
domain.LoginVersionUnspecified,
nil,
nil,
nil,
nil,
nil,
// saml config
nil,
nil,
@@ -657,6 +689,10 @@ func Test_AppsPrepare(t *testing.T) {
"back.channel.logout.ch",
domain.LoginVersionUnspecified,
nil,
nil,
nil,
nil,
nil,
// saml config
nil,
nil,
@@ -751,6 +787,10 @@ func Test_AppsPrepare(t *testing.T) {
"back.channel.logout.ch",
domain.LoginVersionUnspecified,
nil,
nil,
nil,
nil,
nil,
// saml config
nil,
nil,
@@ -845,6 +885,10 @@ func Test_AppsPrepare(t *testing.T) {
"back.channel.logout.ch",
domain.LoginVersionUnspecified,
nil,
nil,
nil,
nil,
nil,
// saml config
nil,
nil,
@@ -939,6 +983,10 @@ func Test_AppsPrepare(t *testing.T) {
"back.channel.logout.ch",
domain.LoginVersionUnspecified,
nil,
nil,
nil,
nil,
nil,
// saml config
nil,
nil,
@@ -1033,6 +1081,10 @@ func Test_AppsPrepare(t *testing.T) {
"back.channel.logout.ch",
domain.LoginVersion2,
"https://login.ch/",
nil,
nil,
nil,
nil,
// saml config
nil,
nil,
@@ -1075,6 +1127,10 @@ func Test_AppsPrepare(t *testing.T) {
nil,
nil,
nil,
nil,
nil,
nil,
nil,
// saml config
nil,
nil,
@@ -1117,6 +1173,10 @@ func Test_AppsPrepare(t *testing.T) {
nil,
nil,
nil,
nil,
nil,
nil,
nil,
// saml config
"saml-app-id",
"https://test.com/saml/metadata",
@@ -1302,6 +1362,10 @@ func Test_AppPrepare(t *testing.T) {
nil,
nil,
nil,
nil,
nil,
nil,
nil,
// saml config
nil,
nil,
@@ -1367,6 +1431,10 @@ func Test_AppPrepare(t *testing.T) {
nil,
nil,
nil,
nil,
nil,
nil,
nil,
// saml config
nil,
nil,
@@ -1437,6 +1505,10 @@ func Test_AppPrepare(t *testing.T) {
"back.channel.logout.ch",
domain.LoginVersionUnspecified,
nil,
nil,
nil,
nil,
nil,
// saml config
nil,
nil,
@@ -1526,6 +1598,10 @@ func Test_AppPrepare(t *testing.T) {
"back.channel.logout.ch",
domain.LoginVersionUnspecified,
nil,
nil,
nil,
nil,
nil,
// saml config
nil,
nil,
@@ -1615,6 +1691,10 @@ func Test_AppPrepare(t *testing.T) {
nil,
nil,
nil,
nil,
nil,
nil,
nil,
// saml config
"app-id",
"https://test.com/saml/metadata",
@@ -1688,6 +1768,10 @@ func Test_AppPrepare(t *testing.T) {
"back.channel.logout.ch",
domain.LoginVersionUnspecified,
nil,
nil,
nil,
nil,
nil,
// saml config
nil,
nil,
@@ -1777,6 +1861,10 @@ func Test_AppPrepare(t *testing.T) {
"back.channel.logout.ch",
domain.LoginVersionUnspecified,
nil,
nil,
nil,
nil,
nil,
// saml config
nil,
nil,
@@ -1866,6 +1954,10 @@ func Test_AppPrepare(t *testing.T) {
"back.channel.logout.ch",
domain.LoginVersionUnspecified,
nil,
nil,
nil,
nil,
nil,
// saml config
nil,
nil,
@@ -1955,6 +2047,10 @@ func Test_AppPrepare(t *testing.T) {
"back.channel.logout.ch",
domain.LoginVersionUnspecified,
nil,
nil,
nil,
nil,
nil,
// saml config
nil,
nil,
+48 -24
View File
@@ -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
+56 -2
View File
@@ -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{