mirror of
https://github.com/andwati/zitadel.git
synced 2026-11-14 01:45: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:
@@ -47,29 +47,35 @@ func AddOIDCAppRequestToDomain(req *mgmt_pb.AddOIDCAppRequest) (*domain.OIDCApp,
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
iosTeamID, iosBundleID := app_grpc.IOSAppLinkConfigToDomain(req.GetIos())
|
||||
androidPackageName, androidFingerprints := app_grpc.AndroidAppLinkConfigToDomain(req.GetAndroid())
|
||||
return &domain.OIDCApp{
|
||||
ObjectRoot: models.ObjectRoot{
|
||||
AggregateID: req.ProjectId,
|
||||
},
|
||||
AppName: req.Name,
|
||||
OIDCVersion: gu.Ptr(app_grpc.OIDCVersionToDomain(req.Version)),
|
||||
RedirectUris: req.RedirectUris,
|
||||
ResponseTypes: app_grpc.OIDCResponseTypesToDomain(req.ResponseTypes),
|
||||
GrantTypes: app_grpc.OIDCGrantTypesToDomain(req.GrantTypes),
|
||||
ApplicationType: gu.Ptr(app_grpc.OIDCApplicationTypeToDomain(req.AppType)),
|
||||
AuthMethodType: gu.Ptr(app_grpc.OIDCAuthMethodTypeToDomain(req.AuthMethodType)),
|
||||
PostLogoutRedirectUris: req.PostLogoutRedirectUris,
|
||||
DevMode: gu.Ptr(req.GetDevMode()),
|
||||
AccessTokenType: gu.Ptr(app_grpc.OIDCTokenTypeToDomain(req.AccessTokenType)),
|
||||
AccessTokenRoleAssertion: gu.Ptr(req.GetAccessTokenRoleAssertion()),
|
||||
IDTokenRoleAssertion: gu.Ptr(req.GetIdTokenRoleAssertion()),
|
||||
IDTokenUserinfoAssertion: gu.Ptr(req.GetIdTokenUserinfoAssertion()),
|
||||
ClockSkew: gu.Ptr(req.GetClockSkew().AsDuration()),
|
||||
AdditionalOrigins: req.AdditionalOrigins,
|
||||
SkipNativeAppSuccessPage: gu.Ptr(req.GetSkipNativeAppSuccessPage()),
|
||||
BackChannelLogoutURI: gu.Ptr(req.GetBackChannelLogoutUri()),
|
||||
LoginVersion: gu.Ptr(loginVersion),
|
||||
LoginBaseURI: gu.Ptr(loginBaseURI),
|
||||
AppName: req.Name,
|
||||
OIDCVersion: gu.Ptr(app_grpc.OIDCVersionToDomain(req.Version)),
|
||||
RedirectUris: req.RedirectUris,
|
||||
ResponseTypes: app_grpc.OIDCResponseTypesToDomain(req.ResponseTypes),
|
||||
GrantTypes: app_grpc.OIDCGrantTypesToDomain(req.GrantTypes),
|
||||
ApplicationType: gu.Ptr(app_grpc.OIDCApplicationTypeToDomain(req.AppType)),
|
||||
AuthMethodType: gu.Ptr(app_grpc.OIDCAuthMethodTypeToDomain(req.AuthMethodType)),
|
||||
PostLogoutRedirectUris: req.PostLogoutRedirectUris,
|
||||
DevMode: gu.Ptr(req.GetDevMode()),
|
||||
AccessTokenType: gu.Ptr(app_grpc.OIDCTokenTypeToDomain(req.AccessTokenType)),
|
||||
AccessTokenRoleAssertion: gu.Ptr(req.GetAccessTokenRoleAssertion()),
|
||||
IDTokenRoleAssertion: gu.Ptr(req.GetIdTokenRoleAssertion()),
|
||||
IDTokenUserinfoAssertion: gu.Ptr(req.GetIdTokenUserinfoAssertion()),
|
||||
ClockSkew: gu.Ptr(req.GetClockSkew().AsDuration()),
|
||||
AdditionalOrigins: req.AdditionalOrigins,
|
||||
SkipNativeAppSuccessPage: gu.Ptr(req.GetSkipNativeAppSuccessPage()),
|
||||
BackChannelLogoutURI: gu.Ptr(req.GetBackChannelLogoutUri()),
|
||||
LoginVersion: gu.Ptr(loginVersion),
|
||||
LoginBaseURI: gu.Ptr(loginBaseURI),
|
||||
IOSTeamID: iosTeamID,
|
||||
IOSBundleID: iosBundleID,
|
||||
AndroidPackageName: androidPackageName,
|
||||
AndroidSHA256CertFingerprints: androidFingerprints,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -112,28 +118,35 @@ func UpdateOIDCAppConfigRequestToDomain(app *mgmt_pb.UpdateOIDCAppConfigRequest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// nested messages: omit = no change; present (incl. empty) = set/clear
|
||||
iosTeamID, iosBundleID := app_grpc.IOSAppLinkConfigToDomain(app.Ios)
|
||||
androidPackageName, androidFingerprints := app_grpc.AndroidAppLinkConfigToDomain(app.Android)
|
||||
return &domain.OIDCApp{
|
||||
ObjectRoot: models.ObjectRoot{
|
||||
AggregateID: app.ProjectId,
|
||||
},
|
||||
AppID: app.AppId,
|
||||
RedirectUris: app.RedirectUris,
|
||||
ResponseTypes: app_grpc.OIDCResponseTypesToDomain(app.ResponseTypes),
|
||||
GrantTypes: app_grpc.OIDCGrantTypesToDomain(app.GrantTypes),
|
||||
ApplicationType: gu.Ptr(app_grpc.OIDCApplicationTypeToDomain(app.AppType)),
|
||||
AuthMethodType: gu.Ptr(app_grpc.OIDCAuthMethodTypeToDomain(app.AuthMethodType)),
|
||||
PostLogoutRedirectUris: app.PostLogoutRedirectUris,
|
||||
DevMode: gu.Ptr(app.GetDevMode()),
|
||||
AccessTokenType: gu.Ptr(app_grpc.OIDCTokenTypeToDomain(app.AccessTokenType)),
|
||||
AccessTokenRoleAssertion: gu.Ptr(app.GetAccessTokenRoleAssertion()),
|
||||
IDTokenRoleAssertion: gu.Ptr(app.GetIdTokenRoleAssertion()),
|
||||
IDTokenUserinfoAssertion: gu.Ptr(app.GetIdTokenUserinfoAssertion()),
|
||||
ClockSkew: gu.Ptr(app.GetClockSkew().AsDuration()),
|
||||
AdditionalOrigins: app.AdditionalOrigins,
|
||||
SkipNativeAppSuccessPage: gu.Ptr(app.GetSkipNativeAppSuccessPage()),
|
||||
BackChannelLogoutURI: gu.Ptr(app.GetBackChannelLogoutUri()),
|
||||
LoginVersion: gu.Ptr(loginVersion),
|
||||
LoginBaseURI: gu.Ptr(loginBaseURI),
|
||||
AppID: app.AppId,
|
||||
RedirectUris: app.RedirectUris,
|
||||
ResponseTypes: app_grpc.OIDCResponseTypesToDomain(app.ResponseTypes),
|
||||
GrantTypes: app_grpc.OIDCGrantTypesToDomain(app.GrantTypes),
|
||||
ApplicationType: gu.Ptr(app_grpc.OIDCApplicationTypeToDomain(app.AppType)),
|
||||
AuthMethodType: gu.Ptr(app_grpc.OIDCAuthMethodTypeToDomain(app.AuthMethodType)),
|
||||
PostLogoutRedirectUris: app.PostLogoutRedirectUris,
|
||||
DevMode: gu.Ptr(app.GetDevMode()),
|
||||
AccessTokenType: gu.Ptr(app_grpc.OIDCTokenTypeToDomain(app.AccessTokenType)),
|
||||
AccessTokenRoleAssertion: gu.Ptr(app.GetAccessTokenRoleAssertion()),
|
||||
IDTokenRoleAssertion: gu.Ptr(app.GetIdTokenRoleAssertion()),
|
||||
IDTokenUserinfoAssertion: gu.Ptr(app.GetIdTokenUserinfoAssertion()),
|
||||
ClockSkew: gu.Ptr(app.GetClockSkew().AsDuration()),
|
||||
AdditionalOrigins: app.AdditionalOrigins,
|
||||
SkipNativeAppSuccessPage: gu.Ptr(app.GetSkipNativeAppSuccessPage()),
|
||||
BackChannelLogoutURI: gu.Ptr(app.GetBackChannelLogoutUri()),
|
||||
LoginVersion: gu.Ptr(loginVersion),
|
||||
LoginBaseURI: gu.Ptr(loginBaseURI),
|
||||
IOSTeamID: iosTeamID,
|
||||
IOSBundleID: iosBundleID,
|
||||
AndroidPackageName: androidPackageName,
|
||||
AndroidSHA256CertFingerprints: androidFingerprints,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user