feat: native app links for passkeys (#12580)

# Which Problems Are Solved

- Native iOS/Android passkeys need OS trust files on the ZITADEL domain;
without them, Associated Domains / App Links verification fails.
- Operators could not configure iOS Team ID + Bundle ID or Android
package name + SHA-256 fingerprints on OIDC apps.
- `/.well-known/apple-app-site-association` and
`/.well-known/assetlinks.json` were not served from application config.

# How the Problems Are Solved

- Add iOS/Android app-link fields on OIDC app create/update (Application
API v2 + Management), with validation.
- Persist and project those fields; query active app-link configs
instance-wide.
- Serve AASA (`webcredentials`) and Digital Asset Links
(`get_login_creds`) from well-known paths, with configurable
`Cache-Control` and fingerprint normalization at serve time.
- Console UI to edit the fields, with links to the well-known endpoints.
- Operator docs for configuration, endpoints, caching, and verification.

# Additional Changes

- Document on API fields that well-known responses may be HTTP-cached
and platform verifiers may delay propagation.
- Runtime config: `WellKnown.AppLinksCacheControlMaxAge` (default `5m`;
`0` → `no-store`).

# Additional Context

- Closes #12497
- Implemented and reviewed as stack:
  - #12531 API contract
  - #12532 storage wiring
  - #12536 well-known endpoints
  - #12537 console
  - #12547 docs

---------

Co-authored-by: Cursor <[email protected]>
This commit is contained in:
Livio Spring
2026-08-12 08:04:27 +02:00
committed by GitHub
co-authored by Cursor
parent 2bd42e8fc4
commit 318bfc36d7
68 changed files with 3227 additions and 467 deletions
@@ -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) **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 ## authorization_endpoint
`${CUSTOM_DOMAIN}/oauth/v2/authorize` `${CUSTOM_DOMAIN}/oauth/v2/authorize`
@@ -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. * 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. * 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. * 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! Don't hesitate to ask if you have further questions about integrating passkeys in your ZITADEL application!
@@ -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. 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/) . Passkeys and there underlying protocols are a standard defined by the [FIDO Standard](https://fidoalliance.org/) .
<Callout type="info">
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).
</Callout>
## Register Passkey ## Register Passkey
### Flow ### Flow
@@ -21,5 +21,6 @@ import { FileText, Folder, Link as LinkIcon } from 'lucide-react';
<Card title="Grant Types" href="/apis/openidoauth/grant-types" icon={<FileText />} /> <Card title="Grant Types" href="/apis/openidoauth/grant-types" icon={<FileText />} />
<Card title="Authrequest" href="/apis/openidoauth/authrequest" icon={<FileText />} /> <Card title="Authrequest" href="/apis/openidoauth/authrequest" icon={<FileText />} />
<Card title="Webkeys" href="/guides/integrate/login/oidc/webkeys" icon={<FileText />} /> <Card title="Webkeys" href="/guides/integrate/login/oidc/webkeys" icon={<FileText />} />
<Card title="Native App Links" href="/guides/integrate/login/oidc/native-app-links" icon={<FileText />} />
<Card title="Token Exchange" href="/guides/integrate/token-exchange" icon={<FileText />} /> <Card title="Token Exchange" href="/guides/integrate/token-exchange" icon={<FileText />} />
</Cards> </Cards>
@@ -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`.
<Callout type="info">
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.
</Callout>
## 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)
@@ -63,6 +63,15 @@ Native Applications are installed on a thin client, such as a smartphone or desk
<AuthType components={props.components} appType="native" /> <AuthType components={props.components} appType="native" />
#### 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
User Agent Applications are executed entirely in the web browser (Client-Side). 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 ## References
- [Recommended OIDC Flows](../../integrate/login/oidc/oauth-recommended-flows) - [Recommended OIDC Flows](../../integrate/login/oidc/oauth-recommended-flows)
- [Native app links for passkeys](../../integrate/login/oidc/native-app-links)
+1
View File
@@ -269,6 +269,7 @@ export const guidesSidebar: readonly SidebarItem[] = [
"apis/openidoauth/grant-types", "apis/openidoauth/grant-types",
"apis/openidoauth/authrequest", "apis/openidoauth/authrequest",
"guides/integrate/login/oidc/webkeys", "guides/integrate/login/oidc/webkeys",
"guides/integrate/login/oidc/native-app-links",
"guides/integrate/token-exchange", "guides/integrate/token-exchange",
], ],
}, },
+5
View File
@@ -701,6 +701,11 @@ OIDC:
# Lifetime of the token used to notify clients through OIDC back-channel logout. # Lifetime of the token used to notify clients through OIDC back-channel logout.
TokenLifetime: 15m # ZITADEL_OIDC_BACKCHANNELLOGOUT_TOKENLIFETIME 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: SAML:
DefaultLoginURLV2: "/ui/v2/login/login?samlRequest=" # ZITADEL_SAML_DEFAULTLOGINURLV2 DefaultLoginURLV2: "/ui/v2/login/login?samlRequest=" # ZITADEL_SAML_DEFAULTLOGINURLV2
ProviderConfig: ProviderConfig:
+27
View File
@@ -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"
}
+4
View File
@@ -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[];
+1
View File
@@ -194,6 +194,7 @@ type Steps struct {
s72AddColumnsToLoginNamesView *AddColumnsToLoginNamesView s72AddColumnsToLoginNamesView *AddColumnsToLoginNamesView
s73FixUserGrantRoles *FixUserGrantRoles s73FixUserGrantRoles *FixUserGrantRoles
s74Apps7OIDCConfigsAddRegistrationToken *Apps7OIDCConfigsAddRegistrationToken s74Apps7OIDCConfigsAddRegistrationToken *Apps7OIDCConfigsAddRegistrationToken
s75Apps7OIDCConfigsAddAppLinkConfig *Apps7OIDCConfigsAddAppLinkConfig
RelationalTables *TransactionalTables RelationalTables *TransactionalTables
} }
+2
View File
@@ -257,6 +257,7 @@ func Setup(ctx context.Context, config *Config, steps *Steps, masterKey string)
steps.s72AddColumnsToLoginNamesView = &AddColumnsToLoginNamesView{dbClient: dbClient} steps.s72AddColumnsToLoginNamesView = &AddColumnsToLoginNamesView{dbClient: dbClient}
steps.s73FixUserGrantRoles = &FixUserGrantRoles{eventstore: eventstoreClient} steps.s73FixUserGrantRoles = &FixUserGrantRoles{eventstore: eventstoreClient}
steps.s74Apps7OIDCConfigsAddRegistrationToken = &Apps7OIDCConfigsAddRegistrationToken{dbClient: dbClient} steps.s74Apps7OIDCConfigsAddRegistrationToken = &Apps7OIDCConfigsAddRegistrationToken{dbClient: dbClient}
steps.s75Apps7OIDCConfigsAddAppLinkConfig = &Apps7OIDCConfigsAddAppLinkConfig{dbClient: dbClient}
err = projection.Create(ctx, dbClient, eventstoreClient, config.Projections, nil, nil, nil) err = projection.Create(ctx, dbClient, eventstoreClient, config.Projections, nil, nil, nil)
if err != nil { if err != nil {
@@ -390,6 +391,7 @@ func Setup(ctx context.Context, config *Config, steps *Steps, masterKey string)
steps.s72AddColumnsToLoginNamesView, steps.s72AddColumnsToLoginNamesView,
steps.s73FixUserGrantRoles, steps.s73FixUserGrantRoles,
steps.s74Apps7OIDCConfigsAddRegistrationToken, steps.s74Apps7OIDCConfigsAddRegistrationToken,
steps.s75Apps7OIDCConfigsAddAppLinkConfig,
} { } {
setupErr = executeMigration(ctx, eventstoreClient, step, "migration failed") setupErr = executeMigration(ctx, eventstoreClient, step, "migration failed")
if setupErr != nil { if setupErr != nil {
+2
View File
@@ -24,6 +24,7 @@ import (
scim_config "github.com/zitadel/zitadel/internal/api/scim/config" 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/console"
"github.com/zitadel/zitadel/internal/api/ui/login" "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" auth_es "github.com/zitadel/zitadel/internal/auth/repository/eventsourcing"
"github.com/zitadel/zitadel/internal/cache/connector" "github.com/zitadel/zitadel/internal/cache/connector"
"github.com/zitadel/zitadel/internal/command" "github.com/zitadel/zitadel/internal/command"
@@ -72,6 +73,7 @@ type Config struct {
SCIM scim_config.Config SCIM scim_config.Config
Login login.Config Login login.Config
Console console.Config Console console.Config
WellKnown well_known.Config
AssetStorage static_config.AssetStorageConfig AssetStorage static_config.AssetStorageConfig
InternalAuthZ authz.Config InternalAuthZ authz.Config
SystemAuthZ authz.Config SystemAuthZ authz.Config
+4
View File
@@ -85,6 +85,7 @@ import (
"github.com/zitadel/zitadel/internal/api/ui/console" "github.com/zitadel/zitadel/internal/api/ui/console"
"github.com/zitadel/zitadel/internal/api/ui/console/path" "github.com/zitadel/zitadel/internal/api/ui/console/path"
"github.com/zitadel/zitadel/internal/api/ui/login" "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" auth_es "github.com/zitadel/zitadel/internal/auth/repository/eventsourcing"
"github.com/zitadel/zitadel/internal/authz" "github.com/zitadel/zitadel/internal/authz"
authz_repo "github.com/zitadel/zitadel/internal/authz/repository" authz_repo "github.com/zitadel/zitadel/internal/authz/repository"
@@ -657,6 +658,9 @@ func startAPIs(
} }
apis.RegisterHandlerOnPrefix(robots_txt.HandlerPrefix, robotsTxtHandler) 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? // TODO: Record openapi access logs?
openAPIHandler, err := openapi.Start() openAPIHandler, err := openapi.Start()
if err != nil { if err != nil {
@@ -462,6 +462,102 @@
</div> </div>
</cnsl-card> </cnsl-card>
} }
@if (currentSetting.id === 'native-app-links' && oidcForm && app?.oidcConfig) {
<cnsl-card title="{{ 'APP.OIDC.APPLINKSSECTION' | translate }}">
<p class="app-desc cnsl-secondary-text">{{ 'APP.OIDC.APPLINKSDESC' | translate }}</p>
<form [formGroup]="oidcForm" (ngSubmit)="saveOIDCApp()">
<h3 class="title">{{ 'APP.OIDC.IOSTITLE' | translate }}</h3>
<cnsl-info-section class="full-width app-desc">
<span>{{ 'APP.OIDC.IOSDESC' | translate }}</span>
@if (issuer$ | async; as issuer) {
<div class="link app-link-endpoint">
<a [href]="issuer + '/.well-known/apple-app-site-association'" rel="noreferrer" target="_blank"
>{{ issuer }}/.well-known/apple-app-site-association</a
>
</div>
}
</cnsl-info-section>
<div class="content">
<cnsl-form-field class="app-formfield">
<cnsl-label>{{ 'APP.OIDC.TEAMID' | translate }}</cnsl-label>
<input
cnslInput
formControlName="iosTeamId"
maxlength="10"
autocomplete="off"
[placeholder]="'APP.OIDC.TEAMIDPLACEHOLDER' | translate"
/>
</cnsl-form-field>
<cnsl-form-field class="app-formfield">
<cnsl-label>{{ 'APP.OIDC.BUNDLEID' | translate }}</cnsl-label>
<input
cnslInput
formControlName="iosBundleId"
autocomplete="off"
[placeholder]="'APP.OIDC.BUNDLEIDPLACEHOLDER' | translate"
/>
</cnsl-form-field>
</div>
<cnsl-info-section class="full-width app-desc">
<span>{{ 'APP.OIDC.TEAMIDDESC' | translate }} {{ 'APP.OIDC.BUNDLEIDDESC' | translate }}</span>
</cnsl-info-section>
@if (showIosAppLinkIncompleteWarning()) {
<cnsl-info-section class="full-width app-desc" [type]="InfoSectionType.WARN">
<span>{{ 'APP.OIDC.IOSAPPLINKINCOMPLETE' | translate }}</span>
</cnsl-info-section>
}
<h3 class="title" style="margin-top: 1.5rem">{{ 'APP.OIDC.ANDROIDTITLE' | translate }}</h3>
<cnsl-info-section class="full-width app-desc">
<span>{{ 'APP.OIDC.ANDROIDDESC' | translate }}</span>
@if (issuer$ | async; as issuer) {
<div class="link app-link-endpoint">
<a [href]="issuer + '/.well-known/assetlinks.json'" rel="noreferrer" target="_blank"
>{{ issuer }}/.well-known/assetlinks.json</a
>
</div>
}
</cnsl-info-section>
<div class="content">
<cnsl-form-field class="app-formfield full-width">
<cnsl-label>{{ 'APP.OIDC.PACKAGENAME' | translate }}</cnsl-label>
<input
cnslInput
formControlName="androidPackageName"
autocomplete="off"
[placeholder]="'APP.OIDC.PACKAGENAMEPLACEHOLDER' | translate"
/>
</cnsl-form-field>
<cnsl-form-field class="app-formfield full-width fingerprints-field">
<cnsl-label>{{ 'APP.OIDC.FINGERPRINTS' | translate }}</cnsl-label>
<textarea
cnslInput
class="fingerprints-input"
formControlName="androidSha256CertFingerprints"
rows="10"
[placeholder]="'APP.OIDC.FINGERPRINTSPLACEHOLDER' | translate"
></textarea>
</cnsl-form-field>
</div>
<cnsl-info-section class="full-width app-desc">
<span>{{ 'APP.OIDC.FINGERPRINTSDESC' | translate }}</span>
</cnsl-info-section>
@if (showAndroidPackageRequiredWarning()) {
<cnsl-info-section class="full-width app-desc" [type]="InfoSectionType.WARN">
<span>{{ 'APP.OIDC.ANDROIDPACKAGEREQUIRED' | translate }}</span>
</cnsl-info-section>
}
<div class="btn-container">
<button class="submit-button" type="submit" color="primary" [disabled]="!canWrite" mat-raised-button>
{{ 'ACTIONS.SAVE' | translate }}
</button>
</div>
</form>
</cnsl-card>
}
@if (currentSetting.id === 'urls') { @if (currentSetting.id === 'urls') {
<cnsl-card title=" {{ 'APP.URLS' | translate }}"> <cnsl-card title=" {{ 'APP.URLS' | translate }}">
@if (issuer$ | async; as issuer) { @if (issuer$ | async; as issuer) {
@@ -53,6 +53,11 @@
.app-desc { .app-desc {
font-size: 14px; font-size: 14px;
margin: 0; margin: 0;
.app-link-endpoint {
margin-top: 0.5rem;
word-break: break-all;
}
} }
.app-input { .app-input {
@@ -281,6 +286,17 @@
input { input {
max-width: none; 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;
}
} }
} }
@@ -1,7 +1,16 @@
import { COMMA, ENTER, SPACE } from '@angular/cdk/keycodes'; import { COMMA, ENTER, SPACE } from '@angular/cdk/keycodes';
import { Location } from '@angular/common'; import { Location } from '@angular/common';
import { Component, OnDestroy, OnInit, signal } from '@angular/core'; 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 { MatCheckboxChange } from '@angular/material/checkbox';
import { MatDialog } from '@angular/material/dialog'; import { MatDialog } from '@angular/material/dialog';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
@@ -206,7 +215,8 @@ export class AppDetailComponent implements OnInit, OnDestroy {
private router: Router, private router: Router,
private breadcrumbService: BreadcrumbService, private breadcrumbService: BreadcrumbService,
) { ) {
this.oidcForm = this.fb.group({ this.oidcForm = this.fb.group(
{
devMode: [{ value: false, disabled: true }], devMode: [{ value: false, disabled: true }],
skipNativeAppSuccessPage: [{ value: false, disabled: true }], skipNativeAppSuccessPage: [{ value: false, disabled: true }],
clientId: [{ value: '', disabled: true }], clientId: [{ value: '', disabled: true }],
@@ -217,7 +227,13 @@ export class AppDetailComponent implements OnInit, OnDestroy {
loginV2: [{ value: false, disabled: true }], loginV2: [{ value: false, disabled: true }],
loginV2BaseURL: [{ value: '', disabled: true }], loginV2BaseURL: [{ value: '', disabled: true }],
backChannelLogoutURI: [{ 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({ this.oidcTokenForm = this.fb.group({
accessTokenType: [{ value: '', disabled: true }], accessTokenType: [{ value: '', disabled: true }],
@@ -375,6 +391,7 @@ export class AppDetailComponent implements OnInit, OnDestroy {
this.settingsList = [ this.settingsList = [
{ id: 'configuration', i18nKey: 'APP.CONFIGURATION' }, { id: 'configuration', i18nKey: 'APP.CONFIGURATION' },
{ id: 'token', i18nKey: 'APP.TOKEN' }, { id: 'token', i18nKey: 'APP.TOKEN' },
{ id: 'native-app-links', i18nKey: 'APP.OIDC.APPLINKSSECTION' },
{ id: 'urls', i18nKey: 'APP.URLS' }, { id: 'urls', i18nKey: 'APP.URLS' },
]; ];
} else { } else {
@@ -383,6 +400,7 @@ export class AppDetailComponent implements OnInit, OnDestroy {
{ id: 'token', i18nKey: 'APP.TOKEN' }, { id: 'token', i18nKey: 'APP.TOKEN' },
{ id: 'redirect-uris', i18nKey: 'APP.OIDC.REDIRECTSECTIONTITLE' }, { id: 'redirect-uris', i18nKey: 'APP.OIDC.REDIRECTSECTIONTITLE' },
{ id: 'additional-origins', i18nKey: 'APP.ADDITIONALORIGINS' }, { id: 'additional-origins', i18nKey: 'APP.ADDITIONALORIGINS' },
{ id: 'native-app-links', i18nKey: 'APP.OIDC.APPLINKSSECTION' },
{ id: 'urls', i18nKey: 'APP.URLS' }, { id: 'urls', i18nKey: 'APP.URLS' },
]; ];
} }
@@ -465,6 +483,12 @@ export class AppDetailComponent implements OnInit, OnDestroy {
if (this.app.oidcConfig) { if (this.app.oidcConfig) {
this.oidcForm.patchValue(this.app.oidcConfig); this.oidcForm.patchValue(this.app.oidcConfig);
this.oidcTokenForm.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) { if (this.app.apiConfig) {
this.apiForm.patchValue(this.app.apiConfig); this.apiForm.patchValue(this.app.apiConfig);
@@ -657,7 +681,10 @@ export class AppDetailComponent implements OnInit, OnDestroy {
public saveOIDCApp(): void { public saveOIDCApp(): void {
this.requestRedirectValuesSubject$.next(); this.requestRedirectValuesSubject$.next();
if (this.oidcForm.valid) { if (!this.oidcForm.valid) {
this.markNativeAppLinkFieldsTouched();
return;
}
if (this.app?.oidcConfig) { if (this.app?.oidcConfig) {
// configuration // configuration
this.app.oidcConfig.responseTypesList = this.responseTypesList?.value; this.app.oidcConfig.responseTypesList = this.responseTypesList?.value;
@@ -719,6 +746,14 @@ export class AppDetailComponent implements OnInit, OnDestroy {
postLogoutRedirectUris: this.normalizeOIDCListForUpdate(this.app.oidcConfig.postLogoutRedirectUrisList), postLogoutRedirectUris: this.normalizeOIDCListForUpdate(this.app.oidcConfig.postLogoutRedirectUrisList),
developmentMode: this.app.oidcConfig.devMode, developmentMode: this.app.oidcConfig.devMode,
skipNativeAppSuccessPage: this.app.oidcConfig.skipNativeAppSuccessPage, 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 ...(this.clockSkewSeconds?.value
? { ? {
clockSkew: { clockSkew: {
@@ -748,12 +783,21 @@ export class AppDetailComponent implements OnInit, OnDestroy {
}); });
} }
} }
}
private normalizeOIDCListForUpdate(values: string[]): string[] { private normalizeOIDCListForUpdate(values: string[]): string[] {
return values.length === 0 ? [''] : values; 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 { private toOIDCV2ResponseType(type: OIDCResponseType): OIDCV2ResponseType {
switch (type) { switch (type) {
case OIDCResponseType.OIDC_RESPONSE_TYPE_CODE: case OIDCResponseType.OIDC_RESPONSE_TYPE_CODE:
@@ -943,6 +987,49 @@ export class AppDetailComponent implements OnInit, OnDestroy {
return this.oidcForm.get('skipNativeAppSuccessPage') as FormControl<boolean>; return this.oidcForm.get('skipNativeAppSuccessPage') as FormControl<boolean>;
} }
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 { public get accessTokenType(): AbstractControl | null {
return this.oidcTokenForm.get('accessTokenType'); 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;
};
}
+22 -1
View File
@@ -2610,6 +2610,25 @@
"DEVMODEDESC": "احذر: مع تمكين وضع التطوير، لن يتم التحقق من عناوين URI لإعادة التوجيه.", "DEVMODEDESC": "احذر: مع تمكين وضع التطوير، لن يتم التحقق من عناوين URI لإعادة التوجيه.",
"SKIPNATIVEAPPSUCCESSPAGE": "تخطي صفحة نجاح تسجيل الدخول", "SKIPNATIVEAPPSUCCESSPAGE": "تخطي صفحة نجاح تسجيل الدخول",
"SKIPNATIVEAPPSUCCESSPAGE_DESCRIPTION": "تخطي صفحة النجاح بعد تسجيل الدخول لهذا التطبيق الأصلي.", "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 لإعادة التوجيه", "REDIRECT": "عناوين URI لإعادة التوجيه",
"REDIRECTSECTION": "عناوين URI لإعادة التوجيه", "REDIRECTSECTION": "عناوين URI لإعادة التوجيه",
"POSTLOGOUTREDIRECT": "عناوين URI بعد تسجيل الخروج", "POSTLOGOUTREDIRECT": "عناوين URI بعد تسجيل الخروج",
@@ -2673,7 +2692,9 @@
} }
} }
}, },
"BACKCHANNELLOOGOUTURI": "عنوان URI لتسجيل الخروج من القناة الخلفية" "BACKCHANNELLOOGOUTURI": "عنوان URI لتسجيل الخروج من القناة الخلفية",
"IOSAPPLINKINCOMPLETE": "يجب تعيين معرّف فريق iOS ومعرّف الحزمة معًا، أو تركهما فارغين معًا.",
"ANDROIDPACKAGEREQUIRED": "اسم حزمة Android مطلوب عند تعيين بصمات شهادة."
}, },
"API": { "API": {
"INFO": { "INFO": {
+22 -1
View File
@@ -2618,6 +2618,25 @@
"DEVMODEDESC": "Внимание: При активиран режим на разработка URI адресите за пренасочване няма да бъдат валидирани.", "DEVMODEDESC": "Внимание: При активиран режим на разработка URI адресите за пренасочване няма да бъдат валидирани.",
"SKIPNATIVEAPPSUCCESSPAGE": "Пропуснете страницата за успешно влизане", "SKIPNATIVEAPPSUCCESSPAGE": "Пропуснете страницата за успешно влизане",
"SKIPNATIVEAPPSUCCESSPAGE_DESCRIPTION": "Пропуснете страницата за успех след влизане в това родно приложение.", "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", "REDIRECT": "Пренасочване на URI",
"REDIRECTSECTION": "Пренасочване на URI", "REDIRECTSECTION": "Пренасочване на URI",
"POSTLOGOUTREDIRECT": "Публикувайте 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": { "API": {
"INFO": { "INFO": {
+22 -1
View File
@@ -2600,6 +2600,25 @@
"DEVMODEDESC": "Pozor: S povoleným vývojovým režimem nebudou URI pro přesměrování ověřovány.", "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": "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.", "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í", "REDIRECT": "URI pro přesměrování",
"REDIRECTSECTION": "URI pro přesměrování", "REDIRECTSECTION": "URI pro přesměrování",
"POSTLOGOUTREDIRECT": "URI po odhlášení", "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": { "API": {
"INFO": { "INFO": {
+22 -1
View File
@@ -2591,6 +2591,25 @@
"DEVMODEDESC": "Bei eingeschaltetem Entwicklermodus werden die Weiterleitungs-URIs im OIDC-Flow nicht validiert.", "DEVMODEDESC": "Bei eingeschaltetem Entwicklermodus werden die Weiterleitungs-URIs im OIDC-Flow nicht validiert.",
"SKIPNATIVEAPPSUCCESSPAGE": "Login Erfolgseite überspringen", "SKIPNATIVEAPPSUCCESSPAGE": "Login Erfolgseite überspringen",
"SKIPNATIVEAPPSUCCESSPAGE_DESCRIPTION": "Erfolgseite nach dem Login für diese Native Applikation ü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", "REDIRECT": "Weiterleitungs-URIs",
"REDIRECTSECTION": "Weiterleitungs-URIs", "REDIRECTSECTION": "Weiterleitungs-URIs",
"POSTLOGOUTREDIRECT": "URIs für Post-Log-out", "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": { "API": {
"INFO": { "INFO": {
+21
View File
@@ -2622,6 +2622,27 @@
"DEVMODEDESC": "Beware: With development mode enabled redirect URIs will not be validated.", "DEVMODEDESC": "Beware: With development mode enabled redirect URIs will not be validated.",
"SKIPNATIVEAPPSUCCESSPAGE": "Skip Login Success Page", "SKIPNATIVEAPPSUCCESSPAGE": "Skip Login Success Page",
"SKIPNATIVEAPPSUCCESSPAGE_DESCRIPTION": "Skip the success page after a login for this native app.", "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", "REDIRECT": "Redirect URIs",
"REDIRECTSECTION": "Redirect URIs", "REDIRECTSECTION": "Redirect URIs",
"POSTLOGOUTREDIRECT": "Post Logout URIs", "POSTLOGOUTREDIRECT": "Post Logout URIs",
+22 -1
View File
@@ -2588,6 +2588,25 @@
"DEVMODEDESC": "Cuidado: Si el modo de desarrollo está activado las URIs de redirección no serán validadas.", "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": "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.", "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", "REDIRECT": "URIs de redirección",
"REDIRECTSECTION": "URIs de redirección", "REDIRECTSECTION": "URIs de redirección",
"POSTLOGOUTREDIRECT": "URIs a usar tras cerrar sesió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": { "API": {
"INFO": { "INFO": {
+22 -1
View File
@@ -2592,6 +2592,25 @@
"DEVMODEDESC": "Attention", "DEVMODEDESC": "Attention",
"SKIPNATIVEAPPSUCCESSPAGE": "Sauter la page de succès de connexion", "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", "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", "REDIRECT": "Rediriger les URIs",
"REDIRECTSECTION": "URIs de redirection", "REDIRECTSECTION": "URIs de redirection",
"POSTLOGOUTREDIRECT": "URIs de post-déconnexion", "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": { "API": {
"INFO": { "INFO": {
+22 -1
View File
@@ -2620,6 +2620,25 @@
"DEVMODEDESC": "Figyelem: Ha a fejlesztői mód be van kapcsolva, a redirect URI-k nem lesznek érvényesítve.", "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": "Login sikeroldal kihagyása",
"SKIPNATIVEAPPSUCCESSPAGE_DESCRIPTION": "Hagyd ki a sikeroldalt a bejelentkezés után ennél a helyi alkalmazásnál.", "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", "REDIRECT": "Redirect URIs",
"REDIRECTSECTION": "Redirect URIs", "REDIRECTSECTION": "Redirect URIs",
"POSTLOGOUTREDIRECT": "Kijelentkezés utáni 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": { "API": {
"INFO": { "INFO": {
+22 -1
View File
@@ -2614,6 +2614,25 @@
"DEVMODEDESC": "Hati-hati: Dengan mengaktifkan mode pengembangan, URI pengalihan tidak akan divalidasi.", "DEVMODEDESC": "Hati-hati: Dengan mengaktifkan mode pengembangan, URI pengalihan tidak akan divalidasi.",
"SKIPNATIVEAPPSUCCESSPAGE": "Lewati Halaman Sukses Login", "SKIPNATIVEAPPSUCCESSPAGE": "Lewati Halaman Sukses Login",
"SKIPNATIVEAPPSUCCESSPAGE_DESCRIPTION": "Lewati halaman sukses setelah login untuk aplikasi asli ini.", "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", "REDIRECT": "Arahkan ulang URI",
"REDIRECTSECTION": "Arahkan ulang URI", "REDIRECTSECTION": "Arahkan ulang URI",
"POSTLOGOUTREDIRECT": "URI Posting Logout", "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": { "API": {
"INFO": { "INFO": {
+22 -1
View File
@@ -2592,6 +2592,25 @@
"DEVMODEDESC": "Attenzione: Con la modalità di sviluppo abilitata, gli URI di reindirizzamento non saranno convalidati.", "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": "Salta la pagina di successo dopo il login",
"SKIPNATIVEAPPSUCCESSPAGE_DESCRIPTION": "Salta la pagina di successo dopo il login per questa applicazione nativa", "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", "REDIRECT": "URI per il reindrizzamento",
"REDIRECTSECTION": "Reindirizzamento", "REDIRECTSECTION": "Reindirizzamento",
"POSTLOGOUTREDIRECT": "URI post logout", "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": { "API": {
"INFO": { "INFO": {
+22 -1
View File
@@ -2617,6 +2617,25 @@
"DEVMODEDESC": "注意:開発モードを有効にすると、URIが認証されません。", "DEVMODEDESC": "注意:開発モードを有効にすると、URIが認証されません。",
"SKIPNATIVEAPPSUCCESSPAGE": "ログイン後に成功ページをスキップする", "SKIPNATIVEAPPSUCCESSPAGE": "ログイン後に成功ページをスキップする",
"SKIPNATIVEAPPSUCCESSPAGE_DESCRIPTION": "このネイティブアプリのログイン後に成功ページをスキップする", "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", "REDIRECT": "リダイレクトURI",
"REDIRECTSECTION": "リダイレクトURI", "REDIRECTSECTION": "リダイレクトURI",
"POSTLOGOUTREDIRECT": "ログアウトURI", "POSTLOGOUTREDIRECT": "ログアウトURI",
@@ -2680,7 +2699,9 @@
} }
} }
}, },
"BACKCHANNELLOOGOUTURI": "バックチャネルログアウトURI" "BACKCHANNELLOOGOUTURI": "バックチャネルログアウトURI",
"IOSAPPLINKINCOMPLETE": "iOS の Team ID と Bundle ID は両方設定するか、両方とも空にしてください。",
"ANDROIDPACKAGEREQUIRED": "証明書フィンガープリントを設定する場合、Android パッケージ名は必須です。"
}, },
"API": { "API": {
"INFO": { "INFO": {
+22 -1
View File
@@ -2616,6 +2616,25 @@
"DEVMODEDESC": "주의: 개발 모드가 활성화된 경우 리디렉션 URI가 검증되지 않습니다.", "DEVMODEDESC": "주의: 개발 모드가 활성화된 경우 리디렉션 URI가 검증되지 않습니다.",
"SKIPNATIVEAPPSUCCESSPAGE": "로그인 성공 페이지 건너뛰기", "SKIPNATIVEAPPSUCCESSPAGE": "로그인 성공 페이지 건너뛰기",
"SKIPNATIVEAPPSUCCESSPAGE_DESCRIPTION": "네이티브 앱 로그인 후 성공 페이지를 건너뜁니다.", "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", "REDIRECT": "리디렉션 URI",
"REDIRECTSECTION": "리디렉션 URI", "REDIRECTSECTION": "리디렉션 URI",
"POSTLOGOUTREDIRECT": "로그아웃 후 리디렉션 URI", "POSTLOGOUTREDIRECT": "로그아웃 후 리디렉션 URI",
@@ -2679,7 +2698,9 @@
} }
} }
}, },
"BACKCHANNELLOOGOUTURI": "백채널 로그아웃 URI" "BACKCHANNELLOOGOUTURI": "백채널 로그아웃 URI",
"IOSAPPLINKINCOMPLETE": "iOS Team ID와 Bundle ID는 둘 다 설정하거나 둘 다 비워 두어야 합니다.",
"ANDROIDPACKAGEREQUIRED": "인증서 지문이 설정된 경우 Android 패키지 이름이 필요합니다."
}, },
"API": { "API": {
"INFO": { "INFO": {
+22 -1
View File
@@ -2588,6 +2588,25 @@
"DEVMODEDESC": "Внимавајте: Со овозможен Development mode, URIs за пренасочување нема да бидат валидирани.", "DEVMODEDESC": "Внимавајте: Со овозможен Development mode, URIs за пренасочување нема да бидат валидирани.",
"SKIPNATIVEAPPSUCCESSPAGE": "Прескокни страница за успешна најава", "SKIPNATIVEAPPSUCCESSPAGE": "Прескокни страница за успешна најава",
"SKIPNATIVEAPPSUCCESSPAGE_DESCRIPTION": "Прескокнете ја страницата за успешна најава за оваа нативна апликација.", "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", "REDIRECT": "Redirect URIs",
"REDIRECTSECTION": "Redirect URIs", "REDIRECTSECTION": "Redirect URIs",
"POSTLOGOUTREDIRECT": "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": { "API": {
"INFO": { "INFO": {
+22 -1
View File
@@ -2610,6 +2610,25 @@
"DEVMODEDESC": "Let op: Met ingeschakelde ontwikkelingsmodus worden redirect URI's niet gevalideerd.", "DEVMODEDESC": "Let op: Met ingeschakelde ontwikkelingsmodus worden redirect URI's niet gevalideerd.",
"SKIPNATIVEAPPSUCCESSPAGE": "Sla Login Succes Pagina over", "SKIPNATIVEAPPSUCCESSPAGE": "Sla Login Succes Pagina over",
"SKIPNATIVEAPPSUCCESSPAGE_DESCRIPTION": "Sla de succespagina na een login voor deze native app 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", "REDIRECT": "Redirect URI's",
"REDIRECTSECTION": "Redirect URI's", "REDIRECTSECTION": "Redirect URI's",
"POSTLOGOUTREDIRECT": "Post Logout 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": { "API": {
"INFO": { "INFO": {
+22 -1
View File
@@ -2591,6 +2591,25 @@
"DEVMODEDESC": "Uwaga: przy włączonym trybie rozwoju adresy URI przekierowania nie będą sprawdzane.", "DEVMODEDESC": "Uwaga: przy włączonym trybie rozwoju adresy URI przekierowania nie będą sprawdzane.",
"SKIPNATIVEAPPSUCCESSPAGE": "Pomiń stronę sukcesu po zalogowaniu", "SKIPNATIVEAPPSUCCESSPAGE": "Pomiń stronę sukcesu po zalogowaniu",
"SKIPNATIVEAPPSUCCESSPAGE_DESCRIPTION": "Pomiń stronę sukcesu po zalogowaniu dla tej Natywny aplikację", "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", "REDIRECT": "Adresy URI przekierowania",
"REDIRECTSECTION": "Adresy URI przekierowania", "REDIRECTSECTION": "Adresy URI przekierowania",
"POSTLOGOUTREDIRECT": "Adresy URI po wylogowaniu", "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": { "API": {
"INFO": { "INFO": {
+22 -1
View File
@@ -2587,6 +2587,25 @@
"DEVMODEDESC": "Atenção: Com o modo de desenvolvimento habilitado, as URIs de redirecionamento não serão validadas.", "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": "Pular Página de Sucesso de Login",
"SKIPNATIVEAPPSUCCESSPAGE_DESCRIPTION": "Pule a página de sucesso após o login para este aplicativo nativo.", "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", "REDIRECT": "URIs de Redirecionamento",
"REDIRECTSECTION": "URIs de Redirecionamento", "REDIRECTSECTION": "URIs de Redirecionamento",
"POSTLOGOUTREDIRECT": "URIs de Pós-Logout", "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": { "API": {
"INFO": { "INFO": {
+22 -1
View File
@@ -2612,6 +2612,25 @@
"DEVMODEDESC": "Atenție: Cu modul de dezvoltare activat, URI-urile de redirecționare nu vor fi validate.", "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": "Omiteți pagina de succes la conectare",
"SKIPNATIVEAPPSUCCESSPAGE_DESCRIPTION": "Omiteți pagina de succes după o conectare pentru această aplicație nativă.", "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", "REDIRECT": "URI-uri de redirecționare",
"REDIRECTSECTION": "URI-uri de redirecționare", "REDIRECTSECTION": "URI-uri de redirecționare",
"POSTLOGOUTREDIRECT": "URI-uri de redirecționare după deconectare", "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": { "API": {
"INFO": { "INFO": {
+22 -1
View File
@@ -2704,6 +2704,25 @@
"DEVMODEDESC": "Осторожно: При включённом режиме разработки URI перенаправления не будут проверяться.", "DEVMODEDESC": "Осторожно: При включённом режиме разработки URI перенаправления не будут проверяться.",
"SKIPNATIVEAPPSUCCESSPAGE": "Пропустить страницу успешного входа в систему", "SKIPNATIVEAPPSUCCESSPAGE": "Пропустить страницу успешного входа в систему",
"SKIPNATIVEAPPSUCCESSPAGE_DESCRIPTION": "Пропустите страницу успеха после входа в систему для этого собственного приложения.", "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", "REDIRECT": "Перенаправление URI",
"REDIRECTSECTION": "Перенаправление URI", "REDIRECTSECTION": "Перенаправление URI",
"POSTLOGOUTREDIRECT": "URI после выхода из системы", "POSTLOGOUTREDIRECT": "URI после выхода из системы",
@@ -2767,7 +2786,9 @@
} }
} }
}, },
"BACKCHANNELLOOGOUTURI": "URI выхода из системы через резервный канал" "BACKCHANNELLOOGOUTURI": "URI выхода из системы через резервный канал",
"IOSAPPLINKINCOMPLETE": "iOS Team ID и Bundle ID должны быть заданы вместе или оба оставлены пустыми.",
"ANDROIDPACKAGEREQUIRED": "Имя пакета Android обязательно, если заданы отпечатки сертификата."
}, },
"API": { "API": {
"INFO": { "INFO": {
+22 -1
View File
@@ -2619,6 +2619,25 @@
"DEVMODEDESC": "Observera: Med utvecklingsläge aktiverat kommer omdirigerings-URI:er inte att valideras.", "DEVMODEDESC": "Observera: Med utvecklingsläge aktiverat kommer omdirigerings-URI:er inte att valideras.",
"SKIPNATIVEAPPSUCCESSPAGE": "Hoppa över inloggningsframgångssida", "SKIPNATIVEAPPSUCCESSPAGE": "Hoppa över inloggningsframgångssida",
"SKIPNATIVEAPPSUCCESSPAGE_DESCRIPTION": "Hoppa över framgångssidan efter en inloggning för denna inhemska app.", "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", "REDIRECT": "Omdirigerings-URI:er",
"REDIRECTSECTION": "Omdirigerings-URI:er", "REDIRECTSECTION": "Omdirigerings-URI:er",
"POSTLOGOUTREDIRECT": "Post Logout-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": { "API": {
"INFO": { "INFO": {
+22 -1
View File
@@ -2620,6 +2620,25 @@
"DEVMODEDESC": "Dikkat: Geliştirme modu etkinleştirildiğinde yönlendirme URI'leri doğrulanmayacaktır.", "DEVMODEDESC": "Dikkat: Geliştirme modu etkinleştirildiğinde yönlendirme URI'leri doğrulanmayacaktır.",
"SKIPNATIVEAPPSUCCESSPAGE": "Giriş Başarı Sayfasını Atla", "SKIPNATIVEAPPSUCCESSPAGE": "Giriş Başarı Sayfasını Atla",
"SKIPNATIVEAPPSUCCESSPAGE_DESCRIPTION": "Bu yerel uygulama için giriş sonrası 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", "REDIRECT": "Yönlendirme URI'leri",
"REDIRECTSECTION": "Yönlendirme URI'leri", "REDIRECTSECTION": "Yönlendirme URI'leri",
"POSTLOGOUTREDIRECT": "Çıkış Sonrası URI'ler", "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": { "API": {
"INFO": { "INFO": {
+22 -1
View File
@@ -2622,6 +2622,25 @@
"DEVMODEDESC": "Увага: з увімкненим режимом розробки URI перенаправлення не перевірятимуться.", "DEVMODEDESC": "Увага: з увімкненим режимом розробки URI перенаправлення не перевірятимуться.",
"SKIPNATIVEAPPSUCCESSPAGE": "Пропустити сторінку успішного входу", "SKIPNATIVEAPPSUCCESSPAGE": "Пропустити сторінку успішного входу",
"SKIPNATIVEAPPSUCCESSPAGE_DESCRIPTION": "Пропустити сторінку успіху після входу для цього нативного додатку.", "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 перенаправлення", "REDIRECT": "URI перенаправлення",
"REDIRECTSECTION": "URI перенаправлення", "REDIRECTSECTION": "URI перенаправлення",
"POSTLOGOUTREDIRECT": "URI після виходу", "POSTLOGOUTREDIRECT": "URI після виходу",
@@ -2685,7 +2704,9 @@
} }
} }
}, },
"BACKCHANNELLOOGOUTURI": "URI виходу з зворотного каналу" "BACKCHANNELLOOGOUTURI": "URI виходу з зворотного каналу",
"IOSAPPLINKINCOMPLETE": "iOS Team ID та Bundle ID мають бути задані разом або обидва залишені порожніми.",
"ANDROIDPACKAGEREQUIRED": "Назва пакета Android обов’язкова, якщо задано відбитки сертифіката."
}, },
"API": { "API": {
"INFO": { "INFO": {
+22 -1
View File
@@ -2624,6 +2624,25 @@
"DEVMODEDESC": "注意:启用开发模式的重定向 URI 将不会被验证。", "DEVMODEDESC": "注意:启用开发模式的重定向 URI 将不会被验证。",
"SKIPNATIVEAPPSUCCESSPAGE": "登录后跳过成功页面", "SKIPNATIVEAPPSUCCESSPAGE": "登录后跳过成功页面",
"SKIPNATIVEAPPSUCCESSPAGE_DESCRIPTION": "登录后跳过本机应用的成功页面", "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", "REDIRECT": "重定向 URLs",
"REDIRECTSECTION": "重定向 URLs", "REDIRECTSECTION": "重定向 URLs",
"POSTLOGOUTREDIRECT": "退出登录重定向 URLs", "POSTLOGOUTREDIRECT": "退出登录重定向 URLs",
@@ -2687,7 +2706,9 @@
} }
} }
}, },
"BACKCHANNELLOOGOUTURI": "反向通道注销URI" "BACKCHANNELLOOGOUTURI": "反向通道注销URI",
"IOSAPPLINKINCOMPLETE": "iOS Team ID 与 Bundle ID 必须同时填写,或同时留空。",
"ANDROIDPACKAGEREQUIRED": "设置证书指纹时必须填写 Android 软件包名称。"
}, },
"API": { "API": {
"INFO": { "INFO": {
@@ -15,6 +15,8 @@ func CreateOIDCAppRequestToDomain(name, appID, projectID string, req *applicatio
if err != nil { if err != nil {
return nil, err return nil, err
} }
iosTeamID, iosBundleID := iosAppLinkConfigToDomain(req.GetIos())
androidPackageName, androidFingerprints := androidAppLinkConfigToDomain(req.GetAndroid())
return &domain.OIDCApp{ return &domain.OIDCApp{
ObjectRoot: models.ObjectRoot{ ObjectRoot: models.ObjectRoot{
AggregateID: projectID, AggregateID: projectID,
@@ -39,6 +41,10 @@ func CreateOIDCAppRequestToDomain(name, appID, projectID string, req *applicatio
BackChannelLogoutURI: gu.Ptr(req.GetBackChannelLogoutUri()), BackChannelLogoutURI: gu.Ptr(req.GetBackChannelLogoutUri()),
LoginVersion: loginVersion, LoginVersion: loginVersion,
LoginBaseURI: loginBaseURI, LoginBaseURI: loginBaseURI,
IOSTeamID: iosTeamID,
IOSBundleID: iosBundleID,
AndroidPackageName: androidPackageName,
AndroidSHA256CertFingerprints: androidFingerprints,
}, nil }, nil
} }
@@ -47,6 +53,9 @@ func UpdateOIDCAppConfigRequestToDomain(appID, projectID string, app *applicatio
if err != nil { if err != nil {
return nil, err 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{ return &domain.OIDCApp{
ObjectRoot: models.ObjectRoot{ ObjectRoot: models.ObjectRoot{
AggregateID: projectID, AggregateID: projectID,
@@ -69,6 +78,10 @@ func UpdateOIDCAppConfigRequestToDomain(appID, projectID string, app *applicatio
BackChannelLogoutURI: app.BackChannelLogoutUri, BackChannelLogoutURI: app.BackChannelLogoutUri,
LoginVersion: loginVersion, LoginVersion: loginVersion,
LoginBaseURI: loginBaseURI, LoginBaseURI: loginBaseURI,
IOSTeamID: iosTeamID,
IOSBundleID: iosBundleID,
AndroidPackageName: androidPackageName,
AndroidSHA256CertFingerprints: androidFingerprints,
}, nil }, nil
} }
@@ -213,10 +226,50 @@ func appOIDCConfigToPb(oidcApp *query.OIDCApp) *application.Application_OidcConf
SkipNativeAppSuccessPage: oidcApp.SkipNativeAppSuccessPage, SkipNativeAppSuccessPage: oidcApp.SkipNativeAppSuccessPage,
BackChannelLogoutUri: oidcApp.BackChannelLogoutURI, BackChannelLogoutUri: oidcApp.BackChannelLogoutURI,
LoginVersion: loginVersionToPb(oidcApp.LoginVersion, oidcApp.LoginBaseURI), 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 { func oidcResponseTypesFromModel(responseTypes []domain.OIDCResponseType) []application.OIDCResponseType {
oidcResponseTypes := make([]application.OIDCResponseType, len(responseTypes)) oidcResponseTypes := make([]application.OIDCResponseType, len(responseTypes))
for i, responseType := range responseTypes { for i, responseType := range responseTypes {
@@ -66,6 +66,14 @@ func TestCreateOIDCAppRequestToDomain(t *testing.T) {
LoginVersion: &application.LoginVersion{Version: &application.LoginVersion_LoginV2{LoginV2: &application.LoginV2{ LoginVersion: &application.LoginVersion{Version: &application.LoginVersion_LoginV2{LoginV2: &application.LoginV2{
BaseUri: gu.Ptr("https://login"), 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{ expectedModel: &domain.OIDCApp{
ObjectRoot: models.ObjectRoot{AggregateID: "project1"}, ObjectRoot: models.ObjectRoot{AggregateID: "project1"},
@@ -89,6 +97,10 @@ func TestCreateOIDCAppRequestToDomain(t *testing.T) {
BackChannelLogoutURI: gu.Ptr("https://backchannel"), BackChannelLogoutURI: gu.Ptr("https://backchannel"),
LoginVersion: gu.Ptr(domain.LoginVersion2), LoginVersion: gu.Ptr(domain.LoginVersion2),
LoginBaseURI: gu.Ptr("https://login"), 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"), 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 { for _, tc := range tt {
@@ -473,6 +550,10 @@ func TestAppOIDCConfigToPb(t *testing.T) {
BackChannelLogoutURI: "https://example.com/backchannel", BackChannelLogoutURI: "https://example.com/backchannel",
LoginVersion: domain.LoginVersion2, LoginVersion: domain.LoginVersion2,
LoginBaseURI: gu.Ptr("https://login.example.com"), 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{ expected: &application.Application_OidcConfiguration{
OidcConfiguration: &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"},
},
}, },
}, },
}, },
@@ -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)
}
@@ -47,6 +47,8 @@ func AddOIDCAppRequestToDomain(req *mgmt_pb.AddOIDCAppRequest) (*domain.OIDCApp,
if err != nil { if err != nil {
return nil, err return nil, err
} }
iosTeamID, iosBundleID := app_grpc.IOSAppLinkConfigToDomain(req.GetIos())
androidPackageName, androidFingerprints := app_grpc.AndroidAppLinkConfigToDomain(req.GetAndroid())
return &domain.OIDCApp{ return &domain.OIDCApp{
ObjectRoot: models.ObjectRoot{ ObjectRoot: models.ObjectRoot{
AggregateID: req.ProjectId, AggregateID: req.ProjectId,
@@ -70,6 +72,10 @@ func AddOIDCAppRequestToDomain(req *mgmt_pb.AddOIDCAppRequest) (*domain.OIDCApp,
BackChannelLogoutURI: gu.Ptr(req.GetBackChannelLogoutUri()), BackChannelLogoutURI: gu.Ptr(req.GetBackChannelLogoutUri()),
LoginVersion: gu.Ptr(loginVersion), LoginVersion: gu.Ptr(loginVersion),
LoginBaseURI: gu.Ptr(loginBaseURI), LoginBaseURI: gu.Ptr(loginBaseURI),
IOSTeamID: iosTeamID,
IOSBundleID: iosBundleID,
AndroidPackageName: androidPackageName,
AndroidSHA256CertFingerprints: androidFingerprints,
}, nil }, nil
} }
@@ -112,6 +118,9 @@ func UpdateOIDCAppConfigRequestToDomain(app *mgmt_pb.UpdateOIDCAppConfigRequest)
if err != nil { if err != nil {
return nil, err 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{ return &domain.OIDCApp{
ObjectRoot: models.ObjectRoot{ ObjectRoot: models.ObjectRoot{
AggregateID: app.ProjectId, AggregateID: app.ProjectId,
@@ -134,6 +143,10 @@ func UpdateOIDCAppConfigRequestToDomain(app *mgmt_pb.UpdateOIDCAppConfigRequest)
BackChannelLogoutURI: gu.Ptr(app.GetBackChannelLogoutUri()), BackChannelLogoutURI: gu.Ptr(app.GetBackChannelLogoutUri()),
LoginVersion: gu.Ptr(loginVersion), LoginVersion: gu.Ptr(loginVersion),
LoginBaseURI: gu.Ptr(loginBaseURI), LoginBaseURI: gu.Ptr(loginBaseURI),
IOSTeamID: iosTeamID,
IOSBundleID: iosBundleID,
AndroidPackageName: androidPackageName,
AndroidSHA256CertFingerprints: androidFingerprints,
}, nil }, nil
} }
+41
View File
@@ -3,6 +3,7 @@ package project
import ( import (
"net/url" "net/url"
"github.com/muhlemmer/gu"
"google.golang.org/protobuf/types/known/durationpb" "google.golang.org/protobuf/types/known/durationpb"
object_grpc "github.com/zitadel/zitadel/internal/api/grpc/object" 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, SkipNativeAppSuccessPage: app.SkipNativeAppSuccessPage,
BackChannelLogoutUri: app.BackChannelLogoutURI, BackChannelLogoutUri: app.BackChannelLogoutURI,
LoginVersion: loginVersionToPb(app.LoginVersion, app.LoginBaseURI), 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 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,
}
}
@@ -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)
}
+196
View File
@@ -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)),
)
}
+241
View File
@@ -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())
})
}
}
+4 -1
View File
@@ -158,7 +158,10 @@ func TestCommandSide_AddInstanceDomain(t *testing.T) {
"", "",
domain.LoginVersionUnspecified, domain.LoginVersionUnspecified,
"", "",
), "",
"",
"",
nil),
), ),
), ),
expectPush( expectPush(
+8 -2
View File
@@ -125,7 +125,10 @@ func oidcAppEvents(ctx context.Context, orgID, projectID, id, name, clientID str
"", "",
domain.LoginVersionUnspecified, domain.LoginVersionUnspecified,
"", "",
), "",
"",
"",
nil),
} }
} }
@@ -468,7 +471,10 @@ func generatedDomainFilters(instanceID, orgID, projectID, appID, generatedDomain
"", "",
domain.LoginVersionUnspecified, domain.LoginVersionUnspecified,
"", "",
), "",
"",
"",
nil),
), ),
expectFilter( expectFilter(
func() eventstore.Event { func() eventstore.Event {
+26 -1
View File
@@ -38,6 +38,10 @@ type addOIDCApp struct {
BackChannelLogoutURI string BackChannelLogoutURI string
LoginVersion domain.LoginVersion LoginVersion domain.LoginVersion
LoginBaseURI string LoginBaseURI string
IOSTeamID string
IOSBundleID string
AndroidPackageName string
AndroidSHA256CertFingerprints []string
ClientID string ClientID string
ClientSecret string ClientSecret string
@@ -118,6 +122,10 @@ func (c *Commands) AddOIDCAppCommand(app *addOIDCApp) preparation.Validation {
app.BackChannelLogoutURI, app.BackChannelLogoutURI,
app.LoginVersion, app.LoginVersion,
app.LoginBaseURI, app.LoginBaseURI,
app.IOSTeamID,
app.IOSBundleID,
app.AndroidPackageName,
app.AndroidSHA256CertFingerprints,
), ),
}, nil }, nil
}, nil }, nil
@@ -251,6 +259,10 @@ func (c *Commands) pushOIDCApplication(ctx context.Context, addedApplication *OI
backchannelLogoutURI, backchannelLogoutURI,
gu.Value(oidcApp.LoginVersion), gu.Value(oidcApp.LoginVersion),
strings.TrimSpace(gu.Value(oidcApp.LoginBaseURI)), 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...) 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. // UpdateDynamicOIDCClient). It reports whether anything actually changed.
func (c *Commands) oidcApplicationChangeEvent(ctx context.Context, existingOIDC *OIDCApplicationWriteModel, oidc *domain.OIDCApp) (*project_repo.OIDCConfigChangedEvent, bool, error) { func (c *Commands) oidcApplicationChangeEvent(ctx context.Context, existingOIDC *OIDCApplicationWriteModel, oidc *domain.OIDCApp) (*project_repo.OIDCConfigChangedEvent, bool, error) {
projectAgg := ProjectAggregateFromWriteModelWithCTX(ctx, &existingOIDC.WriteModel) projectAgg := ProjectAggregateFromWriteModelWithCTX(ctx, &existingOIDC.WriteModel)
var backChannelLogout, loginBaseURI *string var backChannelLogout, loginBaseURI, iosTeamID, iosBundleID, androidPackageName *string
if oidc.BackChannelLogoutURI != nil { if oidc.BackChannelLogoutURI != nil {
bcl, err := c.validateBackchannelLogoutURI(oidc) bcl, err := c.validateBackchannelLogoutURI(oidc)
if err != nil { if err != nil {
@@ -352,6 +364,15 @@ func (c *Commands) oidcApplicationChangeEvent(ctx context.Context, existingOIDC
if oidc.LoginBaseURI != nil { if oidc.LoginBaseURI != nil {
loginBaseURI = gu.Ptr(strings.TrimSpace(*oidc.LoginBaseURI)) 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( return existingOIDC.NewChangedEvent(
ctx, ctx,
@@ -375,6 +396,10 @@ func (c *Commands) oidcApplicationChangeEvent(ctx context.Context, existingOIDC
backChannelLogout, backChannelLogout,
oidc.LoginVersion, oidc.LoginVersion,
loginBaseURI, loginBaseURI,
iosTeamID,
iosBundleID,
androidPackageName,
trimStringSliceWhiteSpaces(oidc.AndroidSHA256CertFingerprints),
) )
} }
@@ -234,7 +234,10 @@ func TestCommandSide_AddDynamicOIDCClient(t *testing.T) {
"", "",
domain.LoginVersionUnspecified, domain.LoginVersionUnspecified,
"", "",
), "",
"",
"",
nil),
// The registration access token (RFC 7592 §3) is persisted in the same // The registration access token (RFC 7592 §3) is persisted in the same
// push as the application, so a registered client is never left // push as the application, so a registered client is never left
// unmanageable. // unmanageable.
@@ -336,7 +339,10 @@ func TestCommandSide_AddDynamicOIDCClient(t *testing.T) {
"", "",
domain.LoginVersionUnspecified, domain.LoginVersionUnspecified,
"", "",
), "",
"",
"",
nil),
project.NewOIDCConfigRegistrationTokenChangedEvent(context.Background(), project.NewOIDCConfigRegistrationTokenChangedEvent(context.Background(),
&project.NewAggregate("project1", "org1").Aggregate, &project.NewAggregate("project1", "org1").Aggregate,
"app1", "app1",
@@ -581,7 +587,11 @@ func TestCommandSide_UpdateDynamicOIDCClient(t *testing.T) {
domain.OIDCApplicationTypeWeb, domain.OIDCAuthMethodTypeNone, domain.OIDCApplicationTypeWeb, domain.OIDCAuthMethodTypeNone,
[]string{}, false, domain.OIDCTokenTypeBearer, []string{}, false, domain.OIDCTokenTypeBearer,
false, false, false, 0, []string{}, false, "", false, false, false, 0, []string{}, false, "",
domain.LoginVersionUnspecified, "")), domain.LoginVersionUnspecified, "",
"",
"",
"",
nil)),
} }
} }
sameMetadata := &domain.OIDCApp{ sameMetadata := &domain.OIDCApp{
@@ -39,6 +39,10 @@ type OIDCApplicationWriteModel struct {
BackChannelLogoutURI string BackChannelLogoutURI string
LoginVersion domain.LoginVersion LoginVersion domain.LoginVersion
LoginBaseURI string LoginBaseURI string
IOSTeamID string
IOSBundleID string
AndroidPackageName string
AndroidSHA256CertFingerprints []string
oidc bool oidc bool
} }
@@ -140,6 +144,10 @@ func (wm *OIDCApplicationWriteModel) Reduce() error {
wm.BackChannelLogoutURI = "" wm.BackChannelLogoutURI = ""
wm.LoginVersion = domain.LoginVersionUnspecified wm.LoginVersion = domain.LoginVersionUnspecified
wm.LoginBaseURI = "" wm.LoginBaseURI = ""
wm.IOSTeamID = ""
wm.IOSBundleID = ""
wm.AndroidPackageName = ""
wm.AndroidSHA256CertFingerprints = nil
wm.oidc = false wm.oidc = false
wm.AppName = e.Name wm.AppName = e.Name
wm.State = domain.AppStateActive wm.State = domain.AppStateActive
@@ -189,6 +197,10 @@ func (wm *OIDCApplicationWriteModel) Reduce() error {
wm.BackChannelLogoutURI = "" wm.BackChannelLogoutURI = ""
wm.LoginVersion = domain.LoginVersionUnspecified wm.LoginVersion = domain.LoginVersionUnspecified
wm.LoginBaseURI = "" wm.LoginBaseURI = ""
wm.IOSTeamID = ""
wm.IOSBundleID = ""
wm.AndroidPackageName = ""
wm.AndroidSHA256CertFingerprints = nil
wm.oidc = false wm.oidc = false
wm.State = domain.AppStateRemoved wm.State = domain.AppStateRemoved
} }
@@ -218,6 +230,10 @@ func (wm *OIDCApplicationWriteModel) appendAddOIDCEvent(e *project.OIDCConfigAdd
wm.BackChannelLogoutURI = e.BackChannelLogoutURI wm.BackChannelLogoutURI = e.BackChannelLogoutURI
wm.LoginVersion = e.LoginVersion wm.LoginVersion = e.LoginVersion
wm.LoginBaseURI = e.LoginBaseURI 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) { func (wm *OIDCApplicationWriteModel) appendChangeOIDCEvent(e *project.OIDCConfigChangedEvent) {
@@ -275,6 +291,18 @@ func (wm *OIDCApplicationWriteModel) appendChangeOIDCEvent(e *project.OIDCConfig
if e.LoginBaseURI != nil { if e.LoginBaseURI != nil {
wm.LoginBaseURI = *e.LoginBaseURI 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 { func (wm *OIDCApplicationWriteModel) Query() *eventstore.SearchQueryBuilder {
@@ -320,6 +348,10 @@ func (wm *OIDCApplicationWriteModel) NewChangedEvent(
backChannelLogoutURI *string, backChannelLogoutURI *string,
loginVersion *domain.LoginVersion, loginVersion *domain.LoginVersion,
loginBaseURI *string, loginBaseURI *string,
iosTeamID *string,
iosBundleID *string,
androidPackageName *string,
androidSHA256CertFingerprints []string,
) (*project.OIDCConfigChangedEvent, bool, error) { ) (*project.OIDCConfigChangedEvent, bool, error) {
changes := make([]project.OIDCConfigChanges, 0) changes := make([]project.OIDCConfigChanges, 0)
var err error var err error
@@ -378,6 +410,18 @@ func (wm *OIDCApplicationWriteModel) NewChangedEvent(
if loginBaseURI != nil && wm.LoginBaseURI != *loginBaseURI { if loginBaseURI != nil && wm.LoginBaseURI != *loginBaseURI {
changes = append(changes, project.ChangeOIDCLoginBaseURI(*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 { if len(changes) == 0 {
return nil, false, nil return nil, false, nil
@@ -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)
})
}
@@ -176,7 +176,10 @@ func TestAddOIDCApp(t *testing.T) {
"", "",
domain.LoginVersionUnspecified, domain.LoginVersionUnspecified,
"", "",
), "",
"",
"",
nil),
}, },
}, },
}, },
@@ -244,7 +247,10 @@ func TestAddOIDCApp(t *testing.T) {
"", "",
domain.LoginVersionUnspecified, domain.LoginVersionUnspecified,
"", "",
), "",
"",
"",
nil),
}, },
}, },
}, },
@@ -312,7 +318,10 @@ func TestAddOIDCApp(t *testing.T) {
"", "",
domain.LoginVersionUnspecified, domain.LoginVersionUnspecified,
"", "",
), "",
"",
"",
nil),
}, },
}, },
}, },
@@ -380,7 +389,10 @@ func TestAddOIDCApp(t *testing.T) {
"", "",
domain.LoginVersionUnspecified, domain.LoginVersionUnspecified,
"", "",
), "",
"",
"",
nil),
}, },
}, },
}, },
@@ -534,7 +546,10 @@ func TestCommandSide_AddOIDCApplication(t *testing.T) {
"https://test.ch/backchannel", "https://test.ch/backchannel",
domain.LoginVersion2, domain.LoginVersion2,
"https://login.test.ch", "https://login.test.ch",
), "",
"",
"",
nil),
), ),
), ),
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "app1", "client1"), idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "app1", "client1"),
@@ -643,7 +658,10 @@ func TestCommandSide_AddOIDCApplication(t *testing.T) {
"https://test.ch/backchannel", "https://test.ch/backchannel",
domain.LoginVersion2, domain.LoginVersion2,
"https://login.test.ch", "https://login.test.ch",
), "",
"",
"",
nil),
), ),
), ),
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "client1"), idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "client1"),
@@ -753,7 +771,10 @@ func TestCommandSide_AddOIDCApplication(t *testing.T) {
"https://test.ch/backchannel", "https://test.ch/backchannel",
domain.LoginVersion2, domain.LoginVersion2,
"https://login.test.ch", "https://login.test.ch",
), "",
"",
"",
nil),
), ),
), ),
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "app1", "client1"), idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "app1", "client1"),
@@ -1072,7 +1093,10 @@ func TestCommandSide_ChangeOIDCApplication(t *testing.T) {
"https://test.ch/backchannel", "https://test.ch/backchannel",
domain.LoginVersion2, domain.LoginVersion2,
"https://login.test.ch", "https://login.test.ch",
), "",
"",
"",
nil),
), ),
), ),
expectFilter(), expectFilter(),
@@ -1147,7 +1171,10 @@ func TestCommandSide_ChangeOIDCApplication(t *testing.T) {
"https://test.ch/backchannel", "https://test.ch/backchannel",
domain.LoginVersion2, domain.LoginVersion2,
"https://login.test.ch", "https://login.test.ch",
), "",
"",
"",
nil),
), ),
), ),
expectFilter(), expectFilter(),
@@ -1222,7 +1249,10 @@ func TestCommandSide_ChangeOIDCApplication(t *testing.T) {
"https://test.ch/backchannel", "https://test.ch/backchannel",
domain.LoginVersion1, domain.LoginVersion1,
"", "",
), "",
"",
"",
nil),
), ),
), ),
expectFilter(), expectFilter(),
@@ -1316,7 +1346,10 @@ func TestCommandSide_ChangeOIDCApplication(t *testing.T) {
"", "",
domain.LoginVersionUnspecified, domain.LoginVersionUnspecified,
"", "",
), "",
"",
"",
nil),
), ),
), ),
expectFilter(), expectFilter(),
@@ -1376,7 +1409,10 @@ func TestCommandSide_ChangeOIDCApplication(t *testing.T) {
"", "",
domain.LoginVersionUnspecified, domain.LoginVersionUnspecified,
"", "",
), "",
"",
"",
nil),
), ),
), ),
expectFilter(), expectFilter(),
@@ -1531,7 +1567,10 @@ func TestCommandSide_ChangeOIDCApplicationSecret(t *testing.T) {
"", "",
domain.LoginVersionUnspecified, domain.LoginVersionUnspecified,
"", "",
), "",
"",
"",
nil),
), ),
), ),
expectPush( expectPush(
@@ -118,7 +118,10 @@ func TestCommandSide_ChangeApplicationSecret(t *testing.T) {
"", "",
domain.LoginVersionUnspecified, domain.LoginVersionUnspecified,
"", "",
), "",
"",
"",
nil),
), ),
), ),
expectPush( expectPush(
+13
View File
@@ -52,9 +52,22 @@ func oidcWriteModelToOIDCConfig(writeModel *OIDCApplicationWriteModel) *domain.O
BackChannelLogoutURI: gu.Ptr(writeModel.BackChannelLogoutURI), BackChannelLogoutURI: gu.Ptr(writeModel.BackChannelLogoutURI),
LoginVersion: gu.Ptr(writeModel.LoginVersion), LoginVersion: gu.Ptr(writeModel.LoginVersion),
LoginBaseURI: gu.Ptr(writeModel.LoginBaseURI), 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 { func samlWriteModelToSAMLConfig(writeModel *SAMLApplicationWriteModel) *domain.SAMLApp {
return &domain.SAMLApp{ return &domain.SAMLApp{
ObjectRoot: writeModelToObjectRoot(writeModel.WriteModel), ObjectRoot: writeModelToObjectRoot(writeModel.WriteModel),
+24 -1
View File
@@ -7,6 +7,8 @@ import (
"strings" "strings"
"time" "time"
"github.com/muhlemmer/gu"
http_util "github.com/zitadel/zitadel/internal/api/http" http_util "github.com/zitadel/zitadel/internal/api/http"
"github.com/zitadel/zitadel/internal/eventstore/v1/models" "github.com/zitadel/zitadel/internal/eventstore/v1/models"
) )
@@ -49,6 +51,14 @@ type OIDCApp struct {
BackChannelLogoutURI *string BackChannelLogoutURI *string
LoginVersion *LoginVersion LoginVersion *LoginVersion
LoginBaseURI *string 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 State AppState
} }
@@ -138,7 +148,7 @@ const (
) )
func (a *OIDCApp) IsValid() bool { 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 return false
} }
grantTypes := a.getRequiredGrantTypes() grantTypes := a.getRequiredGrantTypes()
@@ -154,6 +164,19 @@ func (a *OIDCApp) IsValid() bool {
return true 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 { func (a *OIDCApp) OriginsValid() bool {
for _, origin := range a.AdditionalOrigins { for _, origin := range a.AdditionalOrigins {
if !http_util.IsOrigin(strings.TrimSpace(origin)) { if !http_util.IsOrigin(strings.TrimSpace(origin)) {
+81
View File
@@ -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)
}
})
}
}
+52
View File
@@ -64,6 +64,10 @@ type OIDCApp struct {
BackChannelLogoutURI string BackChannelLogoutURI string
LoginVersion domain.LoginVersion LoginVersion domain.LoginVersion
LoginBaseURI *string LoginBaseURI *string
IOSTeamID string
IOSBundleID string
AndroidPackageName string
AndroidSHA256CertFingerprints database.TextArray[string]
} }
type SAMLApp struct { type SAMLApp struct {
@@ -278,6 +282,22 @@ var (
name: projection.AppOIDCConfigColumnLoginBaseURI, name: projection.AppOIDCConfigColumnLoginBaseURI,
table: appOIDCConfigsTable, 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) { 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(), AppOIDCConfigColumnBackChannelLogoutURI.identifier(),
AppOIDCConfigColumnLoginVersion.identifier(), AppOIDCConfigColumnLoginVersion.identifier(),
AppOIDCConfigColumnLoginBaseURI.identifier(), AppOIDCConfigColumnLoginBaseURI.identifier(),
AppOIDCConfigColumnIOSTeamID.identifier(),
AppOIDCConfigColumnIOSBundleID.identifier(),
AppOIDCConfigColumnAndroidPackageName.identifier(),
AppOIDCConfigColumnAndroidSHA256CertFingerprints.identifier(),
AppSAMLConfigColumnAppID.identifier(), AppSAMLConfigColumnAppID.identifier(),
AppSAMLConfigColumnEntityID.identifier(), AppSAMLConfigColumnEntityID.identifier(),
@@ -794,6 +818,10 @@ func scanApp(row *sql.Row) (*App, error) {
&oidcConfig.backChannelLogoutURI, &oidcConfig.backChannelLogoutURI,
&oidcConfig.loginVersion, &oidcConfig.loginVersion,
&oidcConfig.loginBaseURI, &oidcConfig.loginBaseURI,
&oidcConfig.iosTeamID,
&oidcConfig.iosBundleID,
&oidcConfig.androidPackageName,
&oidcConfig.androidSHA256CertFingerprints,
&samlConfig.appID, &samlConfig.appID,
&samlConfig.entityID, &samlConfig.entityID,
@@ -848,6 +876,10 @@ func prepareOIDCAppQuery() (sq.SelectBuilder, func(*sql.Row) (*App, error)) {
AppOIDCConfigColumnBackChannelLogoutURI.identifier(), AppOIDCConfigColumnBackChannelLogoutURI.identifier(),
AppOIDCConfigColumnLoginVersion.identifier(), AppOIDCConfigColumnLoginVersion.identifier(),
AppOIDCConfigColumnLoginBaseURI.identifier(), AppOIDCConfigColumnLoginBaseURI.identifier(),
AppOIDCConfigColumnIOSTeamID.identifier(),
AppOIDCConfigColumnIOSBundleID.identifier(),
AppOIDCConfigColumnAndroidPackageName.identifier(),
AppOIDCConfigColumnAndroidSHA256CertFingerprints.identifier(),
).From(appsTable.identifier()). ).From(appsTable.identifier()).
Join(join(AppOIDCConfigColumnAppID, AppColumnID)). Join(join(AppOIDCConfigColumnAppID, AppColumnID)).
PlaceholderFormat(sq.Dollar), func(row *sql.Row) (*App, error) { 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.backChannelLogoutURI,
&oidcConfig.loginVersion, &oidcConfig.loginVersion,
&oidcConfig.loginBaseURI, &oidcConfig.loginBaseURI,
&oidcConfig.iosTeamID,
&oidcConfig.iosBundleID,
&oidcConfig.androidPackageName,
&oidcConfig.androidSHA256CertFingerprints,
) )
if err != nil { if err != nil {
@@ -1004,6 +1040,10 @@ func prepareAppsQuery() (sq.SelectBuilder, func(*sql.Rows) (*Apps, error)) {
AppOIDCConfigColumnBackChannelLogoutURI.identifier(), AppOIDCConfigColumnBackChannelLogoutURI.identifier(),
AppOIDCConfigColumnLoginVersion.identifier(), AppOIDCConfigColumnLoginVersion.identifier(),
AppOIDCConfigColumnLoginBaseURI.identifier(), AppOIDCConfigColumnLoginBaseURI.identifier(),
AppOIDCConfigColumnIOSTeamID.identifier(),
AppOIDCConfigColumnIOSBundleID.identifier(),
AppOIDCConfigColumnAndroidPackageName.identifier(),
AppOIDCConfigColumnAndroidSHA256CertFingerprints.identifier(),
AppSAMLConfigColumnAppID.identifier(), AppSAMLConfigColumnAppID.identifier(),
AppSAMLConfigColumnEntityID.identifier(), AppSAMLConfigColumnEntityID.identifier(),
@@ -1061,6 +1101,10 @@ func prepareAppsQuery() (sq.SelectBuilder, func(*sql.Rows) (*Apps, error)) {
&oidcConfig.backChannelLogoutURI, &oidcConfig.backChannelLogoutURI,
&oidcConfig.loginVersion, &oidcConfig.loginVersion,
&oidcConfig.loginBaseURI, &oidcConfig.loginBaseURI,
&oidcConfig.iosTeamID,
&oidcConfig.iosBundleID,
&oidcConfig.androidPackageName,
&oidcConfig.androidSHA256CertFingerprints,
&samlConfig.appID, &samlConfig.appID,
&samlConfig.entityID, &samlConfig.entityID,
@@ -1168,6 +1212,10 @@ type sqlOIDCConfig struct {
backChannelLogoutURI sql.NullString backChannelLogoutURI sql.NullString
loginVersion sql.NullInt16 loginVersion sql.NullInt16
loginBaseURI sql.NullString loginBaseURI sql.NullString
iosTeamID sql.NullString
iosBundleID sql.NullString
androidPackageName sql.NullString
androidSHA256CertFingerprints database.TextArray[string]
} }
func (c sqlOIDCConfig) set(app *App) { func (c sqlOIDCConfig) set(app *App) {
@@ -1193,6 +1241,10 @@ func (c sqlOIDCConfig) set(app *App) {
SkipNativeAppSuccessPage: c.skipNativeAppSuccessPage.Bool, SkipNativeAppSuccessPage: c.skipNativeAppSuccessPage.Bool,
BackChannelLogoutURI: c.backChannelLogoutURI.String, BackChannelLogoutURI: c.backChannelLogoutURI.String,
LoginVersion: domain.LoginVersion(c.loginVersion.Int16), 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 { if c.loginBaseURI.Valid {
app.OIDCConfig.LoginBaseURI = &c.loginBaseURI.String app.OIDCConfig.LoginBaseURI = &c.loginBaseURI.String
+100
View File
@@ -0,0 +1,100 @@
package query
import (
"context"
"database/sql"
"strings"
sq "github.com/Masterminds/squirrel"
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/database"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/telemetry/tracing"
"github.com/zitadel/zitadel/internal/zerrors"
)
// OIDCAppLinkConfig holds iOS/Android association fields for one active OIDC app.
type OIDCAppLinkConfig struct {
AppID string
IOSTeamID string
IOSBundleID string
AndroidPackageName string
AndroidSHA256CertFingerprints []string
}
// SearchOIDCAppLinkConfigs returns association fields for active OIDC apps in the
// current instance that have at least one iOS or Android app-link field set.
func (q *Queries) SearchOIDCAppLinkConfigs(ctx context.Context) (configs []*OIDCAppLinkConfig, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
query, scan := prepareOIDCAppLinkConfigsQuery()
iosTeamIDCol := AppOIDCConfigColumnIOSTeamID.identifier()
iosBundleIDCol := AppOIDCConfigColumnIOSBundleID.identifier()
androidPackageCol := AppOIDCConfigColumnAndroidPackageName.identifier()
stmt, args, err := query.Where(sq.And{
sq.Eq{
AppOIDCConfigColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(),
AppColumnState.identifier(): domain.AppStateActive,
},
sq.Or{
sq.Expr("COALESCE(" + iosTeamIDCol + ", '') <> ''"),
sq.Expr("COALESCE(" + iosBundleIDCol + ", '') <> ''"),
sq.Expr("COALESCE(" + androidPackageCol + ", '') <> ''"),
},
}).OrderBy(AppOIDCConfigColumnAppID.identifier()).ToSql()
if err != nil {
return nil, zerrors.ThrowInvalidArgument(err, "QUERY-ApLn1", "Errors.Query.InvalidRequest")
}
err = q.client.QueryContext(ctx, func(rows *sql.Rows) error {
configs, err = scan(rows)
return err
}, stmt, args...)
if err != nil {
return nil, zerrors.ThrowInternal(err, "QUERY-ApLn2", "Errors.Internal")
}
return configs, nil
}
func prepareOIDCAppLinkConfigsQuery() (sq.SelectBuilder, func(*sql.Rows) ([]*OIDCAppLinkConfig, error)) {
return sq.Select(
AppOIDCConfigColumnAppID.identifier(),
AppOIDCConfigColumnIOSTeamID.identifier(),
AppOIDCConfigColumnIOSBundleID.identifier(),
AppOIDCConfigColumnAndroidPackageName.identifier(),
AppOIDCConfigColumnAndroidSHA256CertFingerprints.identifier(),
).From(appOIDCConfigsTable.identifier()).
Join(join(AppColumnID, AppOIDCConfigColumnAppID)).
PlaceholderFormat(sq.Dollar),
func(rows *sql.Rows) ([]*OIDCAppLinkConfig, error) {
configs := make([]*OIDCAppLinkConfig, 0)
for rows.Next() {
var (
appID sql.NullString
iosTeamID sql.NullString
iosBundleID sql.NullString
androidPackage sql.NullString
fingerprints database.TextArray[string]
)
if err := rows.Scan(
&appID,
&iosTeamID,
&iosBundleID,
&androidPackage,
&fingerprints,
); err != nil {
return nil, zerrors.ThrowInternal(err, "QUERY-ApLn3", "Errors.Internal")
}
configs = append(configs, &OIDCAppLinkConfig{
AppID: appID.String,
IOSTeamID: strings.TrimSpace(iosTeamID.String),
IOSBundleID: strings.TrimSpace(iosBundleID.String),
AndroidPackageName: strings.TrimSpace(androidPackage.String),
AndroidSHA256CertFingerprints: fingerprints,
})
}
return configs, nil
}
}
+96
View File
@@ -51,6 +51,10 @@ var (
` projections.apps7_oidc_configs.back_channel_logout_uri,` + ` projections.apps7_oidc_configs.back_channel_logout_uri,` +
` projections.apps7_oidc_configs.login_version,` + ` projections.apps7_oidc_configs.login_version,` +
` projections.apps7_oidc_configs.login_base_uri,` + ` 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 //saml config
` projections.apps7_saml_configs.app_id,` + ` projections.apps7_saml_configs.app_id,` +
` projections.apps7_saml_configs.entity_id,` + ` projections.apps7_saml_configs.entity_id,` +
@@ -99,6 +103,10 @@ var (
` projections.apps7_oidc_configs.back_channel_logout_uri,` + ` projections.apps7_oidc_configs.back_channel_logout_uri,` +
` projections.apps7_oidc_configs.login_version,` + ` projections.apps7_oidc_configs.login_version,` +
` projections.apps7_oidc_configs.login_base_uri,` + ` 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 //saml config
` projections.apps7_saml_configs.app_id,` + ` projections.apps7_saml_configs.app_id,` +
` projections.apps7_saml_configs.entity_id,` + ` projections.apps7_saml_configs.entity_id,` +
@@ -172,6 +180,10 @@ var (
"back_channel_logout_uri", "back_channel_logout_uri",
"login_version", "login_version",
"login_base_uri", "login_base_uri",
"ios_team_id",
"ios_bundle_id",
"android_package_name",
"android_sha256_cert_fingerprints",
//saml config //saml config
"app_id", "app_id",
"entity_id", "entity_id",
@@ -248,6 +260,10 @@ func Test_AppsPrepare(t *testing.T) {
nil, nil,
nil, nil,
nil, nil,
nil,
nil,
nil,
nil,
// saml config // saml config
nil, nil,
nil, nil,
@@ -319,6 +335,10 @@ func Test_AppsPrepare(t *testing.T) {
nil, nil,
nil, nil,
nil, nil,
nil,
nil,
nil,
nil,
// saml config // saml config
nil, nil,
nil, nil,
@@ -393,6 +413,10 @@ func Test_AppsPrepare(t *testing.T) {
nil, nil,
nil, nil,
nil, nil,
nil,
nil,
nil,
nil,
// saml config // saml config
"app-id", "app-id",
"https://test.com/saml/metadata", "https://test.com/saml/metadata",
@@ -469,6 +493,10 @@ func Test_AppsPrepare(t *testing.T) {
"back.channel.logout.ch", "back.channel.logout.ch",
domain.LoginVersionUnspecified, domain.LoginVersionUnspecified,
nil, nil,
nil,
nil,
nil,
nil,
// saml config // saml config
nil, nil,
nil, nil,
@@ -563,6 +591,10 @@ func Test_AppsPrepare(t *testing.T) {
"back.channel.logout.ch", "back.channel.logout.ch",
domain.LoginVersionUnspecified, domain.LoginVersionUnspecified,
nil, nil,
nil,
nil,
nil,
nil,
// saml config // saml config
nil, nil,
nil, nil,
@@ -657,6 +689,10 @@ func Test_AppsPrepare(t *testing.T) {
"back.channel.logout.ch", "back.channel.logout.ch",
domain.LoginVersionUnspecified, domain.LoginVersionUnspecified,
nil, nil,
nil,
nil,
nil,
nil,
// saml config // saml config
nil, nil,
nil, nil,
@@ -751,6 +787,10 @@ func Test_AppsPrepare(t *testing.T) {
"back.channel.logout.ch", "back.channel.logout.ch",
domain.LoginVersionUnspecified, domain.LoginVersionUnspecified,
nil, nil,
nil,
nil,
nil,
nil,
// saml config // saml config
nil, nil,
nil, nil,
@@ -845,6 +885,10 @@ func Test_AppsPrepare(t *testing.T) {
"back.channel.logout.ch", "back.channel.logout.ch",
domain.LoginVersionUnspecified, domain.LoginVersionUnspecified,
nil, nil,
nil,
nil,
nil,
nil,
// saml config // saml config
nil, nil,
nil, nil,
@@ -939,6 +983,10 @@ func Test_AppsPrepare(t *testing.T) {
"back.channel.logout.ch", "back.channel.logout.ch",
domain.LoginVersionUnspecified, domain.LoginVersionUnspecified,
nil, nil,
nil,
nil,
nil,
nil,
// saml config // saml config
nil, nil,
nil, nil,
@@ -1033,6 +1081,10 @@ func Test_AppsPrepare(t *testing.T) {
"back.channel.logout.ch", "back.channel.logout.ch",
domain.LoginVersion2, domain.LoginVersion2,
"https://login.ch/", "https://login.ch/",
nil,
nil,
nil,
nil,
// saml config // saml config
nil, nil,
nil, nil,
@@ -1075,6 +1127,10 @@ func Test_AppsPrepare(t *testing.T) {
nil, nil,
nil, nil,
nil, nil,
nil,
nil,
nil,
nil,
// saml config // saml config
nil, nil,
nil, nil,
@@ -1117,6 +1173,10 @@ func Test_AppsPrepare(t *testing.T) {
nil, nil,
nil, nil,
nil, nil,
nil,
nil,
nil,
nil,
// saml config // saml config
"saml-app-id", "saml-app-id",
"https://test.com/saml/metadata", "https://test.com/saml/metadata",
@@ -1302,6 +1362,10 @@ func Test_AppPrepare(t *testing.T) {
nil, nil,
nil, nil,
nil, nil,
nil,
nil,
nil,
nil,
// saml config // saml config
nil, nil,
nil, nil,
@@ -1367,6 +1431,10 @@ func Test_AppPrepare(t *testing.T) {
nil, nil,
nil, nil,
nil, nil,
nil,
nil,
nil,
nil,
// saml config // saml config
nil, nil,
nil, nil,
@@ -1437,6 +1505,10 @@ func Test_AppPrepare(t *testing.T) {
"back.channel.logout.ch", "back.channel.logout.ch",
domain.LoginVersionUnspecified, domain.LoginVersionUnspecified,
nil, nil,
nil,
nil,
nil,
nil,
// saml config // saml config
nil, nil,
nil, nil,
@@ -1526,6 +1598,10 @@ func Test_AppPrepare(t *testing.T) {
"back.channel.logout.ch", "back.channel.logout.ch",
domain.LoginVersionUnspecified, domain.LoginVersionUnspecified,
nil, nil,
nil,
nil,
nil,
nil,
// saml config // saml config
nil, nil,
nil, nil,
@@ -1615,6 +1691,10 @@ func Test_AppPrepare(t *testing.T) {
nil, nil,
nil, nil,
nil, nil,
nil,
nil,
nil,
nil,
// saml config // saml config
"app-id", "app-id",
"https://test.com/saml/metadata", "https://test.com/saml/metadata",
@@ -1688,6 +1768,10 @@ func Test_AppPrepare(t *testing.T) {
"back.channel.logout.ch", "back.channel.logout.ch",
domain.LoginVersionUnspecified, domain.LoginVersionUnspecified,
nil, nil,
nil,
nil,
nil,
nil,
// saml config // saml config
nil, nil,
nil, nil,
@@ -1777,6 +1861,10 @@ func Test_AppPrepare(t *testing.T) {
"back.channel.logout.ch", "back.channel.logout.ch",
domain.LoginVersionUnspecified, domain.LoginVersionUnspecified,
nil, nil,
nil,
nil,
nil,
nil,
// saml config // saml config
nil, nil,
nil, nil,
@@ -1866,6 +1954,10 @@ func Test_AppPrepare(t *testing.T) {
"back.channel.logout.ch", "back.channel.logout.ch",
domain.LoginVersionUnspecified, domain.LoginVersionUnspecified,
nil, nil,
nil,
nil,
nil,
nil,
// saml config // saml config
nil, nil,
nil, nil,
@@ -1955,6 +2047,10 @@ func Test_AppPrepare(t *testing.T) {
"back.channel.logout.ch", "back.channel.logout.ch",
domain.LoginVersionUnspecified, domain.LoginVersionUnspecified,
nil, nil,
nil,
nil,
nil,
nil,
// saml config // saml config
nil, nil,
nil, nil,
+24
View File
@@ -61,6 +61,10 @@ const (
AppOIDCConfigColumnBackChannelLogoutURI = "back_channel_logout_uri" AppOIDCConfigColumnBackChannelLogoutURI = "back_channel_logout_uri"
AppOIDCConfigColumnLoginVersion = "login_version" AppOIDCConfigColumnLoginVersion = "login_version"
AppOIDCConfigColumnLoginBaseURI = "login_base_uri" AppOIDCConfigColumnLoginBaseURI = "login_base_uri"
AppOIDCConfigColumnIOSTeamID = "ios_team_id"
AppOIDCConfigColumnIOSBundleID = "ios_bundle_id"
AppOIDCConfigColumnAndroidPackageName = "android_package_name"
AppOIDCConfigColumnAndroidSHA256CertFingerprints = "android_sha256_cert_fingerprints"
AppOIDCConfigColumnRegistrationToken = "registration_token" AppOIDCConfigColumnRegistrationToken = "registration_token"
appSAMLTableSuffix = "saml_configs" appSAMLTableSuffix = "saml_configs"
@@ -134,6 +138,10 @@ func (*appProjection) Init() *old_handler.Check {
handler.NewColumn(AppOIDCConfigColumnBackChannelLogoutURI, handler.ColumnTypeText, handler.Nullable()), handler.NewColumn(AppOIDCConfigColumnBackChannelLogoutURI, handler.ColumnTypeText, handler.Nullable()),
handler.NewColumn(AppOIDCConfigColumnLoginVersion, handler.ColumnTypeEnum, handler.Nullable()), handler.NewColumn(AppOIDCConfigColumnLoginVersion, handler.ColumnTypeEnum, handler.Nullable()),
handler.NewColumn(AppOIDCConfigColumnLoginBaseURI, handler.ColumnTypeText, 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.NewColumn(AppOIDCConfigColumnRegistrationToken, handler.ColumnTypeText, handler.Nullable()),
}, },
handler.NewPrimaryKey(AppOIDCConfigColumnInstanceID, AppOIDCConfigColumnAppID), handler.NewPrimaryKey(AppOIDCConfigColumnInstanceID, AppOIDCConfigColumnAppID),
@@ -519,6 +527,10 @@ func (p *appProjection) reduceOIDCConfigAdded(event eventstore.Event) (*handler.
handler.NewCol(AppOIDCConfigColumnBackChannelLogoutURI, e.BackChannelLogoutURI), handler.NewCol(AppOIDCConfigColumnBackChannelLogoutURI, e.BackChannelLogoutURI),
handler.NewCol(AppOIDCConfigColumnLoginVersion, e.LoginVersion), handler.NewCol(AppOIDCConfigColumnLoginVersion, e.LoginVersion),
handler.NewCol(AppOIDCConfigColumnLoginBaseURI, e.LoginBaseURI), 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), handler.WithTableSuffix(appOIDCTableSuffix),
), ),
@@ -596,6 +608,18 @@ func (p *appProjection) reduceOIDCConfigChanged(event eventstore.Event) (*handle
if e.LoginBaseURI != nil { if e.LoginBaseURI != nil {
cols = append(cols, handler.NewCol(AppOIDCConfigColumnLoginBaseURI, *e.LoginBaseURI)) 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 { if len(cols) == 0 {
return handler.NewNoOpStatement(e), nil return handler.NewNoOpStatement(e), nil
+56 -2
View File
@@ -572,7 +572,7 @@ func TestAppProjection_reduces(t *testing.T) {
executer: &testExecuter{ executer: &testExecuter{
executions: []execution{ 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{}{ expectedArgs: []interface{}{
"app-id", "app-id",
"instance-id", "instance-id",
@@ -596,6 +596,10 @@ func TestAppProjection_reduces(t *testing.T) {
"back.channel.one.ch", "back.channel.one.ch",
domain.LoginVersion2, domain.LoginVersion2,
"https://login.ch/", "https://login.ch/",
"",
"",
"",
database.TextArray[string](nil),
}, },
}, },
{ {
@@ -650,7 +654,7 @@ func TestAppProjection_reduces(t *testing.T) {
executer: &testExecuter{ executer: &testExecuter{
executions: []execution{ 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{}{ expectedArgs: []interface{}{
"app-id", "app-id",
"instance-id", "instance-id",
@@ -674,6 +678,10 @@ func TestAppProjection_reduces(t *testing.T) {
"back.channel.one.ch", "back.channel.one.ch",
domain.LoginVersion2, domain.LoginVersion2,
"https://login.ch/", "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", name: "project reduceOIDCConfigChanged noop",
args: args{ args: args{
+58 -1
View File
@@ -2,6 +2,7 @@ package project
import ( import (
"context" "context"
"slices"
"time" "time"
"github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/crypto"
@@ -48,6 +49,10 @@ type OIDCConfigAddedEvent struct {
BackChannelLogoutURI string `json:"backChannelLogoutURI,omitempty"` BackChannelLogoutURI string `json:"backChannelLogoutURI,omitempty"`
LoginVersion domain.LoginVersion `json:"loginVersion,omitempty"` LoginVersion domain.LoginVersion `json:"loginVersion,omitempty"`
LoginBaseURI string `json:"loginBaseURI,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{} { func (e *OIDCConfigAddedEvent) Payload() interface{} {
@@ -82,6 +87,10 @@ func NewOIDCConfigAddedEvent(
backChannelLogoutURI string, backChannelLogoutURI string,
loginVersion domain.LoginVersion, loginVersion domain.LoginVersion,
loginBaseURI string, loginBaseURI string,
iosTeamID string,
iosBundleID string,
androidPackageName string,
androidSHA256CertFingerprints []string,
) *OIDCConfigAddedEvent { ) *OIDCConfigAddedEvent {
return &OIDCConfigAddedEvent{ return &OIDCConfigAddedEvent{
BaseEvent: *eventstore.NewBaseEventForPush( BaseEvent: *eventstore.NewBaseEventForPush(
@@ -110,6 +119,10 @@ func NewOIDCConfigAddedEvent(
BackChannelLogoutURI: backChannelLogoutURI, BackChannelLogoutURI: backChannelLogoutURI,
LoginVersion: loginVersion, LoginVersion: loginVersion,
LoginBaseURI: loginBaseURI, 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 { if e.LoginVersion != c.LoginVersion {
return false 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) { func OIDCConfigAddedEventMapper(event eventstore.Event) (eventstore.Event, error) {
@@ -242,6 +267,10 @@ type OIDCConfigChangedEvent struct {
BackChannelLogoutURI *string `json:"backChannelLogoutURI,omitempty"` BackChannelLogoutURI *string `json:"backChannelLogoutURI,omitempty"`
LoginVersion *domain.LoginVersion `json:"loginVersion,omitempty"` LoginVersion *domain.LoginVersion `json:"loginVersion,omitempty"`
LoginBaseURI *string `json:"loginBaseURI,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{} { 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) { func OIDCConfigChangedEventMapper(event eventstore.Event) (eventstore.Event, error) {
e := &OIDCConfigChangedEvent{ e := &OIDCConfigChangedEvent{
BaseEvent: *eventstore.BaseEventFromRepo(event), BaseEvent: *eventstore.BaseEventFromRepo(event),
+51
View File
@@ -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."; 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 { enum OIDCResponseType {
@@ -341,6 +341,19 @@ message CreateOIDCApplicationRequest {
// hosted on any other domain. // hosted on any other domain.
// If unset, the login UI is chosen by the instance default. // If unset, the login UI is chosen by the instance default.
LoginVersion login_version = 17; 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 { message CreateOIDCApplicationResponse {
@@ -578,6 +591,21 @@ message UpdateOIDCApplicationConfigurationRequest {
// hosted on any other domain. // hosted on any other domain.
// If unset, the login UI is chosen by the instance default. // If unset, the login UI is chosen by the instance default.
optional LoginVersion login_version = 17; 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 { message UpdateAPIApplicationConfigurationRequest {
+52
View File
@@ -4,6 +4,7 @@ package zitadel.application.v2;
import "google/protobuf/duration.proto"; import "google/protobuf/duration.proto";
import "protoc-gen-openapiv2/options/annotations.proto"; import "protoc-gen-openapiv2/options/annotations.proto";
import "validate/validate.proto";
import "zitadel/application/v2/login.proto"; import "zitadel/application/v2/login.proto";
option go_package = "github.com/zitadel/zitadel/pkg/grpc/application/v2;application"; option go_package = "github.com/zitadel/zitadel/pkg/grpc/application/v2;application";
@@ -157,4 +158,55 @@ message OIDCConfiguration {
// hosted on any other domain. // hosted on any other domain.
// If unset, the login UI is chosen by the instance default. // If unset, the login UI is chosen by the instance default.
LoginVersion login_version = 21; 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})$";
}
}
}];
} }
+20
View File
@@ -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."; 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 { 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."; 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 { message UpdateOIDCAppConfigResponse {