diff --git a/apps/docs/content/apis/openidoauth/endpoints.mdx b/apps/docs/content/apis/openidoauth/endpoints.mdx
index 10dc4b8be..addbfef2a 100644
--- a/apps/docs/content/apis/openidoauth/endpoints.mdx
+++ b/apps/docs/content/apis/openidoauth/endpoints.mdx
@@ -15,6 +15,13 @@ This would give us `${CUSTOM_DOMAIN}/.well-known/openid-configuration`.
**Link to spec.** [OpenID Connect Discovery 1.0 incorporating errata set 1](https://openid.net/specs/openid-connect-discovery-1_0.html)
+ZITADEL also serves instance-scoped association files for native passkeys (not part of OIDC discovery):
+
+- `${CUSTOM_DOMAIN}/.well-known/apple-app-site-association`
+- `${CUSTOM_DOMAIN}/.well-known/assetlinks.json`
+
+See [Native app links for passkeys](/guides/integrate/login/oidc/native-app-links).
+
## authorization_endpoint
`${CUSTOM_DOMAIN}/oauth/v2/authorize`
diff --git a/apps/docs/content/concepts/features/passkeys.mdx b/apps/docs/content/concepts/features/passkeys.mdx
index 8a95dd0d2..2f314c227 100644
--- a/apps/docs/content/concepts/features/passkeys.mdx
+++ b/apps/docs/content/concepts/features/passkeys.mdx
@@ -35,5 +35,6 @@ Imagine signing in without passwords! Passkeys, replacing traditional passwords,
* Passkey support is still evolving in browsers and platforms. Check compatibility for your target audience.
* ZITADEL actively develops its passkey features. Stay updated with documentation and releases.
* Passkeys are bound to your domain, thus we recommend configuring a [Custom Domain](/concepts/features/custom-domain) before setting up passkeys.
+* Native iOS and Android apps that use platform passkey APIs also need domain association files. Configure them on the OIDC app and let ZITADEL serve the well-known documents — see [Native app links for passkeys](/guides/integrate/login/oidc/native-app-links).
Don't hesitate to ask if you have further questions about integrating passkeys in your ZITADEL application!
diff --git a/apps/docs/content/guides/integrate/login-ui/passkey.mdx b/apps/docs/content/guides/integrate/login-ui/passkey.mdx
index b4e643374..8d9ca5d59 100644
--- a/apps/docs/content/guides/integrate/login-ui/passkey.mdx
+++ b/apps/docs/content/guides/integrate/login-ui/passkey.mdx
@@ -12,6 +12,11 @@ Passkeys are a replacement for passwords that provide faster, easier, and more s
Unlike passwords, passkeys are phishing-resistant and can improve the user experience and security at the same time.
Passkeys and there underlying protocols are a standard defined by the [FIDO Standard](https://fidoalliance.org/) .
+
+This guide covers passkeys in a **custom browser login UI**.
+Native iOS/Android apps that use platform passkey APIs also need domain association files served from your issuer — see [Native app links for passkeys](/guides/integrate/login/oidc/native-app-links).
+
+
## Register Passkey
### Flow
diff --git a/apps/docs/content/guides/integrate/login/oidc/index.mdx b/apps/docs/content/guides/integrate/login/oidc/index.mdx
index 4d1edceee..58078d3f8 100644
--- a/apps/docs/content/guides/integrate/login/oidc/index.mdx
+++ b/apps/docs/content/guides/integrate/login/oidc/index.mdx
@@ -21,5 +21,6 @@ import { FileText, Folder, Link as LinkIcon } from 'lucide-react';
} />
} />
} />
+ } />
} />
diff --git a/apps/docs/content/guides/integrate/login/oidc/native-app-links.mdx b/apps/docs/content/guides/integrate/login/oidc/native-app-links.mdx
new file mode 100644
index 000000000..b2a0645c9
--- /dev/null
+++ b/apps/docs/content/guides/integrate/login/oidc/native-app-links.mdx
@@ -0,0 +1,135 @@
+---
+title: Native app links for passkeys
+description: "Configure iOS and Android association on OIDC apps so ZITADEL can serve apple-app-site-association and assetlinks.json for native passkeys"
+sidebar_label: Native App Links
+---
+
+Native mobile apps that create or use passkeys with platform WebAuthn APIs need the issuer domain to publish OS trust association files.
+ZITADEL serves those files from your instance based on iOS and Android settings on OIDC applications.
+
+## Prerequisites
+
+- An OIDC **Native** application in your project
+- Passkeys enabled in your login policy
+- A stable issuer / [custom domain](/concepts/features/custom-domain) (passkeys are domain-bound)
+- Platform association configured in the mobile app project (Associated Domains on iOS; Digital Asset Links / App Links on Android)
+
+## Configure the OIDC application
+
+Set association fields on each Native OIDC app that should participate in passkey trust.
+
+You can do this in the [Console](/guides/manage/console/applications-overview#native-app-links) under the app's **Native App Links** section, or via the Application API / Management API OIDC app create and update endpoints (`ios` / `android`).
+
+### iOS
+
+| Field | Description |
+|-------|-------------|
+| **Team ID** | Apple Developer Team ID (exactly 10 alphanumeric characters), for example `ABCDE12345` |
+| **Bundle ID** | App bundle identifier (`CFBundleIdentifier`), for example `com.example.app` — do not include the Team ID prefix |
+
+ZITADEL publishes these as a single App ID in the association file: `{team_id}.{bundle_id}`.
+
+### Android
+
+| Field | Description |
+|-------|-------------|
+| **Package name** | Android `applicationId` / package name from the app manifest |
+| **SHA-256 certificate fingerprints** | Signing certificate fingerprints (64 hex characters, optionally colon-separated). Include debug and release fingerprints as needed. |
+
+### Update semantics
+
+On update:
+
+- Omit a field group to leave it unchanged
+- Send an empty message / empty values to clear
+- Send populated values to replace
+
+## Well-known endpoints
+
+ZITADEL aggregates association data from **active** OIDC apps on the instance and serves:
+
+| Path | Purpose |
+|------|---------|
+| `${CUSTOM_DOMAIN}/.well-known/apple-app-site-association` | iOS Associated Domains / `webcredentials` |
+| `${CUSTOM_DOMAIN}/.well-known/assetlinks.json` | Android Digital Asset Links for `delegate_permission/common.get_login_creds` |
+
+If no apps are configured, both endpoints still return HTTP `200` with empty collections.
+
+### Example: apple-app-site-association
+
+```json
+{
+ "webcredentials": {
+ "apps": [
+ "ABCDE12345.com.example.one",
+ "FGHIJ67890.com.example.two"
+ ]
+ }
+}
+```
+
+Duplicate iOS App IDs across apps are deduplicated.
+
+### Example: assetlinks.json
+
+```json
+[
+ {
+ "relation": ["delegate_permission/common.get_login_creds"],
+ "target": {
+ "namespace": "android_app",
+ "package_name": "com.example.one",
+ "sha256_cert_fingerprints": [
+ "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"
+ ]
+ }
+ }
+]
+```
+
+Fingerprints are normalized to colon-separated uppercase hex when served.
+
+## Caching
+
+Successful responses include a `Cache-Control` header so intermediate caches may reuse the documents for a short time.
+
+By default (and in ZITADEL Cloud) caching is allowed for 5 minutes:
+
+```
+Cache-Control: public, max-age=300
+```
+
+Self-hosters can change this with the `ZITADEL_WELLKNOWN_APPLINKSCACHECONTROLMAXAGE` environment variable or in the settings YAML:
+
+```yaml
+WellKnown:
+ AppLinksCacheControlMaxAge: 5m
+```
+
+Setting the value to `0` results in `Cache-Control: no-store`.
+
+
+Platform verifiers may cache association files on their side as well.
+After you change app link settings, allow time for HTTP caches and OS verification to refresh before expecting passkeys to work with the new values.
+
+
+## Verify
+
+```bash
+curl -i "${CUSTOM_DOMAIN}/.well-known/apple-app-site-association"
+curl -i "${CUSTOM_DOMAIN}/.well-known/assetlinks.json"
+```
+
+Confirm:
+
+- HTTP status `200`
+- `Content-Type: application/json`
+- Expected `Cache-Control` header
+- JSON contents match your configured apps
+
+## Related
+
+- [Passkeys concept](/concepts/features/passkeys)
+- [Console applications](/guides/manage/console/applications-overview)
+- [Custom domain](/concepts/features/custom-domain)
+- [Passkeys in a custom login UI](/guides/integrate/login-ui/passkey) (browser / WebAuthn APIs)
diff --git a/apps/docs/content/guides/manage/console/applications-overview.mdx b/apps/docs/content/guides/manage/console/applications-overview.mdx
index 41913988e..a01162caf 100644
--- a/apps/docs/content/guides/manage/console/applications-overview.mdx
+++ b/apps/docs/content/guides/manage/console/applications-overview.mdx
@@ -63,6 +63,15 @@ Native Applications are installed on a thin client, such as a smartphone or desk
+#### Native app links
+
+For native passkeys, configure OS association on the OIDC app under **Native App Links**:
+
+* **iOS:** Team ID and Bundle ID (published in `/.well-known/apple-app-site-association`)
+* **Android:** Package name and SHA-256 certificate fingerprints (published in `/.well-known/assetlinks.json`)
+
+See [Native app links for passkeys](/guides/integrate/login/oidc/native-app-links) for endpoint details, caching, and verification.
+
### User Agent
User Agent Applications are executed entirely in the web browser (Client-Side).
@@ -219,3 +228,4 @@ Ensure the management of application settings is limited to authorized users onl
## References
- [Recommended OIDC Flows](../../integrate/login/oidc/oauth-recommended-flows)
+- [Native app links for passkeys](../../integrate/login/oidc/native-app-links)
diff --git a/apps/docs/lib/sidebar-data.ts b/apps/docs/lib/sidebar-data.ts
index e1478525f..fc06e2619 100644
--- a/apps/docs/lib/sidebar-data.ts
+++ b/apps/docs/lib/sidebar-data.ts
@@ -269,6 +269,7 @@ export const guidesSidebar: readonly SidebarItem[] = [
"apis/openidoauth/grant-types",
"apis/openidoauth/authrequest",
"guides/integrate/login/oidc/webkeys",
+ "guides/integrate/login/oidc/native-app-links",
"guides/integrate/token-exchange",
],
},
diff --git a/cmd/defaults.yaml b/cmd/defaults.yaml
index 294e63d2a..c37b3a940 100644
--- a/cmd/defaults.yaml
+++ b/cmd/defaults.yaml
@@ -701,6 +701,11 @@ OIDC:
# Lifetime of the token used to notify clients through OIDC back-channel logout.
TokenLifetime: 15m # ZITADEL_OIDC_BACKCHANNELLOGOUT_TOKENLIFETIME
+WellKnown:
+ # HTTP Cache-Control max-age for /.well-known/apple-app-site-association and
+ # /.well-known/assetlinks.json. 0 sets Cache-Control: no-store.
+ AppLinksCacheControlMaxAge: 5m # ZITADEL_WELLKNOWN_APPLINKSCACHECONTROLMAXAGE
+
SAML:
DefaultLoginURLV2: "/ui/v2/login/login?samlRequest=" # ZITADEL_SAML_DEFAULTLOGINURLV2
ProviderConfig:
diff --git a/cmd/setup/75.go b/cmd/setup/75.go
new file mode 100644
index 000000000..108a8c2c6
--- /dev/null
+++ b/cmd/setup/75.go
@@ -0,0 +1,27 @@
+package setup
+
+import (
+ "context"
+ _ "embed"
+
+ "github.com/zitadel/zitadel/internal/database"
+ "github.com/zitadel/zitadel/internal/eventstore"
+)
+
+var (
+ //go:embed 75.sql
+ addOIDCAppLinkConfig string
+)
+
+type Apps7OIDCConfigsAddAppLinkConfig struct {
+ dbClient *database.DB
+}
+
+func (mig *Apps7OIDCConfigsAddAppLinkConfig) Execute(ctx context.Context, _ eventstore.Event) error {
+ _, err := mig.dbClient.ExecContext(ctx, addOIDCAppLinkConfig)
+ return err
+}
+
+func (mig *Apps7OIDCConfigsAddAppLinkConfig) String() string {
+ return "75_apps7_oidc_configs_add_app_link_config"
+}
diff --git a/cmd/setup/75.sql b/cmd/setup/75.sql
new file mode 100644
index 000000000..b4fc9d99f
--- /dev/null
+++ b/cmd/setup/75.sql
@@ -0,0 +1,4 @@
+ALTER TABLE IF EXISTS projections.apps7_oidc_configs ADD COLUMN IF NOT EXISTS ios_team_id TEXT;
+ALTER TABLE IF EXISTS projections.apps7_oidc_configs ADD COLUMN IF NOT EXISTS ios_bundle_id TEXT;
+ALTER TABLE IF EXISTS projections.apps7_oidc_configs ADD COLUMN IF NOT EXISTS android_package_name TEXT;
+ALTER TABLE IF EXISTS projections.apps7_oidc_configs ADD COLUMN IF NOT EXISTS android_sha256_cert_fingerprints TEXT[];
diff --git a/cmd/setup/config.go b/cmd/setup/config.go
index c8d696864..f0eb6e637 100644
--- a/cmd/setup/config.go
+++ b/cmd/setup/config.go
@@ -194,6 +194,7 @@ type Steps struct {
s72AddColumnsToLoginNamesView *AddColumnsToLoginNamesView
s73FixUserGrantRoles *FixUserGrantRoles
s74Apps7OIDCConfigsAddRegistrationToken *Apps7OIDCConfigsAddRegistrationToken
+ s75Apps7OIDCConfigsAddAppLinkConfig *Apps7OIDCConfigsAddAppLinkConfig
RelationalTables *TransactionalTables
}
diff --git a/cmd/setup/setup.go b/cmd/setup/setup.go
index cebf085b3..4c9942d81 100644
--- a/cmd/setup/setup.go
+++ b/cmd/setup/setup.go
@@ -257,6 +257,7 @@ func Setup(ctx context.Context, config *Config, steps *Steps, masterKey string)
steps.s72AddColumnsToLoginNamesView = &AddColumnsToLoginNamesView{dbClient: dbClient}
steps.s73FixUserGrantRoles = &FixUserGrantRoles{eventstore: eventstoreClient}
steps.s74Apps7OIDCConfigsAddRegistrationToken = &Apps7OIDCConfigsAddRegistrationToken{dbClient: dbClient}
+ steps.s75Apps7OIDCConfigsAddAppLinkConfig = &Apps7OIDCConfigsAddAppLinkConfig{dbClient: dbClient}
err = projection.Create(ctx, dbClient, eventstoreClient, config.Projections, nil, nil, nil)
if err != nil {
@@ -390,6 +391,7 @@ func Setup(ctx context.Context, config *Config, steps *Steps, masterKey string)
steps.s72AddColumnsToLoginNamesView,
steps.s73FixUserGrantRoles,
steps.s74Apps7OIDCConfigsAddRegistrationToken,
+ steps.s75Apps7OIDCConfigsAddAppLinkConfig,
} {
setupErr = executeMigration(ctx, eventstoreClient, step, "migration failed")
if setupErr != nil {
diff --git a/cmd/start/config.go b/cmd/start/config.go
index 1ddc9c71b..2591dfb24 100644
--- a/cmd/start/config.go
+++ b/cmd/start/config.go
@@ -24,6 +24,7 @@ import (
scim_config "github.com/zitadel/zitadel/internal/api/scim/config"
"github.com/zitadel/zitadel/internal/api/ui/console"
"github.com/zitadel/zitadel/internal/api/ui/login"
+ "github.com/zitadel/zitadel/internal/api/well_known"
auth_es "github.com/zitadel/zitadel/internal/auth/repository/eventsourcing"
"github.com/zitadel/zitadel/internal/cache/connector"
"github.com/zitadel/zitadel/internal/command"
@@ -72,6 +73,7 @@ type Config struct {
SCIM scim_config.Config
Login login.Config
Console console.Config
+ WellKnown well_known.Config
AssetStorage static_config.AssetStorageConfig
InternalAuthZ authz.Config
SystemAuthZ authz.Config
diff --git a/cmd/start/start.go b/cmd/start/start.go
index 19e520fec..73c4838df 100644
--- a/cmd/start/start.go
+++ b/cmd/start/start.go
@@ -85,6 +85,7 @@ import (
"github.com/zitadel/zitadel/internal/api/ui/console"
"github.com/zitadel/zitadel/internal/api/ui/console/path"
"github.com/zitadel/zitadel/internal/api/ui/login"
+ "github.com/zitadel/zitadel/internal/api/well_known"
auth_es "github.com/zitadel/zitadel/internal/auth/repository/eventsourcing"
"github.com/zitadel/zitadel/internal/authz"
authz_repo "github.com/zitadel/zitadel/internal/authz/repository"
@@ -657,6 +658,9 @@ func startAPIs(
}
apis.RegisterHandlerOnPrefix(robots_txt.HandlerPrefix, robotsTxtHandler)
+ // native app link well-known files (instance-scoped)
+ apis.RegisterHandlerPrefixes(instanceInterceptor.Handler(well_known.NewHandler(queries, config.WellKnown)), well_known.HandlerPrefixes...)
+
// TODO: Record openapi access logs?
openAPIHandler, err := openapi.Start()
if err != nil {
diff --git a/console/src/app/pages/projects/apps/app-detail/app-detail.component.html b/console/src/app/pages/projects/apps/app-detail/app-detail.component.html
index 5a4c35273..28650627b 100644
--- a/console/src/app/pages/projects/apps/app-detail/app-detail.component.html
+++ b/console/src/app/pages/projects/apps/app-detail/app-detail.component.html
@@ -462,6 +462,102 @@
}
+
+ @if (currentSetting.id === 'native-app-links' && oidcForm && app?.oidcConfig) {
+
+
{{ 'APP.OIDC.APPLINKSDESC' | translate }}
+
+
+ }
+
@if (currentSetting.id === 'urls') {
@if (issuer$ | async; as issuer) {
diff --git a/console/src/app/pages/projects/apps/app-detail/app-detail.component.scss b/console/src/app/pages/projects/apps/app-detail/app-detail.component.scss
index 2efa5d852..a053f8d94 100644
--- a/console/src/app/pages/projects/apps/app-detail/app-detail.component.scss
+++ b/console/src/app/pages/projects/apps/app-detail/app-detail.component.scss
@@ -53,6 +53,11 @@
.app-desc {
font-size: 14px;
margin: 0;
+
+ .app-link-endpoint {
+ margin-top: 0.5rem;
+ word-break: break-all;
+ }
}
.app-input {
@@ -281,6 +286,17 @@
input {
max-width: none;
}
+
+ textarea.fingerprints-input,
+ .fingerprints-input {
+ max-width: none;
+ width: 100%;
+ min-height: 12rem;
+ resize: vertical;
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace;
+ font-size: 0.875rem;
+ line-height: 1.4;
+ }
}
}
diff --git a/console/src/app/pages/projects/apps/app-detail/app-detail.component.ts b/console/src/app/pages/projects/apps/app-detail/app-detail.component.ts
index 4476a8f61..47ec26508 100644
--- a/console/src/app/pages/projects/apps/app-detail/app-detail.component.ts
+++ b/console/src/app/pages/projects/apps/app-detail/app-detail.component.ts
@@ -1,7 +1,16 @@
import { COMMA, ENTER, SPACE } from '@angular/cdk/keycodes';
import { Location } from '@angular/common';
import { Component, OnDestroy, OnInit, signal } from '@angular/core';
-import { AbstractControl, FormControl, UntypedFormBuilder, UntypedFormControl, UntypedFormGroup } from '@angular/forms';
+import {
+ AbstractControl,
+ FormControl,
+ UntypedFormBuilder,
+ UntypedFormControl,
+ UntypedFormGroup,
+ ValidationErrors,
+ ValidatorFn,
+ Validators,
+} from '@angular/forms';
import { MatCheckboxChange } from '@angular/material/checkbox';
import { MatDialog } from '@angular/material/dialog';
import { ActivatedRoute, Router } from '@angular/router';
@@ -206,18 +215,25 @@ export class AppDetailComponent implements OnInit, OnDestroy {
private router: Router,
private breadcrumbService: BreadcrumbService,
) {
- this.oidcForm = this.fb.group({
- devMode: [{ value: false, disabled: true }],
- skipNativeAppSuccessPage: [{ value: false, disabled: true }],
- clientId: [{ value: '', disabled: true }],
- responseTypesList: [{ value: [], disabled: true }],
- grantTypesList: [{ value: [], disabled: true }],
- appType: [{ value: '', disabled: true }],
- authMethodType: [{ value: '', disabled: true }],
- loginV2: [{ value: false, disabled: true }],
- loginV2BaseURL: [{ value: '', disabled: true }],
- backChannelLogoutURI: [{ value: '', disabled: true }],
- });
+ this.oidcForm = this.fb.group(
+ {
+ devMode: [{ value: false, disabled: true }],
+ skipNativeAppSuccessPage: [{ value: false, disabled: true }],
+ clientId: [{ value: '', disabled: true }],
+ responseTypesList: [{ value: [], disabled: true }],
+ grantTypesList: [{ value: [], disabled: true }],
+ appType: [{ value: '', disabled: true }],
+ authMethodType: [{ value: '', disabled: true }],
+ loginV2: [{ value: false, disabled: true }],
+ loginV2BaseURL: [{ value: '', disabled: true }],
+ backChannelLogoutURI: [{ value: '', disabled: true }],
+ iosTeamId: [{ value: '', disabled: true }, [iosTeamIdValidator]],
+ iosBundleId: [{ value: '', disabled: true }, [Validators.maxLength(200)]],
+ androidPackageName: [{ value: '', disabled: true }, [Validators.maxLength(200)]],
+ androidSha256CertFingerprints: [{ value: '', disabled: true }],
+ },
+ { validators: [appLinkConfigValidator()] },
+ );
this.oidcTokenForm = this.fb.group({
accessTokenType: [{ value: '', disabled: true }],
@@ -375,6 +391,7 @@ export class AppDetailComponent implements OnInit, OnDestroy {
this.settingsList = [
{ id: 'configuration', i18nKey: 'APP.CONFIGURATION' },
{ id: 'token', i18nKey: 'APP.TOKEN' },
+ { id: 'native-app-links', i18nKey: 'APP.OIDC.APPLINKSSECTION' },
{ id: 'urls', i18nKey: 'APP.URLS' },
];
} else {
@@ -383,6 +400,7 @@ export class AppDetailComponent implements OnInit, OnDestroy {
{ id: 'token', i18nKey: 'APP.TOKEN' },
{ id: 'redirect-uris', i18nKey: 'APP.OIDC.REDIRECTSECTIONTITLE' },
{ id: 'additional-origins', i18nKey: 'APP.ADDITIONALORIGINS' },
+ { id: 'native-app-links', i18nKey: 'APP.OIDC.APPLINKSSECTION' },
{ id: 'urls', i18nKey: 'APP.URLS' },
];
}
@@ -465,6 +483,12 @@ export class AppDetailComponent implements OnInit, OnDestroy {
if (this.app.oidcConfig) {
this.oidcForm.patchValue(this.app.oidcConfig);
this.oidcTokenForm.patchValue(this.app.oidcConfig);
+ this.oidcForm.controls['iosTeamId'].setValue(this.app.oidcConfig.ios?.teamId ?? '');
+ this.oidcForm.controls['iosBundleId'].setValue(this.app.oidcConfig.ios?.bundleId ?? '');
+ this.oidcForm.controls['androidPackageName'].setValue(this.app.oidcConfig.android?.packageName ?? '');
+ this.oidcForm.controls['androidSha256CertFingerprints'].setValue(
+ (this.app.oidcConfig.android?.sha256CertFingerprintsList ?? []).join('\n'),
+ );
}
if (this.app.apiConfig) {
this.apiForm.patchValue(this.app.apiConfig);
@@ -657,96 +681,106 @@ export class AppDetailComponent implements OnInit, OnDestroy {
public saveOIDCApp(): void {
this.requestRedirectValuesSubject$.next();
- if (this.oidcForm.valid) {
- if (this.app?.oidcConfig) {
- // configuration
- this.app.oidcConfig.responseTypesList = this.responseTypesList?.value;
- this.app.oidcConfig.grantTypesList = this.grantTypesList?.value;
- this.app.oidcConfig.appType = this.appType?.value;
- this.app.oidcConfig.authMethodType = this.authMethodType?.value;
- this.app.oidcConfig.backChannelLogoutUri = this.backChannelLogoutURI?.value;
+ if (!this.oidcForm.valid) {
+ this.markNativeAppLinkFieldsTouched();
+ return;
+ }
+ if (this.app?.oidcConfig) {
+ // configuration
+ this.app.oidcConfig.responseTypesList = this.responseTypesList?.value;
+ this.app.oidcConfig.grantTypesList = this.grantTypesList?.value;
+ this.app.oidcConfig.appType = this.appType?.value;
+ this.app.oidcConfig.authMethodType = this.authMethodType?.value;
+ this.app.oidcConfig.backChannelLogoutUri = this.backChannelLogoutURI?.value;
- // token
- this.app.oidcConfig.accessTokenType = this.accessTokenType?.value;
- this.app.oidcConfig.accessTokenRoleAssertion = this.accessTokenRoleAssertion?.value;
- this.app.oidcConfig.idTokenRoleAssertion = this.idTokenRoleAssertion?.value;
- this.app.oidcConfig.idTokenUserinfoAssertion = this.idTokenUserinfoAssertion?.value;
+ // token
+ this.app.oidcConfig.accessTokenType = this.accessTokenType?.value;
+ this.app.oidcConfig.accessTokenRoleAssertion = this.accessTokenRoleAssertion?.value;
+ this.app.oidcConfig.idTokenRoleAssertion = this.idTokenRoleAssertion?.value;
+ this.app.oidcConfig.idTokenUserinfoAssertion = this.idTokenUserinfoAssertion?.value;
- // redirects
- this.app.oidcConfig.redirectUrisList = this.redirectUrisList;
- this.app.oidcConfig.postLogoutRedirectUrisList = this.postLogoutRedirectUrisList;
- this.app.oidcConfig.additionalOriginsList = this.additionalOriginsList;
- this.app.oidcConfig.devMode = !!this.devMode?.value;
- this.app.oidcConfig.skipNativeAppSuccessPage = !!this.skipNativeAppSuccessPage?.value;
+ // redirects
+ this.app.oidcConfig.redirectUrisList = this.redirectUrisList;
+ this.app.oidcConfig.postLogoutRedirectUrisList = this.postLogoutRedirectUrisList;
+ this.app.oidcConfig.additionalOriginsList = this.additionalOriginsList;
+ this.app.oidcConfig.devMode = !!this.devMode?.value;
+ this.app.oidcConfig.skipNativeAppSuccessPage = !!this.skipNativeAppSuccessPage?.value;
- const loginVersion: MessageInitShape = this.oidcLoginV2?.value
- ? {
- version: {
- case: 'loginV2',
- value: { baseUri: this.oidcLoginV2BaseURL?.value },
- },
- }
- : {
- version: {
- case: 'loginV1',
- value: {},
- },
- };
-
- const req: MessageInitShape = {
- projectId: this.projectId,
- applicationId: this.app.id,
- applicationType: {
- case: 'oidcConfiguration',
- value: {
- // configuration
- responseTypes: this.app.oidcConfig.responseTypesList.map((type) => this.toOIDCV2ResponseType(type)),
- authMethodType: this.app.oidcConfig.authMethodType as unknown as OIDCV2AuthMethodType,
- grantTypes: this.app.oidcConfig.grantTypesList as unknown as OIDCV2GrantType[],
- applicationType: this.app.oidcConfig.appType as unknown as OIDCV2ApplicationType,
- backChannelLogoutUri: this.app.oidcConfig.backChannelLogoutUri,
- loginVersion,
-
- // token
- accessTokenType: this.app.oidcConfig.accessTokenType as unknown as OIDCV2TokenType,
- accessTokenRoleAssertion: this.app.oidcConfig.accessTokenRoleAssertion,
- idTokenRoleAssertion: this.app.oidcConfig.idTokenRoleAssertion,
- idTokenUserinfoAssertion: this.app.oidcConfig.idTokenUserinfoAssertion,
-
- // redirects
- redirectUris: this.normalizeOIDCListForUpdate(this.app.oidcConfig.redirectUrisList),
- additionalOrigins: this.app.oidcConfig.additionalOriginsList,
- postLogoutRedirectUris: this.normalizeOIDCListForUpdate(this.app.oidcConfig.postLogoutRedirectUrisList),
- developmentMode: this.app.oidcConfig.devMode,
- skipNativeAppSuccessPage: this.app.oidcConfig.skipNativeAppSuccessPage,
- ...(this.clockSkewSeconds?.value
- ? {
- clockSkew: {
- seconds: BigInt(Math.floor(this.clockSkewSeconds?.value)),
- nanos: Math.floor(this.clockSkewSeconds?.value % 1) * 10000,
- },
- }
- : {}),
+ const loginVersion: MessageInitShape = this.oidcLoginV2?.value
+ ? {
+ version: {
+ case: 'loginV2',
+ value: { baseUri: this.oidcLoginV2BaseURL?.value },
},
- },
- };
+ }
+ : {
+ version: {
+ case: 'loginV1',
+ value: {},
+ },
+ };
- this.applicationService
- .updateApplication(req)
- .then(() => {
- if (this.app?.oidcConfig) {
- const config = { oidc: this.app.oidcConfig };
- this.currentAuthMethod = this.authMethodFromPartialConfig(config);
- }
- this.toast.showInfo('APP.TOAST.OIDCUPDATED', true);
- setTimeout(() => {
- this.getData(this.projectId, this.appId);
- }, 1000);
- })
- .catch((error) => {
- this.toast.showError(error);
- });
- }
+ const req: MessageInitShape = {
+ projectId: this.projectId,
+ applicationId: this.app.id,
+ applicationType: {
+ case: 'oidcConfiguration',
+ value: {
+ // configuration
+ responseTypes: this.app.oidcConfig.responseTypesList.map((type) => this.toOIDCV2ResponseType(type)),
+ authMethodType: this.app.oidcConfig.authMethodType as unknown as OIDCV2AuthMethodType,
+ grantTypes: this.app.oidcConfig.grantTypesList as unknown as OIDCV2GrantType[],
+ applicationType: this.app.oidcConfig.appType as unknown as OIDCV2ApplicationType,
+ backChannelLogoutUri: this.app.oidcConfig.backChannelLogoutUri,
+ loginVersion,
+
+ // token
+ accessTokenType: this.app.oidcConfig.accessTokenType as unknown as OIDCV2TokenType,
+ accessTokenRoleAssertion: this.app.oidcConfig.accessTokenRoleAssertion,
+ idTokenRoleAssertion: this.app.oidcConfig.idTokenRoleAssertion,
+ idTokenUserinfoAssertion: this.app.oidcConfig.idTokenUserinfoAssertion,
+
+ // redirects
+ redirectUris: this.normalizeOIDCListForUpdate(this.app.oidcConfig.redirectUrisList),
+ additionalOrigins: this.app.oidcConfig.additionalOriginsList,
+ postLogoutRedirectUris: this.normalizeOIDCListForUpdate(this.app.oidcConfig.postLogoutRedirectUrisList),
+ developmentMode: this.app.oidcConfig.devMode,
+ skipNativeAppSuccessPage: this.app.oidcConfig.skipNativeAppSuccessPage,
+ ios: {
+ teamId: (this.iosTeamId?.value ?? '').trim(),
+ bundleId: (this.iosBundleId?.value ?? '').trim(),
+ },
+ android: {
+ packageName: (this.androidPackageName?.value ?? '').trim(),
+ sha256CertFingerprints: this.parseAndroidFingerprints(this.androidSha256CertFingerprints?.value),
+ },
+ ...(this.clockSkewSeconds?.value
+ ? {
+ clockSkew: {
+ seconds: BigInt(Math.floor(this.clockSkewSeconds?.value)),
+ nanos: Math.floor(this.clockSkewSeconds?.value % 1) * 10000,
+ },
+ }
+ : {}),
+ },
+ },
+ };
+
+ this.applicationService
+ .updateApplication(req)
+ .then(() => {
+ if (this.app?.oidcConfig) {
+ const config = { oidc: this.app.oidcConfig };
+ this.currentAuthMethod = this.authMethodFromPartialConfig(config);
+ }
+ this.toast.showInfo('APP.TOAST.OIDCUPDATED', true);
+ setTimeout(() => {
+ this.getData(this.projectId, this.appId);
+ }, 1000);
+ })
+ .catch((error) => {
+ this.toast.showError(error);
+ });
}
}
@@ -754,6 +788,16 @@ export class AppDetailComponent implements OnInit, OnDestroy {
return values.length === 0 ? [''] : values;
}
+ private parseAndroidFingerprints(value: unknown): string[] {
+ if (typeof value !== 'string' || !value.trim()) {
+ return [];
+ }
+ return value
+ .split(/[\n,]+/)
+ .map((fp) => fp.trim())
+ .filter((fp) => fp.length > 0);
+ }
+
private toOIDCV2ResponseType(type: OIDCResponseType): OIDCV2ResponseType {
switch (type) {
case OIDCResponseType.OIDC_RESPONSE_TYPE_CODE:
@@ -943,6 +987,49 @@ export class AppDetailComponent implements OnInit, OnDestroy {
return this.oidcForm.get('skipNativeAppSuccessPage') as FormControl;
}
+ public get iosTeamId(): AbstractControl | null {
+ return this.oidcForm.get('iosTeamId');
+ }
+
+ public get iosBundleId(): AbstractControl | null {
+ return this.oidcForm.get('iosBundleId');
+ }
+
+ public get androidPackageName(): AbstractControl | null {
+ return this.oidcForm.get('androidPackageName');
+ }
+
+ public get androidSha256CertFingerprints(): AbstractControl | null {
+ return this.oidcForm.get('androidSha256CertFingerprints');
+ }
+
+ /** Defer cross-field warnings until the user has visited the paired empty field. */
+ public showIosAppLinkIncompleteWarning(): boolean {
+ if (!this.oidcForm.hasError('iosAppLinkIncomplete')) {
+ return false;
+ }
+ const teamId = String(this.iosTeamId?.value ?? '').trim();
+ const bundleId = String(this.iosBundleId?.value ?? '').trim();
+ if (teamId !== '' && bundleId === '') {
+ return !!this.iosBundleId?.touched;
+ }
+ if (bundleId !== '' && teamId === '') {
+ return !!this.iosTeamId?.touched;
+ }
+ return false;
+ }
+
+ public showAndroidPackageRequiredWarning(): boolean {
+ return this.oidcForm.hasError('androidAppLinkPackageRequired') && !!this.androidPackageName?.touched;
+ }
+
+ private markNativeAppLinkFieldsTouched(): void {
+ this.iosTeamId?.markAsTouched();
+ this.iosBundleId?.markAsTouched();
+ this.androidPackageName?.markAsTouched();
+ this.androidSha256CertFingerprints?.markAsTouched();
+ }
+
public get accessTokenType(): AbstractControl | null {
return this.oidcTokenForm.get('accessTokenType');
}
@@ -1006,3 +1093,49 @@ export class AppDetailComponent implements OnInit, OnDestroy {
}
}
}
+
+function iosTeamIdValidator(c: AbstractControl): ValidationErrors | null {
+ const value = String(c.value ?? '').trim();
+ if (value === '') {
+ return null;
+ }
+ if (!/^[A-Z0-9]+$/.test(value)) {
+ return {
+ invalid: true,
+ errorsinvalidformat: { valid: false, i18nKey: 'ERRORS.INVALID_FORMAT' },
+ };
+ }
+ if (value.length !== 10) {
+ return {
+ invalid: true,
+ errorsapoidcteamidlength: { valid: false, i18nKey: 'APP.OIDC.TEAMIDLENGTH' },
+ };
+ }
+ return null;
+}
+
+/** Mirrors backend AppLinkConfigValid: iOS fields together; package required when fingerprints are set. */
+function appLinkConfigValidator(): ValidatorFn {
+ return (group: AbstractControl): ValidationErrors | null => {
+ const teamId = String(group.get('iosTeamId')?.value ?? '').trim();
+ const bundleId = String(group.get('iosBundleId')?.value ?? '').trim();
+ const packageName = String(group.get('androidPackageName')?.value ?? '').trim();
+ const fingerprintsRaw = group.get('androidSha256CertFingerprints')?.value;
+ const fingerprints =
+ typeof fingerprintsRaw === 'string'
+ ? fingerprintsRaw
+ .split(/[\n,]+/)
+ .map((fp) => fp.trim())
+ .filter((fp) => fp.length > 0)
+ : [];
+
+ const errors: ValidationErrors = {};
+ if ((teamId === '') !== (bundleId === '')) {
+ errors['iosAppLinkIncomplete'] = true;
+ }
+ if (fingerprints.length > 0 && packageName === '') {
+ errors['androidAppLinkPackageRequired'] = true;
+ }
+ return Object.keys(errors).length ? errors : null;
+ };
+}
diff --git a/console/src/assets/i18n/ar.json b/console/src/assets/i18n/ar.json
index f43fe3632..9e8cd600f 100644
--- a/console/src/assets/i18n/ar.json
+++ b/console/src/assets/i18n/ar.json
@@ -2610,6 +2610,25 @@
"DEVMODEDESC": "احذر: مع تمكين وضع التطوير، لن يتم التحقق من عناوين URI لإعادة التوجيه.",
"SKIPNATIVEAPPSUCCESSPAGE": "تخطي صفحة نجاح تسجيل الدخول",
"SKIPNATIVEAPPSUCCESSPAGE_DESCRIPTION": "تخطي صفحة النجاح بعد تسجيل الدخول لهذا التطبيق الأصلي.",
+ "APPLINKSSECTION": "روابط التطبيقات الأصلية",
+ "APPLINKSDESC": "قم بتهيئة ارتباط iOS وAndroid حتى تتمكن هذه المثيلة من تقديم ملفات الثقة بـ Passkey لتطبيقاتك الأصلية.",
+ "IOSTITLE": "iOS",
+ "IOSDESC": "تُقدَّم في apple-app-site-association كمدخل webcredentials بالصيغة TEAMID.BUNDLEID.",
+ "TEAMID": "معرّف الفريق",
+ "TEAMIDPLACEHOLDER": "ABCDE12345",
+ "TEAMIDDESC": "يجب أن يكون Apple Team ID مكونًا من 10 أحرف أبجدية رقمية بالضبط، مثل ABCDE12345.",
+ "TEAMIDLENGTH": "يجب أن يكون Team ID مكونًا من 10 أحرف أبجدية رقمية بالضبط.",
+ "BUNDLEID": "معرّف الحزمة",
+ "BUNDLEIDPLACEHOLDER": "com.example.app",
+ "BUNDLEIDDESC": "معرّف حزمة iOS (CFBundleIdentifier)، مثل com.example.app. لا تُدرج بادئة Team ID.",
+ "ANDROIDTITLE": "Android",
+ "ANDROIDDESC": "تُقدَّم في assetlinks.json لبيانات اعتماد Passkey عبر Digital Asset Links.",
+ "PACKAGENAME": "اسم الحزمة",
+ "PACKAGENAMEPLACEHOLDER": "com.example.app",
+ "PACKAGENAMEDESC": "معرّف تطبيق Android / اسم الحزمة (applicationId)، مثل com.example.app.",
+ "FINGERPRINTS": "بصمات شهادة SHA-256",
+ "FINGERPRINTSPLACEHOLDER": "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",
+ "FINGERPRINTSDESC": "بصمة واحدة لكل سطر (64 حرفًا سادس عشريًا، النقطتان اختياريتان). أدرج إصداري التصحيح والإصدار حسب الحاجة.",
"REDIRECT": "عناوين URI لإعادة التوجيه",
"REDIRECTSECTION": "عناوين URI لإعادة التوجيه",
"POSTLOGOUTREDIRECT": "عناوين URI بعد تسجيل الخروج",
@@ -2673,7 +2692,9 @@
}
}
},
- "BACKCHANNELLOOGOUTURI": "عنوان URI لتسجيل الخروج من القناة الخلفية"
+ "BACKCHANNELLOOGOUTURI": "عنوان URI لتسجيل الخروج من القناة الخلفية",
+ "IOSAPPLINKINCOMPLETE": "يجب تعيين معرّف فريق iOS ومعرّف الحزمة معًا، أو تركهما فارغين معًا.",
+ "ANDROIDPACKAGEREQUIRED": "اسم حزمة Android مطلوب عند تعيين بصمات شهادة."
},
"API": {
"INFO": {
diff --git a/console/src/assets/i18n/bg.json b/console/src/assets/i18n/bg.json
index 11a0f3e0c..885d0ccad 100644
--- a/console/src/assets/i18n/bg.json
+++ b/console/src/assets/i18n/bg.json
@@ -2618,6 +2618,25 @@
"DEVMODEDESC": "Внимание: При активиран режим на разработка URI адресите за пренасочване няма да бъдат валидирани.",
"SKIPNATIVEAPPSUCCESSPAGE": "Пропуснете страницата за успешно влизане",
"SKIPNATIVEAPPSUCCESSPAGE_DESCRIPTION": "Пропуснете страницата за успех след влизане в това родно приложение.",
+ "APPLINKSSECTION": "Връзки за нативни приложения",
+ "APPLINKSDESC": "Конфигурирайте iOS и Android асоциации, за да може тази инстанция да предоставя trust файлове за passkeys на вашите нативни приложения.",
+ "IOSTITLE": "iOS",
+ "IOSDESC": "Предоставя се в apple-app-site-association като webcredentials запис TEAMID.BUNDLEID.",
+ "TEAMID": "Team ID",
+ "TEAMIDPLACEHOLDER": "ABCDE12345",
+ "TEAMIDDESC": "Apple Team ID трябва да е точно 10 буквено-цифрови знака, напр. ABCDE12345.",
+ "TEAMIDLENGTH": "Team ID трябва да е точно 10 буквено-цифрови знака.",
+ "BUNDLEID": "Bundle ID",
+ "BUNDLEIDPLACEHOLDER": "com.example.app",
+ "BUNDLEIDDESC": "iOS Bundle Identifier (CFBundleIdentifier), напр. com.example.app. Не включвайте префикса Team ID.",
+ "ANDROIDTITLE": "Android",
+ "ANDROIDDESC": "Предоставя се в assetlinks.json за Digital Asset Links / passkey credentials.",
+ "PACKAGENAME": "Име на пакета",
+ "PACKAGENAMEPLACEHOLDER": "com.example.app",
+ "PACKAGENAMEDESC": "Android applicationId / име на пакет, напр. com.example.app.",
+ "FINGERPRINTS": "SHA-256 сертификатни отпечатъци",
+ "FINGERPRINTSPLACEHOLDER": "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",
+ "FINGERPRINTSDESC": "Един отпечатък на ред (64 шестнадесетични знака, двоеточията са по избор). Добавете debug и release при нужда.",
"REDIRECT": "Пренасочване на URI",
"REDIRECTSECTION": "Пренасочване на URI",
"POSTLOGOUTREDIRECT": "Публикувайте URI адреси за излизане",
@@ -2681,7 +2700,9 @@
}
}
},
- "BACKCHANNELLOOGOUTURI": "Back-Channel Logout URI"
+ "BACKCHANNELLOOGOUTURI": "Back-Channel Logout URI",
+ "IOSAPPLINKINCOMPLETE": "iOS Team ID и Bundle ID трябва да бъдат зададени заедно или и двете да останат празни.",
+ "ANDROIDPACKAGEREQUIRED": "Името на Android пакета е задължително, когато са зададени сертификатни пръстови отпечатъци."
},
"API": {
"INFO": {
diff --git a/console/src/assets/i18n/cs.json b/console/src/assets/i18n/cs.json
index 939e155a7..5b5b22bb8 100644
--- a/console/src/assets/i18n/cs.json
+++ b/console/src/assets/i18n/cs.json
@@ -2600,6 +2600,25 @@
"DEVMODEDESC": "Pozor: S povoleným vývojovým režimem nebudou URI pro přesměrování ověřovány.",
"SKIPNATIVEAPPSUCCESSPAGE": "Přeskočit stránku úspěchu přihlášení",
"SKIPNATIVEAPPSUCCESSPAGE_DESCRIPTION": "Přeskočit stránku úspěchu po přihlášení pro tuto nativní aplikaci.",
+ "APPLINKSSECTION": "Odkazy pro nativní aplikace",
+ "APPLINKSDESC": "Nakonfigurujte asociaci iOS a Android, aby tato instance mohla poskytovat trust soubory pro passkeys vašich nativních aplikací.",
+ "IOSTITLE": "iOS",
+ "IOSDESC": "Poskytováno v apple-app-site-association jako webcredentials položka TEAMID.BUNDLEID.",
+ "TEAMID": "Team ID",
+ "TEAMIDPLACEHOLDER": "ABCDE12345",
+ "TEAMIDDESC": "Apple Team ID musí mít přesně 10 alfanumerických znaků, např. ABCDE12345.",
+ "TEAMIDLENGTH": "Team ID musí mít přesně 10 alfanumerických znaků.",
+ "BUNDLEID": "Bundle ID",
+ "BUNDLEIDPLACEHOLDER": "com.example.app",
+ "BUNDLEIDDESC": "iOS Bundle Identifier (CFBundleIdentifier), např. com.example.app. Nezahrnujte prefix Team ID.",
+ "ANDROIDTITLE": "Android",
+ "ANDROIDDESC": "Poskytováno v assetlinks.json pro Digital Asset Links / passkey credentials.",
+ "PACKAGENAME": "Název balíčku",
+ "PACKAGENAMEPLACEHOLDER": "com.example.app",
+ "PACKAGENAMEDESC": "Android applicationId / název balíčku, např. com.example.app.",
+ "FINGERPRINTS": "SHA-256 otisky certifikátů",
+ "FINGERPRINTSPLACEHOLDER": "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",
+ "FINGERPRINTSDESC": "Jeden otisk na řádek (64 hexadecimálních znaků, dvojtečky volitelně). Podle potřeby uveďte debug i release.",
"REDIRECT": "URI pro přesměrování",
"REDIRECTSECTION": "URI pro přesměrování",
"POSTLOGOUTREDIRECT": "URI po odhlášení",
@@ -2663,7 +2682,9 @@
}
}
},
- "BACKCHANNELLOOGOUTURI": "Back-Channel Logout URI"
+ "BACKCHANNELLOOGOUTURI": "Back-Channel Logout URI",
+ "IOSAPPLINKINCOMPLETE": "iOS Team ID a Bundle ID musí být nastaveny společně, nebo obě ponechány prázdné.",
+ "ANDROIDPACKAGEREQUIRED": "Název balíčku Android je povinný, pokud jsou nastaveny otisky certifikátu."
},
"API": {
"INFO": {
diff --git a/console/src/assets/i18n/de.json b/console/src/assets/i18n/de.json
index f780fb881..269e8f72e 100644
--- a/console/src/assets/i18n/de.json
+++ b/console/src/assets/i18n/de.json
@@ -2591,6 +2591,25 @@
"DEVMODEDESC": "Bei eingeschaltetem Entwicklermodus werden die Weiterleitungs-URIs im OIDC-Flow nicht validiert.",
"SKIPNATIVEAPPSUCCESSPAGE": "Login Erfolgseite überspringen",
"SKIPNATIVEAPPSUCCESSPAGE_DESCRIPTION": "Erfolgseite nach dem Login für diese Native Applikation überspringen.",
+ "APPLINKSSECTION": "Native App Links",
+ "APPLINKSDESC": "Konfigurieren Sie iOS- und Android-Zuordnungen, damit diese Instanz Passkey-Trust-Dateien für Ihre nativen Apps bereitstellen kann.",
+ "IOSTITLE": "iOS",
+ "IOSDESC": "Wird in apple-app-site-association als webcredentials-Eintrag TEAMID.BUNDLEID ausgeliefert.",
+ "TEAMID": "Team-ID",
+ "TEAMIDPLACEHOLDER": "ABCDE12345",
+ "TEAMIDDESC": "Apple Team-ID muss genau 10 alphanumerische Zeichen haben, z. B. ABCDE12345.",
+ "TEAMIDLENGTH": "Team-ID muss genau 10 alphanumerische Zeichen haben.",
+ "BUNDLEID": "Bundle-ID",
+ "BUNDLEIDPLACEHOLDER": "com.example.app",
+ "BUNDLEIDDESC": "iOS Bundle Identifier (CFBundleIdentifier), z. B. com.example.app. Ohne Team-ID-Präfix.",
+ "ANDROIDTITLE": "Android",
+ "ANDROIDDESC": "Wird in assetlinks.json für Digital Asset Links / Passkey-Credentials ausgeliefert.",
+ "PACKAGENAME": "Package Name",
+ "PACKAGENAMEPLACEHOLDER": "com.example.app",
+ "PACKAGENAMEDESC": "Android applicationId / Package Name, z. B. com.example.app.",
+ "FINGERPRINTS": "SHA-256 Zertifikatsfingerabdrücke",
+ "FINGERPRINTSPLACEHOLDER": "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",
+ "FINGERPRINTSDESC": "Ein Fingerabdruck pro Zeile (64 Hex-Zeichen, Doppelpunkte optional). Debug und Release bei Bedarf angeben.",
"REDIRECT": "Weiterleitungs-URIs",
"REDIRECTSECTION": "Weiterleitungs-URIs",
"POSTLOGOUTREDIRECT": "URIs für Post-Log-out",
@@ -2654,7 +2673,9 @@
}
}
},
- "BACKCHANNELLOOGOUTURI": "Back-Channel Logout URI"
+ "BACKCHANNELLOOGOUTURI": "Back-Channel Logout URI",
+ "IOSAPPLINKINCOMPLETE": "iOS Team ID und Bundle ID müssen beide gesetzt oder beide leer sein.",
+ "ANDROIDPACKAGEREQUIRED": "Android-Paketname ist erforderlich, wenn Zertifikat-Fingerprints gesetzt sind."
},
"API": {
"INFO": {
diff --git a/console/src/assets/i18n/en.json b/console/src/assets/i18n/en.json
index a1669c236..08fceab66 100644
--- a/console/src/assets/i18n/en.json
+++ b/console/src/assets/i18n/en.json
@@ -2622,6 +2622,27 @@
"DEVMODEDESC": "Beware: With development mode enabled redirect URIs will not be validated.",
"SKIPNATIVEAPPSUCCESSPAGE": "Skip Login Success Page",
"SKIPNATIVEAPPSUCCESSPAGE_DESCRIPTION": "Skip the success page after a login for this native app.",
+ "APPLINKSSECTION": "Native App Links",
+ "APPLINKSDESC": "Configure iOS and Android association so this instance can serve passkey trust files for your native apps.",
+ "IOSTITLE": "iOS",
+ "IOSDESC": "Served in apple-app-site-association as a webcredentials entry TEAMID.BUNDLEID.",
+ "TEAMID": "Team ID",
+ "TEAMIDPLACEHOLDER": "ABCDE12345",
+ "TEAMIDDESC": "Apple Team ID must be exactly 10 alphanumeric characters, e.g. ABCDE12345.",
+ "TEAMIDLENGTH": "Team ID must be exactly 10 alphanumeric characters.",
+ "BUNDLEID": "Bundle ID",
+ "BUNDLEIDPLACEHOLDER": "com.example.app",
+ "BUNDLEIDDESC": "iOS Bundle Identifier (CFBundleIdentifier), e.g. com.example.app. Do not include the Team ID prefix.",
+ "ANDROIDTITLE": "Android",
+ "ANDROIDDESC": "Served in assetlinks.json for Digital Asset Links / passkey credentials.",
+ "PACKAGENAME": "Package Name",
+ "PACKAGENAMEPLACEHOLDER": "com.example.app",
+ "PACKAGENAMEDESC": "Android applicationId / package name, e.g. com.example.app.",
+ "FINGERPRINTS": "SHA-256 Certificate Fingerprints",
+ "FINGERPRINTSPLACEHOLDER": "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",
+ "FINGERPRINTSDESC": "One fingerprint per line (64 hex characters, optional colons). Include debug and release as needed.",
+ "IOSAPPLINKINCOMPLETE": "iOS Team ID and Bundle ID must both be set, or both left empty.",
+ "ANDROIDPACKAGEREQUIRED": "Android package name is required when certificate fingerprints are set.",
"REDIRECT": "Redirect URIs",
"REDIRECTSECTION": "Redirect URIs",
"POSTLOGOUTREDIRECT": "Post Logout URIs",
diff --git a/console/src/assets/i18n/es.json b/console/src/assets/i18n/es.json
index 4304e2805..107a67097 100644
--- a/console/src/assets/i18n/es.json
+++ b/console/src/assets/i18n/es.json
@@ -2588,6 +2588,25 @@
"DEVMODEDESC": "Cuidado: Si el modo de desarrollo está activado las URIs de redirección no serán validadas.",
"SKIPNATIVEAPPSUCCESSPAGE": "Saltar página de inicio de sesión con éxito",
"SKIPNATIVEAPPSUCCESSPAGE_DESCRIPTION": "Sáltate la página de éxito después de iniciar sesión en esta app nativa.",
+ "APPLINKSSECTION": "Enlaces de aplicaciones nativas",
+ "APPLINKSDESC": "Configure la asociación de iOS y Android para que esta instancia pueda servir archivos de confianza de passkeys para sus aplicaciones nativas.",
+ "IOSTITLE": "iOS",
+ "IOSDESC": "Se sirve en apple-app-site-association como entrada webcredentials TEAMID.BUNDLEID.",
+ "TEAMID": "Team ID",
+ "TEAMIDPLACEHOLDER": "ABCDE12345",
+ "TEAMIDDESC": "El Apple Team ID debe tener exactamente 10 caracteres alfanuméricos, p. ej. ABCDE12345.",
+ "TEAMIDLENGTH": "El Team ID debe tener exactamente 10 caracteres alfanuméricos.",
+ "BUNDLEID": "Bundle ID",
+ "BUNDLEIDPLACEHOLDER": "com.example.app",
+ "BUNDLEIDDESC": "Identificador de paquete de iOS (CFBundleIdentifier), p. ej. com.example.app. No incluya el prefijo del Team ID.",
+ "ANDROIDTITLE": "Android",
+ "ANDROIDDESC": "Se sirve en assetlinks.json para Digital Asset Links / credenciales de passkey.",
+ "PACKAGENAME": "Nombre del paquete",
+ "PACKAGENAMEPLACEHOLDER": "com.example.app",
+ "PACKAGENAMEDESC": "applicationId / nombre del paquete de Android, p. ej. com.example.app.",
+ "FINGERPRINTS": "Huellas SHA-256 del certificado",
+ "FINGERPRINTSPLACEHOLDER": "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",
+ "FINGERPRINTSDESC": "Una huella por línea (64 caracteres hexadecimales, los dos puntos son opcionales). Incluya debug y release según sea necesario.",
"REDIRECT": "URIs de redirección",
"REDIRECTSECTION": "URIs de redirección",
"POSTLOGOUTREDIRECT": "URIs a usar tras cerrar sesión",
@@ -2651,7 +2670,9 @@
}
}
},
- "BACKCHANNELLOOGOUTURI": "Back-Channel Logout URI"
+ "BACKCHANNELLOOGOUTURI": "Back-Channel Logout URI",
+ "IOSAPPLINKINCOMPLETE": "El Team ID y el Bundle ID de iOS deben estar ambos configurados, o ambos vacíos.",
+ "ANDROIDPACKAGEREQUIRED": "El nombre del paquete de Android es obligatorio cuando se definen huellas de certificado."
},
"API": {
"INFO": {
diff --git a/console/src/assets/i18n/fr.json b/console/src/assets/i18n/fr.json
index e16a73d4d..fcff99e8a 100644
--- a/console/src/assets/i18n/fr.json
+++ b/console/src/assets/i18n/fr.json
@@ -2592,6 +2592,25 @@
"DEVMODEDESC": "Attention",
"SKIPNATIVEAPPSUCCESSPAGE": "Sauter la page de succès de connexion",
"SKIPNATIVEAPPSUCCESSPAGE_DESCRIPTION": "Sauter la page de succès après la connexion pour cette application native",
+ "APPLINKSSECTION": "Liens d'applications natives",
+ "APPLINKSDESC": "Configurez l'association iOS et Android afin que cette instance puisse servir les fichiers de confiance passkey pour vos applications natives.",
+ "IOSTITLE": "iOS",
+ "IOSDESC": "Servi dans apple-app-site-association comme entrée webcredentials TEAMID.BUNDLEID.",
+ "TEAMID": "Team ID",
+ "TEAMIDPLACEHOLDER": "ABCDE12345",
+ "TEAMIDDESC": "L'Apple Team ID doit comporter exactement 10 caractères alphanumériques, p. ex. ABCDE12345.",
+ "TEAMIDLENGTH": "L'ID d'équipe doit comporter exactement 10 caractères alphanumériques.",
+ "BUNDLEID": "Bundle ID",
+ "BUNDLEIDPLACEHOLDER": "com.example.app",
+ "BUNDLEIDDESC": "Identifiant de bundle iOS (CFBundleIdentifier), p. ex. com.example.app. N'incluez pas le préfixe Team ID.",
+ "ANDROIDTITLE": "Android",
+ "ANDROIDDESC": "Servi dans assetlinks.json pour Digital Asset Links / identifiants passkey.",
+ "PACKAGENAME": "Nom du package",
+ "PACKAGENAMEPLACEHOLDER": "com.example.app",
+ "PACKAGENAMEDESC": "applicationId / nom de package Android, p. ex. com.example.app.",
+ "FINGERPRINTS": "Empreintes de certificat SHA-256",
+ "FINGERPRINTSPLACEHOLDER": "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",
+ "FINGERPRINTSDESC": "Une empreinte par ligne (64 caractères hexadécimaux, deux-points optionnels). Incluez debug et release si nécessaire.",
"REDIRECT": "Rediriger les URIs",
"REDIRECTSECTION": "URIs de redirection",
"POSTLOGOUTREDIRECT": "URIs de post-déconnexion",
@@ -2655,7 +2674,9 @@
}
}
},
- "BACKCHANNELLOOGOUTURI": "Back-Channel Logout URI"
+ "BACKCHANNELLOOGOUTURI": "Back-Channel Logout URI",
+ "IOSAPPLINKINCOMPLETE": "L'ID d'équipe iOS et le Bundle ID doivent tous deux être renseignés, ou tous deux laissés vides.",
+ "ANDROIDPACKAGEREQUIRED": "Le nom de package Android est requis lorsque des empreintes de certificat sont définies."
},
"API": {
"INFO": {
diff --git a/console/src/assets/i18n/hu.json b/console/src/assets/i18n/hu.json
index eee5d0361..d32e7eebd 100644
--- a/console/src/assets/i18n/hu.json
+++ b/console/src/assets/i18n/hu.json
@@ -2620,6 +2620,25 @@
"DEVMODEDESC": "Figyelem: Ha a fejlesztői mód be van kapcsolva, a redirect URI-k nem lesznek érvényesítve.",
"SKIPNATIVEAPPSUCCESSPAGE": "Login sikeroldal kihagyása",
"SKIPNATIVEAPPSUCCESSPAGE_DESCRIPTION": "Hagyd ki a sikeroldalt a bejelentkezés után ennél a helyi alkalmazásnál.",
+ "APPLINKSSECTION": "Natív alkalmazáshivatkozások",
+ "APPLINKSDESC": "Állítsa be az iOS és Android társítást, hogy ez a példány passkey trust fájlokat szolgálhasson ki a natív alkalmazásaihoz.",
+ "IOSTITLE": "iOS",
+ "IOSDESC": "Az apple-app-site-association fájlban webcredentials bejegyzésként jelenik meg: TEAMID.BUNDLEID.",
+ "TEAMID": "Team ID",
+ "TEAMIDPLACEHOLDER": "ABCDE12345",
+ "TEAMIDDESC": "Az Apple Team ID pontosan 10 alfanumerikus karakterből álljon, pl. ABCDE12345.",
+ "TEAMIDLENGTH": "A Team ID pontosan 10 alfanumerikus karakterből álljon.",
+ "BUNDLEID": "Bundle ID",
+ "BUNDLEIDPLACEHOLDER": "com.example.app",
+ "BUNDLEIDDESC": "iOS Bundle Identifier (CFBundleIdentifier), pl. com.example.app. Ne tartalmazza a Team ID előtagot.",
+ "ANDROIDTITLE": "Android",
+ "ANDROIDDESC": "Az assetlinks.json fájlban Digital Asset Links / passkey hitelesítő adatokhoz szolgál.",
+ "PACKAGENAME": "Csomagnév",
+ "PACKAGENAMEPLACEHOLDER": "com.example.app",
+ "PACKAGENAMEDESC": "Android applicationId / csomagnév, pl. com.example.app.",
+ "FINGERPRINTS": "SHA-256 tanúsítvány ujjlenyomatok",
+ "FINGERPRINTSPLACEHOLDER": "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",
+ "FINGERPRINTSDESC": "Soronként egy ujjlenyomat (64 hexadecimális karakter, a kettőspontok opcionálisak). Szükség szerint adja meg a debug és release értékeket.",
"REDIRECT": "Redirect URIs",
"REDIRECTSECTION": "Redirect URIs",
"POSTLOGOUTREDIRECT": "Kijelentkezés utáni URIs",
@@ -2683,7 +2702,9 @@
}
}
},
- "BACKCHANNELLOOGOUTURI": "Back-Channel Logout URI"
+ "BACKCHANNELLOOGOUTURI": "Back-Channel Logout URI",
+ "IOSAPPLINKINCOMPLETE": "Az iOS Team ID-t és Bundle ID-t együtt kell megadni, vagy mindkettőt üresen hagyni.",
+ "ANDROIDPACKAGEREQUIRED": "Android csomagnév kötelező, ha tanúsítvány ujjlenyomatok vannak megadva."
},
"API": {
"INFO": {
diff --git a/console/src/assets/i18n/id.json b/console/src/assets/i18n/id.json
index a09066f8b..bcccf186b 100644
--- a/console/src/assets/i18n/id.json
+++ b/console/src/assets/i18n/id.json
@@ -2614,6 +2614,25 @@
"DEVMODEDESC": "Hati-hati: Dengan mengaktifkan mode pengembangan, URI pengalihan tidak akan divalidasi.",
"SKIPNATIVEAPPSUCCESSPAGE": "Lewati Halaman Sukses Login",
"SKIPNATIVEAPPSUCCESSPAGE_DESCRIPTION": "Lewati halaman sukses setelah login untuk aplikasi asli ini.",
+ "APPLINKSSECTION": "Tautan Aplikasi Native",
+ "APPLINKSDESC": "Konfigurasikan asosiasi iOS dan Android agar instance ini dapat menyajikan file kepercayaan passkey untuk aplikasi native Anda.",
+ "IOSTITLE": "iOS",
+ "IOSDESC": "Disajikan di apple-app-site-association sebagai entri webcredentials TEAMID.BUNDLEID.",
+ "TEAMID": "Team ID",
+ "TEAMIDPLACEHOLDER": "ABCDE12345",
+ "TEAMIDDESC": "Apple Team ID harus tepat 10 karakter alfanumerik, mis. ABCDE12345.",
+ "TEAMIDLENGTH": "Team ID harus tepat 10 karakter alfanumerik.",
+ "BUNDLEID": "Bundle ID",
+ "BUNDLEIDPLACEHOLDER": "com.example.app",
+ "BUNDLEIDDESC": "iOS Bundle Identifier (CFBundleIdentifier), mis. com.example.app. Jangan sertakan awalan Team ID.",
+ "ANDROIDTITLE": "Android",
+ "ANDROIDDESC": "Disajikan di assetlinks.json untuk Digital Asset Links / kredensial passkey.",
+ "PACKAGENAME": "Nama Paket",
+ "PACKAGENAMEPLACEHOLDER": "com.example.app",
+ "PACKAGENAMEDESC": "Android applicationId / nama paket, mis. com.example.app.",
+ "FINGERPRINTS": "Sidik Jari Sertifikat SHA-256",
+ "FINGERPRINTSPLACEHOLDER": "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",
+ "FINGERPRINTSDESC": "Satu sidik jari per baris (64 karakter heksadesimal, titik dua opsional). Sertakan debug dan release sesuai kebutuhan.",
"REDIRECT": "Arahkan ulang URI",
"REDIRECTSECTION": "Arahkan ulang URI",
"POSTLOGOUTREDIRECT": "URI Posting Logout",
@@ -2677,7 +2696,9 @@
}
}
},
- "BACKCHANNELLOOGOUTURI": "Back-Channel Logout URI"
+ "BACKCHANNELLOOGOUTURI": "Back-Channel Logout URI",
+ "IOSAPPLINKINCOMPLETE": "Team ID dan Bundle ID iOS harus keduanya diisi, atau keduanya dikosongkan.",
+ "ANDROIDPACKAGEREQUIRED": "Nama paket Android wajib diisi bila sidik jari sertifikat diatur."
},
"API": {
"INFO": {
diff --git a/console/src/assets/i18n/it.json b/console/src/assets/i18n/it.json
index 25efe6954..1b97c3147 100644
--- a/console/src/assets/i18n/it.json
+++ b/console/src/assets/i18n/it.json
@@ -2592,6 +2592,25 @@
"DEVMODEDESC": "Attenzione: Con la modalità di sviluppo abilitata, gli URI di reindirizzamento non saranno convalidati.",
"SKIPNATIVEAPPSUCCESSPAGE": "Salta la pagina di successo dopo il login",
"SKIPNATIVEAPPSUCCESSPAGE_DESCRIPTION": "Salta la pagina di successo dopo il login per questa applicazione nativa",
+ "APPLINKSSECTION": "Collegamenti app native",
+ "APPLINKSDESC": "Configura l'associazione iOS e Android in modo che questa istanza possa fornire i file di trust passkey per le tue app native.",
+ "IOSTITLE": "iOS",
+ "IOSDESC": "Fornito in apple-app-site-association come voce webcredentials TEAMID.BUNDLEID.",
+ "TEAMID": "Team ID",
+ "TEAMIDPLACEHOLDER": "ABCDE12345",
+ "TEAMIDDESC": "L'Apple Team ID deve contenere esattamente 10 caratteri alfanumerici, ad es. ABCDE12345.",
+ "TEAMIDLENGTH": "Il Team ID deve contenere esattamente 10 caratteri alfanumerici.",
+ "BUNDLEID": "Bundle ID",
+ "BUNDLEIDPLACEHOLDER": "com.example.app",
+ "BUNDLEIDDESC": "Bundle Identifier iOS (CFBundleIdentifier), ad es. com.example.app. Non includere il prefisso Team ID.",
+ "ANDROIDTITLE": "Android",
+ "ANDROIDDESC": "Fornito in assetlinks.json per Digital Asset Links / credenziali passkey.",
+ "PACKAGENAME": "Nome del package",
+ "PACKAGENAMEPLACEHOLDER": "com.example.app",
+ "PACKAGENAMEDESC": "applicationId / nome del package Android, ad es. com.example.app.",
+ "FINGERPRINTS": "Impronte del certificato SHA-256",
+ "FINGERPRINTSPLACEHOLDER": "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",
+ "FINGERPRINTSDESC": "Un'impronta per riga (64 caratteri esadecimali, i due punti sono opzionali). Includere debug e release se necessario.",
"REDIRECT": "URI per il reindrizzamento",
"REDIRECTSECTION": "Reindirizzamento",
"POSTLOGOUTREDIRECT": "URI post logout",
@@ -2655,7 +2674,9 @@
}
}
},
- "BACKCHANNELLOOGOUTURI": "Back-Channel Logout URI"
+ "BACKCHANNELLOOGOUTURI": "Back-Channel Logout URI",
+ "IOSAPPLINKINCOMPLETE": "Team ID e Bundle ID iOS devono essere entrambi impostati oppure entrambi vuoti.",
+ "ANDROIDPACKAGEREQUIRED": "Il nome del package Android è obbligatorio quando sono impostate le impronte del certificato."
},
"API": {
"INFO": {
diff --git a/console/src/assets/i18n/ja.json b/console/src/assets/i18n/ja.json
index dabf404f7..6a3a2048d 100644
--- a/console/src/assets/i18n/ja.json
+++ b/console/src/assets/i18n/ja.json
@@ -2617,6 +2617,25 @@
"DEVMODEDESC": "注意:開発モードを有効にすると、URIが認証されません。",
"SKIPNATIVEAPPSUCCESSPAGE": "ログイン後に成功ページをスキップする",
"SKIPNATIVEAPPSUCCESSPAGE_DESCRIPTION": "このネイティブアプリのログイン後に成功ページをスキップする",
+ "APPLINKSSECTION": "ネイティブアプリリンク",
+ "APPLINKSDESC": "このインスタンスがネイティブアプリ向けのパスキー信頼ファイルを提供できるよう、iOS / Android の関連付けを設定します。",
+ "IOSTITLE": "iOS",
+ "IOSDESC": "apple-app-site-association の webcredentials エントリ TEAMID.BUNDLEID として提供されます。",
+ "TEAMID": "チーム ID",
+ "TEAMIDPLACEHOLDER": "ABCDE12345",
+ "TEAMIDDESC": "Apple Team ID は英数字ちょうど 10 文字である必要があります(例: ABCDE12345)。",
+ "TEAMIDLENGTH": "Team ID は英数字ちょうど 10 文字である必要があります。",
+ "BUNDLEID": "バンドル ID",
+ "BUNDLEIDPLACEHOLDER": "com.example.app",
+ "BUNDLEIDDESC": "iOS の Bundle Identifier(CFBundleIdentifier)、例: com.example.app。Team ID プレフィックスは含めないでください。",
+ "ANDROIDTITLE": "Android",
+ "ANDROIDDESC": "Digital Asset Links / パスキー認証情報用に assetlinks.json で提供されます。",
+ "PACKAGENAME": "パッケージ名",
+ "PACKAGENAMEPLACEHOLDER": "com.example.app",
+ "PACKAGENAMEDESC": "Android の applicationId / パッケージ名、例: com.example.app。",
+ "FINGERPRINTS": "SHA-256 証明書フィンガープリント",
+ "FINGERPRINTSPLACEHOLDER": "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",
+ "FINGERPRINTSDESC": "1 行に 1 つ(16 進 64 文字、コロンは任意)。必要に応じてデバッグ用とリリース用を含めてください。",
"REDIRECT": "リダイレクトURI",
"REDIRECTSECTION": "リダイレクトURI",
"POSTLOGOUTREDIRECT": "ログアウトURI",
@@ -2680,7 +2699,9 @@
}
}
},
- "BACKCHANNELLOOGOUTURI": "バックチャネルログアウトURI"
+ "BACKCHANNELLOOGOUTURI": "バックチャネルログアウトURI",
+ "IOSAPPLINKINCOMPLETE": "iOS の Team ID と Bundle ID は両方設定するか、両方とも空にしてください。",
+ "ANDROIDPACKAGEREQUIRED": "証明書フィンガープリントを設定する場合、Android パッケージ名は必須です。"
},
"API": {
"INFO": {
diff --git a/console/src/assets/i18n/ko.json b/console/src/assets/i18n/ko.json
index d4ea7e626..5476f8f89 100644
--- a/console/src/assets/i18n/ko.json
+++ b/console/src/assets/i18n/ko.json
@@ -2616,6 +2616,25 @@
"DEVMODEDESC": "주의: 개발 모드가 활성화된 경우 리디렉션 URI가 검증되지 않습니다.",
"SKIPNATIVEAPPSUCCESSPAGE": "로그인 성공 페이지 건너뛰기",
"SKIPNATIVEAPPSUCCESSPAGE_DESCRIPTION": "네이티브 앱 로그인 후 성공 페이지를 건너뜁니다.",
+ "APPLINKSSECTION": "네이티브 앱 링크",
+ "APPLINKSDESC": "이 인스턴스가 네이티브 앱용 패스키 신뢰 파일을 제공할 수 있도록 iOS 및 Android 연결을 구성합니다.",
+ "IOSTITLE": "iOS",
+ "IOSDESC": "apple-app-site-association의 webcredentials 항목 TEAMID.BUNDLEID로 제공됩니다.",
+ "TEAMID": "팀 ID",
+ "TEAMIDPLACEHOLDER": "ABCDE12345",
+ "TEAMIDDESC": "Apple Team ID는 영숫자 정확히 10자여야 합니다(예: ABCDE12345).",
+ "TEAMIDLENGTH": "Team ID는 영숫자 정확히 10자여야 합니다.",
+ "BUNDLEID": "번들 ID",
+ "BUNDLEIDPLACEHOLDER": "com.example.app",
+ "BUNDLEIDDESC": "iOS Bundle Identifier(CFBundleIdentifier), 예: com.example.app. Team ID 접두사는 포함하지 마세요.",
+ "ANDROIDTITLE": "Android",
+ "ANDROIDDESC": "Digital Asset Links / 패스키 자격 증명을 위해 assetlinks.json에서 제공됩니다.",
+ "PACKAGENAME": "패키지 이름",
+ "PACKAGENAMEPLACEHOLDER": "com.example.app",
+ "PACKAGENAMEDESC": "Android applicationId / 패키지 이름, 예: com.example.app.",
+ "FINGERPRINTS": "SHA-256 인증서 지문",
+ "FINGERPRINTSPLACEHOLDER": "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",
+ "FINGERPRINTSDESC": "줄당 지문 하나(16진수 64자, 콜론 선택 사항). 필요에 따라 디버그 및 릴리스를 포함하세요.",
"REDIRECT": "리디렉션 URI",
"REDIRECTSECTION": "리디렉션 URI",
"POSTLOGOUTREDIRECT": "로그아웃 후 리디렉션 URI",
@@ -2679,7 +2698,9 @@
}
}
},
- "BACKCHANNELLOOGOUTURI": "백채널 로그아웃 URI"
+ "BACKCHANNELLOOGOUTURI": "백채널 로그아웃 URI",
+ "IOSAPPLINKINCOMPLETE": "iOS Team ID와 Bundle ID는 둘 다 설정하거나 둘 다 비워 두어야 합니다.",
+ "ANDROIDPACKAGEREQUIRED": "인증서 지문이 설정된 경우 Android 패키지 이름이 필요합니다."
},
"API": {
"INFO": {
diff --git a/console/src/assets/i18n/mk.json b/console/src/assets/i18n/mk.json
index d8f8d5062..13510c17e 100644
--- a/console/src/assets/i18n/mk.json
+++ b/console/src/assets/i18n/mk.json
@@ -2588,6 +2588,25 @@
"DEVMODEDESC": "Внимавајте: Со овозможен Development mode, URIs за пренасочување нема да бидат валидирани.",
"SKIPNATIVEAPPSUCCESSPAGE": "Прескокни страница за успешна најава",
"SKIPNATIVEAPPSUCCESSPAGE_DESCRIPTION": "Прескокнете ја страницата за успешна најава за оваа нативна апликација.",
+ "APPLINKSSECTION": "Врски за нативни апликации",
+ "APPLINKSDESC": "Конфигурирајте асоцијација за iOS и Android за оваа инстанца да може да доставува trust датотеки за passkeys на вашите нативни апликации.",
+ "IOSTITLE": "iOS",
+ "IOSDESC": "Се доставува во apple-app-site-association како webcredentials запис TEAMID.BUNDLEID.",
+ "TEAMID": "Team ID",
+ "TEAMIDPLACEHOLDER": "ABCDE12345",
+ "TEAMIDDESC": "Apple Team ID мора да има точно 10 алфанумерички знаци, нпр. ABCDE12345.",
+ "TEAMIDLENGTH": "Team ID мора да има точно 10 алфанумерички знаци.",
+ "BUNDLEID": "Bundle ID",
+ "BUNDLEIDPLACEHOLDER": "com.example.app",
+ "BUNDLEIDDESC": "iOS Bundle Identifier (CFBundleIdentifier), нпр. com.example.app. Не го вклучувајте префиксот Team ID.",
+ "ANDROIDTITLE": "Android",
+ "ANDROIDDESC": "Се доставува во assetlinks.json за Digital Asset Links / passkey credentials.",
+ "PACKAGENAME": "Име на пакет",
+ "PACKAGENAMEPLACEHOLDER": "com.example.app",
+ "PACKAGENAMEDESC": "Android applicationId / име на пакет, нпр. com.example.app.",
+ "FINGERPRINTS": "SHA-256 отпечатоци на сертификат",
+ "FINGERPRINTSPLACEHOLDER": "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",
+ "FINGERPRINTSDESC": "Еден отпечаток по ред (64 хексадецимални знаци, двоточките се опционални). Вклучете debug и release по потреба.",
"REDIRECT": "Redirect URIs",
"REDIRECTSECTION": "Redirect URIs",
"POSTLOGOUTREDIRECT": "URIs по одјавувањето",
@@ -2651,7 +2670,9 @@
}
}
},
- "BACKCHANNELLOOGOUTURI": "Back-Channel Logout URI"
+ "BACKCHANNELLOOGOUTURI": "Back-Channel Logout URI",
+ "IOSAPPLINKINCOMPLETE": "iOS Team ID и Bundle ID мора да се постават заедно, или обете да останат празни.",
+ "ANDROIDPACKAGEREQUIRED": "Името на Android пакетот е задолжително кога се поставени отпечатоци од сертификат."
},
"API": {
"INFO": {
diff --git a/console/src/assets/i18n/nl.json b/console/src/assets/i18n/nl.json
index 9cb338c7f..0479ea20c 100644
--- a/console/src/assets/i18n/nl.json
+++ b/console/src/assets/i18n/nl.json
@@ -2610,6 +2610,25 @@
"DEVMODEDESC": "Let op: Met ingeschakelde ontwikkelingsmodus worden redirect URI's niet gevalideerd.",
"SKIPNATIVEAPPSUCCESSPAGE": "Sla Login Succes Pagina over",
"SKIPNATIVEAPPSUCCESSPAGE_DESCRIPTION": "Sla de succespagina na een login voor deze native app over.",
+ "APPLINKSSECTION": "Native app-koppelingen",
+ "APPLINKSDESC": "Configureer iOS- en Android-associatie zodat deze instance passkey-trustbestanden voor uw native apps kan serveren.",
+ "IOSTITLE": "iOS",
+ "IOSDESC": "Wordt geserveerd in apple-app-site-association als webcredentials-vermelding TEAMID.BUNDLEID.",
+ "TEAMID": "Team-ID",
+ "TEAMIDPLACEHOLDER": "ABCDE12345",
+ "TEAMIDDESC": "Apple Team ID moet precies 10 alfanumerieke tekens zijn, bijv. ABCDE12345.",
+ "TEAMIDLENGTH": "Team ID moet precies 10 alfanumerieke tekens zijn.",
+ "BUNDLEID": "Bundle-ID",
+ "BUNDLEIDPLACEHOLDER": "com.example.app",
+ "BUNDLEIDDESC": "iOS Bundle Identifier (CFBundleIdentifier), bijv. com.example.app. Neem het Team ID-voorvoegsel niet op.",
+ "ANDROIDTITLE": "Android",
+ "ANDROIDDESC": "Wordt geserveerd in assetlinks.json voor Digital Asset Links / passkey-credentials.",
+ "PACKAGENAME": "Pakketnaam",
+ "PACKAGENAMEPLACEHOLDER": "com.example.app",
+ "PACKAGENAMEDESC": "Android applicationId / pakketnaam, bijv. com.example.app.",
+ "FINGERPRINTS": "SHA-256-certificaatvingerafdrukken",
+ "FINGERPRINTSPLACEHOLDER": "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",
+ "FINGERPRINTSDESC": "Eén vingerafdruk per regel (64 hexadecimale tekens, dubbele punten optioneel). Voeg desgewenst debug en release toe.",
"REDIRECT": "Redirect URI's",
"REDIRECTSECTION": "Redirect URI's",
"POSTLOGOUTREDIRECT": "Post Logout URI's",
@@ -2673,7 +2692,9 @@
}
}
},
- "BACKCHANNELLOOGOUTURI": "Back-Channel Logout URI"
+ "BACKCHANNELLOOGOUTURI": "Back-Channel Logout URI",
+ "IOSAPPLINKINCOMPLETE": "iOS Team ID en Bundle ID moeten beide zijn ingesteld, of beide leeg blijven.",
+ "ANDROIDPACKAGEREQUIRED": "Android-pakketnaam is verplicht wanneer certificaatfingerprints zijn ingesteld."
},
"API": {
"INFO": {
diff --git a/console/src/assets/i18n/pl.json b/console/src/assets/i18n/pl.json
index 11806ca69..1f255aea3 100644
--- a/console/src/assets/i18n/pl.json
+++ b/console/src/assets/i18n/pl.json
@@ -2591,6 +2591,25 @@
"DEVMODEDESC": "Uwaga: przy włączonym trybie rozwoju adresy URI przekierowania nie będą sprawdzane.",
"SKIPNATIVEAPPSUCCESSPAGE": "Pomiń stronę sukcesu po zalogowaniu",
"SKIPNATIVEAPPSUCCESSPAGE_DESCRIPTION": "Pomiń stronę sukcesu po zalogowaniu dla tej Natywny aplikację",
+ "APPLINKSSECTION": "Łącza aplikacji natywnych",
+ "APPLINKSDESC": "Skonfiguruj powiązanie iOS i Android, aby ta instancja mogła serwować pliki zaufania passkey dla Twoich aplikacji natywnych.",
+ "IOSTITLE": "iOS",
+ "IOSDESC": "Serwowane w apple-app-site-association jako wpis webcredentials TEAMID.BUNDLEID.",
+ "TEAMID": "Team ID",
+ "TEAMIDPLACEHOLDER": "ABCDE12345",
+ "TEAMIDDESC": "Apple Team ID musi mieć dokładnie 10 znaków alfanumerycznych, np. ABCDE12345.",
+ "TEAMIDLENGTH": "Team ID musi mieć dokładnie 10 znaków alfanumerycznych.",
+ "BUNDLEID": "Bundle ID",
+ "BUNDLEIDPLACEHOLDER": "com.example.app",
+ "BUNDLEIDDESC": "iOS Bundle Identifier (CFBundleIdentifier), np. com.example.app. Nie dołączaj prefiksu Team ID.",
+ "ANDROIDTITLE": "Android",
+ "ANDROIDDESC": "Serwowane w assetlinks.json dla Digital Asset Links / poświadczeń passkey.",
+ "PACKAGENAME": "Nazwa pakietu",
+ "PACKAGENAMEPLACEHOLDER": "com.example.app",
+ "PACKAGENAMEDESC": "Android applicationId / nazwa pakietu, np. com.example.app.",
+ "FINGERPRINTS": "Odciski certyfikatu SHA-256",
+ "FINGERPRINTSPLACEHOLDER": "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",
+ "FINGERPRINTSDESC": "Jeden odcisk w wierszu (64 znaki szesnastkowe, dwukropki opcjonalne). W razie potrzeby dodaj debug i release.",
"REDIRECT": "Adresy URI przekierowania",
"REDIRECTSECTION": "Adresy URI przekierowania",
"POSTLOGOUTREDIRECT": "Adresy URI po wylogowaniu",
@@ -2654,7 +2673,9 @@
}
}
},
- "BACKCHANNELLOOGOUTURI": "Back-Channel Logout URI"
+ "BACKCHANNELLOOGOUTURI": "Back-Channel Logout URI",
+ "IOSAPPLINKINCOMPLETE": "Team ID i Bundle ID iOS muszą być ustawione razem albo oba pozostawione puste.",
+ "ANDROIDPACKAGEREQUIRED": "Nazwa pakietu Android jest wymagana, gdy ustawiono odciski certyfikatu."
},
"API": {
"INFO": {
diff --git a/console/src/assets/i18n/pt.json b/console/src/assets/i18n/pt.json
index e65176774..7184bc7ca 100644
--- a/console/src/assets/i18n/pt.json
+++ b/console/src/assets/i18n/pt.json
@@ -2587,6 +2587,25 @@
"DEVMODEDESC": "Atenção: Com o modo de desenvolvimento habilitado, as URIs de redirecionamento não serão validadas.",
"SKIPNATIVEAPPSUCCESSPAGE": "Pular Página de Sucesso de Login",
"SKIPNATIVEAPPSUCCESSPAGE_DESCRIPTION": "Pule a página de sucesso após o login para este aplicativo nativo.",
+ "APPLINKSSECTION": "Links de aplicativos nativos",
+ "APPLINKSDESC": "Configure a associação de iOS e Android para que esta instância possa servir arquivos de confiança de passkeys para seus aplicativos nativos.",
+ "IOSTITLE": "iOS",
+ "IOSDESC": "Servido em apple-app-site-association como entrada webcredentials TEAMID.BUNDLEID.",
+ "TEAMID": "Team ID",
+ "TEAMIDPLACEHOLDER": "ABCDE12345",
+ "TEAMIDDESC": "O Apple Team ID deve ter exatamente 10 caracteres alfanuméricos, p. ex. ABCDE12345.",
+ "TEAMIDLENGTH": "O Team ID deve ter exatamente 10 caracteres alfanuméricos.",
+ "BUNDLEID": "Bundle ID",
+ "BUNDLEIDPLACEHOLDER": "com.example.app",
+ "BUNDLEIDDESC": "Identificador de pacote iOS (CFBundleIdentifier), p. ex. com.example.app. Não inclua o prefixo do Team ID.",
+ "ANDROIDTITLE": "Android",
+ "ANDROIDDESC": "Servido em assetlinks.json para Digital Asset Links / credenciais de passkey.",
+ "PACKAGENAME": "Nome do pacote",
+ "PACKAGENAMEPLACEHOLDER": "com.example.app",
+ "PACKAGENAMEDESC": "applicationId / nome do pacote Android, p. ex. com.example.app.",
+ "FINGERPRINTS": "Impressões digitais do certificado SHA-256",
+ "FINGERPRINTSPLACEHOLDER": "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",
+ "FINGERPRINTSDESC": "Uma impressão por linha (64 caracteres hexadecimais, dois-pontos opcionais). Inclua debug e release conforme necessário.",
"REDIRECT": "URIs de Redirecionamento",
"REDIRECTSECTION": "URIs de Redirecionamento",
"POSTLOGOUTREDIRECT": "URIs de Pós-Logout",
@@ -2650,7 +2669,9 @@
}
}
},
- "BACKCHANNELLOOGOUTURI": "Back-Channel Logout URI"
+ "BACKCHANNELLOOGOUTURI": "Back-Channel Logout URI",
+ "IOSAPPLINKINCOMPLETE": "O Team ID e o Bundle ID do iOS devem estar ambos definidos, ou ambos vazios.",
+ "ANDROIDPACKAGEREQUIRED": "O nome do pacote Android é obrigatório quando as impressões digitais do certificado estão definidas."
},
"API": {
"INFO": {
diff --git a/console/src/assets/i18n/ro.json b/console/src/assets/i18n/ro.json
index 0a4d7e764..89ba1e100 100644
--- a/console/src/assets/i18n/ro.json
+++ b/console/src/assets/i18n/ro.json
@@ -2612,6 +2612,25 @@
"DEVMODEDESC": "Atenție: Cu modul de dezvoltare activat, URI-urile de redirecționare nu vor fi validate.",
"SKIPNATIVEAPPSUCCESSPAGE": "Omiteți pagina de succes la conectare",
"SKIPNATIVEAPPSUCCESSPAGE_DESCRIPTION": "Omiteți pagina de succes după o conectare pentru această aplicație nativă.",
+ "APPLINKSSECTION": "Linkuri pentru aplicații native",
+ "APPLINKSDESC": "Configurați asocierea iOS și Android pentru ca această instanță să poată servi fișierele de încredere passkey pentru aplicațiile dvs. native.",
+ "IOSTITLE": "iOS",
+ "IOSDESC": "Servit în apple-app-site-association ca intrare webcredentials TEAMID.BUNDLEID.",
+ "TEAMID": "Team ID",
+ "TEAMIDPLACEHOLDER": "ABCDE12345",
+ "TEAMIDDESC": "Apple Team ID trebuie să aibă exact 10 caractere alfanumerice, de ex. ABCDE12345.",
+ "TEAMIDLENGTH": "Team ID trebuie să aibă exact 10 caractere alfanumerice.",
+ "BUNDLEID": "Bundle ID",
+ "BUNDLEIDPLACEHOLDER": "com.example.app",
+ "BUNDLEIDDESC": "Identificatorul de bundle iOS (CFBundleIdentifier), de ex. com.example.app. Nu includeți prefixul Team ID.",
+ "ANDROIDTITLE": "Android",
+ "ANDROIDDESC": "Servit în assetlinks.json pentru Digital Asset Links / acreditări passkey.",
+ "PACKAGENAME": "Numele pachetului",
+ "PACKAGENAMEPLACEHOLDER": "com.example.app",
+ "PACKAGENAMEDESC": "Android applicationId / numele pachetului, de ex. com.example.app.",
+ "FINGERPRINTS": "Amprente de certificat SHA-256",
+ "FINGERPRINTSPLACEHOLDER": "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",
+ "FINGERPRINTSDESC": "O amprentă pe linie (64 caractere hexazecimale, două puncte opționale). Includeți debug și release după nevoie.",
"REDIRECT": "URI-uri de redirecționare",
"REDIRECTSECTION": "URI-uri de redirecționare",
"POSTLOGOUTREDIRECT": "URI-uri de redirecționare după deconectare",
@@ -2675,7 +2694,9 @@
}
}
},
- "BACKCHANNELLOOGOUTURI": "Back-Channel Logout URI"
+ "BACKCHANNELLOOGOUTURI": "Back-Channel Logout URI",
+ "IOSAPPLINKINCOMPLETE": "Team ID și Bundle ID iOS trebuie setate împreună sau ambele lăsate goale.",
+ "ANDROIDPACKAGEREQUIRED": "Numele pachetului Android este obligatoriu când sunt setate amprentele certificatului."
},
"API": {
"INFO": {
diff --git a/console/src/assets/i18n/ru.json b/console/src/assets/i18n/ru.json
index 8531d404a..a0114b9b1 100644
--- a/console/src/assets/i18n/ru.json
+++ b/console/src/assets/i18n/ru.json
@@ -2704,6 +2704,25 @@
"DEVMODEDESC": "Осторожно: При включённом режиме разработки URI перенаправления не будут проверяться.",
"SKIPNATIVEAPPSUCCESSPAGE": "Пропустить страницу успешного входа в систему",
"SKIPNATIVEAPPSUCCESSPAGE_DESCRIPTION": "Пропустите страницу успеха после входа в систему для этого собственного приложения.",
+ "APPLINKSSECTION": "Ссылки нативных приложений",
+ "APPLINKSDESC": "Настройте ассоциацию iOS и Android, чтобы этот инстанс мог отдавать trust-файлы passkey для ваших нативных приложений.",
+ "IOSTITLE": "iOS",
+ "IOSDESC": "Отдаётся в apple-app-site-association как запись webcredentials TEAMID.BUNDLEID.",
+ "TEAMID": "Team ID",
+ "TEAMIDPLACEHOLDER": "ABCDE12345",
+ "TEAMIDDESC": "Apple Team ID должен состоять ровно из 10 буквенно-цифровых символов, напр. ABCDE12345.",
+ "TEAMIDLENGTH": "Team ID должен состоять ровно из 10 буквенно-цифровых символов.",
+ "BUNDLEID": "Bundle ID",
+ "BUNDLEIDPLACEHOLDER": "com.example.app",
+ "BUNDLEIDDESC": "iOS Bundle Identifier (CFBundleIdentifier), напр. com.example.app. Не включайте префикс Team ID.",
+ "ANDROIDTITLE": "Android",
+ "ANDROIDDESC": "Отдаётся в assetlinks.json для Digital Asset Links / учётных данных passkey.",
+ "PACKAGENAME": "Имя пакета",
+ "PACKAGENAMEPLACEHOLDER": "com.example.app",
+ "PACKAGENAMEDESC": "Android applicationId / имя пакета, напр. com.example.app.",
+ "FINGERPRINTS": "Отпечатки сертификата SHA-256",
+ "FINGERPRINTSPLACEHOLDER": "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",
+ "FINGERPRINTSDESC": "Один отпечаток в строке (64 шестнадцатеричных символа, двоеточия необязательны). При необходимости укажите debug и release.",
"REDIRECT": "Перенаправление URI",
"REDIRECTSECTION": "Перенаправление URI",
"POSTLOGOUTREDIRECT": "URI после выхода из системы",
@@ -2767,7 +2786,9 @@
}
}
},
- "BACKCHANNELLOOGOUTURI": "URI выхода из системы через резервный канал"
+ "BACKCHANNELLOOGOUTURI": "URI выхода из системы через резервный канал",
+ "IOSAPPLINKINCOMPLETE": "iOS Team ID и Bundle ID должны быть заданы вместе или оба оставлены пустыми.",
+ "ANDROIDPACKAGEREQUIRED": "Имя пакета Android обязательно, если заданы отпечатки сертификата."
},
"API": {
"INFO": {
diff --git a/console/src/assets/i18n/sv.json b/console/src/assets/i18n/sv.json
index 515382478..1fa5db325 100644
--- a/console/src/assets/i18n/sv.json
+++ b/console/src/assets/i18n/sv.json
@@ -2619,6 +2619,25 @@
"DEVMODEDESC": "Observera: Med utvecklingsläge aktiverat kommer omdirigerings-URI:er inte att valideras.",
"SKIPNATIVEAPPSUCCESSPAGE": "Hoppa över inloggningsframgångssida",
"SKIPNATIVEAPPSUCCESSPAGE_DESCRIPTION": "Hoppa över framgångssidan efter en inloggning för denna inhemska app.",
+ "APPLINKSSECTION": "Länkar för inbyggda appar",
+ "APPLINKSDESC": "Konfigurera iOS- och Android-association så att den här instansen kan servera passkey-trustfiler för dina inbyggda appar.",
+ "IOSTITLE": "iOS",
+ "IOSDESC": "Serveras i apple-app-site-association som webcredentials-posten TEAMID.BUNDLEID.",
+ "TEAMID": "Team-ID",
+ "TEAMIDPLACEHOLDER": "ABCDE12345",
+ "TEAMIDDESC": "Apple Team ID måste vara exakt 10 alfanumeriska tecken, t.ex. ABCDE12345.",
+ "TEAMIDLENGTH": "Team ID måste vara exakt 10 alfanumeriska tecken.",
+ "BUNDLEID": "Bundle-ID",
+ "BUNDLEIDPLACEHOLDER": "com.example.app",
+ "BUNDLEIDDESC": "iOS Bundle Identifier (CFBundleIdentifier), t.ex. com.example.app. Inkludera inte Team ID-prefixet.",
+ "ANDROIDTITLE": "Android",
+ "ANDROIDDESC": "Serveras i assetlinks.json för Digital Asset Links / passkey-uppgifter.",
+ "PACKAGENAME": "Paketnamn",
+ "PACKAGENAMEPLACEHOLDER": "com.example.app",
+ "PACKAGENAMEDESC": "Android applicationId / paketnamn, t.ex. com.example.app.",
+ "FINGERPRINTS": "SHA-256-certifikatfingeravtryck",
+ "FINGERPRINTSPLACEHOLDER": "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",
+ "FINGERPRINTSDESC": "Ett fingeravtryck per rad (64 hexadecimala tecken, kolon valfritt). Inkludera debug och release vid behov.",
"REDIRECT": "Omdirigerings-URI:er",
"REDIRECTSECTION": "Omdirigerings-URI:er",
"POSTLOGOUTREDIRECT": "Post Logout-URI:er",
@@ -2682,7 +2701,9 @@
}
}
},
- "BACKCHANNELLOOGOUTURI": "Back-Channel Logout URI"
+ "BACKCHANNELLOOGOUTURI": "Back-Channel Logout URI",
+ "IOSAPPLINKINCOMPLETE": "iOS Team ID och Bundle ID måste båda anges, eller båda lämnas tomma.",
+ "ANDROIDPACKAGEREQUIRED": "Android-paketnamn krävs när certifikatfingeravtryck anges."
},
"API": {
"INFO": {
diff --git a/console/src/assets/i18n/tr.json b/console/src/assets/i18n/tr.json
index 5538a0c8e..a13aecea4 100644
--- a/console/src/assets/i18n/tr.json
+++ b/console/src/assets/i18n/tr.json
@@ -2620,6 +2620,25 @@
"DEVMODEDESC": "Dikkat: Geliştirme modu etkinleştirildiğinde yönlendirme URI'leri doğrulanmayacaktır.",
"SKIPNATIVEAPPSUCCESSPAGE": "Giriş Başarı Sayfasını Atla",
"SKIPNATIVEAPPSUCCESSPAGE_DESCRIPTION": "Bu yerel uygulama için giriş sonrası başarı sayfasını atla.",
+ "APPLINKSSECTION": "Yerel uygulama bağlantıları",
+ "APPLINKSDESC": "Bu instansın yerel uygulamalarınız için passkey güven dosyalarını sunabilmesi amacıyla iOS ve Android ilişkilendirmesini yapılandırın.",
+ "IOSTITLE": "iOS",
+ "IOSDESC": "apple-app-site-association içinde webcredentials girdisi TEAMID.BUNDLEID olarak sunulur.",
+ "TEAMID": "Team ID",
+ "TEAMIDPLACEHOLDER": "ABCDE12345",
+ "TEAMIDDESC": "Apple Team ID tam olarak 10 alfanümerik karakter olmalıdır, örn. ABCDE12345.",
+ "TEAMIDLENGTH": "Team ID tam olarak 10 alfanümerik karakter olmalıdır.",
+ "BUNDLEID": "Bundle ID",
+ "BUNDLEIDPLACEHOLDER": "com.example.app",
+ "BUNDLEIDDESC": "iOS Bundle Identifier (CFBundleIdentifier), örn. com.example.app. Team ID önekini eklemeyin.",
+ "ANDROIDTITLE": "Android",
+ "ANDROIDDESC": "Digital Asset Links / passkey kimlik bilgileri için assetlinks.json içinde sunulur.",
+ "PACKAGENAME": "Paket adı",
+ "PACKAGENAMEPLACEHOLDER": "com.example.app",
+ "PACKAGENAMEDESC": "Android applicationId / paket adı, örn. com.example.app.",
+ "FINGERPRINTS": "SHA-256 sertifika parmak izleri",
+ "FINGERPRINTSPLACEHOLDER": "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",
+ "FINGERPRINTSDESC": "Satır başına bir parmak izi (64 onaltılık karakter, iki nokta isteğe bağlı). Gerektiğinde debug ve release ekleyin.",
"REDIRECT": "Yönlendirme URI'leri",
"REDIRECTSECTION": "Yönlendirme URI'leri",
"POSTLOGOUTREDIRECT": "Çıkış Sonrası URI'ler",
@@ -2683,7 +2702,9 @@
}
}
},
- "BACKCHANNELLOOGOUTURI": "Back-Channel Logout URI"
+ "BACKCHANNELLOOGOUTURI": "Back-Channel Logout URI",
+ "IOSAPPLINKINCOMPLETE": "iOS Team ID ve Bundle ID ikisi birden ayarlanmalı veya ikisi de boş bırakılmalıdır.",
+ "ANDROIDPACKAGEREQUIRED": "Sertifika parmak izleri ayarlandığında Android paket adı zorunludur."
},
"API": {
"INFO": {
diff --git a/console/src/assets/i18n/uk.json b/console/src/assets/i18n/uk.json
index 5d93f7f65..d58823b99 100644
--- a/console/src/assets/i18n/uk.json
+++ b/console/src/assets/i18n/uk.json
@@ -2622,6 +2622,25 @@
"DEVMODEDESC": "Увага: з увімкненим режимом розробки URI перенаправлення не перевірятимуться.",
"SKIPNATIVEAPPSUCCESSPAGE": "Пропустити сторінку успішного входу",
"SKIPNATIVEAPPSUCCESSPAGE_DESCRIPTION": "Пропустити сторінку успіху після входу для цього нативного додатку.",
+ "APPLINKSSECTION": "Посилання нативних застосунків",
+ "APPLINKSDESC": "Налаштуйте асоціацію iOS та Android, щоб цей інстанс міг віддавати trust-файли passkey для ваших нативних застосунків.",
+ "IOSTITLE": "iOS",
+ "IOSDESC": "Віддається в apple-app-site-association як запис webcredentials TEAMID.BUNDLEID.",
+ "TEAMID": "Team ID",
+ "TEAMIDPLACEHOLDER": "ABCDE12345",
+ "TEAMIDDESC": "Apple Team ID має складатися рівно з 10 буквено-цифрових символів, напр. ABCDE12345.",
+ "TEAMIDLENGTH": "Team ID має складатися рівно з 10 буквено-цифрових символів.",
+ "BUNDLEID": "Bundle ID",
+ "BUNDLEIDPLACEHOLDER": "com.example.app",
+ "BUNDLEIDDESC": "iOS Bundle Identifier (CFBundleIdentifier), напр. com.example.app. Не включайте префікс Team ID.",
+ "ANDROIDTITLE": "Android",
+ "ANDROIDDESC": "Віддається в assetlinks.json для Digital Asset Links / облікових даних passkey.",
+ "PACKAGENAME": "Назва пакета",
+ "PACKAGENAMEPLACEHOLDER": "com.example.app",
+ "PACKAGENAMEDESC": "Android applicationId / назва пакета, напр. com.example.app.",
+ "FINGERPRINTS": "Відбитки сертифіката SHA-256",
+ "FINGERPRINTSPLACEHOLDER": "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",
+ "FINGERPRINTSDESC": "Один відбиток у рядку (64 шістнадцяткові символи, двокрапки необов’язкові). За потреби вкажіть debug і release.",
"REDIRECT": "URI перенаправлення",
"REDIRECTSECTION": "URI перенаправлення",
"POSTLOGOUTREDIRECT": "URI після виходу",
@@ -2685,7 +2704,9 @@
}
}
},
- "BACKCHANNELLOOGOUTURI": "URI виходу з зворотного каналу"
+ "BACKCHANNELLOOGOUTURI": "URI виходу з зворотного каналу",
+ "IOSAPPLINKINCOMPLETE": "iOS Team ID та Bundle ID мають бути задані разом або обидва залишені порожніми.",
+ "ANDROIDPACKAGEREQUIRED": "Назва пакета Android обов’язкова, якщо задано відбитки сертифіката."
},
"API": {
"INFO": {
diff --git a/console/src/assets/i18n/zh.json b/console/src/assets/i18n/zh.json
index e7a9932c6..25cdd9df6 100644
--- a/console/src/assets/i18n/zh.json
+++ b/console/src/assets/i18n/zh.json
@@ -2624,6 +2624,25 @@
"DEVMODEDESC": "注意:启用开发模式的重定向 URI 将不会被验证。",
"SKIPNATIVEAPPSUCCESSPAGE": "登录后跳过成功页面",
"SKIPNATIVEAPPSUCCESSPAGE_DESCRIPTION": "登录后跳过本机应用的成功页面",
+ "APPLINKSSECTION": "原生应用关联",
+ "APPLINKSDESC": "配置 iOS 和 Android 关联,使此实例能够为您的原生应用提供通行密钥信任文件。",
+ "IOSTITLE": "iOS",
+ "IOSDESC": "在 apple-app-site-association 中作为 webcredentials 条目 TEAMID.BUNDLEID 提供。",
+ "TEAMID": "团队 ID",
+ "TEAMIDPLACEHOLDER": "ABCDE12345",
+ "TEAMIDDESC": "Apple Team ID 必须恰好为 10 个字母或数字,例如 ABCDE12345。",
+ "TEAMIDLENGTH": "Team ID 必须恰好为 10 个字母或数字。",
+ "BUNDLEID": "Bundle ID",
+ "BUNDLEIDPLACEHOLDER": "com.example.app",
+ "BUNDLEIDDESC": "iOS Bundle Identifier(CFBundleIdentifier),例如 com.example.app。请勿包含 Team ID 前缀。",
+ "ANDROIDTITLE": "Android",
+ "ANDROIDDESC": "在 assetlinks.json 中为 Digital Asset Links / 通行密钥凭据提供。",
+ "PACKAGENAME": "软件包名称",
+ "PACKAGENAMEPLACEHOLDER": "com.example.app",
+ "PACKAGENAMEDESC": "Android applicationId / 软件包名称,例如 com.example.app。",
+ "FINGERPRINTS": "SHA-256 证书指纹",
+ "FINGERPRINTSPLACEHOLDER": "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",
+ "FINGERPRINTSDESC": "每行一个指纹(64 个十六进制字符,冒号可选)。按需包含调试版和正式版。",
"REDIRECT": "重定向 URLs",
"REDIRECTSECTION": "重定向 URLs",
"POSTLOGOUTREDIRECT": "退出登录重定向 URLs",
@@ -2687,7 +2706,9 @@
}
}
},
- "BACKCHANNELLOOGOUTURI": "反向通道注销URI"
+ "BACKCHANNELLOOGOUTURI": "反向通道注销URI",
+ "IOSAPPLINKINCOMPLETE": "iOS Team ID 与 Bundle ID 必须同时填写,或同时留空。",
+ "ANDROIDPACKAGEREQUIRED": "设置证书指纹时必须填写 Android 软件包名称。"
},
"API": {
"INFO": {
diff --git a/internal/api/grpc/application/v2/convert/oidc_app.go b/internal/api/grpc/application/v2/convert/oidc_app.go
index fc3765a43..a62a84a3a 100644
--- a/internal/api/grpc/application/v2/convert/oidc_app.go
+++ b/internal/api/grpc/application/v2/convert/oidc_app.go
@@ -15,30 +15,36 @@ func CreateOIDCAppRequestToDomain(name, appID, projectID string, req *applicatio
if err != nil {
return nil, err
}
+ iosTeamID, iosBundleID := iosAppLinkConfigToDomain(req.GetIos())
+ androidPackageName, androidFingerprints := androidAppLinkConfigToDomain(req.GetAndroid())
return &domain.OIDCApp{
ObjectRoot: models.ObjectRoot{
AggregateID: projectID,
},
- AppID: appID,
- AppName: name,
- OIDCVersion: gu.Ptr(domain.OIDCVersionV1),
- RedirectUris: req.GetRedirectUris(),
- ResponseTypes: oidcResponseTypesToDomain(req.GetResponseTypes()),
- GrantTypes: oidcGrantTypesToDomain(req.GetGrantTypes()),
- ApplicationType: gu.Ptr(oidcApplicationTypeToDomain(req.GetApplicationType())),
- AuthMethodType: gu.Ptr(oidcAuthMethodTypeToDomain(req.GetAuthMethodType())),
- PostLogoutRedirectUris: req.GetPostLogoutRedirectUris(),
- DevMode: &req.DevelopmentMode,
- AccessTokenType: gu.Ptr(oidcTokenTypeToDomain(req.GetAccessTokenType())),
- AccessTokenRoleAssertion: gu.Ptr(req.GetAccessTokenRoleAssertion()),
- IDTokenRoleAssertion: gu.Ptr(req.GetIdTokenRoleAssertion()),
- IDTokenUserinfoAssertion: gu.Ptr(req.GetIdTokenUserinfoAssertion()),
- ClockSkew: gu.Ptr(req.GetClockSkew().AsDuration()),
- AdditionalOrigins: req.GetAdditionalOrigins(),
- SkipNativeAppSuccessPage: gu.Ptr(req.GetSkipNativeAppSuccessPage()),
- BackChannelLogoutURI: gu.Ptr(req.GetBackChannelLogoutUri()),
- LoginVersion: loginVersion,
- LoginBaseURI: loginBaseURI,
+ AppID: appID,
+ AppName: name,
+ OIDCVersion: gu.Ptr(domain.OIDCVersionV1),
+ RedirectUris: req.GetRedirectUris(),
+ ResponseTypes: oidcResponseTypesToDomain(req.GetResponseTypes()),
+ GrantTypes: oidcGrantTypesToDomain(req.GetGrantTypes()),
+ ApplicationType: gu.Ptr(oidcApplicationTypeToDomain(req.GetApplicationType())),
+ AuthMethodType: gu.Ptr(oidcAuthMethodTypeToDomain(req.GetAuthMethodType())),
+ PostLogoutRedirectUris: req.GetPostLogoutRedirectUris(),
+ DevMode: &req.DevelopmentMode,
+ AccessTokenType: gu.Ptr(oidcTokenTypeToDomain(req.GetAccessTokenType())),
+ AccessTokenRoleAssertion: gu.Ptr(req.GetAccessTokenRoleAssertion()),
+ IDTokenRoleAssertion: gu.Ptr(req.GetIdTokenRoleAssertion()),
+ IDTokenUserinfoAssertion: gu.Ptr(req.GetIdTokenUserinfoAssertion()),
+ ClockSkew: gu.Ptr(req.GetClockSkew().AsDuration()),
+ AdditionalOrigins: req.GetAdditionalOrigins(),
+ SkipNativeAppSuccessPage: gu.Ptr(req.GetSkipNativeAppSuccessPage()),
+ BackChannelLogoutURI: gu.Ptr(req.GetBackChannelLogoutUri()),
+ LoginVersion: loginVersion,
+ LoginBaseURI: loginBaseURI,
+ IOSTeamID: iosTeamID,
+ IOSBundleID: iosBundleID,
+ AndroidPackageName: androidPackageName,
+ AndroidSHA256CertFingerprints: androidFingerprints,
}, nil
}
@@ -47,28 +53,35 @@ func UpdateOIDCAppConfigRequestToDomain(appID, projectID string, app *applicatio
if err != nil {
return nil, err
}
+ // nested messages: omit = no change; present (incl. empty) = set/clear
+ iosTeamID, iosBundleID := iosAppLinkConfigToDomain(app.Ios)
+ androidPackageName, androidFingerprints := androidAppLinkConfigToDomain(app.Android)
return &domain.OIDCApp{
ObjectRoot: models.ObjectRoot{
AggregateID: projectID,
},
- AppID: appID,
- RedirectUris: app.RedirectUris,
- ResponseTypes: oidcResponseTypesToDomain(app.ResponseTypes),
- GrantTypes: oidcGrantTypesToDomain(app.GrantTypes),
- ApplicationType: oidcApplicationTypeToDomainPtr(app.ApplicationType),
- AuthMethodType: oidcAuthMethodTypeToDomainPtr(app.AuthMethodType),
- PostLogoutRedirectUris: app.PostLogoutRedirectUris,
- DevMode: app.DevelopmentMode,
- AccessTokenType: oidcTokenTypeToDomainPtr(app.AccessTokenType),
- AccessTokenRoleAssertion: app.AccessTokenRoleAssertion,
- IDTokenRoleAssertion: app.IdTokenRoleAssertion,
- IDTokenUserinfoAssertion: app.IdTokenUserinfoAssertion,
- ClockSkew: gu.Ptr(app.GetClockSkew().AsDuration()),
- AdditionalOrigins: app.AdditionalOrigins,
- SkipNativeAppSuccessPage: app.SkipNativeAppSuccessPage,
- BackChannelLogoutURI: app.BackChannelLogoutUri,
- LoginVersion: loginVersion,
- LoginBaseURI: loginBaseURI,
+ AppID: appID,
+ RedirectUris: app.RedirectUris,
+ ResponseTypes: oidcResponseTypesToDomain(app.ResponseTypes),
+ GrantTypes: oidcGrantTypesToDomain(app.GrantTypes),
+ ApplicationType: oidcApplicationTypeToDomainPtr(app.ApplicationType),
+ AuthMethodType: oidcAuthMethodTypeToDomainPtr(app.AuthMethodType),
+ PostLogoutRedirectUris: app.PostLogoutRedirectUris,
+ DevMode: app.DevelopmentMode,
+ AccessTokenType: oidcTokenTypeToDomainPtr(app.AccessTokenType),
+ AccessTokenRoleAssertion: app.AccessTokenRoleAssertion,
+ IDTokenRoleAssertion: app.IdTokenRoleAssertion,
+ IDTokenUserinfoAssertion: app.IdTokenUserinfoAssertion,
+ ClockSkew: gu.Ptr(app.GetClockSkew().AsDuration()),
+ AdditionalOrigins: app.AdditionalOrigins,
+ SkipNativeAppSuccessPage: app.SkipNativeAppSuccessPage,
+ BackChannelLogoutURI: app.BackChannelLogoutUri,
+ LoginVersion: loginVersion,
+ LoginBaseURI: loginBaseURI,
+ IOSTeamID: iosTeamID,
+ IOSBundleID: iosBundleID,
+ AndroidPackageName: androidPackageName,
+ AndroidSHA256CertFingerprints: androidFingerprints,
}, nil
}
@@ -213,10 +226,50 @@ func appOIDCConfigToPb(oidcApp *query.OIDCApp) *application.Application_OidcConf
SkipNativeAppSuccessPage: oidcApp.SkipNativeAppSuccessPage,
BackChannelLogoutUri: oidcApp.BackChannelLogoutURI,
LoginVersion: loginVersionToPb(oidcApp.LoginVersion, oidcApp.LoginBaseURI),
+ Ios: iosAppLinkConfigToPb(oidcApp.IOSTeamID, oidcApp.IOSBundleID),
+ Android: androidAppLinkConfigToPb(oidcApp.AndroidPackageName, oidcApp.AndroidSHA256CertFingerprints),
},
}
}
+func iosAppLinkConfigToDomain(cfg *application.IOSAppLinkConfig) (teamID, bundleID *string) {
+ if cfg == nil {
+ return nil, nil
+ }
+ return gu.Ptr(cfg.GetTeamId()), gu.Ptr(cfg.GetBundleId())
+}
+
+func androidAppLinkConfigToDomain(cfg *application.AndroidAppLinkConfig) (packageName *string, fingerprints []string) {
+ if cfg == nil {
+ return nil, nil
+ }
+ fps := cfg.GetSha256CertFingerprints()
+ if fps == nil {
+ fps = []string{}
+ }
+ return gu.Ptr(cfg.GetPackageName()), fps
+}
+
+func iosAppLinkConfigToPb(teamID, bundleID string) *application.IOSAppLinkConfig {
+ if teamID == "" && bundleID == "" {
+ return nil
+ }
+ return &application.IOSAppLinkConfig{
+ TeamId: teamID,
+ BundleId: bundleID,
+ }
+}
+
+func androidAppLinkConfigToPb(packageName string, fingerprints []string) *application.AndroidAppLinkConfig {
+ if packageName == "" && len(fingerprints) == 0 {
+ return nil
+ }
+ return &application.AndroidAppLinkConfig{
+ PackageName: packageName,
+ Sha256CertFingerprints: fingerprints,
+ }
+}
+
func oidcResponseTypesFromModel(responseTypes []domain.OIDCResponseType) []application.OIDCResponseType {
oidcResponseTypes := make([]application.OIDCResponseType, len(responseTypes))
for i, responseType := range responseTypes {
diff --git a/internal/api/grpc/application/v2/convert/oidc_app_test.go b/internal/api/grpc/application/v2/convert/oidc_app_test.go
index 58ce20ed2..b4d96f521 100644
--- a/internal/api/grpc/application/v2/convert/oidc_app_test.go
+++ b/internal/api/grpc/application/v2/convert/oidc_app_test.go
@@ -66,29 +66,41 @@ func TestCreateOIDCAppRequestToDomain(t *testing.T) {
LoginVersion: &application.LoginVersion{Version: &application.LoginVersion_LoginV2{LoginV2: &application.LoginV2{
BaseUri: gu.Ptr("https://login"),
}}},
+ Ios: &application.IOSAppLinkConfig{
+ TeamId: "TEAMID",
+ BundleId: "com.example.app",
+ },
+ Android: &application.AndroidAppLinkConfig{
+ PackageName: "com.example.app",
+ Sha256CertFingerprints: []string{"AA:BB:CC"},
+ },
},
expectedModel: &domain.OIDCApp{
- ObjectRoot: models.ObjectRoot{AggregateID: "project1"},
- AppName: "all fields set",
- AppID: "app1",
- OIDCVersion: gu.Ptr(domain.OIDCVersionV1),
- RedirectUris: []string{"https://redirect"},
- ResponseTypes: []domain.OIDCResponseType{domain.OIDCResponseTypeCode},
- GrantTypes: []domain.OIDCGrantType{domain.OIDCGrantTypeAuthorizationCode},
- ApplicationType: gu.Ptr(domain.OIDCApplicationTypeWeb),
- AuthMethodType: gu.Ptr(domain.OIDCAuthMethodTypeBasic),
- PostLogoutRedirectUris: []string{"https://logout"},
- DevMode: gu.Ptr(true),
- AccessTokenType: gu.Ptr(domain.OIDCTokenTypeBearer),
- AccessTokenRoleAssertion: gu.Ptr(true),
- IDTokenRoleAssertion: gu.Ptr(true),
- IDTokenUserinfoAssertion: gu.Ptr(true),
- ClockSkew: gu.Ptr(5 * time.Second),
- AdditionalOrigins: []string{"https://origin"},
- SkipNativeAppSuccessPage: gu.Ptr(true),
- BackChannelLogoutURI: gu.Ptr("https://backchannel"),
- LoginVersion: gu.Ptr(domain.LoginVersion2),
- LoginBaseURI: gu.Ptr("https://login"),
+ ObjectRoot: models.ObjectRoot{AggregateID: "project1"},
+ AppName: "all fields set",
+ AppID: "app1",
+ OIDCVersion: gu.Ptr(domain.OIDCVersionV1),
+ RedirectUris: []string{"https://redirect"},
+ ResponseTypes: []domain.OIDCResponseType{domain.OIDCResponseTypeCode},
+ GrantTypes: []domain.OIDCGrantType{domain.OIDCGrantTypeAuthorizationCode},
+ ApplicationType: gu.Ptr(domain.OIDCApplicationTypeWeb),
+ AuthMethodType: gu.Ptr(domain.OIDCAuthMethodTypeBasic),
+ PostLogoutRedirectUris: []string{"https://logout"},
+ DevMode: gu.Ptr(true),
+ AccessTokenType: gu.Ptr(domain.OIDCTokenTypeBearer),
+ AccessTokenRoleAssertion: gu.Ptr(true),
+ IDTokenRoleAssertion: gu.Ptr(true),
+ IDTokenUserinfoAssertion: gu.Ptr(true),
+ ClockSkew: gu.Ptr(5 * time.Second),
+ AdditionalOrigins: []string{"https://origin"},
+ SkipNativeAppSuccessPage: gu.Ptr(true),
+ BackChannelLogoutURI: gu.Ptr("https://backchannel"),
+ LoginVersion: gu.Ptr(domain.LoginVersion2),
+ LoginBaseURI: gu.Ptr("https://login"),
+ IOSTeamID: gu.Ptr("TEAMID"),
+ IOSBundleID: gu.Ptr("com.example.app"),
+ AndroidPackageName: gu.Ptr("com.example.app"),
+ AndroidSHA256CertFingerprints: []string{"AA:BB:CC"},
},
},
}
@@ -182,6 +194,71 @@ func TestUpdateOIDCAppConfigRequestToDomain(t *testing.T) {
LoginBaseURI: gu.Ptr("https://login"),
},
},
+ {
+ testName: "omit app links leaves nil",
+ appID: "app1",
+ projectID: "proj1",
+ req: &application.UpdateOIDCApplicationConfigurationRequest{},
+ expectedModel: &domain.OIDCApp{
+ ObjectRoot: models.ObjectRoot{AggregateID: "proj1"},
+ AppID: "app1",
+ ResponseTypes: []domain.OIDCResponseType{domain.OIDCResponseTypeCode},
+ GrantTypes: []domain.OIDCGrantType{domain.OIDCGrantTypeAuthorizationCode},
+ ClockSkew: gu.Ptr(time.Duration(0)),
+ LoginVersion: gu.Ptr(domain.LoginVersionUnspecified),
+ LoginBaseURI: gu.Ptr(""),
+ },
+ },
+ {
+ testName: "set app links",
+ appID: "app1",
+ projectID: "proj1",
+ req: &application.UpdateOIDCApplicationConfigurationRequest{
+ Ios: &application.IOSAppLinkConfig{
+ TeamId: "TEAMID",
+ BundleId: "com.example.app",
+ },
+ Android: &application.AndroidAppLinkConfig{
+ PackageName: "com.example.app",
+ Sha256CertFingerprints: []string{"AA:BB:CC"},
+ },
+ },
+ expectedModel: &domain.OIDCApp{
+ ObjectRoot: models.ObjectRoot{AggregateID: "proj1"},
+ AppID: "app1",
+ ResponseTypes: []domain.OIDCResponseType{domain.OIDCResponseTypeCode},
+ GrantTypes: []domain.OIDCGrantType{domain.OIDCGrantTypeAuthorizationCode},
+ ClockSkew: gu.Ptr(time.Duration(0)),
+ LoginVersion: gu.Ptr(domain.LoginVersionUnspecified),
+ LoginBaseURI: gu.Ptr(""),
+ IOSTeamID: gu.Ptr("TEAMID"),
+ IOSBundleID: gu.Ptr("com.example.app"),
+ AndroidPackageName: gu.Ptr("com.example.app"),
+ AndroidSHA256CertFingerprints: []string{"AA:BB:CC"},
+ },
+ },
+ {
+ testName: "clear app links with empty messages",
+ appID: "app1",
+ projectID: "proj1",
+ req: &application.UpdateOIDCApplicationConfigurationRequest{
+ Ios: &application.IOSAppLinkConfig{},
+ Android: &application.AndroidAppLinkConfig{},
+ },
+ expectedModel: &domain.OIDCApp{
+ ObjectRoot: models.ObjectRoot{AggregateID: "proj1"},
+ AppID: "app1",
+ ResponseTypes: []domain.OIDCResponseType{domain.OIDCResponseTypeCode},
+ GrantTypes: []domain.OIDCGrantType{domain.OIDCGrantTypeAuthorizationCode},
+ ClockSkew: gu.Ptr(time.Duration(0)),
+ LoginVersion: gu.Ptr(domain.LoginVersionUnspecified),
+ LoginBaseURI: gu.Ptr(""),
+ IOSTeamID: gu.Ptr(""),
+ IOSBundleID: gu.Ptr(""),
+ AndroidPackageName: gu.Ptr(""),
+ AndroidSHA256CertFingerprints: []string{},
+ },
+ },
}
for _, tc := range tt {
@@ -453,26 +530,30 @@ func TestAppOIDCConfigToPb(t *testing.T) {
{
name: "full config",
input: &query.OIDCApp{
- RedirectURIs: []string{"https://example.com/callback"},
- ResponseTypes: []domain.OIDCResponseType{domain.OIDCResponseTypeCode},
- GrantTypes: []domain.OIDCGrantType{domain.OIDCGrantTypeAuthorizationCode},
- AppType: domain.OIDCApplicationTypeWeb,
- ClientID: "client123",
- AuthMethodType: domain.OIDCAuthMethodTypeBasic,
- PostLogoutRedirectURIs: []string{"https://example.com/logout"},
- ComplianceProblems: []string{"problem1", "problem2"},
- IsDevMode: true,
- AccessTokenType: domain.OIDCTokenTypeBearer,
- AssertAccessTokenRole: true,
- AssertIDTokenRole: true,
- AssertIDTokenUserinfo: true,
- ClockSkew: 5 * time.Second,
- AdditionalOrigins: []string{"https://app.example.com"},
- AllowedOrigins: []string{"https://allowed.example.com"},
- SkipNativeAppSuccessPage: true,
- BackChannelLogoutURI: "https://example.com/backchannel",
- LoginVersion: domain.LoginVersion2,
- LoginBaseURI: gu.Ptr("https://login.example.com"),
+ RedirectURIs: []string{"https://example.com/callback"},
+ ResponseTypes: []domain.OIDCResponseType{domain.OIDCResponseTypeCode},
+ GrantTypes: []domain.OIDCGrantType{domain.OIDCGrantTypeAuthorizationCode},
+ AppType: domain.OIDCApplicationTypeWeb,
+ ClientID: "client123",
+ AuthMethodType: domain.OIDCAuthMethodTypeBasic,
+ PostLogoutRedirectURIs: []string{"https://example.com/logout"},
+ ComplianceProblems: []string{"problem1", "problem2"},
+ IsDevMode: true,
+ AccessTokenType: domain.OIDCTokenTypeBearer,
+ AssertAccessTokenRole: true,
+ AssertIDTokenRole: true,
+ AssertIDTokenUserinfo: true,
+ ClockSkew: 5 * time.Second,
+ AdditionalOrigins: []string{"https://app.example.com"},
+ AllowedOrigins: []string{"https://allowed.example.com"},
+ SkipNativeAppSuccessPage: true,
+ BackChannelLogoutURI: "https://example.com/backchannel",
+ LoginVersion: domain.LoginVersion2,
+ LoginBaseURI: gu.Ptr("https://login.example.com"),
+ IOSTeamID: "TEAMID",
+ IOSBundleID: "com.example.app",
+ AndroidPackageName: "com.example.app",
+ AndroidSHA256CertFingerprints: []string{"AA:BB:CC"},
},
expected: &application.Application_OidcConfiguration{
OidcConfiguration: &application.OIDCConfiguration{
@@ -506,6 +587,14 @@ func TestAppOIDCConfigToPb(t *testing.T) {
},
},
},
+ Ios: &application.IOSAppLinkConfig{
+ TeamId: "TEAMID",
+ BundleId: "com.example.app",
+ },
+ Android: &application.AndroidAppLinkConfig{
+ PackageName: "com.example.app",
+ Sha256CertFingerprints: []string{"AA:BB:CC"},
+ },
},
},
},
diff --git a/internal/api/grpc/application/v2/integration_test/app_link_test.go b/internal/api/grpc/application/v2/integration_test/app_link_test.go
new file mode 100644
index 000000000..7a6c38843
--- /dev/null
+++ b/internal/api/grpc/application/v2/integration_test/app_link_test.go
@@ -0,0 +1,159 @@
+//go:build integration
+
+package app_test
+
+import (
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+ "google.golang.org/grpc/codes"
+ "google.golang.org/grpc/status"
+
+ "github.com/zitadel/zitadel/internal/integration"
+ "github.com/zitadel/zitadel/pkg/grpc/application/v2"
+)
+
+func TestOIDCAppLinkConfig(t *testing.T) {
+ p := instance.CreateProject(IAMOwnerCtx, t, instance.DefaultOrg.GetId(), integration.ProjectName(), false, false)
+
+ ios := &application.IOSAppLinkConfig{
+ TeamId: "ABCDE12345",
+ BundleId: "com.example.app",
+ }
+ android := &application.AndroidAppLinkConfig{
+ PackageName: "com.example.app",
+ Sha256CertFingerprints: []string{"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"},
+ }
+
+ t.Run("create with incomplete ios config is rejected", func(t *testing.T) {
+ _, err := instance.Client.ApplicationV2.CreateApplication(IAMOwnerCtx, &application.CreateApplicationRequest{
+ ProjectId: p.GetId(),
+ Name: integration.ApplicationName(),
+ ApplicationType: &application.CreateApplicationRequest_OidcConfiguration{
+ OidcConfiguration: baseOIDCCreateRequest(iosOnlyTeamID()),
+ },
+ })
+ require.Equal(t, codes.InvalidArgument, status.Code(err))
+ })
+
+ t.Run("create get update omit and clear round trip", func(t *testing.T) {
+ created, err := instance.Client.ApplicationV2.CreateApplication(IAMOwnerCtx, &application.CreateApplicationRequest{
+ ProjectId: p.GetId(),
+ Name: integration.ApplicationName(),
+ ApplicationType: &application.CreateApplicationRequest_OidcConfiguration{
+ OidcConfiguration: baseOIDCCreateRequestWithAppLinks(ios, android),
+ },
+ })
+ require.NoError(t, err)
+ require.NotEmpty(t, created.GetApplicationId())
+
+ assertOIDCAppLinks(t, created.GetApplicationId(), ios, android)
+
+ updatedIos := &application.IOSAppLinkConfig{
+ TeamId: "NEWTEAM123",
+ BundleId: "com.example.app.updated",
+ }
+ updatedAndroid := &application.AndroidAppLinkConfig{
+ PackageName: "com.example.app.updated",
+ Sha256CertFingerprints: []string{"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:AA:BB:CC:DD:EE:FF:00"},
+ }
+ _, err = instance.Client.ApplicationV2.UpdateApplication(IAMOwnerCtx, &application.UpdateApplicationRequest{
+ ProjectId: p.GetId(),
+ ApplicationId: created.GetApplicationId(),
+ ApplicationType: &application.UpdateApplicationRequest_OidcConfiguration{
+ OidcConfiguration: &application.UpdateOIDCApplicationConfigurationRequest{
+ Ios: updatedIos,
+ Android: updatedAndroid,
+ },
+ },
+ })
+ require.NoError(t, err)
+ assertOIDCAppLinks(t, created.GetApplicationId(), updatedIos, updatedAndroid)
+
+ _, err = instance.Client.ApplicationV2.UpdateApplication(IAMOwnerCtx, &application.UpdateApplicationRequest{
+ ProjectId: p.GetId(),
+ ApplicationId: created.GetApplicationId(),
+ ApplicationType: &application.UpdateApplicationRequest_OidcConfiguration{
+ OidcConfiguration: &application.UpdateOIDCApplicationConfigurationRequest{
+ PostLogoutRedirectUris: []string{"http://example.com/home-omitted"},
+ },
+ },
+ })
+ require.NoError(t, err)
+ assertOIDCAppLinks(t, created.GetApplicationId(), updatedIos, updatedAndroid)
+
+ _, err = instance.Client.ApplicationV2.UpdateApplication(IAMOwnerCtx, &application.UpdateApplicationRequest{
+ ProjectId: p.GetId(),
+ ApplicationId: created.GetApplicationId(),
+ ApplicationType: &application.UpdateApplicationRequest_OidcConfiguration{
+ OidcConfiguration: &application.UpdateOIDCApplicationConfigurationRequest{
+ Ios: &application.IOSAppLinkConfig{},
+ Android: &application.AndroidAppLinkConfig{},
+ },
+ },
+ })
+ require.NoError(t, err)
+ assertOIDCAppLinks(t, created.GetApplicationId(), nil, nil)
+ })
+}
+
+func baseOIDCCreateRequest(opts ...func(*application.CreateOIDCApplicationRequest)) *application.CreateOIDCApplicationRequest {
+ req := &application.CreateOIDCApplicationRequest{
+ RedirectUris: []string{"http://example.com"},
+ ResponseTypes: []application.OIDCResponseType{application.OIDCResponseType_OIDC_RESPONSE_TYPE_CODE},
+ GrantTypes: []application.OIDCGrantType{application.OIDCGrantType_OIDC_GRANT_TYPE_AUTHORIZATION_CODE},
+ ApplicationType: application.OIDCApplicationType_OIDC_APP_TYPE_NATIVE,
+ AuthMethodType: application.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_NONE,
+ PostLogoutRedirectUris: []string{"http://example.com/home"},
+ Version: application.OIDCVersion_OIDC_VERSION_1_0,
+ AccessTokenType: application.OIDCTokenType_OIDC_TOKEN_TYPE_BEARER,
+ }
+ for _, opt := range opts {
+ opt(req)
+ }
+ return req
+}
+
+func baseOIDCCreateRequestWithAppLinks(ios *application.IOSAppLinkConfig, android *application.AndroidAppLinkConfig) *application.CreateOIDCApplicationRequest {
+ return baseOIDCCreateRequest(func(req *application.CreateOIDCApplicationRequest) {
+ req.Ios = ios
+ req.Android = android
+ })
+}
+
+func iosOnlyTeamID() func(*application.CreateOIDCApplicationRequest) {
+ return func(req *application.CreateOIDCApplicationRequest) {
+ req.Ios = &application.IOSAppLinkConfig{TeamId: "ABCDE12345"}
+ }
+}
+
+func assertOIDCAppLinks(t *testing.T, appID string, wantIOS *application.IOSAppLinkConfig, wantAndroid *application.AndroidAppLinkConfig) {
+ t.Helper()
+ retryDuration, tick := integration.WaitForAndTickWithMaxDuration(IAMOwnerCtx, time.Minute)
+ require.EventuallyWithT(t, func(ct *assert.CollectT) {
+ res, err := instance.Client.ApplicationV2.GetApplication(IAMOwnerCtx, &application.GetApplicationRequest{
+ ApplicationId: appID,
+ })
+ require.NoError(ct, err)
+ cfg := res.GetApplication().GetOidcConfiguration()
+ require.NotNil(ct, cfg)
+
+ if wantIOS == nil {
+ assert.Nil(ct, cfg.GetIos())
+ } else {
+ require.NotNil(ct, cfg.GetIos())
+ assert.Equal(ct, wantIOS.GetTeamId(), cfg.GetIos().GetTeamId())
+ assert.Equal(ct, wantIOS.GetBundleId(), cfg.GetIos().GetBundleId())
+ }
+
+ if wantAndroid == nil {
+ assert.Nil(ct, cfg.GetAndroid())
+ } else {
+ require.NotNil(ct, cfg.GetAndroid())
+ assert.Equal(ct, wantAndroid.GetPackageName(), cfg.GetAndroid().GetPackageName())
+ assert.Equal(ct, wantAndroid.GetSha256CertFingerprints(), cfg.GetAndroid().GetSha256CertFingerprints())
+ }
+ }, retryDuration, tick)
+}
diff --git a/internal/api/grpc/management/project_application_converter.go b/internal/api/grpc/management/project_application_converter.go
index 5021870b3..82768b52c 100644
--- a/internal/api/grpc/management/project_application_converter.go
+++ b/internal/api/grpc/management/project_application_converter.go
@@ -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
}
diff --git a/internal/api/grpc/project/application.go b/internal/api/grpc/project/application.go
index 29fbd4cd2..41ff39eae 100644
--- a/internal/api/grpc/project/application.go
+++ b/internal/api/grpc/project/application.go
@@ -3,6 +3,7 @@ package project
import (
"net/url"
+ "github.com/muhlemmer/gu"
"google.golang.org/protobuf/types/known/durationpb"
object_grpc "github.com/zitadel/zitadel/internal/api/grpc/object"
@@ -68,6 +69,8 @@ func AppOIDCConfigToPb(app *query.OIDCApp) *app_pb.App_OidcConfig {
SkipNativeAppSuccessPage: app.SkipNativeAppSuccessPage,
BackChannelLogoutUri: app.BackChannelLogoutURI,
LoginVersion: loginVersionToPb(app.LoginVersion, app.LoginBaseURI),
+ Ios: iosAppLinkConfigToPb(app.IOSTeamID, app.IOSBundleID),
+ Android: androidAppLinkConfigToPb(app.AndroidPackageName, app.AndroidSHA256CertFingerprints),
},
}
}
@@ -345,3 +348,41 @@ func LoginVersionToDomain(version *app_pb.LoginVersion) (domain.LoginVersion, st
return domain.LoginVersionUnspecified, "", nil
}
}
+
+func IOSAppLinkConfigToDomain(cfg *app_pb.IOSAppLinkConfig) (teamID, bundleID *string) {
+ if cfg == nil {
+ return nil, nil
+ }
+ return gu.Ptr(cfg.GetTeamId()), gu.Ptr(cfg.GetBundleId())
+}
+
+func AndroidAppLinkConfigToDomain(cfg *app_pb.AndroidAppLinkConfig) (packageName *string, fingerprints []string) {
+ if cfg == nil {
+ return nil, nil
+ }
+ fps := cfg.GetSha256CertFingerprints()
+ if fps == nil {
+ fps = []string{}
+ }
+ return gu.Ptr(cfg.GetPackageName()), fps
+}
+
+func iosAppLinkConfigToPb(teamID, bundleID string) *app_pb.IOSAppLinkConfig {
+ if teamID == "" && bundleID == "" {
+ return nil
+ }
+ return &app_pb.IOSAppLinkConfig{
+ TeamId: teamID,
+ BundleId: bundleID,
+ }
+}
+
+func androidAppLinkConfigToPb(packageName string, fingerprints []string) *app_pb.AndroidAppLinkConfig {
+ if packageName == "" && len(fingerprints) == 0 {
+ return nil
+ }
+ return &app_pb.AndroidAppLinkConfig{
+ PackageName: packageName,
+ Sha256CertFingerprints: fingerprints,
+ }
+}
diff --git a/internal/api/well_known/integration_test/well_known_test.go b/internal/api/well_known/integration_test/well_known_test.go
new file mode 100644
index 000000000..d5e900881
--- /dev/null
+++ b/internal/api/well_known/integration_test/well_known_test.go
@@ -0,0 +1,187 @@
+//go:build integration
+
+package well_known_test
+
+import (
+ "context"
+ "encoding/json"
+ "io"
+ "net/http"
+ "os"
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+
+ "github.com/zitadel/zitadel/internal/api/well_known"
+ "github.com/zitadel/zitadel/internal/integration"
+ "github.com/zitadel/zitadel/pkg/grpc/application/v2"
+)
+
+var (
+ CTX context.Context
+ IAMOwnerCtx context.Context
+ Instance *integration.Instance
+)
+
+func TestMain(m *testing.M) {
+ os.Exit(func() int {
+ ctx, cancel := context.WithTimeout(context.Background(), 15*time.Minute)
+ defer cancel()
+
+ Instance = integration.NewInstance(ctx)
+ CTX = ctx
+ IAMOwnerCtx = Instance.WithAuthorization(ctx, integration.UserTypeIAMOwner)
+ return m.Run()
+ }())
+}
+
+func TestWellKnownAppLinks(t *testing.T) {
+ issuer := Instance.OIDCIssuer()
+
+ t.Run("empty when no apps configured", func(t *testing.T) {
+ assertEventuallyJSON(t, issuer+well_known.AppleAppSiteAssociationPath, map[string]any{
+ "webcredentials": map[string]any{
+ "apps": []any{},
+ },
+ })
+ assertEventuallyJSON(t, issuer+well_known.AssetLinksPath, []any{})
+ })
+
+ project := Instance.CreateProject(IAMOwnerCtx, t, Instance.DefaultOrg.GetId(), integration.ProjectName(), false, false)
+
+ ios1 := &application.IOSAppLinkConfig{TeamId: "TEAMAAAAAA", BundleId: "com.example.one"}
+ android1 := &application.AndroidAppLinkConfig{
+ PackageName: "com.example.one",
+ Sha256CertFingerprints: []string{"11:11:11:11:11:11:11:11:11:11:11:11:11:11:11:11:11:11:11:11:11:11:11:11:11:11:11:11:11:11:11:11"},
+ }
+ createOIDCAppWithLinks(t, project.GetId(), ios1, android1)
+
+ ios2 := &application.IOSAppLinkConfig{TeamId: "TEAMBBBBBB", BundleId: "com.example.two"}
+ createOIDCAppWithLinks(t, project.GetId(), ios2, nil)
+
+ android2 := &application.AndroidAppLinkConfig{
+ PackageName: "com.example.two",
+ Sha256CertFingerprints: []string{"22:22:22:22:22:22:22:22:22:22:22:22:22:22:22:22:22:22:22:22:22:22:22:22:22:22:22:22:22:22:22:22"},
+ }
+ createOIDCAppWithLinks(t, project.GetId(), nil, android2)
+
+ // duplicate iOS app id should be deduped in AASA
+ createOIDCAppWithLinks(t, project.GetId(), ios1, nil)
+
+ assertEventuallyJSONUnorderedApps(t, issuer,
+ []string{
+ "TEAMAAAAAA.com.example.one",
+ "TEAMBBBBBB.com.example.two",
+ },
+ []any{
+ map[string]any{
+ "relation": []any{"delegate_permission/common.get_login_creds"},
+ "target": map[string]any{
+ "namespace": "android_app",
+ "package_name": "com.example.one",
+ "sha256_cert_fingerprints": []any{android1.Sha256CertFingerprints[0]},
+ },
+ },
+ map[string]any{
+ "relation": []any{"delegate_permission/common.get_login_creds"},
+ "target": map[string]any{
+ "namespace": "android_app",
+ "package_name": "com.example.two",
+ "sha256_cert_fingerprints": []any{android2.Sha256CertFingerprints[0]},
+ },
+ },
+ },
+ )
+}
+
+func createOIDCAppWithLinks(t *testing.T, projectID string, ios *application.IOSAppLinkConfig, android *application.AndroidAppLinkConfig) {
+ t.Helper()
+ _, err := Instance.Client.ApplicationV2.CreateApplication(IAMOwnerCtx, &application.CreateApplicationRequest{
+ ProjectId: projectID,
+ Name: integration.ApplicationName(),
+ ApplicationType: &application.CreateApplicationRequest_OidcConfiguration{
+ OidcConfiguration: &application.CreateOIDCApplicationRequest{
+ RedirectUris: []string{"http://example.com"},
+ ResponseTypes: []application.OIDCResponseType{application.OIDCResponseType_OIDC_RESPONSE_TYPE_CODE},
+ GrantTypes: []application.OIDCGrantType{application.OIDCGrantType_OIDC_GRANT_TYPE_AUTHORIZATION_CODE},
+ ApplicationType: application.OIDCApplicationType_OIDC_APP_TYPE_NATIVE,
+ AuthMethodType: application.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_NONE,
+ Version: application.OIDCVersion_OIDC_VERSION_1_0,
+ AccessTokenType: application.OIDCTokenType_OIDC_TOKEN_TYPE_BEARER,
+ Ios: ios,
+ Android: android,
+ },
+ },
+ })
+ require.NoError(t, err)
+}
+
+func assertEventuallyJSON(t *testing.T, url string, want any) {
+ t.Helper()
+ retryDuration, tick := integration.WaitForAndTickWithMaxDuration(CTX, time.Minute)
+ require.EventuallyWithT(t, func(ct *assert.CollectT) {
+ resp, err := http.Get(url)
+ if !assert.NoError(ct, err) {
+ return
+ }
+ defer func() {
+ _, _ = io.Copy(io.Discard, resp.Body)
+ _ = resp.Body.Close()
+ }()
+ if !assert.Equal(ct, http.StatusOK, resp.StatusCode) {
+ return
+ }
+ assert.Contains(ct, resp.Header.Get("Content-Type"), "application/json")
+ assert.Equal(ct, "public, max-age=300", resp.Header.Get("Cache-Control"))
+
+ body, err := io.ReadAll(resp.Body)
+ if !assert.NoError(ct, err) {
+ return
+ }
+ var got any
+ if !assert.NoError(ct, json.Unmarshal(body, &got)) {
+ return
+ }
+ assert.Equal(ct, want, got)
+ }, retryDuration, tick)
+}
+
+func assertEventuallyJSONUnorderedApps(t *testing.T, url string, wantApps []string, wantAssetLinks []any) {
+ t.Helper()
+ retryDuration, tick := integration.WaitForAndTickWithMaxDuration(CTX, time.Minute)
+ require.EventuallyWithT(t, func(ct *assert.CollectT) {
+ aasaResp, err := http.Get(url + well_known.AppleAppSiteAssociationPath)
+ if !assert.NoError(ct, err) {
+ return
+ }
+ defer aasaResp.Body.Close()
+ if !assert.Equal(ct, http.StatusOK, aasaResp.StatusCode) {
+ return
+ }
+ var aasa struct {
+ WebCredentials struct {
+ Apps []string `json:"apps"`
+ } `json:"webcredentials"`
+ }
+ if !assert.NoError(ct, json.NewDecoder(aasaResp.Body).Decode(&aasa)) {
+ return
+ }
+ assert.ElementsMatch(ct, wantApps, aasa.WebCredentials.Apps)
+
+ assetResp, err := http.Get(url + well_known.AssetLinksPath)
+ if !assert.NoError(ct, err) {
+ return
+ }
+ defer assetResp.Body.Close()
+ if !assert.Equal(ct, http.StatusOK, assetResp.StatusCode) {
+ return
+ }
+ var got []any
+ if !assert.NoError(ct, json.NewDecoder(assetResp.Body).Decode(&got)) {
+ return
+ }
+ assert.ElementsMatch(ct, wantAssetLinks, got)
+ }, retryDuration, tick)
+}
diff --git a/internal/api/well_known/well_known.go b/internal/api/well_known/well_known.go
new file mode 100644
index 000000000..15007d80c
--- /dev/null
+++ b/internal/api/well_known/well_known.go
@@ -0,0 +1,196 @@
+package well_known
+
+import (
+ "context"
+ "crypto/sha256"
+ "encoding/hex"
+ "encoding/json"
+ "fmt"
+ "net/http"
+ "strings"
+ "time"
+
+ "github.com/zitadel/zitadel/backend/v3/instrumentation/logging"
+ http_util "github.com/zitadel/zitadel/internal/api/http"
+ "github.com/zitadel/zitadel/internal/query"
+)
+
+const (
+ AppleAppSiteAssociationPath = "/.well-known/apple-app-site-association"
+ AssetLinksPath = "/.well-known/assetlinks.json"
+)
+
+// HandlerPrefixes are registered with [api.API.RegisterHandlerPrefixes].
+var HandlerPrefixes = []string{
+ AppleAppSiteAssociationPath,
+ AssetLinksPath,
+}
+
+// Config holds runtime options for the well-known handlers.
+type Config struct {
+ // AppLinksCacheControlMaxAge sets the Cache-Control max-age for
+ // apple-app-site-association and assetlinks.json responses.
+ // 0 sets Cache-Control: no-store.
+ AppLinksCacheControlMaxAge time.Duration
+}
+
+type appLinkQuerier interface {
+ SearchOIDCAppLinkConfigs(ctx context.Context) ([]*query.OIDCAppLinkConfig, error)
+}
+
+type Handler struct {
+ queries appLinkQuerier
+ cacheControlMaxAge time.Duration
+}
+
+func NewHandler(queries appLinkQuerier, config Config) *Handler {
+ return &Handler{
+ queries: queries,
+ cacheControlMaxAge: config.AppLinksCacheControlMaxAge,
+ }
+}
+
+func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+ switch r.URL.Path {
+ case AppleAppSiteAssociationPath:
+ h.serveAppleAppSiteAssociation(w, r)
+ case AssetLinksPath:
+ h.serveAssetLinks(w, r)
+ default:
+ http.NotFound(w, r)
+ }
+}
+
+type appleAppSiteAssociation struct {
+ WebCredentials appleWebCredentials `json:"webcredentials"`
+}
+
+type appleWebCredentials struct {
+ Apps []string `json:"apps"`
+}
+
+type assetLink struct {
+ Relation []string `json:"relation"`
+ Target assetLinkTarget `json:"target"`
+}
+
+type assetLinkTarget struct {
+ Namespace string `json:"namespace"`
+ PackageName string `json:"package_name"`
+ SHA256CertFingerprints []string `json:"sha256_cert_fingerprints"`
+}
+
+func (h *Handler) serveAppleAppSiteAssociation(w http.ResponseWriter, r *http.Request) {
+ if r.Method != http.MethodGet && r.Method != http.MethodHead {
+ http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
+ return
+ }
+ configs, err := h.queries.SearchOIDCAppLinkConfigs(r.Context())
+ if err != nil {
+ logging.Error(r.Context(), "unable to query OIDC app link configs for apple-app-site-association", "err", err)
+ http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
+ return
+ }
+ h.writeJSON(w, r, buildAppleAppSiteAssociation(configs))
+}
+
+func (h *Handler) serveAssetLinks(w http.ResponseWriter, r *http.Request) {
+ if r.Method != http.MethodGet && r.Method != http.MethodHead {
+ http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
+ return
+ }
+ configs, err := h.queries.SearchOIDCAppLinkConfigs(r.Context())
+ if err != nil {
+ logging.Error(r.Context(), "unable to query OIDC app link configs for assetlinks.json", "err", err)
+ http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
+ return
+ }
+ h.writeJSON(w, r, buildAssetLinks(r.Context(), configs))
+}
+
+func buildAppleAppSiteAssociation(configs []*query.OIDCAppLinkConfig) appleAppSiteAssociation {
+ apps := make([]string, 0)
+ seen := make(map[string]struct{})
+ for _, cfg := range configs {
+ if cfg.IOSTeamID == "" || cfg.IOSBundleID == "" {
+ continue
+ }
+ appID := cfg.IOSTeamID + "." + cfg.IOSBundleID
+ if _, ok := seen[appID]; ok {
+ continue
+ }
+ seen[appID] = struct{}{}
+ apps = append(apps, appID)
+ }
+ return appleAppSiteAssociation{
+ WebCredentials: appleWebCredentials{Apps: apps},
+ }
+}
+
+func buildAssetLinks(ctx context.Context, configs []*query.OIDCAppLinkConfig) []assetLink {
+ links := make([]assetLink, 0)
+ for _, cfg := range configs {
+ if cfg.AndroidPackageName == "" || len(cfg.AndroidSHA256CertFingerprints) == 0 {
+ continue
+ }
+ links = append(links, assetLink{
+ Relation: []string{"delegate_permission/common.get_login_creds"},
+ Target: assetLinkTarget{
+ Namespace: "android_app",
+ PackageName: cfg.AndroidPackageName,
+ SHA256CertFingerprints: normalizeSHA256Fingerprints(ctx, cfg.AndroidSHA256CertFingerprints),
+ },
+ })
+ }
+ return links
+}
+
+// normalizeSHA256Fingerprints returns colon-separated uppercase hex fingerprints
+// for assetlinks.json. Normalization is applied at serve time only so stored /
+// client state can keep the format clients sent.
+func normalizeSHA256Fingerprints(ctx context.Context, fps []string) []string {
+ out := make([]string, len(fps))
+ for i, fp := range fps {
+ out[i] = normalizeSHA256Fingerprint(ctx, fp)
+ }
+ return out
+}
+
+func normalizeSHA256Fingerprint(ctx context.Context, fp string) string {
+ if strings.Contains(fp, ":") {
+ return strings.ToUpper(fp) // already 32 valid pairs per API validation; only case may vary
+ }
+ raw, err := hex.DecodeString(fp) // guaranteed 64 hex chars
+ if err != nil || len(raw) != sha256.Size {
+ logging.Warn(ctx, "stored android sha256 fingerprint is not canonical hex; serving as-is",
+ "fingerprint", fp, "err", err)
+ return strings.ToUpper(fp)
+ }
+ parts := make([]string, len(raw))
+ for i, b := range raw {
+ parts[i] = fmt.Sprintf("%02X", b)
+ }
+ return strings.Join(parts, ":")
+}
+
+func (h *Handler) writeJSON(w http.ResponseWriter, r *http.Request, v any) {
+ w.Header().Set("Content-Type", "application/json")
+ h.setCacheControl(w)
+ w.WriteHeader(http.StatusOK)
+ if r.Method == http.MethodHead {
+ return
+ }
+ if err := json.NewEncoder(w).Encode(v); err != nil {
+ logging.Error(r.Context(), "unable to encode well-known response", "err", err)
+ }
+}
+
+func (h *Handler) setCacheControl(w http.ResponseWriter) {
+ if h.cacheControlMaxAge <= 0 {
+ w.Header().Set(http_util.CacheControl, "no-store")
+ return
+ }
+ w.Header().Set(http_util.CacheControl,
+ fmt.Sprintf("public, max-age=%d", int(h.cacheControlMaxAge/time.Second)),
+ )
+}
diff --git a/internal/api/well_known/well_known_test.go b/internal/api/well_known/well_known_test.go
new file mode 100644
index 000000000..868b11645
--- /dev/null
+++ b/internal/api/well_known/well_known_test.go
@@ -0,0 +1,241 @@
+package well_known
+
+import (
+ "context"
+ "net/http"
+ "net/http/httptest"
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+
+ http_util "github.com/zitadel/zitadel/internal/api/http"
+ "github.com/zitadel/zitadel/internal/query"
+)
+
+func TestBuildAppleAppSiteAssociation(t *testing.T) {
+ t.Parallel()
+
+ tt := []struct {
+ name string
+ configs []*query.OIDCAppLinkConfig
+ wantApps []string
+ }{
+ {
+ name: "empty",
+ configs: nil,
+ wantApps: []string{},
+ },
+ {
+ name: "skips incomplete ios",
+ configs: []*query.OIDCAppLinkConfig{
+ {IOSTeamID: "TEAM"},
+ {IOSBundleID: "com.example"},
+ },
+ wantApps: []string{},
+ },
+ {
+ name: "aggregates and dedupes",
+ configs: []*query.OIDCAppLinkConfig{
+ {IOSTeamID: "TEAM1", IOSBundleID: "com.one"},
+ {IOSTeamID: "TEAM2", IOSBundleID: "com.two"},
+ {IOSTeamID: "TEAM1", IOSBundleID: "com.one"},
+ {AndroidPackageName: "com.android", AndroidSHA256CertFingerprints: []string{"AA"}},
+ },
+ wantApps: []string{"TEAM1.com.one", "TEAM2.com.two"},
+ },
+ }
+
+ for _, tc := range tt {
+ t.Run(tc.name, func(t *testing.T) {
+ t.Parallel()
+ got := buildAppleAppSiteAssociation(tc.configs)
+ assert.Equal(t, tc.wantApps, got.WebCredentials.Apps)
+ })
+ }
+}
+
+func TestBuildAssetLinks(t *testing.T) {
+ t.Parallel()
+
+ tt := []struct {
+ name string
+ configs []*query.OIDCAppLinkConfig
+ want []assetLink
+ }{
+ {
+ name: "empty",
+ configs: nil,
+ want: []assetLink{},
+ },
+ {
+ name: "skips incomplete android",
+ configs: []*query.OIDCAppLinkConfig{
+ {AndroidPackageName: "com.example"},
+ {AndroidSHA256CertFingerprints: []string{"AA"}},
+ },
+ want: []assetLink{},
+ },
+ {
+ name: "aggregates and normalizes fingerprints",
+ configs: []*query.OIDCAppLinkConfig{
+ {
+ IOSTeamID: "TEAM",
+ IOSBundleID: "com.ios",
+ AndroidPackageName: "com.one",
+ AndroidSHA256CertFingerprints: []string{"aabbccddeeff00112233445566778899aabbccddeeff00112233445566778899"},
+ },
+ {
+ AndroidPackageName: "com.two",
+ AndroidSHA256CertFingerprints: []string{
+ "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:aa:bb",
+ "eeff00112233445566778899aabbccddeeff00112233445566778899aabbccdd",
+ },
+ },
+ },
+ want: []assetLink{
+ {
+ Relation: []string{"delegate_permission/common.get_login_creds"},
+ Target: assetLinkTarget{
+ Namespace: "android_app",
+ PackageName: "com.one",
+ SHA256CertFingerprints: []string{
+ "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",
+ },
+ },
+ },
+ {
+ Relation: []string{"delegate_permission/common.get_login_creds"},
+ Target: assetLinkTarget{
+ Namespace: "android_app",
+ PackageName: "com.two",
+ SHA256CertFingerprints: []string{
+ "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:AA:BB",
+ "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:AA:BB:CC:DD",
+ },
+ },
+ },
+ },
+ },
+ }
+
+ for _, tc := range tt {
+ t.Run(tc.name, func(t *testing.T) {
+ t.Parallel()
+ assert.Equal(t, tc.want, buildAssetLinks(t.Context(), tc.configs))
+ })
+ }
+}
+
+func TestNormalizeSHA256Fingerprint(t *testing.T) {
+ t.Parallel()
+
+ tt := []struct {
+ name string
+ in string
+ want string
+ }{
+ {
+ name: "already canonical",
+ in: "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",
+ want: "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",
+ },
+ {
+ name: "lowercase with colons",
+ in: "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",
+ want: "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",
+ },
+ {
+ name: "lowercase without separators",
+ in: "aabbccddeeff00112233445566778899aabbccddeeff00112233445566778899",
+ want: "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",
+ },
+ {
+ name: "invalid continuous hex uppercased",
+ in: "aabb",
+ want: "AABB",
+ },
+ {
+ name: "colon form uppercased without reparse",
+ in: "aa:bb",
+ want: "AA:BB",
+ },
+ }
+
+ for _, tc := range tt {
+ t.Run(tc.name, func(t *testing.T) {
+ t.Parallel()
+ assert.Equal(t, tc.want, normalizeSHA256Fingerprint(t.Context(), tc.in))
+ })
+ }
+}
+
+type stubAppLinkQuerier struct {
+ configs []*query.OIDCAppLinkConfig
+ err error
+}
+
+func (s stubAppLinkQuerier) SearchOIDCAppLinkConfigs(_ context.Context) ([]*query.OIDCAppLinkConfig, error) {
+ return s.configs, s.err
+}
+
+func TestHandlerCacheControl(t *testing.T) {
+ t.Parallel()
+
+ tt := []struct {
+ name string
+ maxAge time.Duration
+ wantHeader string
+ }{
+ {
+ name: "default max-age",
+ maxAge: 5 * time.Minute,
+ wantHeader: "public, max-age=300",
+ },
+ {
+ name: "no-store when zero",
+ maxAge: 0,
+ wantHeader: "no-store",
+ },
+ {
+ name: "no-store when negative",
+ maxAge: -time.Minute,
+ wantHeader: "no-store",
+ },
+ }
+
+ for _, tc := range tt {
+ t.Run(tc.name, func(t *testing.T) {
+ t.Parallel()
+ h := NewHandler(stubAppLinkQuerier{}, Config{AppLinksCacheControlMaxAge: tc.maxAge})
+
+ for _, path := range []string{AppleAppSiteAssociationPath, AssetLinksPath} {
+ req := httptest.NewRequest(http.MethodGet, path, nil)
+ rec := httptest.NewRecorder()
+ h.ServeHTTP(rec, req)
+ require.Equal(t, http.StatusOK, rec.Code)
+ assert.Equal(t, tc.wantHeader, rec.Header().Get(http_util.CacheControl), path)
+ assert.NotContains(t, rec.Header().Get(http_util.CacheControl), "stale-while-revalidate")
+ }
+ })
+ }
+}
+
+func TestHandlerHeadOmitsBody(t *testing.T) {
+ t.Parallel()
+
+ h := NewHandler(stubAppLinkQuerier{}, Config{AppLinksCacheControlMaxAge: 5 * time.Minute})
+ for _, path := range []string{AppleAppSiteAssociationPath, AssetLinksPath} {
+ t.Run(path, func(t *testing.T) {
+ t.Parallel()
+ req := httptest.NewRequest(http.MethodHead, path, nil)
+ rec := httptest.NewRecorder()
+ h.ServeHTTP(rec, req)
+ require.Equal(t, http.StatusOK, rec.Code)
+ assert.Equal(t, "application/json", rec.Header().Get("Content-Type"))
+ assert.Equal(t, "public, max-age=300", rec.Header().Get(http_util.CacheControl))
+ assert.Empty(t, rec.Body.Bytes())
+ })
+ }
+}
diff --git a/internal/command/instance_domain_test.go b/internal/command/instance_domain_test.go
index 7b2a436ae..96e65a0ec 100644
--- a/internal/command/instance_domain_test.go
+++ b/internal/command/instance_domain_test.go
@@ -158,7 +158,10 @@ func TestCommandSide_AddInstanceDomain(t *testing.T) {
"",
domain.LoginVersionUnspecified,
"",
- ),
+ "",
+ "",
+ "",
+ nil),
),
),
expectPush(
diff --git a/internal/command/instance_test.go b/internal/command/instance_test.go
index b564f3c02..da48474cf 100644
--- a/internal/command/instance_test.go
+++ b/internal/command/instance_test.go
@@ -125,7 +125,10 @@ func oidcAppEvents(ctx context.Context, orgID, projectID, id, name, clientID str
"",
domain.LoginVersionUnspecified,
"",
- ),
+ "",
+ "",
+ "",
+ nil),
}
}
@@ -468,7 +471,10 @@ func generatedDomainFilters(instanceID, orgID, projectID, appID, generatedDomain
"",
domain.LoginVersionUnspecified,
"",
- ),
+ "",
+ "",
+ "",
+ nil),
),
expectFilter(
func() eventstore.Event {
diff --git a/internal/command/project_application_oidc.go b/internal/command/project_application_oidc.go
index ae1b7392e..67caf4220 100644
--- a/internal/command/project_application_oidc.go
+++ b/internal/command/project_application_oidc.go
@@ -20,24 +20,28 @@ import (
type addOIDCApp struct {
AddApp
- Version domain.OIDCVersion
- RedirectUris []string
- ResponseTypes []domain.OIDCResponseType
- GrantTypes []domain.OIDCGrantType
- ApplicationType domain.OIDCApplicationType
- AuthMethodType domain.OIDCAuthMethodType
- PostLogoutRedirectUris []string
- DevMode bool
- AccessTokenType domain.OIDCTokenType
- AccessTokenRoleAssertion bool
- IDTokenRoleAssertion bool
- IDTokenUserinfoAssertion bool
- ClockSkew time.Duration
- AdditionalOrigins []string
- SkipSuccessPageForNativeApp bool
- BackChannelLogoutURI string
- LoginVersion domain.LoginVersion
- LoginBaseURI string
+ Version domain.OIDCVersion
+ RedirectUris []string
+ ResponseTypes []domain.OIDCResponseType
+ GrantTypes []domain.OIDCGrantType
+ ApplicationType domain.OIDCApplicationType
+ AuthMethodType domain.OIDCAuthMethodType
+ PostLogoutRedirectUris []string
+ DevMode bool
+ AccessTokenType domain.OIDCTokenType
+ AccessTokenRoleAssertion bool
+ IDTokenRoleAssertion bool
+ IDTokenUserinfoAssertion bool
+ ClockSkew time.Duration
+ AdditionalOrigins []string
+ SkipSuccessPageForNativeApp bool
+ BackChannelLogoutURI string
+ LoginVersion domain.LoginVersion
+ LoginBaseURI string
+ IOSTeamID string
+ IOSBundleID string
+ AndroidPackageName string
+ AndroidSHA256CertFingerprints []string
ClientID string
ClientSecret string
@@ -118,6 +122,10 @@ func (c *Commands) AddOIDCAppCommand(app *addOIDCApp) preparation.Validation {
app.BackChannelLogoutURI,
app.LoginVersion,
app.LoginBaseURI,
+ app.IOSTeamID,
+ app.IOSBundleID,
+ app.AndroidPackageName,
+ app.AndroidSHA256CertFingerprints,
),
}, nil
}, nil
@@ -251,6 +259,10 @@ func (c *Commands) pushOIDCApplication(ctx context.Context, addedApplication *OI
backchannelLogoutURI,
gu.Value(oidcApp.LoginVersion),
strings.TrimSpace(gu.Value(oidcApp.LoginBaseURI)),
+ strings.TrimSpace(gu.Value(oidcApp.IOSTeamID)),
+ strings.TrimSpace(gu.Value(oidcApp.IOSBundleID)),
+ strings.TrimSpace(gu.Value(oidcApp.AndroidPackageName)),
+ trimStringSliceWhiteSpaces(oidcApp.AndroidSHA256CertFingerprints),
))
events = append(events, extraEvents...)
@@ -340,7 +352,7 @@ func (c *Commands) UpdateOIDCApplication(ctx context.Context, oidc *domain.OIDCA
// UpdateDynamicOIDCClient). It reports whether anything actually changed.
func (c *Commands) oidcApplicationChangeEvent(ctx context.Context, existingOIDC *OIDCApplicationWriteModel, oidc *domain.OIDCApp) (*project_repo.OIDCConfigChangedEvent, bool, error) {
projectAgg := ProjectAggregateFromWriteModelWithCTX(ctx, &existingOIDC.WriteModel)
- var backChannelLogout, loginBaseURI *string
+ var backChannelLogout, loginBaseURI, iosTeamID, iosBundleID, androidPackageName *string
if oidc.BackChannelLogoutURI != nil {
bcl, err := c.validateBackchannelLogoutURI(oidc)
if err != nil {
@@ -352,6 +364,15 @@ func (c *Commands) oidcApplicationChangeEvent(ctx context.Context, existingOIDC
if oidc.LoginBaseURI != nil {
loginBaseURI = gu.Ptr(strings.TrimSpace(*oidc.LoginBaseURI))
}
+ if oidc.IOSTeamID != nil {
+ iosTeamID = gu.Ptr(strings.TrimSpace(*oidc.IOSTeamID))
+ }
+ if oidc.IOSBundleID != nil {
+ iosBundleID = gu.Ptr(strings.TrimSpace(*oidc.IOSBundleID))
+ }
+ if oidc.AndroidPackageName != nil {
+ androidPackageName = gu.Ptr(strings.TrimSpace(*oidc.AndroidPackageName))
+ }
return existingOIDC.NewChangedEvent(
ctx,
@@ -375,6 +396,10 @@ func (c *Commands) oidcApplicationChangeEvent(ctx context.Context, existingOIDC
backChannelLogout,
oidc.LoginVersion,
loginBaseURI,
+ iosTeamID,
+ iosBundleID,
+ androidPackageName,
+ trimStringSliceWhiteSpaces(oidc.AndroidSHA256CertFingerprints),
)
}
diff --git a/internal/command/project_application_oidc_dcr_test.go b/internal/command/project_application_oidc_dcr_test.go
index a84e11cd1..9fbe07546 100644
--- a/internal/command/project_application_oidc_dcr_test.go
+++ b/internal/command/project_application_oidc_dcr_test.go
@@ -234,7 +234,10 @@ func TestCommandSide_AddDynamicOIDCClient(t *testing.T) {
"",
domain.LoginVersionUnspecified,
"",
- ),
+ "",
+ "",
+ "",
+ nil),
// The registration access token (RFC 7592 §3) is persisted in the same
// push as the application, so a registered client is never left
// unmanageable.
@@ -336,7 +339,10 @@ func TestCommandSide_AddDynamicOIDCClient(t *testing.T) {
"",
domain.LoginVersionUnspecified,
"",
- ),
+ "",
+ "",
+ "",
+ nil),
project.NewOIDCConfigRegistrationTokenChangedEvent(context.Background(),
&project.NewAggregate("project1", "org1").Aggregate,
"app1",
@@ -581,7 +587,11 @@ func TestCommandSide_UpdateDynamicOIDCClient(t *testing.T) {
domain.OIDCApplicationTypeWeb, domain.OIDCAuthMethodTypeNone,
[]string{}, false, domain.OIDCTokenTypeBearer,
false, false, false, 0, []string{}, false, "",
- domain.LoginVersionUnspecified, "")),
+ domain.LoginVersionUnspecified, "",
+ "",
+ "",
+ "",
+ nil)),
}
}
sameMetadata := &domain.OIDCApp{
diff --git a/internal/command/project_application_oidc_model.go b/internal/command/project_application_oidc_model.go
index 45dcd2e8c..9f1dd6d5d 100644
--- a/internal/command/project_application_oidc_model.go
+++ b/internal/command/project_application_oidc_model.go
@@ -14,32 +14,36 @@ import (
type OIDCApplicationWriteModel struct {
eventstore.WriteModel
- AppID string
- AppName string
- ClientID string
- HashedSecret string
- ClientSecretString string
- RedirectUris []string
- ResponseTypes []domain.OIDCResponseType
- GrantTypes []domain.OIDCGrantType
- ApplicationType domain.OIDCApplicationType
- AuthMethodType domain.OIDCAuthMethodType
- PostLogoutRedirectUris []string
- OIDCVersion domain.OIDCVersion
- Compliance *domain.Compliance
- DevMode bool
- AccessTokenType domain.OIDCTokenType
- AccessTokenRoleAssertion bool
- IDTokenRoleAssertion bool
- IDTokenUserinfoAssertion bool
- ClockSkew time.Duration
- State domain.AppState
- AdditionalOrigins []string
- SkipNativeAppSuccessPage bool
- BackChannelLogoutURI string
- LoginVersion domain.LoginVersion
- LoginBaseURI string
- oidc bool
+ AppID string
+ AppName string
+ ClientID string
+ HashedSecret string
+ ClientSecretString string
+ RedirectUris []string
+ ResponseTypes []domain.OIDCResponseType
+ GrantTypes []domain.OIDCGrantType
+ ApplicationType domain.OIDCApplicationType
+ AuthMethodType domain.OIDCAuthMethodType
+ PostLogoutRedirectUris []string
+ OIDCVersion domain.OIDCVersion
+ Compliance *domain.Compliance
+ DevMode bool
+ AccessTokenType domain.OIDCTokenType
+ AccessTokenRoleAssertion bool
+ IDTokenRoleAssertion bool
+ IDTokenUserinfoAssertion bool
+ ClockSkew time.Duration
+ State domain.AppState
+ AdditionalOrigins []string
+ SkipNativeAppSuccessPage bool
+ BackChannelLogoutURI string
+ LoginVersion domain.LoginVersion
+ LoginBaseURI string
+ IOSTeamID string
+ IOSBundleID string
+ AndroidPackageName string
+ AndroidSHA256CertFingerprints []string
+ oidc bool
}
func NewOIDCApplicationWriteModelWithAppID(projectID, appID, resourceOwner string) *OIDCApplicationWriteModel {
@@ -140,6 +144,10 @@ func (wm *OIDCApplicationWriteModel) Reduce() error {
wm.BackChannelLogoutURI = ""
wm.LoginVersion = domain.LoginVersionUnspecified
wm.LoginBaseURI = ""
+ wm.IOSTeamID = ""
+ wm.IOSBundleID = ""
+ wm.AndroidPackageName = ""
+ wm.AndroidSHA256CertFingerprints = nil
wm.oidc = false
wm.AppName = e.Name
wm.State = domain.AppStateActive
@@ -189,6 +197,10 @@ func (wm *OIDCApplicationWriteModel) Reduce() error {
wm.BackChannelLogoutURI = ""
wm.LoginVersion = domain.LoginVersionUnspecified
wm.LoginBaseURI = ""
+ wm.IOSTeamID = ""
+ wm.IOSBundleID = ""
+ wm.AndroidPackageName = ""
+ wm.AndroidSHA256CertFingerprints = nil
wm.oidc = false
wm.State = domain.AppStateRemoved
}
@@ -218,6 +230,10 @@ func (wm *OIDCApplicationWriteModel) appendAddOIDCEvent(e *project.OIDCConfigAdd
wm.BackChannelLogoutURI = e.BackChannelLogoutURI
wm.LoginVersion = e.LoginVersion
wm.LoginBaseURI = e.LoginBaseURI
+ wm.IOSTeamID = e.IOSTeamID
+ wm.IOSBundleID = e.IOSBundleID
+ wm.AndroidPackageName = e.AndroidPackageName
+ wm.AndroidSHA256CertFingerprints = e.AndroidSHA256CertFingerprints
}
func (wm *OIDCApplicationWriteModel) appendChangeOIDCEvent(e *project.OIDCConfigChangedEvent) {
@@ -275,6 +291,18 @@ func (wm *OIDCApplicationWriteModel) appendChangeOIDCEvent(e *project.OIDCConfig
if e.LoginBaseURI != nil {
wm.LoginBaseURI = *e.LoginBaseURI
}
+ if e.IOSTeamID != nil {
+ wm.IOSTeamID = *e.IOSTeamID
+ }
+ if e.IOSBundleID != nil {
+ wm.IOSBundleID = *e.IOSBundleID
+ }
+ if e.AndroidPackageName != nil {
+ wm.AndroidPackageName = *e.AndroidPackageName
+ }
+ if e.AndroidSHA256CertFingerprints != nil {
+ wm.AndroidSHA256CertFingerprints = *e.AndroidSHA256CertFingerprints
+ }
}
func (wm *OIDCApplicationWriteModel) Query() *eventstore.SearchQueryBuilder {
@@ -320,6 +348,10 @@ func (wm *OIDCApplicationWriteModel) NewChangedEvent(
backChannelLogoutURI *string,
loginVersion *domain.LoginVersion,
loginBaseURI *string,
+ iosTeamID *string,
+ iosBundleID *string,
+ androidPackageName *string,
+ androidSHA256CertFingerprints []string,
) (*project.OIDCConfigChangedEvent, bool, error) {
changes := make([]project.OIDCConfigChanges, 0)
var err error
@@ -378,6 +410,18 @@ func (wm *OIDCApplicationWriteModel) NewChangedEvent(
if loginBaseURI != nil && wm.LoginBaseURI != *loginBaseURI {
changes = append(changes, project.ChangeOIDCLoginBaseURI(*loginBaseURI))
}
+ if iosTeamID != nil && wm.IOSTeamID != *iosTeamID {
+ changes = append(changes, project.ChangeIOSTeamID(*iosTeamID))
+ }
+ if iosBundleID != nil && wm.IOSBundleID != *iosBundleID {
+ changes = append(changes, project.ChangeIOSBundleID(*iosBundleID))
+ }
+ if androidPackageName != nil && wm.AndroidPackageName != *androidPackageName {
+ changes = append(changes, project.ChangeAndroidPackageName(*androidPackageName))
+ }
+ if androidSHA256CertFingerprints != nil && !slices.Equal(wm.AndroidSHA256CertFingerprints, androidSHA256CertFingerprints) {
+ changes = append(changes, project.ChangeAndroidSHA256CertFingerprints(androidSHA256CertFingerprints))
+ }
if len(changes) == 0 {
return nil, false, nil
diff --git a/internal/command/project_application_oidc_model_test.go b/internal/command/project_application_oidc_model_test.go
new file mode 100644
index 000000000..628f0d5d3
--- /dev/null
+++ b/internal/command/project_application_oidc_model_test.go
@@ -0,0 +1,88 @@
+package command
+
+import (
+ "context"
+ "testing"
+
+ "github.com/muhlemmer/gu"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+
+ "github.com/zitadel/zitadel/internal/eventstore"
+ "github.com/zitadel/zitadel/internal/repository/project"
+)
+
+func TestOIDCApplicationWriteModel_NewChangedEvent_AppLinks(t *testing.T) {
+ t.Parallel()
+
+ agg := &project.NewAggregate("project-id", "org-id").Aggregate
+ base := func() *OIDCApplicationWriteModel {
+ return &OIDCApplicationWriteModel{
+ WriteModel: eventstore.WriteModel{
+ AggregateID: "project-id",
+ ResourceOwner: "org-id",
+ },
+ AppID: "app-id",
+ IOSTeamID: "OLDTEAM",
+ IOSBundleID: "com.old.app",
+ AndroidPackageName: "com.old.app",
+ AndroidSHA256CertFingerprints: []string{"AA:AA"},
+ }
+ }
+
+ t.Run("omit keeps existing", func(t *testing.T) {
+ t.Parallel()
+ wm := base()
+ event, hasChanged, err := wm.NewChangedEvent(
+ context.Background(), agg, "app-id",
+ nil, nil, nil, nil, nil, nil, nil, nil,
+ nil, nil, nil, nil, nil, nil, nil, nil, nil, nil,
+ nil, nil, nil, nil,
+ )
+ require.NoError(t, err)
+ assert.False(t, hasChanged)
+ assert.Nil(t, event)
+ })
+
+ t.Run("set changes fields", func(t *testing.T) {
+ t.Parallel()
+ wm := base()
+ event, hasChanged, err := wm.NewChangedEvent(
+ context.Background(), agg, "app-id",
+ nil, nil, nil, nil, nil, nil, nil, nil,
+ nil, nil, nil, nil, nil, nil, nil, nil, nil, nil,
+ gu.Ptr("NEWTEAM"),
+ gu.Ptr("com.new.app"),
+ gu.Ptr("com.new.app"),
+ []string{"BB:BB"},
+ )
+ require.NoError(t, err)
+ require.True(t, hasChanged)
+ require.NotNil(t, event)
+ assert.Equal(t, gu.Ptr("NEWTEAM"), event.IOSTeamID)
+ assert.Equal(t, gu.Ptr("com.new.app"), event.IOSBundleID)
+ assert.Equal(t, gu.Ptr("com.new.app"), event.AndroidPackageName)
+ assert.Equal(t, &[]string{"BB:BB"}, event.AndroidSHA256CertFingerprints)
+ })
+
+ t.Run("clear with empty values", func(t *testing.T) {
+ t.Parallel()
+ wm := base()
+ event, hasChanged, err := wm.NewChangedEvent(
+ context.Background(), agg, "app-id",
+ nil, nil, nil, nil, nil, nil, nil, nil,
+ nil, nil, nil, nil, nil, nil, nil, nil, nil, nil,
+ gu.Ptr(""),
+ gu.Ptr(""),
+ gu.Ptr(""),
+ []string{},
+ )
+ require.NoError(t, err)
+ require.True(t, hasChanged)
+ require.NotNil(t, event)
+ assert.Equal(t, gu.Ptr(""), event.IOSTeamID)
+ assert.Equal(t, gu.Ptr(""), event.IOSBundleID)
+ assert.Equal(t, gu.Ptr(""), event.AndroidPackageName)
+ assert.Equal(t, &[]string{}, event.AndroidSHA256CertFingerprints)
+ })
+}
diff --git a/internal/command/project_application_oidc_test.go b/internal/command/project_application_oidc_test.go
index be4ee73d7..4ca0c4a66 100644
--- a/internal/command/project_application_oidc_test.go
+++ b/internal/command/project_application_oidc_test.go
@@ -176,7 +176,10 @@ func TestAddOIDCApp(t *testing.T) {
"",
domain.LoginVersionUnspecified,
"",
- ),
+ "",
+ "",
+ "",
+ nil),
},
},
},
@@ -244,7 +247,10 @@ func TestAddOIDCApp(t *testing.T) {
"",
domain.LoginVersionUnspecified,
"",
- ),
+ "",
+ "",
+ "",
+ nil),
},
},
},
@@ -312,7 +318,10 @@ func TestAddOIDCApp(t *testing.T) {
"",
domain.LoginVersionUnspecified,
"",
- ),
+ "",
+ "",
+ "",
+ nil),
},
},
},
@@ -380,7 +389,10 @@ func TestAddOIDCApp(t *testing.T) {
"",
domain.LoginVersionUnspecified,
"",
- ),
+ "",
+ "",
+ "",
+ nil),
},
},
},
@@ -534,7 +546,10 @@ func TestCommandSide_AddOIDCApplication(t *testing.T) {
"https://test.ch/backchannel",
domain.LoginVersion2,
"https://login.test.ch",
- ),
+ "",
+ "",
+ "",
+ nil),
),
),
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "app1", "client1"),
@@ -643,7 +658,10 @@ func TestCommandSide_AddOIDCApplication(t *testing.T) {
"https://test.ch/backchannel",
domain.LoginVersion2,
"https://login.test.ch",
- ),
+ "",
+ "",
+ "",
+ nil),
),
),
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "client1"),
@@ -753,7 +771,10 @@ func TestCommandSide_AddOIDCApplication(t *testing.T) {
"https://test.ch/backchannel",
domain.LoginVersion2,
"https://login.test.ch",
- ),
+ "",
+ "",
+ "",
+ nil),
),
),
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "app1", "client1"),
@@ -1072,7 +1093,10 @@ func TestCommandSide_ChangeOIDCApplication(t *testing.T) {
"https://test.ch/backchannel",
domain.LoginVersion2,
"https://login.test.ch",
- ),
+ "",
+ "",
+ "",
+ nil),
),
),
expectFilter(),
@@ -1147,7 +1171,10 @@ func TestCommandSide_ChangeOIDCApplication(t *testing.T) {
"https://test.ch/backchannel",
domain.LoginVersion2,
"https://login.test.ch",
- ),
+ "",
+ "",
+ "",
+ nil),
),
),
expectFilter(),
@@ -1222,7 +1249,10 @@ func TestCommandSide_ChangeOIDCApplication(t *testing.T) {
"https://test.ch/backchannel",
domain.LoginVersion1,
"",
- ),
+ "",
+ "",
+ "",
+ nil),
),
),
expectFilter(),
@@ -1316,7 +1346,10 @@ func TestCommandSide_ChangeOIDCApplication(t *testing.T) {
"",
domain.LoginVersionUnspecified,
"",
- ),
+ "",
+ "",
+ "",
+ nil),
),
),
expectFilter(),
@@ -1376,7 +1409,10 @@ func TestCommandSide_ChangeOIDCApplication(t *testing.T) {
"",
domain.LoginVersionUnspecified,
"",
- ),
+ "",
+ "",
+ "",
+ nil),
),
),
expectFilter(),
@@ -1531,7 +1567,10 @@ func TestCommandSide_ChangeOIDCApplicationSecret(t *testing.T) {
"",
domain.LoginVersionUnspecified,
"",
- ),
+ "",
+ "",
+ "",
+ nil),
),
),
expectPush(
diff --git a/internal/command/project_application_secret_test.go b/internal/command/project_application_secret_test.go
index 74426fa89..791ccaf54 100644
--- a/internal/command/project_application_secret_test.go
+++ b/internal/command/project_application_secret_test.go
@@ -118,7 +118,10 @@ func TestCommandSide_ChangeApplicationSecret(t *testing.T) {
"",
domain.LoginVersionUnspecified,
"",
- ),
+ "",
+ "",
+ "",
+ nil),
),
),
expectPush(
diff --git a/internal/command/project_converter.go b/internal/command/project_converter.go
index e88a1cb75..597b96594 100644
--- a/internal/command/project_converter.go
+++ b/internal/command/project_converter.go
@@ -29,32 +29,45 @@ func projectGrantWriteModelToProjectGrant(writeModel *ProjectGrantWriteModel) *d
func oidcWriteModelToOIDCConfig(writeModel *OIDCApplicationWriteModel) *domain.OIDCApp {
return &domain.OIDCApp{
- ObjectRoot: writeModelToObjectRoot(writeModel.WriteModel),
- AppID: writeModel.AppID,
- AppName: writeModel.AppName,
- State: writeModel.State,
- ClientID: writeModel.ClientID,
- RedirectUris: writeModel.RedirectUris,
- ResponseTypes: writeModel.ResponseTypes,
- GrantTypes: writeModel.GrantTypes,
- ApplicationType: gu.Ptr(writeModel.ApplicationType),
- AuthMethodType: gu.Ptr(writeModel.AuthMethodType),
- PostLogoutRedirectUris: writeModel.PostLogoutRedirectUris,
- OIDCVersion: gu.Ptr(writeModel.OIDCVersion),
- DevMode: gu.Ptr(writeModel.DevMode),
- AccessTokenType: gu.Ptr(writeModel.AccessTokenType),
- AccessTokenRoleAssertion: gu.Ptr(writeModel.AccessTokenRoleAssertion),
- IDTokenRoleAssertion: gu.Ptr(writeModel.IDTokenRoleAssertion),
- IDTokenUserinfoAssertion: gu.Ptr(writeModel.IDTokenUserinfoAssertion),
- ClockSkew: gu.Ptr(writeModel.ClockSkew),
- AdditionalOrigins: writeModel.AdditionalOrigins,
- SkipNativeAppSuccessPage: gu.Ptr(writeModel.SkipNativeAppSuccessPage),
- BackChannelLogoutURI: gu.Ptr(writeModel.BackChannelLogoutURI),
- LoginVersion: gu.Ptr(writeModel.LoginVersion),
- LoginBaseURI: gu.Ptr(writeModel.LoginBaseURI),
+ ObjectRoot: writeModelToObjectRoot(writeModel.WriteModel),
+ AppID: writeModel.AppID,
+ AppName: writeModel.AppName,
+ State: writeModel.State,
+ ClientID: writeModel.ClientID,
+ RedirectUris: writeModel.RedirectUris,
+ ResponseTypes: writeModel.ResponseTypes,
+ GrantTypes: writeModel.GrantTypes,
+ ApplicationType: gu.Ptr(writeModel.ApplicationType),
+ AuthMethodType: gu.Ptr(writeModel.AuthMethodType),
+ PostLogoutRedirectUris: writeModel.PostLogoutRedirectUris,
+ OIDCVersion: gu.Ptr(writeModel.OIDCVersion),
+ DevMode: gu.Ptr(writeModel.DevMode),
+ AccessTokenType: gu.Ptr(writeModel.AccessTokenType),
+ AccessTokenRoleAssertion: gu.Ptr(writeModel.AccessTokenRoleAssertion),
+ IDTokenRoleAssertion: gu.Ptr(writeModel.IDTokenRoleAssertion),
+ IDTokenUserinfoAssertion: gu.Ptr(writeModel.IDTokenUserinfoAssertion),
+ ClockSkew: gu.Ptr(writeModel.ClockSkew),
+ AdditionalOrigins: writeModel.AdditionalOrigins,
+ SkipNativeAppSuccessPage: gu.Ptr(writeModel.SkipNativeAppSuccessPage),
+ BackChannelLogoutURI: gu.Ptr(writeModel.BackChannelLogoutURI),
+ LoginVersion: gu.Ptr(writeModel.LoginVersion),
+ LoginBaseURI: gu.Ptr(writeModel.LoginBaseURI),
+ IOSTeamID: emptyStringPtr(writeModel.IOSTeamID),
+ IOSBundleID: emptyStringPtr(writeModel.IOSBundleID),
+ AndroidPackageName: emptyStringPtr(writeModel.AndroidPackageName),
+ AndroidSHA256CertFingerprints: writeModel.AndroidSHA256CertFingerprints,
}
}
+// emptyStringPtr returns nil for empty strings so unset app-link fields stay
+// distinguishable from explicitly set values in equality checks / APIs.
+func emptyStringPtr(s string) *string {
+ if s == "" {
+ return nil
+ }
+ return gu.Ptr(s)
+}
+
func samlWriteModelToSAMLConfig(writeModel *SAMLApplicationWriteModel) *domain.SAMLApp {
return &domain.SAMLApp{
ObjectRoot: writeModelToObjectRoot(writeModel.WriteModel),
diff --git a/internal/domain/application_oidc.go b/internal/domain/application_oidc.go
index 83746925f..2fce868bd 100644
--- a/internal/domain/application_oidc.go
+++ b/internal/domain/application_oidc.go
@@ -7,6 +7,8 @@ import (
"strings"
"time"
+ "github.com/muhlemmer/gu"
+
http_util "github.com/zitadel/zitadel/internal/api/http"
"github.com/zitadel/zitadel/internal/eventstore/v1/models"
)
@@ -49,6 +51,14 @@ type OIDCApp struct {
BackChannelLogoutURI *string
LoginVersion *LoginVersion
LoginBaseURI *string
+ // IOSTeamID and IOSBundleID are Associated Domains / passkey trust fields.
+ // Both must be set together, or both empty. Composed as TEAMID.BUNDLEID in AASA.
+ IOSTeamID *string
+ IOSBundleID *string
+ // AndroidPackageName and AndroidSHA256CertFingerprints are Digital Asset Links /
+ // passkey trust fields. Package name is required when fingerprints are non-empty.
+ AndroidPackageName *string
+ AndroidSHA256CertFingerprints []string
State AppState
}
@@ -138,7 +148,7 @@ const (
)
func (a *OIDCApp) IsValid() bool {
- if (a.ClockSkew != nil && (*a.ClockSkew > time.Second*5 || *a.ClockSkew < time.Second*0)) || !a.OriginsValid() {
+ if (a.ClockSkew != nil && (*a.ClockSkew > time.Second*5 || *a.ClockSkew < time.Second*0)) || !a.OriginsValid() || !a.AppLinkConfigValid() {
return false
}
grantTypes := a.getRequiredGrantTypes()
@@ -154,6 +164,19 @@ func (a *OIDCApp) IsValid() bool {
return true
}
+// AppLinkConfigValid checks sibling rules for iOS/Android association fields.
+// Nil and blank (after trim) are treated the same: both iOS fields must be set or
+// both empty; Android package name is required whenever fingerprints are present.
+func (a *OIDCApp) AppLinkConfigValid() bool {
+ teamID := strings.TrimSpace(gu.Value(a.IOSTeamID))
+ bundleID := strings.TrimSpace(gu.Value(a.IOSBundleID))
+ if (teamID == "") != (bundleID == "") {
+ return false
+ }
+ packageName := strings.TrimSpace(gu.Value(a.AndroidPackageName))
+ return len(a.AndroidSHA256CertFingerprints) == 0 || packageName != ""
+}
+
func (a *OIDCApp) OriginsValid() bool {
for _, origin := range a.AdditionalOrigins {
if !http_util.IsOrigin(strings.TrimSpace(origin)) {
diff --git a/internal/domain/application_oidc_test.go b/internal/domain/application_oidc_test.go
index 143239eea..d735d514f 100644
--- a/internal/domain/application_oidc_test.go
+++ b/internal/domain/application_oidc_test.go
@@ -669,3 +669,84 @@ func TestOIDCOriginAllowList(t *testing.T) {
})
}
}
+
+func TestOIDCApp_AppLinkConfigValid(t *testing.T) {
+ tests := []struct {
+ name string
+ app *OIDCApp
+ want bool
+ }{
+ {
+ name: "empty config",
+ app: &OIDCApp{},
+ want: true,
+ },
+ {
+ name: "ios both set",
+ app: &OIDCApp{
+ IOSTeamID: gu.Ptr("TEAMID"),
+ IOSBundleID: gu.Ptr("com.example.app"),
+ },
+ want: true,
+ },
+ {
+ name: "ios team only",
+ app: &OIDCApp{
+ IOSTeamID: gu.Ptr("TEAMID"),
+ },
+ want: false,
+ },
+ {
+ name: "ios bundle only",
+ app: &OIDCApp{
+ IOSBundleID: gu.Ptr("com.example.app"),
+ },
+ want: false,
+ },
+ {
+ name: "ios both empty strings",
+ app: &OIDCApp{
+ IOSTeamID: gu.Ptr(""),
+ IOSBundleID: gu.Ptr(""),
+ },
+ want: true,
+ },
+ {
+ name: "android package only",
+ app: &OIDCApp{
+ AndroidPackageName: gu.Ptr("com.example.app"),
+ },
+ want: true,
+ },
+ {
+ name: "android package and fingerprints",
+ app: &OIDCApp{
+ AndroidPackageName: gu.Ptr("com.example.app"),
+ AndroidSHA256CertFingerprints: []string{"AA:BB"},
+ },
+ want: true,
+ },
+ {
+ name: "android fingerprints without package",
+ app: &OIDCApp{
+ AndroidSHA256CertFingerprints: []string{"AA:BB"},
+ },
+ want: false,
+ },
+ {
+ name: "android fingerprints with empty package",
+ app: &OIDCApp{
+ AndroidPackageName: gu.Ptr(" "),
+ AndroidSHA256CertFingerprints: []string{"AA:BB"},
+ },
+ want: false,
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ if got := tt.app.AppLinkConfigValid(); got != tt.want {
+ t.Errorf("AppLinkConfigValid() = %v, want %v", got, tt.want)
+ }
+ })
+ }
+}
diff --git a/internal/query/app.go b/internal/query/app.go
index d07a7aa1a..eab69f14a 100644
--- a/internal/query/app.go
+++ b/internal/query/app.go
@@ -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
diff --git a/internal/query/app_link.go b/internal/query/app_link.go
new file mode 100644
index 000000000..05f28fa0e
--- /dev/null
+++ b/internal/query/app_link.go
@@ -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
+ }
+}
diff --git a/internal/query/app_test.go b/internal/query/app_test.go
index c24060a60..880ea5bf8 100644
--- a/internal/query/app_test.go
+++ b/internal/query/app_test.go
@@ -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,
diff --git a/internal/query/projection/app.go b/internal/query/projection/app.go
index 0105b5566..09ba28faf 100644
--- a/internal/query/projection/app.go
+++ b/internal/query/projection/app.go
@@ -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
diff --git a/internal/query/projection/app_test.go b/internal/query/projection/app_test.go
index c693d7c3c..c50906eff 100644
--- a/internal/query/projection/app_test.go
+++ b/internal/query/projection/app_test.go
@@ -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{
diff --git a/internal/repository/project/oidc_config.go b/internal/repository/project/oidc_config.go
index 5df0fa1ec..98d4881ee 100644
--- a/internal/repository/project/oidc_config.go
+++ b/internal/repository/project/oidc_config.go
@@ -2,6 +2,7 @@ package project
import (
"context"
+ "slices"
"time"
"github.com/zitadel/zitadel/internal/crypto"
@@ -31,23 +32,27 @@ type OIDCConfigAddedEvent struct {
ClientSecret *crypto.CryptoValue `json:"clientSecret,omitempty"`
HashedSecret string `json:"hashedSecret,omitempty"`
- RedirectUris []string `json:"redirectUris,omitempty"`
- ResponseTypes []domain.OIDCResponseType `json:"responseTypes,omitempty"`
- GrantTypes []domain.OIDCGrantType `json:"grantTypes,omitempty"`
- ApplicationType domain.OIDCApplicationType `json:"applicationType,omitempty"`
- AuthMethodType domain.OIDCAuthMethodType `json:"authMethodType,omitempty"`
- PostLogoutRedirectUris []string `json:"postLogoutRedirectUris,omitempty"`
- DevMode bool `json:"devMode,omitempty"`
- AccessTokenType domain.OIDCTokenType `json:"accessTokenType,omitempty"`
- AccessTokenRoleAssertion bool `json:"accessTokenRoleAssertion,omitempty"`
- IDTokenRoleAssertion bool `json:"idTokenRoleAssertion,omitempty"`
- IDTokenUserinfoAssertion bool `json:"idTokenUserinfoAssertion,omitempty"`
- ClockSkew time.Duration `json:"clockSkew,omitempty"`
- AdditionalOrigins []string `json:"additionalOrigins,omitempty"`
- SkipNativeAppSuccessPage bool `json:"skipNativeAppSuccessPage,omitempty"`
- BackChannelLogoutURI string `json:"backChannelLogoutURI,omitempty"`
- LoginVersion domain.LoginVersion `json:"loginVersion,omitempty"`
- LoginBaseURI string `json:"loginBaseURI,omitempty"`
+ RedirectUris []string `json:"redirectUris,omitempty"`
+ ResponseTypes []domain.OIDCResponseType `json:"responseTypes,omitempty"`
+ GrantTypes []domain.OIDCGrantType `json:"grantTypes,omitempty"`
+ ApplicationType domain.OIDCApplicationType `json:"applicationType,omitempty"`
+ AuthMethodType domain.OIDCAuthMethodType `json:"authMethodType,omitempty"`
+ PostLogoutRedirectUris []string `json:"postLogoutRedirectUris,omitempty"`
+ DevMode bool `json:"devMode,omitempty"`
+ AccessTokenType domain.OIDCTokenType `json:"accessTokenType,omitempty"`
+ AccessTokenRoleAssertion bool `json:"accessTokenRoleAssertion,omitempty"`
+ IDTokenRoleAssertion bool `json:"idTokenRoleAssertion,omitempty"`
+ IDTokenUserinfoAssertion bool `json:"idTokenUserinfoAssertion,omitempty"`
+ ClockSkew time.Duration `json:"clockSkew,omitempty"`
+ AdditionalOrigins []string `json:"additionalOrigins,omitempty"`
+ SkipNativeAppSuccessPage bool `json:"skipNativeAppSuccessPage,omitempty"`
+ BackChannelLogoutURI string `json:"backChannelLogoutURI,omitempty"`
+ LoginVersion domain.LoginVersion `json:"loginVersion,omitempty"`
+ LoginBaseURI string `json:"loginBaseURI,omitempty"`
+ IOSTeamID string `json:"iosTeamId,omitempty"`
+ IOSBundleID string `json:"iosBundleId,omitempty"`
+ AndroidPackageName string `json:"androidPackageName,omitempty"`
+ AndroidSHA256CertFingerprints []string `json:"androidSha256CertFingerprints,omitempty"`
}
func (e *OIDCConfigAddedEvent) Payload() interface{} {
@@ -82,6 +87,10 @@ func NewOIDCConfigAddedEvent(
backChannelLogoutURI string,
loginVersion domain.LoginVersion,
loginBaseURI string,
+ iosTeamID string,
+ iosBundleID string,
+ androidPackageName string,
+ androidSHA256CertFingerprints []string,
) *OIDCConfigAddedEvent {
return &OIDCConfigAddedEvent{
BaseEvent: *eventstore.NewBaseEventForPush(
@@ -89,27 +98,31 @@ func NewOIDCConfigAddedEvent(
aggregate,
OIDCConfigAddedType,
),
- Version: version,
- AppID: appID,
- ClientID: clientID,
- HashedSecret: hashedSecret,
- RedirectUris: redirectUris,
- ResponseTypes: responseTypes,
- GrantTypes: grantTypes,
- ApplicationType: applicationType,
- AuthMethodType: authMethodType,
- PostLogoutRedirectUris: postLogoutRedirectUris,
- DevMode: devMode,
- AccessTokenType: accessTokenType,
- AccessTokenRoleAssertion: accessTokenRoleAssertion,
- IDTokenRoleAssertion: idTokenRoleAssertion,
- IDTokenUserinfoAssertion: idTokenUserinfoAssertion,
- ClockSkew: clockSkew,
- AdditionalOrigins: additionalOrigins,
- SkipNativeAppSuccessPage: skipNativeAppSuccessPage,
- BackChannelLogoutURI: backChannelLogoutURI,
- LoginVersion: loginVersion,
- LoginBaseURI: loginBaseURI,
+ Version: version,
+ AppID: appID,
+ ClientID: clientID,
+ HashedSecret: hashedSecret,
+ RedirectUris: redirectUris,
+ ResponseTypes: responseTypes,
+ GrantTypes: grantTypes,
+ ApplicationType: applicationType,
+ AuthMethodType: authMethodType,
+ PostLogoutRedirectUris: postLogoutRedirectUris,
+ DevMode: devMode,
+ AccessTokenType: accessTokenType,
+ AccessTokenRoleAssertion: accessTokenRoleAssertion,
+ IDTokenRoleAssertion: idTokenRoleAssertion,
+ IDTokenUserinfoAssertion: idTokenUserinfoAssertion,
+ ClockSkew: clockSkew,
+ AdditionalOrigins: additionalOrigins,
+ SkipNativeAppSuccessPage: skipNativeAppSuccessPage,
+ BackChannelLogoutURI: backChannelLogoutURI,
+ LoginVersion: loginVersion,
+ LoginBaseURI: loginBaseURI,
+ IOSTeamID: iosTeamID,
+ IOSBundleID: iosBundleID,
+ AndroidPackageName: androidPackageName,
+ AndroidSHA256CertFingerprints: androidSHA256CertFingerprints,
}
}
@@ -204,7 +217,19 @@ func (e *OIDCConfigAddedEvent) Validate(cmd eventstore.Command) bool {
if e.LoginVersion != c.LoginVersion {
return false
}
- return e.LoginBaseURI == c.LoginBaseURI
+ if e.LoginBaseURI != c.LoginBaseURI {
+ return false
+ }
+ if e.IOSTeamID != c.IOSTeamID {
+ return false
+ }
+ if e.IOSBundleID != c.IOSBundleID {
+ return false
+ }
+ if e.AndroidPackageName != c.AndroidPackageName {
+ return false
+ }
+ return slices.Equal(e.AndroidSHA256CertFingerprints, c.AndroidSHA256CertFingerprints)
}
func OIDCConfigAddedEventMapper(event eventstore.Event) (eventstore.Event, error) {
@@ -223,25 +248,29 @@ func OIDCConfigAddedEventMapper(event eventstore.Event) (eventstore.Event, error
type OIDCConfigChangedEvent struct {
eventstore.BaseEvent `json:"-"`
- Version *domain.OIDCVersion `json:"oidcVersion,omitempty"`
- AppID string `json:"appId"`
- RedirectUris *[]string `json:"redirectUris,omitempty"`
- ResponseTypes *[]domain.OIDCResponseType `json:"responseTypes,omitempty"`
- GrantTypes *[]domain.OIDCGrantType `json:"grantTypes,omitempty"`
- ApplicationType *domain.OIDCApplicationType `json:"applicationType,omitempty"`
- AuthMethodType *domain.OIDCAuthMethodType `json:"authMethodType,omitempty"`
- PostLogoutRedirectUris *[]string `json:"postLogoutRedirectUris,omitempty"`
- DevMode *bool `json:"devMode,omitempty"`
- AccessTokenType *domain.OIDCTokenType `json:"accessTokenType,omitempty"`
- AccessTokenRoleAssertion *bool `json:"accessTokenRoleAssertion,omitempty"`
- IDTokenRoleAssertion *bool `json:"idTokenRoleAssertion,omitempty"`
- IDTokenUserinfoAssertion *bool `json:"idTokenUserinfoAssertion,omitempty"`
- ClockSkew *time.Duration `json:"clockSkew,omitempty"`
- AdditionalOrigins *[]string `json:"additionalOrigins,omitempty"`
- SkipNativeAppSuccessPage *bool `json:"skipNativeAppSuccessPage,omitempty"`
- BackChannelLogoutURI *string `json:"backChannelLogoutURI,omitempty"`
- LoginVersion *domain.LoginVersion `json:"loginVersion,omitempty"`
- LoginBaseURI *string `json:"loginBaseURI,omitempty"`
+ Version *domain.OIDCVersion `json:"oidcVersion,omitempty"`
+ AppID string `json:"appId"`
+ RedirectUris *[]string `json:"redirectUris,omitempty"`
+ ResponseTypes *[]domain.OIDCResponseType `json:"responseTypes,omitempty"`
+ GrantTypes *[]domain.OIDCGrantType `json:"grantTypes,omitempty"`
+ ApplicationType *domain.OIDCApplicationType `json:"applicationType,omitempty"`
+ AuthMethodType *domain.OIDCAuthMethodType `json:"authMethodType,omitempty"`
+ PostLogoutRedirectUris *[]string `json:"postLogoutRedirectUris,omitempty"`
+ DevMode *bool `json:"devMode,omitempty"`
+ AccessTokenType *domain.OIDCTokenType `json:"accessTokenType,omitempty"`
+ AccessTokenRoleAssertion *bool `json:"accessTokenRoleAssertion,omitempty"`
+ IDTokenRoleAssertion *bool `json:"idTokenRoleAssertion,omitempty"`
+ IDTokenUserinfoAssertion *bool `json:"idTokenUserinfoAssertion,omitempty"`
+ ClockSkew *time.Duration `json:"clockSkew,omitempty"`
+ AdditionalOrigins *[]string `json:"additionalOrigins,omitempty"`
+ SkipNativeAppSuccessPage *bool `json:"skipNativeAppSuccessPage,omitempty"`
+ BackChannelLogoutURI *string `json:"backChannelLogoutURI,omitempty"`
+ LoginVersion *domain.LoginVersion `json:"loginVersion,omitempty"`
+ LoginBaseURI *string `json:"loginBaseURI,omitempty"`
+ IOSTeamID *string `json:"iosTeamId,omitempty"`
+ IOSBundleID *string `json:"iosBundleId,omitempty"`
+ AndroidPackageName *string `json:"androidPackageName,omitempty"`
+ AndroidSHA256CertFingerprints *[]string `json:"androidSha256CertFingerprints,omitempty"`
}
func (e *OIDCConfigChangedEvent) Payload() interface{} {
@@ -398,6 +427,34 @@ func ChangeOIDCLoginBaseURI(loginBaseURI string) func(event *OIDCConfigChangedEv
}
}
+func ChangeIOSTeamID(iosTeamID string) func(event *OIDCConfigChangedEvent) {
+ return func(e *OIDCConfigChangedEvent) {
+ e.IOSTeamID = &iosTeamID
+ }
+}
+
+func ChangeIOSBundleID(iosBundleID string) func(event *OIDCConfigChangedEvent) {
+ return func(e *OIDCConfigChangedEvent) {
+ e.IOSBundleID = &iosBundleID
+ }
+}
+
+func ChangeAndroidPackageName(androidPackageName string) func(event *OIDCConfigChangedEvent) {
+ return func(e *OIDCConfigChangedEvent) {
+ e.AndroidPackageName = &androidPackageName
+ }
+}
+
+func ChangeAndroidSHA256CertFingerprints(fingerprints []string) func(event *OIDCConfigChangedEvent) {
+ return func(e *OIDCConfigChangedEvent) {
+ if fingerprints == nil {
+ // explicitly set them to empty so we can differentiate "not set" in the event in case of no changes
+ fingerprints = make([]string, 0)
+ }
+ e.AndroidSHA256CertFingerprints = &fingerprints
+ }
+}
+
func OIDCConfigChangedEventMapper(event eventstore.Event) (eventstore.Event, error) {
e := &OIDCConfigChangedEvent{
BaseEvent: *eventstore.BaseEventFromRepo(event),
diff --git a/proto/zitadel/app.proto b/proto/zitadel/app.proto
index 08359e376..c4486c17a 100644
--- a/proto/zitadel/app.proto
+++ b/proto/zitadel/app.proto
@@ -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 {
diff --git a/proto/zitadel/application/v2/application_service.proto b/proto/zitadel/application/v2/application_service.proto
index 533d00963..a84d62345 100644
--- a/proto/zitadel/application/v2/application_service.proto
+++ b/proto/zitadel/application/v2/application_service.proto
@@ -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 {
diff --git a/proto/zitadel/application/v2/oidc.proto b/proto/zitadel/application/v2/oidc.proto
index 9469e3f3a..95f7b4355 100644
--- a/proto/zitadel/application/v2/oidc.proto
+++ b/proto/zitadel/application/v2/oidc.proto
@@ -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})$";
+ }
+ }
+ }];
}
diff --git a/proto/zitadel/management.proto b/proto/zitadel/management.proto
index 1bd4c7f81..a416a3912 100644
--- a/proto/zitadel/management.proto
+++ b/proto/zitadel/management.proto
@@ -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 {