Merge branch 'main' into next

# Conflicts:
#	docs/docs/guides/integrate/login-ui/external-login.mdx
#	internal/command/idp_model.go
#	proto/zitadel/user/v2alpha/user_service.proto
This commit is contained in:
Livio Spring 2023-09-05 07:04:42 +02:00
commit 38f7b1bd06
No known key found for this signature in database
GPG Key ID: 26BB1C2FA5952CF0
172 changed files with 10138 additions and 2294 deletions

View File

@ -17,7 +17,7 @@ jobs:
with:
node_version: '18'
buf_version: 'latest'
go_version: '1.20'
go_version: '1.21'
console:
uses: ./.github/workflows/console.yml
@ -35,7 +35,7 @@ jobs:
needs: [core, console, version]
uses: ./.github/workflows/compile.yml
with:
go_version: '1.20'
go_version: '1.21'
core_cache_key: ${{ needs.core.outputs.cache_key }}
console_cache_key: ${{ needs.console.outputs.cache_key }}
core_cache_path: ${{ needs.core.outputs.cache_path }}
@ -46,7 +46,7 @@ jobs:
needs: core
uses: ./.github/workflows/core-unit-test.yml
with:
go_version: '1.20'
go_version: '1.21'
core_cache_key: ${{ needs.core.outputs.cache_key }}
core_cache_path: ${{ needs.core.outputs.cache_path }}
@ -54,7 +54,7 @@ jobs:
needs: core
uses: ./.github/workflows/core-integration-test.yml
with:
go_version: '1.20'
go_version: '1.21'
core_cache_key: ${{ needs.core.outputs.cache_key }}
core_cache_path: ${{ needs.core.outputs.cache_path }}
@ -62,7 +62,7 @@ jobs:
needs: [core, console]
uses: ./.github/workflows/lint.yml
with:
go_version: '1.20'
go_version: '1.21'
node_version: '18'
buf_version: 'latest'
go_lint_version: 'v1.53.2'

View File

@ -8,7 +8,7 @@ issues:
run:
concurrency: 4
timeout: 10m
go: '1.19'
go: '1.21'
skip-dirs:
- .artifacts
- .backups

View File

@ -135,7 +135,7 @@ ZITADEL uses [golangci-lint](https://golangci-lint.run) for code quality checks.
The commands in this section are tested against the following software versions:
- [Docker version 20.10.17](https://docs.docker.com/engine/install/)
- [Go version 1.20](https://go.dev/doc/install)
- [Go version 1.21](https://go.dev/doc/install)
- [Delve 1.9.1](https://github.com/go-delve/delve/tree/v1.9.1/Documentation/installation)
Make some changes to the source code, then run the database locally.

View File

@ -4,21 +4,23 @@
</p>
<p align="center">
<a href="https://github.com/zitadel/zitadel/blob/main/LICENSE" alt="License">
<img src="https://badgen.net/github/license/zitadel/zitadel/" /></a>
<a href="https://bestpractices.coreinfrastructure.org/projects/6662"><img src="https://bestpractices.coreinfrastructure.org/projects/6662/badge"></a>
<a href="https://github.com/zitadel/zitadel/graphs/contributors" alt="Release">
<img src="https://badgen.net/github/contributors/zitadel/zitadel" /></a>
<a href="https://github.com/semantic-release/semantic-release" alt="semantic-release">
<img src="https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg" /></a>
<a href="https://github.com/zitadel/zitadel/actions" alt="ZITADEL Release">
<img src="https://github.com/zitadel/zitadel/actions/workflows/zitadel.yml/badge.svg" /></a>
<a href="https://github.com/zitadel/zitadel/blob/main/LICENSE" alt="License">
<img src="https://badgen.net/github/license/zitadel/zitadel/" /></a>
<img alt="GitHub Workflow Status (with event)" src="https://img.shields.io/github/actions/workflow/status/zitadel/zitadel/build.yml?event=pull_request"></a>
<a href="https://github.com/zitadel/zitadel/releases" alt="Release">
<img src="https://badgen.net/github/release/zitadel/zitadel/stable" /></a>
<a href="https://github.com/zitadel/zitadel/releases" alt="Release">
<img alt="Dynamic YAML Badge" src="https://img.shields.io/badge/dynamic/yaml?url=https%3A%2F%2Fraw.githubusercontent.com%2Fzitadel%2Fzitadel%2Fmain%2Frelease-channels.yaml&query=%24.stable&label=stable"></a>
<a href="https://goreportcard.com/report/github.com/zitadel/zitadel" alt="Go Report Card">
<img src="https://goreportcard.com/badge/github.com/zitadel/zitadel" /></a>
<a href="https://codecov.io/gh/zitadel/zitadel" alt="Code Coverage">
<img src="https://codecov.io/gh/zitadel/zitadel/branch/main/graph/badge.svg" /></a>
<a href="https://github.com/zitadel/zitadel/graphs/contributors" alt="Release">
<img alt="GitHub contributors" src="https://img.shields.io/github/contributors/zitadel/zitadel"></a>
<a href="https://discord.gg/erh5Brh7jE" alt="Discord Chat">
<img src="https://badgen.net/discord/online-members/erh5Brh7jE" /></a>
</p>
@ -94,7 +96,7 @@ Authentication
- Single Sign On (SSO)
- Passwordless with FIDO2 support (Including Passkeys)
- Username / Password
- Multifactor authentication with OTP, U2F
- Multifactor authentication with OTP, U2F, Email OTP, SMS OTP
- LDAP
- [OpenID Connect certified](https://openid.net/certification/#OPs) => [OIDC Endpoints](https://zitadel.com/docs/apis/openidoauth/endpoints)
- [SAML 2.0](http://docs.oasis-open.org/security/saml/Post2.0/sstc-saml-tech-overview-2.0.html) => [SAML Endpoints](https://zitadel.com/docs/apis/saml/endpoints)
@ -103,12 +105,12 @@ Authentication
Multi-Tenancy
- [Identity Brokering](https://zitadel.com/docs/guides/integrate/identity-brokering) with templates for popular identity providers
- [Delegate role management to third-parties](https://zitadel.com/docs/guides/manage/console/projects)
- Domain discovery
- [Domain discovery](https://zitadel.com/docs/guides/solution-scenarios/domain-discovery)
Integration
- [GRPC and REST APIs](https://zitadel.com/docs/apis/introduction)
- [Actions](https://zitadel.com/docs/apis/actions/introduction) to call any API, send webhooks, adjust workflows, or customize tokens
- Role Based Access Control (RBAC)
- [Role Based Access Control (RBAC)](https://zitadel.com/docs/guides/integrate/retrieve-user-roles)
Self-Service
- [Self-registration](https://zitadel.com/docs/concepts/features/selfservice#registration) including verification

View File

@ -307,6 +307,7 @@ Login:
MaxAge: 12h # ZITADEL_LOGIN_CACHE_MAXAGE
# 168h is 7 days, one week
SharedMaxAge: 168h # ZITADEL_LOGIN_CACHE_SHAREDMAXAGE
DefaultOTPEmailURLV2: "/otp/verify?loginName={{.LoginName}}&code={{.Code}}" # ZITADEL_LOGIN_CACHE_DEFAULTOTPEMAILURLV2
Console:
ShortCache:
@ -370,10 +371,10 @@ SystemDefaults:
MachineKeySize: 2048 # ZITADEL_SYSTEMDEFAULTS_SECRETGENERATORS_MACHINEKEYSIZE
ApplicationKeySize: 2048 # ZITADEL_SYSTEMDEFAULTS_SECRETGENERATORS_APPLICATIONKEYSIZE
PasswordHasher:
# Set hasher configuration for user passwords.
# Passwords previously hashed with a different algorithm
# or cost are automatically re-hashed using this config,
# upon password validation or update.
# Set hasher configuration for user passwords.
# Passwords previously hashed with a different algorithm
# or cost are automatically re-hashed using this config,
# upon password validation or update.
Hasher:
Algorithm: "bcrypt" # ZITADEL_SYSTEMDEFAULTS_PASSWORDHASHER_HASHER_ALGORITHM
Cost: 14 # ZITADEL_SYSTEMDEFAULTS_PASSWORDHASHER_HASHER_COST
@ -687,6 +688,7 @@ DefaultInstance:
# If the host of the sender is different from ExternalDomain set DefaultInstance.DomainPolicy.SMTPSenderAddressMatchesInstanceDomain to false
From: # ZITADEL_DEFAULTINSTANCE_SMTPCONFIGURATION_SMTP_FROM
FromName: # ZITADEL_DEFAULTINSTANCE_SMTPCONFIGURATION_SMTP_FROMNAME
ReplyToAddress: # ZITADEL_DEFAULTINSTANCE_SMTPCONFIGURATION_SMTP_REPLYTOADDRESS
MessageTexts:
- MessageTextType: InitCode
Language: de

View File

@ -222,7 +222,25 @@ func startZitadel(config *Config, masterKey string, server chan<- *Server) error
actionsLogstoreSvc := logstore.New(queries, usageReporter, actionsExecutionDBEmitter, actionsExecutionStdoutEmitter)
actions.SetLogstoreService(actionsLogstoreSvc)
notification.Start(ctx, config.Projections.Customizations["notifications"], config.Projections.Customizations["notificationsquotas"], config.Projections.Customizations["telemetry"], *config.Telemetry, config.ExternalDomain, config.ExternalPort, config.ExternalSecure, commands, queries, eventstoreClient, assets.AssetAPIFromDomain(config.ExternalSecure, config.ExternalPort), config.SystemDefaults.Notifications.FileSystemPath, keys.User, keys.SMTP, keys.SMS)
notification.Start(
ctx,
config.Projections.Customizations["notifications"],
config.Projections.Customizations["notificationsquotas"],
config.Projections.Customizations["telemetry"],
*config.Telemetry,
config.ExternalDomain,
config.ExternalPort,
config.ExternalSecure,
commands,
queries,
eventstoreClient,
assets.AssetAPIFromDomain(config.ExternalSecure, config.ExternalPort),
config.Login.DefaultOTPEmailURLV2,
config.SystemDefaults.Notifications.FileSystemPath,
keys.User,
keys.SMTP,
keys.SMS,
)
router := mux.NewRouter()
tlsConfig, err := config.TLS.Config()
@ -362,7 +380,7 @@ func startAPIs(
apis.RegisterHandlerOnPrefix(idp.HandlerPrefix, idp.NewHandler(commands, queries, keys.IDPConfig, config.ExternalSecure, instanceInterceptor.Handler))
userAgentInterceptor, err := middleware.NewUserAgentHandler(config.UserAgentCookie, keys.UserAgentCookieKey, id.SonyFlakeGenerator(), config.ExternalSecure, login.EndpointResources)
userAgentInterceptor, err := middleware.NewUserAgentHandler(config.UserAgentCookie, keys.UserAgentCookieKey, id.SonyFlakeGenerator(), config.ExternalSecure, login.EndpointResources, login.EndpointExternalLoginCallbackFormPost)
if err != nil {
return err
}

View File

@ -56,8 +56,8 @@
"budgets": [
{
"type": "initial",
"maximumWarning": "6mb",
"maximumError": "7mb"
"maximumWarning": "8mb",
"maximumError": "9mb"
},
{
"type": "anyComponentStyle",

View File

@ -173,7 +173,43 @@ export class FilterEventsComponent implements OnInit {
return this.adminService
.listEventTypes(req)
.then((list) => {
this.eventTypes = list.eventTypesList ?? [];
this.eventTypes =
list.eventTypesList.sort((a, b) => {
if (b.localized && b.localized.localizedMessage) {
if (a.localized && a.localized.localizedMessage) {
if (a.localized.localizedMessage < b.localized.localizedMessage) {
return -1;
}
if (a.localized.localizedMessage > b.localized.localizedMessage) {
return 1;
}
} else {
if (a.type < b.localized.localizedMessage) {
return -1;
}
if (a.type > b.localized.localizedMessage) {
return 1;
}
}
} else {
if (a.localized && a.localized.localizedMessage) {
if (a.localized.localizedMessage < b.type) {
return -1;
}
if (a.localized.localizedMessage > b.type) {
return 1;
}
} else {
if (a.type < b.type) {
return -1;
}
if (a.type > b.type) {
return 1;
}
}
}
return 0;
}) ?? [];
})
.catch((error) => {
this.toast.showError(error);

View File

@ -80,6 +80,11 @@
<i class="idp-icon las la-building"></i>
Active Directory / LDAP
</div>
<div class="idp-table-provider-type" *ngSwitchCase="ProviderType.PROVIDER_TYPE_APPLE">
<img class="idp-logo apple dark" src="./assets/images/idp/apple-dark.svg" alt="apple" />
<img class="idp-logo apple light" src="./assets/images/idp/apple.svg" alt="apple" />
Apple
</div>
<div class="idp-table-provider-type" *ngSwitchDefault>coming soon</div>
</div>
</td>

View File

@ -21,6 +21,10 @@
width: 28px;
flex-shrink: 0;
&.apple {
margin-bottom: 4px;
}
&.dark {
display: if($is-dark-theme, block, none);
}

View File

@ -261,6 +261,8 @@ export class IdpTableComponent implements OnInit, OnDestroy {
];
case ProviderType.PROVIDER_TYPE_GITHUB:
return [row.owner === IDPOwnerType.IDP_OWNER_TYPE_SYSTEM ? '/instance' : '/org', 'provider', 'github', row.id];
case ProviderType.PROVIDER_TYPE_APPLE:
return [row.owner === IDPOwnerType.IDP_OWNER_TYPE_SYSTEM ? '/instance' : '/org', 'provider', 'apple', row.id];
}
}
}

View File

@ -109,6 +109,23 @@
</div>
</a>
<a
class="item card"
[routerLink]="
serviceType === PolicyComponentServiceType.ADMIN
? ['/instance', 'provider', 'apple', 'create']
: serviceType === PolicyComponentServiceType.MGMT
? ['/org', 'provider', 'apple', 'create']
: []
"
>
<img class="idp-logo apple dark" src="./assets/images/idp/apple-dark.svg" alt="Apple" />
<img class="idp-logo apple light" src="./assets/images/idp/apple.svg" alt="Apple" />
<div class="text-container">
<span class="title">Apple</span>
</div>
</a>
<a
class="item card"
[routerLink]="

View File

@ -64,6 +64,10 @@
height: 36px;
width: 36px;
&.apple {
margin-bottom: 4px;
}
&.dark {
display: if($is-dark-theme, block, none);
}

View File

@ -15,12 +15,17 @@
<form (ngSubmit)="savePolicy()" [formGroup]="form" autocomplete="off">
<cnsl-form-field class="smtp-form-field" label="Sender Address" required="true">
<cnsl-label>{{ 'SETTING.SMTP.SENDERADDRESS' | translate }}</cnsl-label>
<input cnslInput name="senderAddress" formControlName="senderAddress" />
<input cnslInput name="senderAddress" formControlName="senderAddress" required />
</cnsl-form-field>
<cnsl-form-field class="smtp-form-field" label="Sender Name" required="true">
<cnsl-label>{{ 'SETTING.SMTP.SENDERNAME' | translate }}</cnsl-label>
<input cnslInput name="senderName" formControlName="senderName" />
<input cnslInput name="senderName" formControlName="senderName" required />
</cnsl-form-field>
<cnsl-form-field class="smtp-form-field" label="Reply-To Address">
<cnsl-label>{{ 'SETTING.SMTP.REPLYTOADDRESS' | translate }}</cnsl-label>
<input cnslInput name="senderReplyToAddress" formControlName="replyToAddress" />
</cnsl-form-field>
<mat-checkbox class="smtp-checkbox" formControlName="tls">
@ -29,12 +34,12 @@
<cnsl-form-field class="smtp-form-field" label="Host And Port" required="true">
<cnsl-label>{{ 'SETTING.SMTP.HOSTANDPORT' | translate }}</cnsl-label>
<input cnslInput name="hostAndPort" formControlName="hostAndPort" placeholder="smtp.mailtrap.io:2525" />
<input cnslInput name="hostAndPort" formControlName="hostAndPort" placeholder="smtp.mailtrap.io:2525" required />
</cnsl-form-field>
<cnsl-form-field class="smtp-form-field" label="User" required="true">
<cnsl-label>{{ 'SETTING.SMTP.USER' | translate }}</cnsl-label>
<input id="smtp-user" cnslInput name="smtp-user" autocomplete="smtp-user" formControlName="user" />
<input id="smtp-user" cnslInput name="smtp-user" autocomplete="smtp-user" formControlName="user" required />
</cnsl-form-field>
<button

View File

@ -58,6 +58,7 @@ export class NotificationSettingsComponent implements OnInit {
this.form = this.fb.group({
senderAddress: [{ disabled: true, value: '' }, [requiredValidator]],
senderName: [{ disabled: true, value: '' }, [requiredValidator]],
replyToAddress: [{ disabled: true, value: '' }],
tls: [{ disabled: true, value: true }, [requiredValidator]],
hostAndPort: [{ disabled: true, value: '' }, [requiredValidator]],
user: [{ disabled: true, value: '' }, [requiredValidator]],
@ -143,6 +144,7 @@ export class NotificationSettingsComponent implements OnInit {
req.setHost(this.hostAndPort?.value ?? '');
req.setSenderAddress(this.senderAddress?.value ?? '');
req.setSenderName(this.senderName?.value ?? '');
req.setReplyToAddress(this.replyToAddress?.value ?? '');
req.setTls(this.tls?.value ?? false);
req.setUser(this.user?.value ?? '');
@ -152,6 +154,7 @@ export class NotificationSettingsComponent implements OnInit {
req.setHost(this.hostAndPort?.value ?? '');
req.setSenderAddress(this.senderAddress?.value ?? '');
req.setSenderName(this.senderName?.value ?? '');
req.setReplyToAddress(this.replyToAddress?.value ?? '');
req.setTls(this.tls?.value ?? false);
req.setUser(this.user?.value ?? '');
@ -298,6 +301,10 @@ export class NotificationSettingsComponent implements OnInit {
return this.form.get('senderName');
}
public get replyToAddress(): AbstractControl | null {
return this.form.get('replyToAddress');
}
public get tls(): AbstractControl | null {
return this.form.get('tls');
}

View File

@ -0,0 +1,146 @@
<cnsl-create-layout
title="{{ id ? ('IDP.DETAIL.TITLE' | translate) : ('IDP.CREATE.TITLE' | translate) }}"
(closed)="close()"
>
<div class="identity-provider-create-content">
<div class="title-row">
<img class="idp-logo apple dark" src="./assets/images/idp/apple-dark.svg" alt="apple" />
<img class="idp-logo apple light" src="./assets/images/idp/apple.svg" alt="apple" />
<h1>{{ 'IDP.CREATE.APPLE.TITLE' | translate }}</h1>
<mat-spinner diameter="25" *ngIf="loading" color="primary"></mat-spinner>
</div>
<p class="identity-provider-desc cnsl-secondary-text">
{{ !provider ? ('IDP.CREATE.APPLE.DESCRIPTION' | translate) : ('IDP.DETAIL.DESCRIPTION' | translate) }}
</p>
<form [formGroup]="form" (ngSubmit)="submitForm()">
<div class="identity-provider-content">
<cnsl-form-field class="formfield">
<cnsl-label>{{ 'IDP.CLIENTID' | translate }}</cnsl-label>
<input cnslInput formControlName="clientId" />
</cnsl-form-field>
<cnsl-form-field class="formfield">
<cnsl-label>{{ 'IDP.APPLE.TEAMID' | translate }}</cnsl-label>
<input cnslInput formControlName="teamId" />
</cnsl-form-field>
<cnsl-form-field class="formfield">
<cnsl-label>{{ 'IDP.APPLE.KEYID' | translate }}</cnsl-label>
<input cnslInput formControlName="keyId" />
</cnsl-form-field>
<mat-checkbox
class="update-secret-checkbox"
*ngIf="provider"
[(ngModel)]="updatePrivateKey"
[ngModelOptions]="{ standalone: true }"
>{{ 'IDP.APPLE.UPDATEPRIVATEKEY' | translate }}</mat-checkbox
>
<ng-container *ngIf="!provider || (provider && updatePrivateKey)">
<span class="pk-label cnsl-secondary-text">{{ 'IDP.APPLE.PRIVATEKEY' | translate }}</span>
<div class="private-key-wrapper">
<ng-container *ngIf="privateKey?.value; else addLogoButton">
<i class="las la-file"></i>
<button
class="dl-btn"
mat-icon-button
color="warn"
(click)="privateKey?.setValue('')"
matTooltip="{{ 'ACTIONS.DELETE' | translate }}"
>
<i class="las la-trash"></i>
</button>
</ng-container>
<ng-template #addLogoButton>
<input
#keyFileInput
style="display: none"
class="file-input"
type="file"
accept=".p8"
(change)="onDropKey($any($event.target).files)"
/>
<button
class="asset-add-btn"
mat-icon-button
matTooltip="{{ 'IDP.APPLE.UPLOADPRIVATEKEY' | translate }}"
(click)="$event.preventDefault(); keyFileInput.click()"
>
<mat-icon>add</mat-icon>
</button>
</ng-template>
</div>
</ng-container>
<div class="identity-provider-optional-h-wrapper">
<h2>{{ 'IDP.OPTIONAL' | translate }}</h2>
<button (click)="showOptional = !showOptional" type="button" mat-icon-button>
<mat-icon *ngIf="showOptional">keyboard_arrow_up</mat-icon
><mat-icon *ngIf="!showOptional">keyboard_arrow_down</mat-icon>
</button>
</div>
<div *ngIf="showOptional">
<div class="idp-scopes">
<div class="flex-line">
<cnsl-form-field class="formfield">
<cnsl-label>{{ 'IDP.SCOPESLIST' | translate }}</cnsl-label>
<input
cnslInput
[matChipInputFor]="chipScopesList"
[matChipInputSeparatorKeyCodes]="separatorKeysCodes"
[matChipInputAddOnBlur]="true"
(matChipInputTokenEnd)="addScope($event)"
/>
</cnsl-form-field>
<button class="scope-add-button" (click)="addScope($any($event))" mat-icon-button>
<mat-icon>add</mat-icon>
</button>
</div>
<cnsl-form-field class="formfield">
<mat-chip-list #chipScopesList aria-label="scope selection">
<mat-chip
class="chip"
*ngFor="let scope of scopesList?.value"
selectable="false"
removable
(removed)="removeScope(scope)"
>
{{ scope }} <mat-icon matChipRemove>cancel</mat-icon>
</mat-chip>
</mat-chip-list>
</cnsl-form-field>
</div>
<cnsl-form-field class="formfield">
<cnsl-label>{{ 'IDP.NAME' | translate }}</cnsl-label>
<input cnslInput formControlName="name" />
<span class="name-hint cnsl-secondary-text" cnslHint>{{ 'IDP.NAMEHINT' | translate }}</span>
</cnsl-form-field>
<cnsl-provider-options
[initialOptions]="provider?.config?.options"
(optionsChanged)="options = $event"
></cnsl-provider-options>
</div>
</div>
<div class="identity-provider-create-actions">
<button
color="primary"
mat-raised-button
class="continue-button"
[disabled]="form.invalid || form.disabled"
type="submit"
>
<span *ngIf="id">{{ 'ACTIONS.SAVE' | translate }}</span>
<span *ngIf="!id">{{ 'ACTIONS.CREATE' | translate }}</span>
</button>
</div>
</form>
</div>
</cnsl-create-layout>

View File

@ -0,0 +1,24 @@
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { ProviderAppleComponent } from './provider-apple.component';
describe('ProviderGoogleComponent', () => {
let component: ProviderAppleComponent;
let fixture: ComponentFixture<ProviderAppleComponent>;
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [ProviderAppleComponent],
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(ProviderAppleComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,306 @@
import { COMMA, ENTER, SPACE } from '@angular/cdk/keycodes';
import { Location } from '@angular/common';
import { Component, Injector, Type } from '@angular/core';
import { AbstractControl, FormControl, FormGroup } from '@angular/forms';
import { MatLegacyChipInputEvent as MatChipInputEvent } from '@angular/material/legacy-chips';
import { ActivatedRoute } from '@angular/router';
import { take } from 'rxjs';
import {
AddAppleProviderRequest as AdminAddAppleProviderRequest,
GetProviderByIDRequest as AdminGetProviderByIDRequest,
UpdateAppleProviderRequest as AdminUpdateAppleProviderRequest,
} from 'src/app/proto/generated/zitadel/admin_pb';
import { Options, Provider } from 'src/app/proto/generated/zitadel/idp_pb';
import {
AddAppleProviderRequest as MgmtAddAppleProviderRequest,
GetProviderByIDRequest as MgmtGetProviderByIDRequest,
UpdateAppleProviderRequest as MgmtUpdateAppleProviderRequest,
} from 'src/app/proto/generated/zitadel/management_pb';
import { AdminService } from 'src/app/services/admin.service';
import { Breadcrumb, BreadcrumbService, BreadcrumbType } from 'src/app/services/breadcrumb.service';
import { GrpcAuthService } from 'src/app/services/grpc-auth.service';
import { ManagementService } from 'src/app/services/mgmt.service';
import { ToastService } from 'src/app/services/toast.service';
import { requiredValidator } from '../../form-field/validators/validators';
import { PolicyComponentServiceType } from '../../policies/policy-component-types.enum';
const MAX_ALLOWED_SIZE = 5 * 1024;
@Component({
selector: 'cnsl-provider-apple',
templateUrl: './provider-apple.component.html',
})
export class ProviderAppleComponent {
public showOptional: boolean = false;
public options: Options = new Options().setIsCreationAllowed(true).setIsLinkingAllowed(true);
public id: string | null = '';
public serviceType: PolicyComponentServiceType = PolicyComponentServiceType.MGMT;
private service!: ManagementService | AdminService;
public readonly separatorKeysCodes: number[] = [ENTER, COMMA, SPACE];
public form!: FormGroup;
public loading: boolean = false;
public provider?: Provider.AsObject;
public updatePrivateKey: boolean = false;
constructor(
private authService: GrpcAuthService,
private route: ActivatedRoute,
private toast: ToastService,
private injector: Injector,
private _location: Location,
private breadcrumbService: BreadcrumbService,
) {
this.form = new FormGroup({
name: new FormControl('', []),
clientId: new FormControl('', [requiredValidator]),
teamId: new FormControl('', [requiredValidator]),
keyId: new FormControl('', [requiredValidator]),
privateKey: new FormControl('', [requiredValidator]),
scopesList: new FormControl(['name', 'email'], []),
});
this.authService
.isAllowed(
this.serviceType === PolicyComponentServiceType.ADMIN
? ['iam.idp.write']
: this.serviceType === PolicyComponentServiceType.MGMT
? ['org.idp.write']
: [],
)
.pipe(take(1))
.subscribe((allowed) => {
if (allowed) {
this.form.enable();
} else {
this.form.disable();
}
});
this.route.data.pipe(take(1)).subscribe((data) => {
this.serviceType = data['serviceType'];
switch (this.serviceType) {
case PolicyComponentServiceType.MGMT:
this.service = this.injector.get(ManagementService as Type<ManagementService>);
const bread: Breadcrumb = {
type: BreadcrumbType.ORG,
routerLink: ['/org'],
};
this.breadcrumbService.setBreadcrumb([bread]);
break;
case PolicyComponentServiceType.ADMIN:
this.service = this.injector.get(AdminService as Type<AdminService>);
const iamBread = new Breadcrumb({
type: BreadcrumbType.ORG,
name: 'Instance',
routerLink: ['/instance'],
});
this.breadcrumbService.setBreadcrumb([iamBread]);
break;
}
this.id = this.route.snapshot.paramMap.get('id');
if (this.id) {
this.privateKey?.setValidators([]);
this.getData(this.id);
}
});
}
private getData(id: string): void {
const req =
this.serviceType === PolicyComponentServiceType.ADMIN
? new AdminGetProviderByIDRequest()
: new MgmtGetProviderByIDRequest();
req.setId(id);
this.service
.getProviderByID(req)
.then((resp) => {
this.provider = resp.idp;
this.loading = false;
if (this.provider?.config?.apple) {
this.form.patchValue(this.provider.config.apple);
this.name?.setValue(this.provider.name);
}
})
.catch((error) => {
this.toast.showError(error);
this.loading = false;
});
}
public submitForm(): void {
this.provider ? this.updateAppleProvider() : this.addAppleProvider();
}
public addAppleProvider(): void {
const req =
this.serviceType === PolicyComponentServiceType.MGMT
? new MgmtAddAppleProviderRequest()
: new AdminAddAppleProviderRequest();
req.setName(this.name?.value);
req.setClientId(this.clientId?.value);
req.setTeamId(this.teamId?.value);
req.setKeyId(this.keyId?.value);
req.setPrivateKey(this.privateKey?.value);
req.setScopesList(this.scopesList?.value);
req.setProviderOptions(this.options);
this.loading = true;
this.service
.addAppleProvider(req)
.then((idp) => {
setTimeout(() => {
this.loading = false;
this.close();
}, 2000);
})
.catch((error) => {
this.toast.showError(error);
this.loading = false;
});
}
public updateAppleProvider(): void {
if (this.provider) {
if (this.serviceType === PolicyComponentServiceType.MGMT) {
const req = new MgmtUpdateAppleProviderRequest();
req.setId(this.provider.id);
req.setName(this.name?.value);
req.setClientId(this.clientId?.value);
req.setTeamId(this.teamId?.value);
req.setKeyId(this.keyId?.value);
req.setScopesList(this.scopesList?.value);
req.setProviderOptions(this.options);
if (this.updatePrivateKey) {
req.setPrivateKey(this.privateKey?.value);
}
this.loading = true;
(this.service as ManagementService)
.updateAppleProvider(req)
.then((idp) => {
setTimeout(() => {
this.loading = false;
this.close();
}, 2000);
})
.catch((error) => {
this.toast.showError(error);
this.loading = false;
});
} else if (PolicyComponentServiceType.ADMIN) {
const req = new AdminUpdateAppleProviderRequest();
req.setId(this.provider.id);
req.setName(this.name?.value);
req.setClientId(this.clientId?.value);
req.setTeamId(this.teamId?.value);
req.setKeyId(this.keyId?.value);
req.setScopesList(this.scopesList?.value);
req.setProviderOptions(this.options);
if (this.updatePrivateKey) {
req.setPrivateKey(this.privateKey?.value);
}
this.loading = true;
(this.service as AdminService)
.updateAppleProvider(req)
.then((idp) => {
setTimeout(() => {
this.loading = false;
this.close();
}, 2000);
})
.catch((error) => {
this.loading = false;
this.toast.showError(error);
});
}
}
}
public close(): void {
this._location.back();
}
public onDropKey(filelist: FileList): void {
const file = filelist.item(0);
if (file) {
if (file.size > MAX_ALLOWED_SIZE) {
this.toast.showInfo('IDP.APPLE.KEYMAXSIZEEXCEEDED', true);
} else {
this.privateKey?.setValue('');
const reader = new FileReader();
reader.onload = ((aXML) => {
return (e) => {
const keyBase64 = e.target?.result;
if (keyBase64 && typeof keyBase64 === 'string') {
const cropped = keyBase64.replace('data:application/octet-stream;base64,', '');
this.privateKey?.setValue(cropped);
}
};
})(file);
reader.readAsDataURL(file);
}
}
}
public addScope(event: MatChipInputEvent): void {
const input = event.chipInput?.inputElement;
const value = event.value.trim();
if (value !== '') {
if (this.scopesList?.value) {
this.scopesList.value.push(value);
if (input) {
input.value = '';
}
}
}
}
public removeScope(uri: string): void {
if (this.scopesList?.value) {
const index = this.scopesList.value.indexOf(uri);
if (index !== undefined && index >= 0) {
this.scopesList.value.splice(index, 1);
}
}
}
public get name(): AbstractControl | null {
return this.form.get('name');
}
public get clientId(): AbstractControl | null {
return this.form.get('clientId');
}
public get teamId(): AbstractControl | null {
return this.form.get('teamId');
}
public get keyId(): AbstractControl | null {
return this.form.get('keyId');
}
public get privateKey(): AbstractControl | null {
return this.form.get('privateKey');
}
public get scopesList(): AbstractControl | null {
return this.form.get('scopesList');
}
}

View File

@ -2,6 +2,7 @@ import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { ProviderType } from 'src/app/proto/generated/zitadel/idp_pb';
import { ProviderAppleComponent } from './provider-apple/provider-apple.component';
import { ProviderAzureADComponent } from './provider-azure-ad/provider-azure-ad.component';
import { ProviderGithubESComponent } from './provider-github-es/provider-github-es.component';
import { ProviderGithubComponent } from './provider-github/provider-github.component';
@ -27,6 +28,7 @@ const typeMap = {
[ProviderType.PROVIDER_TYPE_OAUTH]: { path: 'oauth', component: ProviderOAuthComponent },
[ProviderType.PROVIDER_TYPE_OIDC]: { path: 'oidc', component: ProviderOIDCComponent },
[ProviderType.PROVIDER_TYPE_LDAP]: { path: 'ldap', component: ProviderLDAPComponent },
[ProviderType.PROVIDER_TYPE_APPLE]: { path: 'apple', component: ProviderAppleComponent },
};
const routes: Routes = Object.entries(typeMap).map(([key, value]) => {

View File

@ -17,6 +17,7 @@ import { InfoSectionModule } from '../info-section/info-section.module';
import { ProviderOptionsModule } from '../provider-options/provider-options.module';
import { StringListModule } from '../string-list/string-list.module';
import { LDAPAttributesComponent } from './ldap-attributes/ldap-attributes.component';
import { ProviderAppleComponent } from './provider-apple/provider-apple.component';
import { ProviderAzureADComponent } from './provider-azure-ad/provider-azure-ad.component';
import { ProviderGithubESComponent } from './provider-github-es/provider-github-es.component';
import { ProviderGithubComponent } from './provider-github/provider-github.component';
@ -43,6 +44,7 @@ import { ProvidersRoutingModule } from './providers-routing.module';
ProviderOIDCComponent,
ProviderOAuthComponent,
ProviderLDAPComponent,
ProviderAppleComponent,
],
imports: [
ProvidersRoutingModule,

View File

@ -3,6 +3,7 @@
@mixin identity-provider-theme($theme) {
$is-dark-theme: map-get($theme, is-dark);
$background: map-get($theme, background);
$foreground: map-get($theme, foreground);
.identity-provider-desc {
font-size: 14px;
@ -19,6 +20,10 @@
margin-right: 1rem;
flex-shrink: 0;
&.apple {
margin-bottom: 4px;
}
&.dark {
display: if($is-dark-theme, block, none);
}
@ -74,6 +79,58 @@
}
}
.pk-label {
font-size: 12px;
margin-top: 0.5rem;
}
.private-key-wrapper {
position: relative;
height: 70px;
width: 70px;
box-sizing: border-box;
border-radius: 0.5rem;
margin-top: 0.5rem;
border: 1px solid map-get($foreground, divider);
display: flex;
justify-content: center;
align-items: center;
.dl-btn {
z-index: 2;
position: absolute;
right: 0;
top: 0;
cursor: pointer;
visibility: hidden;
transform: translateX(50%) translateY(-50%);
}
img {
top: 0;
left: 0;
width: 70px;
height: 70px;
object-fit: contain;
object-position: center;
border-radius: 0.5rem;
}
&.icon {
border-radius: 50%;
img {
border-radius: 50%;
}
}
&:hover {
.dl-btn {
visibility: visible;
}
}
}
.string-list-component-wrapper {
max-width: 400px;
}

View File

@ -248,7 +248,7 @@ export class OrgDetailComponent implements OnInit, OnDestroy {
this.metadata = resp.resultList.map((md) => {
return {
key: md.key,
value: Buffer.from(md.value as string, 'base64').toString('ascii'),
value: Buffer.from(md.value as string, 'base64').toString('utf-8'),
};
});
})

View File

@ -521,7 +521,7 @@ export class AppCreateComponent implements OnInit, OnDestroy {
get decodedBase64(): string {
const samlReq = this.samlAppRequest.toObject();
if (samlReq && samlReq.metadataXml && typeof samlReq.metadataXml === 'string') {
return Buffer.from(samlReq.metadataXml, 'base64').toString('ascii');
return Buffer.from(samlReq.metadataXml, 'base64').toString('utf-8');
} else {
return '';
}
@ -529,7 +529,7 @@ export class AppCreateComponent implements OnInit, OnDestroy {
set decodedBase64(xmlString) {
if (this.samlAppRequest) {
const base64 = Buffer.from(xmlString, 'ascii').toString('base64');
const base64 = Buffer.from(xmlString, 'utf-8').toString('base64');
this.samlAppRequest.setMetadataXml(base64);
}
}

View File

@ -786,7 +786,7 @@ export class AppDetailComponent implements OnInit, OnDestroy {
this.app.samlConfig.metadataXml &&
typeof this.app.samlConfig.metadataXml === 'string'
) {
return Buffer.from(this.app?.samlConfig.metadataXml, 'base64').toString('ascii');
return Buffer.from(this.app?.samlConfig.metadataXml, 'base64').toString('utf-8');
} else {
return '';
}
@ -794,7 +794,7 @@ export class AppDetailComponent implements OnInit, OnDestroy {
set decodedBase64(xmlString: string) {
if (this.app && this.app.samlConfig && this.app.samlConfig.metadataXml) {
const base64 = Buffer.from(xmlString, 'ascii').toString('base64');
const base64 = Buffer.from(xmlString, 'utf-8').toString('base64');
if (this.app.samlConfig) {
this.app.samlConfig.metadataXml = base64;

View File

@ -406,7 +406,7 @@ export class AuthUserDetailComponent implements OnDestroy {
this.metadata = resp.resultList.map((md) => {
return {
key: md.key,
value: Buffer.from(md.value as string, 'base64').toString('ascii'),
value: Buffer.from(md.value as string, 'base64').toString('utf8'),
};
});
})

View File

@ -508,7 +508,7 @@ export class UserDetailComponent implements OnInit {
this.metadata = resp.resultList.map((md) => {
return {
key: md.key,
value: Buffer.from(md.value as string, 'base64').toString('ascii'),
value: Buffer.from(md.value as string, 'base64').toString('utf-8'),
};
});
})

View File

@ -6,6 +6,8 @@ import {
ActivateLabelPolicyResponse,
ActivateSMSProviderRequest,
ActivateSMSProviderResponse,
AddAppleProviderRequest,
AddAppleProviderResponse,
AddAzureADProviderRequest,
AddAzureADProviderResponse,
AddCustomDomainPolicyRequest,
@ -214,6 +216,8 @@ import {
SetSecurityPolicyResponse,
SetUpOrgRequest,
SetUpOrgResponse,
UpdateAppleProviderRequest,
UpdateAppleProviderResponse,
UpdateAzureADProviderRequest,
UpdateAzureADProviderResponse,
UpdateCustomDomainPolicyRequest,
@ -1173,6 +1177,14 @@ export class AdminService {
return this.grpcService.admin.updateGitHubEnterpriseServerProvider(req, null).then((resp) => resp.toObject());
}
public addAppleProvider(req: AddAppleProviderRequest): Promise<AddAppleProviderResponse.AsObject> {
return this.grpcService.admin.addAppleProvider(req, null).then((resp) => resp.toObject());
}
public updateAppleProvider(req: UpdateAppleProviderRequest): Promise<UpdateAppleProviderResponse.AsObject> {
return this.grpcService.admin.updateAppleProvider(req, null).then((resp) => resp.toObject());
}
public deleteProvider(id: string): Promise<DeleteProviderResponse.AsObject> {
const req = new DeleteProviderRequest();
req.setId(id);

View File

@ -15,6 +15,8 @@ import {
AddAPIAppResponse,
AddAppKeyRequest,
AddAppKeyResponse,
AddAppleProviderRequest,
AddAppleProviderResponse,
AddAzureADProviderRequest,
AddAzureADProviderResponse,
AddCustomLabelPolicyRequest,
@ -441,6 +443,8 @@ import {
UpdateActionResponse,
UpdateAPIAppConfigRequest,
UpdateAPIAppConfigResponse,
UpdateAppleProviderRequest,
UpdateAppleProviderResponse,
UpdateAppRequest,
UpdateAppResponse,
UpdateAzureADProviderRequest,
@ -1041,6 +1045,14 @@ export class ManagementService {
return this.grpcService.mgmt.updateGitHubEnterpriseServerProvider(req, null).then((resp) => resp.toObject());
}
public addAppleProvider(req: AddAppleProviderRequest): Promise<AddAppleProviderResponse.AsObject> {
return this.grpcService.mgmt.addAppleProvider(req, null).then((resp) => resp.toObject());
}
public updateAppleProvider(req: UpdateAppleProviderRequest): Promise<UpdateAppleProviderResponse.AsObject> {
return this.grpcService.mgmt.updateAppleProvider(req, null).then((resp) => resp.toObject());
}
public deleteProvider(id: string): Promise<DeleteProviderResponse.AsObject> {
const req = new DeleteProviderRequest();
req.setId(id);

View File

@ -1049,6 +1049,7 @@
"TITLE": "SMTP настройки",
"SENDERADDRESS": "Имейл адрес на изпращача",
"SENDERNAME": "Име на изпращача",
"REPLYTOADDRESS": "Reply-to адрес",
"HOSTANDPORT": "Хост и порт",
"USER": "Потребител",
"PASSWORD": "Парола",

View File

@ -1055,6 +1055,7 @@
"TITLE": "SMTP Einstellungen",
"SENDERADDRESS": "Sender Email-Adresse",
"SENDERNAME": "Sender Name",
"REPLYTOADDRESS": "Reply-to-Adresse",
"HOSTANDPORT": "Host und Port",
"USER": "Benutzer",
"PASSWORD": "Passwort",

View File

@ -1056,6 +1056,7 @@
"TITLE": "SMTP Settings",
"SENDERADDRESS": "Sender Email Address",
"SENDERNAME": "Sender Name",
"REPLYTOADDRESS": "Reply-to Address",
"HOSTANDPORT": "Host And Port",
"USER": "User",
"PASSWORD": "Password",
@ -1705,6 +1706,10 @@
"LDAP": {
"TITLE": "Active Directory / LDAP",
"DESCRIPTION": "Enter the credentials for your LDAP Provider"
},
"APPLE": {
"TITLE": "Sign in with Apple",
"DESCRIPTION": "Enter the credentials for your Apple Provider"
}
},
"DETAIL": {
@ -1818,6 +1823,14 @@
"JWTENDPOINT": "JWT Endpoint",
"JWTKEYSENDPOINT": "JWT Keys Endpoint"
},
"APPLE": {
"TEAMID": "Team ID",
"KEYID": "Key ID",
"PRIVATEKEY": "Private Key",
"UPDATEPRIVATEKEY": "Update Private Key",
"UPLOADPRIVATEKEY": "Upload Private Key",
"KEYMAXSIZEEXCEEDED": "Maximum size of 5kB exceeded."
},
"TOAST": {
"SAVED": "Successfully saved.",
"REACTIVATED": "Idp reactivated.",

View File

@ -1056,6 +1056,7 @@
"TITLE": "Ajustes SMTP",
"SENDERADDRESS": "Dirección email del emisor",
"SENDERNAME": "Nombre del emisor",
"REPLYTOADDRESS": "Dirección Reply-To",
"HOSTANDPORT": "Servidor y puerto",
"USER": "Usuario",
"PASSWORD": "Contraseña",

View File

@ -1055,6 +1055,7 @@
"TITLE": "Paramètres SMTP",
"SENDERADDRESS": "Adresse e-mail de l'expéditeur",
"SENDERNAME": "Nom de l'expéditeur",
"REPLYTOADDRESS": "Adresse Reply-to",
"HOSTANDPORT": "Hôte et port",
"USER": "Utilisateur",
"PASSWORD": "Mot de passe",

View File

@ -1055,6 +1055,7 @@
"TITLE": "Impostazioni SMTP",
"SENDERADDRESS": "Indirizzo email del mittente",
"SENDERNAME": "Nome del mittente",
"REPLYTOADDRESS": "Indirizzo Reply-to",
"HOSTANDPORT": "Host e porta",
"USER": "Utente",
"PASSWORD": "Password",

View File

@ -1056,6 +1056,7 @@
"TITLE": "SMTP設定",
"SENDERADDRESS": "送信者のメールアドレス",
"SENDERNAME": "送信者名",
"REPLYTOADDRESS": "返信先アドレス",
"HOSTANDPORT": "ホストとポート",
"USER": "ユーザー",
"PASSWORD": "パスワード",

View File

@ -1056,6 +1056,7 @@
"TITLE": "SMTP подесувања",
"SENDERADDRESS": "Адреса на испраќачот",
"SENDERNAME": "Име на испраќачот",
"REPLYTOADDRESS": "Reply-to адреса",
"HOSTANDPORT": "Host и Port",
"USER": "Корисник",
"PASSWORD": "Лозинка",

View File

@ -1055,6 +1055,7 @@
"TITLE": "Ustawienia SMTP",
"SENDERADDRESS": "Adres e-mail nadawcy",
"SENDERNAME": "Nazwa nadawcy",
"REPLYTOADDRESS": "Adres Reply-to",
"HOSTANDPORT": "Host i port",
"USER": "Użytkownik",
"PASSWORD": "Hasło",

View File

@ -1056,6 +1056,7 @@
"TITLE": "Configurações SMTP",
"SENDERADDRESS": "Endereço de e-mail do remetente",
"SENDERNAME": "Nome do remetente",
"REPLYTOADDRESS": "Endereço Reply-To",
"HOSTANDPORT": "Host e porta",
"USER": "Usuário",
"PASSWORD": "Senha",

View File

@ -1055,6 +1055,7 @@
"TITLE": "SMTP 设置",
"SENDERADDRESS": "发件人地址",
"SENDERNAME": "发件人名称",
"REPLYTOADDRESS": "Reply-to 地址",
"HOSTANDPORT": "主机和端口",
"USER": "用户名",
"PASSWORD": "密码",

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="56px" height="56px" viewBox="18 15.5 20 20" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 61 (89581) - https://sketch.com -->
<title>White Logo Square </title>
<desc>Created with Sketch.</desc>
<g id="White-Logo-Square-" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<rect id="Rectangle" fill="" x="6" y="6" width="44" height="44"></rect>
<path d="M28.2226562,20.3846154 C29.0546875,20.3846154 30.0976562,19.8048315 30.71875,19.0317864 C31.28125,18.3312142 31.6914062,17.352829 31.6914062,16.3744437 C31.6914062,16.2415766 31.6796875,16.1087095 31.65625,16 C30.7304687,16.0362365 29.6171875,16.640178 28.9492187,17.4494596 C28.421875,18.06548 27.9414062,19.0317864 27.9414062,20.0222505 C27.9414062,20.1671964 27.9648438,20.3121424 27.9765625,20.3604577 C28.0351562,20.3725366 28.1289062,20.3846154 28.2226562,20.3846154 Z M25.2929688,35 C26.4296875,35 26.9335938,34.214876 28.3515625,34.214876 C29.7929688,34.214876 30.109375,34.9758423 31.375,34.9758423 C32.6171875,34.9758423 33.4492188,33.792117 34.234375,32.6325493 C35.1132812,31.3038779 35.4765625,29.9993643 35.5,29.9389701 C35.4179688,29.9148125 33.0390625,28.9122695 33.0390625,26.0979021 C33.0390625,23.6579784 34.9140625,22.5588048 35.0195312,22.474253 C33.7773438,20.6382708 31.890625,20.5899555 31.375,20.5899555 C29.9804688,20.5899555 28.84375,21.4596313 28.1289062,21.4596313 C27.3554688,21.4596313 26.3359375,20.6382708 25.1289062,20.6382708 C22.8320312,20.6382708 20.5,22.5950413 20.5,26.2911634 C20.5,28.5861411 21.3671875,31.013986 22.4335938,32.5842339 C23.3476562,33.9129053 24.1445312,35 25.2929688,35 Z" id="" fill="#FFFFFF" fill-rule="nonzero"></path>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="56px" height="56px" viewBox="18 15.5 20 20" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 61 (89581) - https://sketch.com -->
<title>Black Logo Square</title>
<desc>Created with Sketch.</desc>
<g id="Black-Logo-Square" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<rect id="Rectangle" fill="" x="6" y="6" width="44" height="44"></rect>
<path d="M28.2226562,20.3846154 C29.0546875,20.3846154 30.0976562,19.8048315 30.71875,19.0317864 C31.28125,18.3312142 31.6914062,17.352829 31.6914062,16.3744437 C31.6914062,16.2415766 31.6796875,16.1087095 31.65625,16 C30.7304687,16.0362365 29.6171875,16.640178 28.9492187,17.4494596 C28.421875,18.06548 27.9414062,19.0317864 27.9414062,20.0222505 C27.9414062,20.1671964 27.9648438,20.3121424 27.9765625,20.3604577 C28.0351562,20.3725366 28.1289062,20.3846154 28.2226562,20.3846154 Z M25.2929688,35 C26.4296875,35 26.9335938,34.214876 28.3515625,34.214876 C29.7929688,34.214876 30.109375,34.9758423 31.375,34.9758423 C32.6171875,34.9758423 33.4492188,33.792117 34.234375,32.6325493 C35.1132812,31.3038779 35.4765625,29.9993643 35.5,29.9389701 C35.4179688,29.9148125 33.0390625,28.9122695 33.0390625,26.0979021 C33.0390625,23.6579784 34.9140625,22.5588048 35.0195312,22.474253 C33.7773438,20.6382708 31.890625,20.5899555 31.375,20.5899555 C29.9804688,20.5899555 28.84375,21.4596313 28.1289062,21.4596313 C27.3554688,21.4596313 26.3359375,20.6382708 25.1289062,20.6382708 C22.8320312,20.6382708 20.5,22.5950413 20.5,26.2911634 C20.5,28.5861411 21.3671875,31.013986 22.4335938,32.5842339 C23.3476562,33.9129053 24.1445312,35 25.2929688,35 Z" id="" fill="#000000" fill-rule="nonzero"></path>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@ -429,6 +429,7 @@ $custom-typography: mat.define-legacy-typography-config(
background-color: map-get($background, cards);
transition: background-color 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
border-radius: 6px;
min-height: fit-content;
}
.mat-option {
@ -500,6 +501,7 @@ $custom-typography: mat.define-legacy-typography-config(
background-color: map-get($background, cards);
transition: background-color 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
border-radius: 6px;
min-height: fit-content;
}
.mat-option {

View File

@ -26,7 +26,7 @@ In the response, you will get an authentication URL of the provider you like.
```bash
curl --request POST \
--url https://$ZITADEL_DOMAIN/v2alpha/idp_intents/start \
--url https://$ZITADEL_DOMAIN/v2alpha/idp_intents \
--header 'Accept: application/json' \
--header 'Authorization: Bearer '"$TOKEN"''\
--header 'Content-Type: application/json' \

View File

@ -212,7 +212,7 @@ Next step is to authenticate the user with the new registered passkey.
### Create Session
First step is to ask the user for his username and create a new session with the ZITADEL API.
When creating the new session make sure to include the challenge for passkey.
When creating the new session make sure to include the challenge for passkey, resp. webAuthN with a required user verification and the domain of your login UI.
The response will include the public key credential request options for the passkey in the challenges.
More detailed information about the API: [Create Session Documentation](/apis/resources/session_service/session-service-create-session)
@ -231,9 +231,12 @@ curl --request POST \
}
},
"metadata": {},
"challenges": [
"CHALLENGE_KIND_PASSKEY"
]
"challenges": {
"webAuthN": {
"domain": "example.domain.com",
"userVerificationRequirement": "USER_VERIFICATION_REQUIREMENT_REQUIRED"
}
}
}'
```
@ -248,7 +251,7 @@ Example Response:
"sessionId": "d654e6ba-70a3-48ef-a95d-37c8d8a7901a",
"sessionToken": "string",
"challenges": {
"passkey": {
"webAuthN": {
"publicKeyCredentialRequestOptions": {
"publicKey": {
"allowCredentials": [
@ -274,7 +277,7 @@ After starting the passkey authentication on the side of ZITADEL you have to cha
To do this you need to call the browser API to get the credentials.
Make sure to send the public key credential request options you got from ZITADEL.
```bash
```javascript
const credential = await navigator.credentials.get({
publicKey: publicKeyCredentialRequestOptions
});
@ -300,7 +303,7 @@ curl --request PATCH \
--data '{
"sessionToken": "yMDi6uVPJAcphbbz0LaxC07ihWkNTe7m0Xqch8SzfM5Cz3HSIQIDZ65x1f5Qal0jxz0MEyo-_zYcUg",
"checks": {
"passkey": {
"webAuthN": {
"credentialAssertionData": {}
}
}

View File

@ -75,7 +75,7 @@ Authorization: "Basic " + base64( formUrlEncode(client_id) + ":" + formUrlEncode
The request from the API to the introspection endpoint should be in the following format:
```
```bash
curl --request POST \
--url {your_domain}/oauth/v2/introspect \
--header 'Content-Type: application/x-www-form-urlencoded' \
@ -85,7 +85,7 @@ curl --request POST \
Here's an example of how this is done in Python code:
```
```python
def introspect_token(self, token_string):
url = ZITADEL_INTROSPECTION_URL
data = {'token': token_string, 'token_type_hint': 'access_token', 'scope': 'openid'}

View File

@ -31,7 +31,7 @@ you can setup ZITADEL and either
# Install CockroachDB
helm install crdb cockroachdb/cockroachdb \
--set fullnameOverride=crdb \
--set single-node=true \
--set conf.single-node=true \
--set statefulset.replicas=1
# Install ZITADEL
@ -65,7 +65,7 @@ With this setup you only get a key for a service account. Logging in at ZITADEL
# Install CockroachDB
helm install crdb cockroachdb/cockroachdb \
--set fullnameOverride=crdb \
--set single-node=true \
--set conf.single-node=true \
--set statefulset.replicas=1
# Install ZITADEL

View File

@ -23,16 +23,16 @@ By executing the commands below, you will download the following files:
```bash
# Download the docker compose example configuration for a secure CockroachDB.
wget https://raw.githubusercontent.com/zitadel/zitadel/main/docs/docs/self-hosting/manage/production/docker-compose.yaml
wget https://raw.githubusercontent.com/zitadel/zitadel/main/docs/docs/self-hosting/manage/configure/docker-compose.yaml
# Download and adjust the example configuration file containing standard configuration.
wget https://raw.githubusercontent.com/zitadel/zitadel/main/docs/docs/self-hosting/manage/production/example-zitadel-config.yaml
wget https://raw.githubusercontent.com/zitadel/zitadel/main/docs/docs/self-hosting/manage/configure/example-zitadel-config.yaml
# Download and adjust the example configuration file containing secret configuration.
wget https://raw.githubusercontent.com/zitadel/zitadel/main/docs/docs/self-hosting/manage/production/example-zitadel-secrets.yaml
wget https://raw.githubusercontent.com/zitadel/zitadel/main/docs/docs/self-hosting/manage/configure/example-zitadel-secrets.yaml
# Download and adjust the example configuration file containing database initialization configuration.
wget https://raw.githubusercontent.com/zitadel/zitadel/main/docs/docs/self-hosting/manage/production/example-zitadel-init-steps.yaml
wget https://raw.githubusercontent.com/zitadel/zitadel/main/docs/docs/self-hosting/manage/configure/example-zitadel-init-steps.yaml
# A single ZITADEL instance always needs the same 32 characters long masterkey
# If you haven't done so already, you can generate a new one

View File

@ -200,6 +200,7 @@ DefaultInstance:
# if the host of the sender is different from ExternalDomain set DefaultInstance.DomainPolicy.SMTPSenderAddressMatchesInstanceDomain to false
From:
FromName:
ReplyToAddress:
```
- If you don't want to use the DefaultInstance configuration for the first instance that ZITADEL automatically creates for you during the [setup phase](/self-hosting/manage/configure#database-initialization), you can provide a FirstInstance YAML section using the --steps argument.

View File

@ -13,8 +13,8 @@ To address this, we are going to change this behavior so that users will be auto
## Statement
This behaviour change is tracked in the following issue: [Reuse current session if no prompt is selected ](https://github.com/zitadel/zitadel/issues/4841)
As soon as the release version is published, we will include the version here.
This behaviour change was tracked in the following issue: [Reuse current session if no prompt is selected](https://github.com/zitadel/zitadel/issues/4841)
and released in Version [v2.32.0](https://github.com/zitadel/zitadel/releases/tag/v2.32.0)
## Mitigation

View File

@ -0,0 +1,26 @@
---
title: Technical Advisory 10001
---
## Description
Currently, disabling the `Allow Register` setting in the Login Policy, will disable any registration - local and through External Identity Providers (IDP).
This might be a good solution, if you manage all users yourself and do not want them to create any new account.
If you on the other hand want users to be able to federate their accounts from another IDP and only want to disable local registration, there's currently no option to do so.
Further ZITADEL provided the possibility to disable registration on each IDP with the introduction of IDP Templates.
To address this, we are going to change the behavior of the setting mentioned above, so that if disable, it will only prevent local registration. Registration of a federated user will still be possible - if not disabled by the corresponding IDP Template.
## Statement
This behaviour change is tracked in the following PR: [Restrict AllowRegistration check to local registration](https://github.com/zitadel/zitadel/pull/5939).
As soon as the release version is published, we will include the version here.
## Mitigation
If you want to prevent user creation / registration through an IDP, be sure to disable the `isCreationAllowed` option on the desired IDP Templates.
## Impact
Once this update has been released and deployed, the `Allow Register` setting in the Login Policy will only affect local registrations and users might be able to create a ZITADEL account through an IDP, depending on your IDP provider options.

View File

@ -26,6 +26,14 @@ We understand that these advisories may include breaking changes, and we aim to
<td>2.32.0</td>
<td>Calendar week 32</td>
</tr>
<tr>
<td><a href="./advisory/a10001">A-10001</a></td>
<td>Login Policy - Allow Register</td>
<td>Breaking Behaviour Change</td>
<td>When disabling the option, users are currently not able to register locally and also not through an external IDP. With the upcoming change, the setting will only prevent local registration. Restriction to Identity Providers can be managed through the corresponding IDP Template. No action is required on your side if this is the intended behaviour or if you already disabled registration on your IDP.</td>
<td>TBD</td>
<td>Calendar week 34/35</td>
</tr>
</table>
## Subscribe to our Mailing List

View File

@ -20,7 +20,7 @@ module.exports = {
],
customFields: {
description:
"Documentation for ZITADEL - The best of Auth0 and Keycloak combined. Built for the serverless era.",
"Documentation for ZITADEL - Identity infrastructure, simplified for you.",
},
themeConfig: {
metadata: [

View File

@ -284,6 +284,31 @@ module.exports = {
"guides/integrate/services/auth0-oidc",
"guides/integrate/services/auth0-saml",
"guides/integrate/services/pingidentity-saml",
{
type: 'link',
label: 'Nextcloud',
href: 'https://zitadel.com/blog/zitadel-as-sso-provider-for-selfhosting',
},
{
type: 'link',
label: 'Cloudflare workers',
href: 'https://zitadel.com/blog/increase-spa-security-with-cloudflare-workers',
},
{
type: 'link',
label: 'Firezone (firezone.dev)',
href: 'https://www.firezone.dev/docs/authenticate/oidc/zitadel',
},
{
type: 'link',
label: 'Psono (psono.com)',
href: 'https://doc.psono.com/admin/configuration/oidc-zitadel.html',
},
{
type: 'link',
label: 'Netbird (netbird.io)',
href: 'https://docs.netbird.io/selfhosted/identity-providers',
},
],
},
{

File diff suppressed because it is too large Load Diff

2
go.mod
View File

@ -1,6 +1,6 @@
module github.com/zitadel/zitadel
go 1.20
go 1.21
require (
cloud.google.com/go/storage v1.32.0

View File

@ -42,7 +42,7 @@ func (h *Handler) Commands() *command.Commands {
}
func (h *Handler) ErrorHandler() ErrorHandler {
return DefaultErrorHandler
return h.errorHandler
}
func (h *Handler) Storage() static.Storage {
@ -75,10 +75,14 @@ type Downloader interface {
ResourceOwner(ctx context.Context, ownerPath string) string
}
type ErrorHandler func(http.ResponseWriter, *http.Request, error, int)
type ErrorHandler func(w http.ResponseWriter, r *http.Request, err error, defaultCode int)
func DefaultErrorHandler(w http.ResponseWriter, r *http.Request, err error, code int) {
func DefaultErrorHandler(w http.ResponseWriter, r *http.Request, err error, defaultCode int) {
logging.WithFields("uri", r.RequestURI).WithError(err).Warn("error occurred on asset api")
code, ok := http_util.ZitadelErrorToHTTPStatusCode(err)
if !ok {
code = defaultCode
}
http.Error(w, err.Error(), code)
}
@ -162,7 +166,7 @@ func UploadHandleFunc(s AssetsService, uploader Uploader) func(http.ResponseWrit
}
err = uploader.UploadAsset(ctx, ctxData.OrgID, uploadInfo, s.Commands())
if err != nil {
s.ErrorHandler()(w, r, fmt.Errorf("upload failed: %v", err), http.StatusInternalServerError)
s.ErrorHandler()(w, r, fmt.Errorf("upload failed: %w", err), http.StatusInternalServerError)
return
}
}
@ -190,10 +194,6 @@ func DownloadHandleFunc(s AssetsService, downloader Downloader) func(http.Respon
return
}
if err = GetAsset(w, r, resourceOwner, objectName, s.Storage()); err != nil {
if strings.Contains(err.Error(), "DATAB-pCP8P") {
s.ErrorHandler()(w, r, err, http.StatusNotFound)
return
}
s.ErrorHandler()(w, r, err, http.StatusInternalServerError)
}
}
@ -206,11 +206,11 @@ func GetAsset(w http.ResponseWriter, r *http.Request, resourceOwner, objectName
}
data, getInfo, err := storage.GetObject(r.Context(), authz.GetInstance(r.Context()).InstanceID(), resourceOwner, objectName)
if err != nil {
return fmt.Errorf("download failed: %v", err)
return fmt.Errorf("download failed: %w", err)
}
info, err := getInfo()
if err != nil {
return fmt.Errorf("download failed: %v", err)
return fmt.Errorf("download failed: %w", err)
}
if info.Hash == strings.Trim(r.Header.Get(http_util.IfNoneMatch), "\"") {
w.Header().Set(http_util.LastModified, info.LastModified.Format(time.RFC1123))

View File

@ -132,9 +132,10 @@ func SecretGeneratorTypeToDomain(generatorType settings_pb.SecretGeneratorType)
func AddSMTPToConfig(req *admin_pb.AddSMTPConfigRequest) *smtp.Config {
return &smtp.Config{
Tls: req.Tls,
From: req.SenderAddress,
FromName: req.SenderName,
Tls: req.Tls,
From: req.SenderAddress,
FromName: req.SenderName,
ReplyToAddress: req.ReplyToAddress,
SMTP: smtp.SMTP{
Host: req.Host,
User: req.User,
@ -145,9 +146,10 @@ func AddSMTPToConfig(req *admin_pb.AddSMTPConfigRequest) *smtp.Config {
func UpdateSMTPToConfig(req *admin_pb.UpdateSMTPConfigRequest) *smtp.Config {
return &smtp.Config{
Tls: req.Tls,
From: req.SenderAddress,
FromName: req.SenderName,
Tls: req.Tls,
From: req.SenderAddress,
FromName: req.SenderName,
ReplyToAddress: req.ReplyToAddress,
SMTP: smtp.SMTP{
Host: req.Host,
User: req.User,
@ -157,12 +159,13 @@ func UpdateSMTPToConfig(req *admin_pb.UpdateSMTPConfigRequest) *smtp.Config {
func SMTPConfigToPb(smtp *query.SMTPConfig) *settings_pb.SMTPConfig {
mapped := &settings_pb.SMTPConfig{
Tls: smtp.TLS,
SenderAddress: smtp.SenderAddress,
SenderName: smtp.SenderName,
Host: smtp.Host,
User: smtp.User,
Details: obj_grpc.ToViewDetailsPb(smtp.Sequence, smtp.CreationDate, smtp.ChangeDate, smtp.AggregateID),
Tls: smtp.TLS,
SenderAddress: smtp.SenderAddress,
SenderName: smtp.SenderName,
ReplyToAddress: smtp.ReplyToAddress,
Host: smtp.Host,
User: smtp.User,
Details: obj_grpc.ToViewDetailsPb(smtp.Sequence, smtp.CreationDate, smtp.ChangeDate, smtp.AggregateID),
}
return mapped
}

View File

@ -405,6 +405,27 @@ func (s *Server) UpdateLDAPProvider(ctx context.Context, req *admin_pb.UpdateLDA
}, nil
}
func (s *Server) AddAppleProvider(ctx context.Context, req *admin_pb.AddAppleProviderRequest) (*admin_pb.AddAppleProviderResponse, error) {
id, details, err := s.command.AddInstanceAppleProvider(ctx, addAppleProviderToCommand(req))
if err != nil {
return nil, err
}
return &admin_pb.AddAppleProviderResponse{
Id: id,
Details: object_pb.DomainToAddDetailsPb(details),
}, nil
}
func (s *Server) UpdateAppleProvider(ctx context.Context, req *admin_pb.UpdateAppleProviderRequest) (*admin_pb.UpdateAppleProviderResponse, error) {
details, err := s.command.UpdateInstanceAppleProvider(ctx, req.Id, updateAppleProviderToCommand(req))
if err != nil {
return nil, err
}
return &admin_pb.UpdateAppleProviderResponse{
Details: object_pb.DomainToChangeDetailsPb(details),
}, nil
}
func (s *Server) DeleteProvider(ctx context.Context, req *admin_pb.DeleteProviderRequest) (*admin_pb.DeleteProviderResponse, error) {
details, err := s.command.DeleteInstanceProvider(ctx, req.Id)
if err != nil {

View File

@ -440,3 +440,27 @@ func updateLDAPProviderToCommand(req *admin_pb.UpdateLDAPProviderRequest) comman
IDPOptions: idp_grpc.OptionsToCommand(req.ProviderOptions),
}
}
func addAppleProviderToCommand(req *admin_pb.AddAppleProviderRequest) command.AppleProvider {
return command.AppleProvider{
Name: req.Name,
ClientID: req.ClientId,
TeamID: req.TeamId,
KeyID: req.KeyId,
PrivateKey: req.PrivateKey,
Scopes: req.Scopes,
IDPOptions: idp_grpc.OptionsToCommand(req.ProviderOptions),
}
}
func updateAppleProviderToCommand(req *admin_pb.UpdateAppleProviderRequest) command.AppleProvider {
return command.AppleProvider{
Name: req.Name,
ClientID: req.ClientId,
TeamID: req.TeamId,
KeyID: req.KeyId,
PrivateKey: req.PrivateKey,
Scopes: req.Scopes,
IDPOptions: idp_grpc.OptionsToCommand(req.ProviderOptions),
}
}

View File

@ -414,6 +414,8 @@ func providerTypeToPb(idpType domain.IDPType) idp_pb.ProviderType {
return idp_pb.ProviderType_PROVIDER_TYPE_GITLAB_SELF_HOSTED
case domain.IDPTypeGoogle:
return idp_pb.ProviderType_PROVIDER_TYPE_GOOGLE
case domain.IDPTypeApple:
return idp_pb.ProviderType_PROVIDER_TYPE_APPLE
case domain.IDPTypeUnspecified:
return idp_pb.ProviderType_PROVIDER_TYPE_UNSPECIFIED
default:
@ -470,6 +472,10 @@ func configToPb(config *query.IDPTemplate) *idp_pb.ProviderConfig {
ldapConfigToPb(providerConfig, config.LDAPIDPTemplate)
return providerConfig
}
if config.AppleIDPTemplate != nil {
appleConfigToPb(providerConfig, config.AppleIDPTemplate)
return providerConfig
}
return providerConfig
}
@ -620,3 +626,14 @@ func ldapAttributesToPb(attributes idp.LDAPAttributes) *idp_pb.LDAPAttributes {
ProfileAttribute: attributes.ProfileAttribute,
}
}
func appleConfigToPb(providerConfig *idp_pb.ProviderConfig, template *query.AppleIDPTemplate) {
providerConfig.Config = &idp_pb.ProviderConfig_Apple{
Apple: &idp_pb.AppleConfig{
ClientId: template.ClientID,
TeamId: template.TeamID,
KeyId: template.KeyID,
Scopes: template.Scopes,
},
}
}

View File

@ -397,6 +397,27 @@ func (s *Server) UpdateLDAPProvider(ctx context.Context, req *mgmt_pb.UpdateLDAP
}, nil
}
func (s *Server) AddAppleProvider(ctx context.Context, req *mgmt_pb.AddAppleProviderRequest) (*mgmt_pb.AddAppleProviderResponse, error) {
id, details, err := s.command.AddOrgAppleProvider(ctx, authz.GetCtxData(ctx).OrgID, addAppleProviderToCommand(req))
if err != nil {
return nil, err
}
return &mgmt_pb.AddAppleProviderResponse{
Id: id,
Details: object_pb.DomainToAddDetailsPb(details),
}, nil
}
func (s *Server) UpdateAppleProvider(ctx context.Context, req *mgmt_pb.UpdateAppleProviderRequest) (*mgmt_pb.UpdateAppleProviderResponse, error) {
details, err := s.command.UpdateOrgAppleProvider(ctx, authz.GetCtxData(ctx).OrgID, req.Id, updateAppleProviderToCommand(req))
if err != nil {
return nil, err
}
return &mgmt_pb.UpdateAppleProviderResponse{
Details: object_pb.DomainToChangeDetailsPb(details),
}, nil
}
func (s *Server) DeleteProvider(ctx context.Context, req *mgmt_pb.DeleteProviderRequest) (*mgmt_pb.DeleteProviderResponse, error) {
details, err := s.command.DeleteOrgProvider(ctx, authz.GetCtxData(ctx).OrgID, req.Id)
if err != nil {

View File

@ -457,3 +457,27 @@ func updateLDAPProviderToCommand(req *mgmt_pb.UpdateLDAPProviderRequest) command
IDPOptions: idp_grpc.OptionsToCommand(req.ProviderOptions),
}
}
func addAppleProviderToCommand(req *mgmt_pb.AddAppleProviderRequest) command.AppleProvider {
return command.AppleProvider{
Name: req.Name,
ClientID: req.ClientId,
TeamID: req.TeamId,
KeyID: req.KeyId,
PrivateKey: req.PrivateKey,
Scopes: req.Scopes,
IDPOptions: idp_grpc.OptionsToCommand(req.ProviderOptions),
}
}
func updateAppleProviderToCommand(req *mgmt_pb.UpdateAppleProviderRequest) command.AppleProvider {
return command.AppleProvider{
Name: req.Name,
ClientID: req.ClientId,
TeamID: req.TeamId,
KeyID: req.KeyId,
PrivateKey: req.PrivateKey,
Scopes: req.Scopes,
IDPOptions: idp_grpc.OptionsToCommand(req.ProviderOptions),
}
}

View File

@ -45,7 +45,10 @@ func (s *Server) CreateSession(ctx context.Context, req *session.CreateSessionRe
if err != nil {
return nil, err
}
challengeResponse, cmds := s.challengesToCommand(req.GetChallenges(), checks)
challengeResponse, cmds, err := s.challengesToCommand(req.GetChallenges(), checks)
if err != nil {
return nil, err
}
set, err := s.command.CreateSession(ctx, cmds, metadata)
if err != nil {
@ -64,7 +67,10 @@ func (s *Server) SetSession(ctx context.Context, req *session.SetSessionRequest)
if err != nil {
return nil, err
}
challengeResponse, cmds := s.challengesToCommand(req.GetChallenges(), checks)
challengeResponse, cmds, err := s.challengesToCommand(req.GetChallenges(), checks)
if err != nil {
return nil, err
}
set, err := s.command.UpdateSession(ctx, req.GetSessionId(), req.GetSessionToken(), cmds, req.GetMetadata())
if err != nil {
@ -121,6 +127,8 @@ func factorsToPb(s *query.Session) *session.Factors {
WebAuthN: webAuthNFactorToPb(s.WebAuthNFactor),
Intent: intentFactorToPb(s.IntentFactor),
Totp: totpFactorToPb(s.TOTPFactor),
OtpSms: otpFactorToPb(s.OTPSMSFactor),
OtpEmail: otpFactorToPb(s.OTPEmailFactor),
}
}
@ -161,6 +169,15 @@ func totpFactorToPb(factor query.SessionTOTPFactor) *session.TOTPFactor {
}
}
func otpFactorToPb(factor query.SessionOTPFactor) *session.OTPFactor {
if factor.OTPCheckedAt.IsZero() {
return nil
}
return &session.OTPFactor{
VerifiedAt: timestamppb.New(factor.OTPCheckedAt),
}
}
func userFactorToPb(factor query.SessionUserFactor) *session.UserFactor {
if factor.UserID == "" || factor.UserCheckedAt.IsZero() {
return nil
@ -240,7 +257,7 @@ func (s *Server) checksToCommand(ctx context.Context, checks *session.Checks) ([
if err != nil {
return nil, err
}
sessionChecks := make([]command.SessionCommand, 0, 3)
sessionChecks := make([]command.SessionCommand, 0, 7)
if checkUser != nil {
user, err := checkUser.search(ctx, s.query)
if err != nil {
@ -258,14 +275,20 @@ func (s *Server) checksToCommand(ctx context.Context, checks *session.Checks) ([
sessionChecks = append(sessionChecks, s.command.CheckWebAuthN(passkey.GetCredentialAssertionData()))
}
if totp := checks.GetTotp(); totp != nil {
sessionChecks = append(sessionChecks, command.CheckTOTP(totp.GetTotp()))
sessionChecks = append(sessionChecks, command.CheckTOTP(totp.GetCode()))
}
if otp := checks.GetOtpSms(); otp != nil {
sessionChecks = append(sessionChecks, command.CheckOTPSMS(otp.GetCode()))
}
if otp := checks.GetOtpEmail(); otp != nil {
sessionChecks = append(sessionChecks, command.CheckOTPEmail(otp.GetCode()))
}
return sessionChecks, nil
}
func (s *Server) challengesToCommand(challenges *session.RequestChallenges, cmds []command.SessionCommand) (*session.Challenges, []command.SessionCommand) {
func (s *Server) challengesToCommand(challenges *session.RequestChallenges, cmds []command.SessionCommand) (*session.Challenges, []command.SessionCommand, error) {
if challenges == nil {
return nil, cmds
return nil, cmds, nil
}
resp := new(session.Challenges)
if req := challenges.GetWebAuthN(); req != nil {
@ -273,7 +296,20 @@ func (s *Server) challengesToCommand(challenges *session.RequestChallenges, cmds
resp.WebAuthN = challenge
cmds = append(cmds, cmd)
}
return resp, cmds
if req := challenges.GetOtpSms(); req != nil {
challenge, cmd := s.createOTPSMSChallengeCommand(req)
resp.OtpSms = challenge
cmds = append(cmds, cmd)
}
if req := challenges.GetOtpEmail(); req != nil {
challenge, cmd, err := s.createOTPEmailChallengeCommand(req)
if err != nil {
return nil, nil, err
}
resp.OtpEmail = challenge
cmds = append(cmds, cmd)
}
return resp, cmds, nil
}
func (s *Server) createWebAuthNChallengeCommand(req *session.RequestChallenges_WebAuthN) (*session.Challenges_WebAuthN, command.SessionCommand) {
@ -299,6 +335,34 @@ func userVerificationRequirementToDomain(req session.UserVerificationRequirement
}
}
func (s *Server) createOTPSMSChallengeCommand(req *session.RequestChallenges_OTPSMS) (*string, command.SessionCommand) {
if req.GetReturnCode() {
challenge := new(string)
return challenge, s.command.CreateOTPSMSChallengeReturnCode(challenge)
}
return nil, s.command.CreateOTPSMSChallenge()
}
func (s *Server) createOTPEmailChallengeCommand(req *session.RequestChallenges_OTPEmail) (*string, command.SessionCommand, error) {
switch t := req.GetDeliveryType().(type) {
case *session.RequestChallenges_OTPEmail_SendCode_:
cmd, err := s.command.CreateOTPEmailChallengeURLTemplate(t.SendCode.GetUrlTemplate())
if err != nil {
return nil, nil, err
}
return nil, cmd, nil
case *session.RequestChallenges_OTPEmail_ReturnCode_:
challenge := new(string)
return challenge, s.command.CreateOTPEmailChallengeReturnCode(challenge), nil
case nil:
return nil, s.command.CreateOTPEmailChallenge(), nil
default:
return nil, nil, caos_errs.ThrowUnimplementedf(nil, "SESSION-k3ng0", "delivery_type oneOf %T in OTPEmailChallenge not implemented", t)
}
}
func userCheck(user *session.CheckUser) (userSearch, error) {
if user == nil {
return nil, nil

View File

@ -39,6 +39,14 @@ func TestMain(m *testing.M) {
CTX, _ = Tester.WithAuthorization(ctx, integration.OrgOwner), errCtx
User = Tester.CreateHumanUser(CTX)
Tester.Client.UserV2.VerifyEmail(CTX, &user.VerifyEmailRequest{
UserId: User.GetUserId(),
VerificationCode: User.GetEmailCode(),
})
Tester.Client.UserV2.VerifyPhone(CTX, &user.VerifyPhoneRequest{
UserId: User.GetUserId(),
VerificationCode: User.GetPhoneCode(),
})
Tester.SetUserPassword(CTX, User.GetUserId(), integration.UserPassword)
Tester.RegisterUserPasskey(CTX, User.GetUserId())
return m.Run()
@ -75,6 +83,8 @@ const (
wantWebAuthNFactorUserVerified
wantTOTPFactor
wantIntentFactor
wantOTPSMSFactor
wantOTPEmailFactor
)
func verifyFactors(t testing.TB, factors *session.Factors, window time.Duration, want []wantFactor) {
@ -107,6 +117,14 @@ func verifyFactors(t testing.TB, factors *session.Factors, window time.Duration,
pf := factors.GetIntent()
assert.NotNil(t, pf)
assert.WithinRange(t, pf.GetVerifiedAt().AsTime(), time.Now().Add(-window), time.Now().Add(window))
case wantOTPSMSFactor:
pf := factors.GetOtpSms()
assert.NotNil(t, pf)
assert.WithinRange(t, pf.GetVerifiedAt().AsTime(), time.Now().Add(-window), time.Now().Add(window))
case wantOTPEmailFactor:
pf := factors.GetOtpEmail()
assert.NotNil(t, pf)
assert.WithinRange(t, pf.GetVerifiedAt().AsTime(), time.Now().Add(-window), time.Now().Add(window))
}
}
}
@ -362,6 +380,20 @@ func registerTOTP(ctx context.Context, t *testing.T, userID string) (secret stri
return secret
}
func registerOTPSMS(ctx context.Context, t *testing.T, userID string) {
_, err := Tester.Client.UserV2.AddOTPSMS(ctx, &user.AddOTPSMSRequest{
UserId: userID,
})
require.NoError(t, err)
}
func registerOTPEmail(ctx context.Context, t *testing.T, userID string) {
_, err := Tester.Client.UserV2.AddOTPEmail(ctx, &user.AddOTPEmailRequest{
UserId: userID,
})
require.NoError(t, err)
}
func TestServer_SetSession_flow(t *testing.T) {
// create new, empty session
createResp, err := Client.CreateSession(CTX, &session.CreateSessionRequest{})
@ -421,6 +453,8 @@ func TestServer_SetSession_flow(t *testing.T) {
userAuthCtx := Tester.WithAuthorizationToken(CTX, sessionToken)
Tester.RegisterUserU2F(userAuthCtx, User.GetUserId())
totpSecret := registerTOTP(userAuthCtx, t, User.GetUserId())
registerOTPSMS(userAuthCtx, t, User.GetUserId())
registerOTPEmail(userAuthCtx, t, User.GetUserId())
t.Run("check webauthn, user not verified (U2F)", func(t *testing.T) {
@ -470,7 +504,7 @@ func TestServer_SetSession_flow(t *testing.T) {
SessionToken: sessionToken,
Checks: &session.Checks{
Totp: &session.CheckTOTP{
Totp: code,
Code: code,
},
},
})
@ -478,6 +512,66 @@ func TestServer_SetSession_flow(t *testing.T) {
sessionToken = resp.GetSessionToken()
verifyCurrentSession(t, createResp.GetSessionId(), sessionToken, resp.GetDetails().GetSequence(), time.Minute, nil, wantUserFactor, wantWebAuthNFactor, wantTOTPFactor)
})
t.Run("check OTP SMS", func(t *testing.T) {
resp, err := Client.SetSession(CTX, &session.SetSessionRequest{
SessionId: createResp.GetSessionId(),
SessionToken: sessionToken,
Challenges: &session.RequestChallenges{
OtpSms: &session.RequestChallenges_OTPSMS{ReturnCode: true},
},
})
require.NoError(t, err)
verifyCurrentSession(t, createResp.GetSessionId(), resp.GetSessionToken(), resp.GetDetails().GetSequence(), time.Minute, nil)
sessionToken = resp.GetSessionToken()
otp := resp.GetChallenges().GetOtpSms()
require.NotEmpty(t, otp)
resp, err = Client.SetSession(CTX, &session.SetSessionRequest{
SessionId: createResp.GetSessionId(),
SessionToken: sessionToken,
Checks: &session.Checks{
OtpSms: &session.CheckOTP{
Code: otp,
},
},
})
require.NoError(t, err)
sessionToken = resp.GetSessionToken()
verifyCurrentSession(t, createResp.GetSessionId(), sessionToken, resp.GetDetails().GetSequence(), time.Minute, nil, wantUserFactor, wantWebAuthNFactor, wantOTPSMSFactor)
})
t.Run("check OTP Email", func(t *testing.T) {
resp, err := Client.SetSession(CTX, &session.SetSessionRequest{
SessionId: createResp.GetSessionId(),
SessionToken: sessionToken,
Challenges: &session.RequestChallenges{
OtpEmail: &session.RequestChallenges_OTPEmail{
DeliveryType: &session.RequestChallenges_OTPEmail_ReturnCode_{},
},
},
})
require.NoError(t, err)
verifyCurrentSession(t, createResp.GetSessionId(), resp.GetSessionToken(), resp.GetDetails().GetSequence(), time.Minute, nil)
sessionToken = resp.GetSessionToken()
otp := resp.GetChallenges().GetOtpEmail()
require.NotEmpty(t, otp)
resp, err = Client.SetSession(CTX, &session.SetSessionRequest{
SessionId: createResp.GetSessionId(),
SessionToken: sessionToken,
Checks: &session.Checks{
OtpEmail: &session.CheckOTP{
Code: otp,
},
},
})
require.NoError(t, err)
sessionToken = resp.GetSessionToken()
verifyCurrentSession(t, createResp.GetSessionId(), sessionToken, resp.GetDetails().GetSequence(), time.Minute, nil, wantUserFactor, wantWebAuthNFactor, wantOTPEmailFactor)
})
}
func Test_ZITADEL_API_missing_authentication(t *testing.T) {

View File

@ -0,0 +1,47 @@
package http
import (
"errors"
"net/http"
caos_errs "github.com/zitadel/zitadel/internal/errors"
)
func ZitadelErrorToHTTPStatusCode(err error) (statusCode int, ok bool) {
if err == nil {
return http.StatusOK, true
}
//nolint:errorlint
switch err.(type) {
case *caos_errs.AlreadyExistsError:
return http.StatusConflict, true
case *caos_errs.DeadlineExceededError:
return http.StatusGatewayTimeout, true
case *caos_errs.InternalError:
return http.StatusInternalServerError, true
case *caos_errs.InvalidArgumentError:
return http.StatusBadRequest, true
case *caos_errs.NotFoundError:
return http.StatusNotFound, true
case *caos_errs.PermissionDeniedError:
return http.StatusForbidden, true
case *caos_errs.PreconditionFailedError:
// use the same code as grpc-gateway:
// https://github.com/grpc-ecosystem/grpc-gateway/blob/9e33e38f15cb7d2f11096366e62ea391a3459ba9/runtime/errors.go#L59
return http.StatusBadRequest, true
case *caos_errs.UnauthenticatedError:
return http.StatusUnauthorized, true
case *caos_errs.UnavailableError:
return http.StatusServiceUnavailable, true
case *caos_errs.UnimplementedError:
return http.StatusNotImplemented, true
case *caos_errs.ResourceExhaustedError:
return http.StatusTooManyRequests, true
default:
c := new(caos_errs.CaosError)
if errors.As(err, &c) {
return ZitadelErrorToHTTPStatusCode(errors.Unwrap(err))
}
return http.StatusInternalServerError, false
}
}

View File

@ -0,0 +1,138 @@
package http
import (
"errors"
"fmt"
"net/http"
"testing"
caos_errors "github.com/zitadel/zitadel/internal/errors"
)
func TestZitadelErrorToHTTPStatusCode(t *testing.T) {
type args struct {
err error
}
tests := []struct {
name string
args args
wantStatusCode int
wantOk bool
}{
{
name: "no error",
args: args{
err: nil,
},
wantStatusCode: http.StatusOK,
wantOk: true,
},
{
name: "wrapped already exists",
args: args{
err: fmt.Errorf("wrapped %w", caos_errors.ThrowAlreadyExists(nil, "id", "message")),
},
wantStatusCode: http.StatusConflict,
wantOk: true,
},
{
name: "wrapped deadline exceeded",
args: args{
err: fmt.Errorf("wrapped %w", caos_errors.ThrowDeadlineExceeded(nil, "id", "message")),
},
wantStatusCode: http.StatusGatewayTimeout,
wantOk: true,
},
{
name: "wrapped internal",
args: args{
err: fmt.Errorf("wrapped %w", caos_errors.ThrowInternal(nil, "id", "message")),
},
wantStatusCode: http.StatusInternalServerError,
wantOk: true,
},
{
name: "wrapped invalid argument",
args: args{
err: fmt.Errorf("wrapped %w", caos_errors.ThrowInvalidArgument(nil, "id", "message")),
},
wantStatusCode: http.StatusBadRequest,
wantOk: true,
},
{
name: "wrapped not found",
args: args{
err: fmt.Errorf("wrapped %w", caos_errors.ThrowNotFound(nil, "id", "message")),
},
wantStatusCode: http.StatusNotFound,
wantOk: true,
},
{
name: "wrapped permission denied",
args: args{
err: fmt.Errorf("wrapped %w", caos_errors.ThrowPermissionDenied(nil, "id", "message")),
},
wantStatusCode: http.StatusForbidden,
wantOk: true,
},
{
name: "wrapped precondition failed",
args: args{
err: fmt.Errorf("wrapped %w", caos_errors.ThrowPreconditionFailed(nil, "id", "message")),
},
wantStatusCode: http.StatusBadRequest,
wantOk: true,
},
{
name: "wrapped unauthenticated",
args: args{
err: fmt.Errorf("wrapped %w", caos_errors.ThrowUnauthenticated(nil, "id", "message")),
},
wantStatusCode: http.StatusUnauthorized,
wantOk: true,
},
{
name: "wrapped unavailable",
args: args{
err: fmt.Errorf("wrapped %w", caos_errors.ThrowUnavailable(nil, "id", "message")),
},
wantStatusCode: http.StatusServiceUnavailable,
wantOk: true,
},
{
name: "wrapped unimplemented",
args: args{
err: fmt.Errorf("wrapped %w", caos_errors.ThrowUnimplemented(nil, "id", "message")),
},
wantStatusCode: http.StatusNotImplemented,
wantOk: true,
},
{
name: "wrapped resource exhausted",
args: args{
err: fmt.Errorf("wrapped %w", caos_errors.ThrowResourceExhausted(nil, "id", "message")),
},
wantStatusCode: http.StatusTooManyRequests,
wantOk: true,
},
{
name: "no caos/zitadel error",
args: args{
err: errors.New("error"),
},
wantStatusCode: http.StatusInternalServerError,
wantOk: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
gotStatusCode, gotOk := ZitadelErrorToHTTPStatusCode(tt.args.err)
if gotStatusCode != tt.wantStatusCode {
t.Errorf("ZitadelErrorToHTTPStatusCode() gotStatusCode = %v, want %v", gotStatusCode, tt.wantStatusCode)
}
if gotOk != tt.wantOk {
t.Errorf("ZitadelErrorToHTTPStatusCode() gotOk = %v, want %v", gotOk, tt.wantOk)
}
})
}
}

View File

@ -16,6 +16,7 @@ import (
z_errs "github.com/zitadel/zitadel/internal/errors"
"github.com/zitadel/zitadel/internal/form"
"github.com/zitadel/zitadel/internal/idp"
"github.com/zitadel/zitadel/internal/idp/providers/apple"
"github.com/zitadel/zitadel/internal/idp/providers/azuread"
"github.com/zitadel/zitadel/internal/idp/providers/github"
"github.com/zitadel/zitadel/internal/idp/providers/gitlab"
@ -52,6 +53,9 @@ type externalIDPCallbackData struct {
Code string `schema:"code"`
Error string `schema:"error"`
ErrorDescription string `schema:"error_description"`
// Apple returns a user on first registration
User string `schema:"user"`
}
// CallbackURL generates the instance specific URL to the IDP callback handler
@ -115,7 +119,7 @@ func (h *Handler) handleCallback(w http.ResponseWriter, r *http.Request) {
return
}
idpUser, idpSession, err := h.fetchIDPUser(ctx, provider, data.Code)
idpUser, idpSession, err := h.fetchIDPUser(ctx, provider, data.Code, data.User)
if err != nil {
cmdErr := h.commands.FailIDPIntent(ctx, intent, err.Error())
logging.WithFields("intent", intent.AggregateID).OnError(cmdErr).Error("failed to push failed event on idp intent")
@ -214,7 +218,7 @@ func redirectToFailureURL(w http.ResponseWriter, r *http.Request, i *command.IDP
http.Redirect(w, r, i.FailureURL.String(), http.StatusFound)
}
func (h *Handler) fetchIDPUser(ctx context.Context, identityProvider idp.Provider, code string) (user idp.User, idpTokens idp.Session, err error) {
func (h *Handler) fetchIDPUser(ctx context.Context, identityProvider idp.Provider, code string, appleUser string) (user idp.User, idpTokens idp.Session, err error) {
var session idp.Session
switch provider := identityProvider.(type) {
case *oauth.Provider:
@ -229,6 +233,8 @@ func (h *Handler) fetchIDPUser(ctx context.Context, identityProvider idp.Provide
session = &openid.Session{Provider: provider.Provider, Code: code}
case *google.Provider:
session = &openid.Session{Provider: provider.Provider, Code: code}
case *apple.Provider:
session = &apple.Session{Session: &openid.Session{Provider: provider.Provider, Code: code}, UserFormValue: appleUser}
case *jwt.Provider, *ldap.Provider:
return nil, nil, z_errs.ThrowInvalidArgument(nil, "IDP-52jmn", "Errors.ExternalIDP.IDPTypeNotImplemented")
default:

View File

@ -3,9 +3,8 @@ package login
import (
"net/http"
"github.com/zitadel/zitadel/internal/domain"
http_mw "github.com/zitadel/zitadel/internal/api/http/middleware"
"github.com/zitadel/zitadel/internal/domain"
)
const (

View File

@ -18,6 +18,7 @@ import (
"github.com/zitadel/zitadel/internal/errors"
"github.com/zitadel/zitadel/internal/eventstore/v1/models"
"github.com/zitadel/zitadel/internal/idp"
"github.com/zitadel/zitadel/internal/idp/providers/apple"
"github.com/zitadel/zitadel/internal/idp/providers/azuread"
"github.com/zitadel/zitadel/internal/idp/providers/github"
"github.com/zitadel/zitadel/internal/idp/providers/gitlab"
@ -41,6 +42,9 @@ type externalIDPData struct {
type externalIDPCallbackData struct {
State string `schema:"state"`
Code string `schema:"code"`
// Apple returns a user on first registration
User string `schema:"user"`
}
type externalNotFoundOptionFormData struct {
@ -159,6 +163,8 @@ func (l *Login) handleIDP(w http.ResponseWriter, r *http.Request, authReq *domai
provider, err = l.gitlabSelfHostedProvider(r.Context(), identityProvider)
case domain.IDPTypeGoogle:
provider, err = l.googleProvider(r.Context(), identityProvider)
case domain.IDPTypeApple:
provider, err = l.appleProvider(r.Context(), identityProvider)
case domain.IDPTypeLDAP:
provider, err = l.ldapProvider(r.Context(), identityProvider)
case domain.IDPTypeUnspecified:
@ -180,6 +186,18 @@ func (l *Login) handleIDP(w http.ResponseWriter, r *http.Request, authReq *domai
http.Redirect(w, r, session.GetAuthURL(), http.StatusFound)
}
// handleExternalLoginCallbackForm handles the callback from a IDP with form_post.
// It will redirect to the "normal" callback endpoint with the form data as query parameter.
// This way cookies will be handled correctly (same site = lax).
func (l *Login) handleExternalLoginCallbackForm(w http.ResponseWriter, r *http.Request) {
err := r.ParseForm()
if err != nil {
l.renderLogin(w, r, nil, err)
return
}
http.Redirect(w, r, HandlerPrefix+EndpointExternalLoginCallback+"?"+r.Form.Encode(), 302)
}
// handleExternalLoginCallback handles the callback from a IDP
// and tries to extract the user with the provided data
func (l *Login) handleExternalLoginCallback(w http.ResponseWriter, r *http.Request) {
@ -259,6 +277,13 @@ func (l *Login) handleExternalLoginCallback(w http.ResponseWriter, r *http.Reque
return
}
session = &openid.Session{Provider: provider.(*google.Provider).Provider, Code: data.Code}
case domain.IDPTypeApple:
provider, err = l.appleProvider(r.Context(), identityProvider)
if err != nil {
l.externalAuthFailed(w, r, authReq, nil, nil, err)
return
}
session = &apple.Session{Session: &openid.Session{Provider: provider.(*apple.Provider).Provider, Code: data.Code}, UserFormValue: data.User}
case domain.IDPTypeJWT,
domain.IDPTypeLDAP,
domain.IDPTypeUnspecified:
@ -936,6 +961,21 @@ func (l *Login) gitlabSelfHostedProvider(ctx context.Context, identityProvider *
)
}
func (l *Login) appleProvider(ctx context.Context, identityProvider *query.IDPTemplate) (*apple.Provider, error) {
privateKey, err := crypto.Decrypt(identityProvider.AppleIDPTemplate.PrivateKey, l.idpConfigAlg)
if err != nil {
return nil, err
}
return apple.New(
identityProvider.AppleIDPTemplate.ClientID,
identityProvider.AppleIDPTemplate.TeamID,
identityProvider.AppleIDPTemplate.KeyID,
l.baseURL(ctx)+EndpointExternalLoginCallbackFormPost,
privateKey,
identityProvider.AppleIDPTemplate.Scopes,
)
}
func (l *Login) appendUserGrants(ctx context.Context, userGrants []*domain.UserGrant, resourceOwner string) error {
if len(userGrants) == 0 {
return nil
@ -971,6 +1011,8 @@ func tokens(session idp.Session) *oidc.Tokens[*oidc.IDTokenClaims] {
return s.Tokens
case *azuread.Session:
return s.Tokens
case *apple.Session:
return s.Tokens
}
return nil
}

View File

@ -47,6 +47,9 @@ type Config struct {
CSRFCookieName string
Cache middleware.CacheConfig
AssetCache middleware.CacheConfig
// LoginV2
DefaultOTPEmailURLV2 string
}
const (
@ -117,6 +120,12 @@ func createCSRFInterceptor(cookieName string, csrfCookieKey []byte, externalSecu
handler.ServeHTTP(w, r)
return
}
// ignore form post callback
// it will redirect to the "normal" callback, where the cookie is set again
if r.URL.Path == EndpointExternalLoginCallbackFormPost && r.Method == http.MethodPost {
handler.ServeHTTP(w, r)
return
}
csrf.Protect(csrfCookieKey,
csrf.Secure(externalSecure),
csrf.CookieName(http_utils.SetCookiePrefix(cookieName, "", path, externalSecure)),

View File

@ -7,44 +7,45 @@ import (
)
const (
EndpointRoot = "/"
EndpointHealthz = "/healthz"
EndpointReadiness = "/ready"
EndpointLogin = "/login"
EndpointExternalLogin = "/login/externalidp"
EndpointExternalLoginCallback = "/login/externalidp/callback"
EndpointJWTAuthorize = "/login/jwt/authorize"
EndpointJWTCallback = "/login/jwt/callback"
EndpointLDAPLogin = "/login/ldap"
EndpointLDAPCallback = "/login/ldap/callback"
EndpointPasswordlessLogin = "/login/passwordless"
EndpointPasswordlessRegistration = "/login/passwordless/init"
EndpointPasswordlessPrompt = "/login/passwordless/prompt"
EndpointLoginName = "/loginname"
EndpointUserSelection = "/userselection"
EndpointChangeUsername = "/username/change"
EndpointPassword = "/password"
EndpointInitPassword = "/password/init"
EndpointChangePassword = "/password/change"
EndpointPasswordReset = "/password/reset"
EndpointInitUser = "/user/init"
EndpointMFAVerify = "/mfa/verify"
EndpointMFAPrompt = "/mfa/prompt"
EndpointMFAInitVerify = "/mfa/init/verify"
EndpointMFASMSInitVerify = "/mfa/init/sms/verify"
EndpointMFAOTPVerify = "/mfa/otp/verify"
EndpointMFAInitU2FVerify = "/mfa/init/u2f/verify"
EndpointU2FVerification = "/mfa/u2f/verify"
EndpointMailVerification = "/mail/verification"
EndpointMailVerified = "/mail/verified"
EndpointRegisterOption = "/register/option"
EndpointRegister = "/register"
EndpointExternalRegister = "/register/externalidp"
EndpointExternalRegisterCallback = "/register/externalidp/callback"
EndpointRegisterOrg = "/register/org"
EndpointLogoutDone = "/logout/done"
EndpointLoginSuccess = "/login/success"
EndpointExternalNotFoundOption = "/externaluser/option"
EndpointRoot = "/"
EndpointHealthz = "/healthz"
EndpointReadiness = "/ready"
EndpointLogin = "/login"
EndpointExternalLogin = "/login/externalidp"
EndpointExternalLoginCallback = "/login/externalidp/callback"
EndpointExternalLoginCallbackFormPost = "/login/externalidp/callback/form"
EndpointJWTAuthorize = "/login/jwt/authorize"
EndpointJWTCallback = "/login/jwt/callback"
EndpointLDAPLogin = "/login/ldap"
EndpointLDAPCallback = "/login/ldap/callback"
EndpointPasswordlessLogin = "/login/passwordless"
EndpointPasswordlessRegistration = "/login/passwordless/init"
EndpointPasswordlessPrompt = "/login/passwordless/prompt"
EndpointLoginName = "/loginname"
EndpointUserSelection = "/userselection"
EndpointChangeUsername = "/username/change"
EndpointPassword = "/password"
EndpointInitPassword = "/password/init"
EndpointChangePassword = "/password/change"
EndpointPasswordReset = "/password/reset"
EndpointInitUser = "/user/init"
EndpointMFAVerify = "/mfa/verify"
EndpointMFAPrompt = "/mfa/prompt"
EndpointMFAInitVerify = "/mfa/init/verify"
EndpointMFASMSInitVerify = "/mfa/init/sms/verify"
EndpointMFAOTPVerify = "/mfa/otp/verify"
EndpointMFAInitU2FVerify = "/mfa/init/u2f/verify"
EndpointU2FVerification = "/mfa/u2f/verify"
EndpointMailVerification = "/mail/verification"
EndpointMailVerified = "/mail/verified"
EndpointRegisterOption = "/register/option"
EndpointRegister = "/register"
EndpointExternalRegister = "/register/externalidp"
EndpointExternalRegisterCallback = "/register/externalidp/callback"
EndpointRegisterOrg = "/register/org"
EndpointLogoutDone = "/logout/done"
EndpointLoginSuccess = "/login/success"
EndpointExternalNotFoundOption = "/externaluser/option"
EndpointResources = "/resources"
EndpointDynamicResources = "/resources/dynamic"
@ -71,6 +72,7 @@ func CreateRouter(login *Login, staticDir http.FileSystem, interceptors ...mux.M
router.HandleFunc(EndpointLogin, login.handleLogin).Methods(http.MethodGet, http.MethodPost)
router.HandleFunc(EndpointExternalLogin, login.handleExternalLogin).Methods(http.MethodGet)
router.HandleFunc(EndpointExternalLoginCallback, login.handleExternalLoginCallback).Methods(http.MethodGet)
router.HandleFunc(EndpointExternalLoginCallbackFormPost, login.handleExternalLoginCallbackForm).Methods(http.MethodPost)
router.HandleFunc(EndpointJWTAuthorize, login.handleJWTRequest).Methods(http.MethodGet)
router.HandleFunc(EndpointJWTCallback, login.handleJWTCallback).Methods(http.MethodGet)
router.HandleFunc(EndpointPasswordlessLogin, login.handlePasswordlessVerification).Methods(http.MethodPost)

View File

@ -360,6 +360,7 @@ Footer:
PrivacyPolicy: Политика за поверителност
Help: Помогне
SupportEmail: Поддръжка на имейл
SignIn: Влезте с {{.Provider}}
Errors:
Internal: Възникна вътрешна грешка
AuthRequest:

View File

@ -372,6 +372,8 @@ Footer:
Help: Hilfe
SupportEmail: Support E-Mail
SignIn: Mit {{.Provider}} anmelden
Errors:
Internal: Es ist ein interner Fehler aufgetreten
AuthRequest:

View File

@ -372,6 +372,8 @@ Footer:
Help: Help
SupportEmail: Support E-mail
SignIn: Sign in with {{.Provider}}
Errors:
Internal: An internal error occurred
AuthRequest:

View File

@ -354,6 +354,8 @@ Footer:
Help: Ayuda
SupportEmail: Email de soporte
SignIn: Iniciar sesión con {{.Provider}}
Errors:
Internal: Se produjo un error interno
AuthRequest:

View File

@ -372,6 +372,8 @@ Footer:
Help: Aide
SupportEmail: E-mail d'assistance
SignIn: Connexion avec {{.Provider}}
Errors:
Internal: Une erreur interne s'est produite
AuthRequest:

View File

@ -372,6 +372,8 @@ Footer:
Help: Aiuto
SupportEmail: E-mail di supporto
SignIn: Accedi con {{.Provider}}
Errors:
Internal: Si è verificato un errore interno
AuthRequest:

View File

@ -363,6 +363,8 @@ Footer:
PrivacyPolicy: プライバシーポリシー
Help: ヘルプ
SignIn: '{{.Provider}} でサインイン'
Errors:
Internal: 内部でエラーが発生しました
AuthRequest:

View File

@ -372,6 +372,8 @@ Footer:
Help: Помош
SupportEmail: Е-пошта за поддршка
SignIn: Пријавете се со {{.Provider}}
Errors:
Internal: Се појави внатрешна грешка
AuthRequest:

View File

@ -372,6 +372,8 @@ Footer:
Help: Pomoc
SupportEmail: E-mail wsparcia
SignIn: Zaloguj się, używając konta {{.Provider}}
Errors:
Internal: Wewnętrzny błąd
AuthRequest:

View File

@ -366,6 +366,8 @@ Footer:
Help: Ajuda
SupportEmail: E-mail de suporte
SignIn: Iniciar sessão com a {{.Provider}}
Errors:
Internal: Ocorreu um erro interno
AuthRequest:

View File

@ -372,6 +372,8 @@ Footer:
Help: 帮助
SupportEmail: 支持邮箱
SignIn: 通过 {{.Provider}} 登录
Errors:
Internal: 发生了内部错误
AuthRequest:

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="56px" height="56px" viewBox="18 15.5 20 20" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 61 (89581) - https://sketch.com -->
<title>White Logo Square </title>
<desc>Created with Sketch.</desc>
<g id="White-Logo-Square-" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<rect id="Rectangle" fill="" x="6" y="6" width="44" height="44"></rect>
<path d="M28.2226562,20.3846154 C29.0546875,20.3846154 30.0976562,19.8048315 30.71875,19.0317864 C31.28125,18.3312142 31.6914062,17.352829 31.6914062,16.3744437 C31.6914062,16.2415766 31.6796875,16.1087095 31.65625,16 C30.7304687,16.0362365 29.6171875,16.640178 28.9492187,17.4494596 C28.421875,18.06548 27.9414062,19.0317864 27.9414062,20.0222505 C27.9414062,20.1671964 27.9648438,20.3121424 27.9765625,20.3604577 C28.0351562,20.3725366 28.1289062,20.3846154 28.2226562,20.3846154 Z M25.2929688,35 C26.4296875,35 26.9335938,34.214876 28.3515625,34.214876 C29.7929688,34.214876 30.109375,34.9758423 31.375,34.9758423 C32.6171875,34.9758423 33.4492188,33.792117 34.234375,32.6325493 C35.1132812,31.3038779 35.4765625,29.9993643 35.5,29.9389701 C35.4179688,29.9148125 33.0390625,28.9122695 33.0390625,26.0979021 C33.0390625,23.6579784 34.9140625,22.5588048 35.0195312,22.474253 C33.7773438,20.6382708 31.890625,20.5899555 31.375,20.5899555 C29.9804688,20.5899555 28.84375,21.4596313 28.1289062,21.4596313 C27.3554688,21.4596313 26.3359375,20.6382708 25.1289062,20.6382708 C22.8320312,20.6382708 20.5,22.5950413 20.5,26.2911634 C20.5,28.5861411 21.3671875,31.013986 22.4335938,32.5842339 C23.3476562,33.9129053 24.1445312,35 25.2929688,35 Z" id="" fill="#FFFFFF" fill-rule="nonzero"></path>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="56px" height="56px" viewBox="18 15.5 20 20" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 61 (89581) - https://sketch.com -->
<title>Black Logo Square</title>
<desc>Created with Sketch.</desc>
<g id="Black-Logo-Square" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<rect id="Rectangle" fill="" x="6" y="6" width="44" height="44"></rect>
<path d="M28.2226562,20.3846154 C29.0546875,20.3846154 30.0976562,19.8048315 30.71875,19.0317864 C31.28125,18.3312142 31.6914062,17.352829 31.6914062,16.3744437 C31.6914062,16.2415766 31.6796875,16.1087095 31.65625,16 C30.7304687,16.0362365 29.6171875,16.640178 28.9492187,17.4494596 C28.421875,18.06548 27.9414062,19.0317864 27.9414062,20.0222505 C27.9414062,20.1671964 27.9648438,20.3121424 27.9765625,20.3604577 C28.0351562,20.3725366 28.1289062,20.3846154 28.2226562,20.3846154 Z M25.2929688,35 C26.4296875,35 26.9335938,34.214876 28.3515625,34.214876 C29.7929688,34.214876 30.109375,34.9758423 31.375,34.9758423 C32.6171875,34.9758423 33.4492188,33.792117 34.234375,32.6325493 C35.1132812,31.3038779 35.4765625,29.9993643 35.5,29.9389701 C35.4179688,29.9148125 33.0390625,28.9122695 33.0390625,26.0979021 C33.0390625,23.6579784 34.9140625,22.5588048 35.0195312,22.474253 C33.7773438,20.6382708 31.890625,20.5899555 31.375,20.5899555 C29.9804688,20.5899555 28.84375,21.4596313 28.1289062,21.4596313 C27.3554688,21.4596313 26.3359375,20.6382708 25.1289062,20.6382708 C22.8320312,20.6382708 20.5,22.5950413 20.5,26.2911634 C20.5,28.5861411 21.3671875,31.013986 22.4335938,32.5842339 C23.3476562,33.9129053 24.1445312,35 25.2929688,35 Z" id="" fill="#000000" fill-rule="nonzero"></path>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@ -2,6 +2,7 @@ $lgn-idp-margin: 0.5rem 0;
$lgn-idp-padding: 0 1px;
$lgn-idp-provider-name-line-height: 36px;
$lgn-idp-border-radius: 0.5rem;
$lgn-idp-logo-size: 46px;
@mixin lgn-idp-base {
display: block;
@ -17,14 +18,14 @@ $lgn-idp-border-radius: 0.5rem;
transition: border-color 0.2s ease-in-out;
span.logo {
height: 46px;
width: 46px;
height: $lgn-idp-logo-size;
width: $lgn-idp-logo-size;
}
span.provider-name {
line-height: $lgn-idp-provider-name-line-height;
position: absolute;
left: 50%;
position: relative;
left: calc(50% - $lgn-idp-logo-size);
transform: translateX(-50%);
}
@ -75,4 +76,17 @@ $lgn-idp-border-radius: 0.5rem;
border-radius: 5px;
}
}
&.apple {
span.logo {
height: 46px;
width: 46px;
background-image: var(--apple-image-src);
background-size: 25px;
background-position: center;
background-repeat: no-repeat;
border-radius: 5px;
transform: translateY(-2px);
}
}
}

View File

@ -117,6 +117,8 @@
--zitadel-color-github-background: #ffffff;
--zitadel-color-gitlab-text: #8b8d8d;
--zitadel-color-gitlab-background: #ffffff;
--zitadel-color-apple-text: #8b8d8d;
--zitadel-color-apple-background: #ffffff;
--zitadel-color-qr: var(--zitadel-color-black);
--zitadel-color-qr-background: var(--zitadel-color-white);
@ -125,6 +127,7 @@
--github-image-src: url(../../../images/idp/github.png);
--gitlab-image-src: url(../../../images/idp/gitlab.png);
--azure-image-src: url(../../../images/idp/ms.svg);
--apple-image-src: url(../../../images/idp/apple.svg);
}
.lgn-dark-theme {
@ -227,9 +230,12 @@
--zitadel-color-github-background: #ffffff;
--zitadel-color-gitlab-text: #8b8d8d;
--zitadel-color-gitlab-background: #ffffff;
--zitadel-color-apple-text: #8b8d8d;
--zitadel-color-apple-background: #ffffff;
--google-image-src: url(../../../images/idp/google.png);
--github-image-src: url(../../../images/idp/github-white.png);
--gitlab-image-src: url(../../../images/idp/gitlab.png);
--azure-image-src: url(../../../images/idp/ms.svg);
--apple-image-src: url(../../../images/idp/apple-dark.svg);
}

View File

@ -52,7 +52,11 @@
<a href="{{ externalIDPAuthURL $reqid $provider.IDPConfigID}}"
class="lgn-idp {{idpProviderClass $provider.IDPType}}">
<span class="logo"></span>
{{if $provider.IDPType.IsSignInButton}}
<span class="provider-name">{{t "SignIn" "Provider" $provider.DisplayName}}</span>
{{else}}
<span class="provider-name">{{$provider.DisplayName}}</span>
{{end}}
</a>
{{end}}
</div>

View File

@ -29,7 +29,11 @@
<a href="{{ externalIDPRegisterURL $reqid $provider.IDPConfigID}}"
class="lgn-idp {{idpProviderClass $provider.IDPType}}">
<span class="logo"></span>
{{if $provider.IDPType.IsSignInButton}}
<span class="provider-name">{{t "SignIn" "Provider" $provider.DisplayName}}</span>
{{else}}
<span class="provider-name">{{$provider.DisplayName}}</span>
{{end}}
</a>
{{end}}
{{end}}

View File

@ -200,21 +200,15 @@ func authMethodsFromSession(session *query.Session) []domain.UserAuthMethodType
if !session.IntentFactor.IntentCheckedAt.IsZero() {
types = append(types, domain.UserAuthMethodTypeIDP)
}
// TODO: add checks with https://github.com/zitadel/zitadel/issues/5477
/*
if !session.TOTPFactor.TOTPCheckedAt.IsZero() {
types = append(types, domain.UserAuthMethodTypeTOTP)
}
*/
// TODO: add checks with https://github.com/zitadel/zitadel/issues/6224
/*
if !session.TOTPFactor.OTPSMSCheckedAt.IsZero() {
types = append(types, domain.UserAuthMethodTypeOTPSMS)
}
if !session.TOTPFactor.OTPEmailCheckedAt.IsZero() {
types = append(types, domain.UserAuthMethodTypeOTPEmail)
}
*/
if !session.TOTPFactor.TOTPCheckedAt.IsZero() {
types = append(types, domain.UserAuthMethodTypeTOTP)
}
if !session.OTPSMSFactor.OTPCheckedAt.IsZero() {
types = append(types, domain.UserAuthMethodTypeOTPSMS)
}
if !session.OTPEmailFactor.OTPCheckedAt.IsZero() {
types = append(types, domain.UserAuthMethodTypeOTPEmail)
}
return types
}

View File

@ -34,8 +34,9 @@ import (
type Commands struct {
httpClient *http.Client
checkPermission domain.PermissionCheck
newCode cryptoCodeFunc
checkPermission domain.PermissionCheck
newCode cryptoCodeFunc
newCodeWithDefault cryptoCodeWithDefaultFunc
eventstore *eventstore.Eventstore
static static.Storage
@ -122,6 +123,7 @@ func StartCommands(
httpClient: httpClient,
checkPermission: permissionCheck,
newCode: newCryptoCode,
newCodeWithDefault: newCryptoCodeWithDefaultConfig,
sessionTokenCreator: sessionTokenCreator(idGenerator, sessionAlg),
sessionTokenVerifier: sessionTokenVerifier,
defaultAccessTokenLifetime: defaultAccessTokenLifetime,

View File

@ -12,6 +12,10 @@ import (
type cryptoCodeFunc func(ctx context.Context, filter preparation.FilterToQueryReducer, typ domain.SecretGeneratorType, alg crypto.Crypto) (*CryptoCode, error)
type cryptoCodeWithDefaultFunc func(ctx context.Context, filter preparation.FilterToQueryReducer, typ domain.SecretGeneratorType, alg crypto.Crypto, defaultConfig *crypto.GeneratorConfig) (*CryptoCode, error)
var emptyConfig = &crypto.GeneratorConfig{}
type CryptoCode struct {
Crypted *crypto.CryptoValue
Plain string
@ -19,7 +23,11 @@ type CryptoCode struct {
}
func newCryptoCode(ctx context.Context, filter preparation.FilterToQueryReducer, typ domain.SecretGeneratorType, alg crypto.Crypto) (*CryptoCode, error) {
gen, config, err := secretGenerator(ctx, filter, typ, alg)
return newCryptoCodeWithDefaultConfig(ctx, filter, typ, alg, emptyConfig)
}
func newCryptoCodeWithDefaultConfig(ctx context.Context, filter preparation.FilterToQueryReducer, typ domain.SecretGeneratorType, alg crypto.Crypto, defaultConfig *crypto.GeneratorConfig) (*CryptoCode, error) {
gen, config, err := secretGenerator(ctx, filter, typ, alg, defaultConfig)
if err != nil {
return nil, err
}
@ -35,15 +43,15 @@ func newCryptoCode(ctx context.Context, filter preparation.FilterToQueryReducer,
}
func verifyCryptoCode(ctx context.Context, filter preparation.FilterToQueryReducer, typ domain.SecretGeneratorType, alg crypto.Crypto, creation time.Time, expiry time.Duration, crypted *crypto.CryptoValue, plain string) error {
gen, _, err := secretGenerator(ctx, filter, typ, alg)
gen, _, err := secretGenerator(ctx, filter, typ, alg, emptyConfig)
if err != nil {
return err
}
return crypto.VerifyCode(creation, expiry, crypted, plain, gen)
}
func secretGenerator(ctx context.Context, filter preparation.FilterToQueryReducer, typ domain.SecretGeneratorType, alg crypto.Crypto) (crypto.Generator, *crypto.GeneratorConfig, error) {
config, err := secretGeneratorConfig(ctx, filter, typ)
func secretGenerator(ctx context.Context, filter preparation.FilterToQueryReducer, typ domain.SecretGeneratorType, alg crypto.Crypto, defaultConfig *crypto.GeneratorConfig) (crypto.Generator, *crypto.GeneratorConfig, error) {
config, err := secretGeneratorConfigWithDefault(ctx, filter, typ, defaultConfig)
if err != nil {
return nil, nil, err
}
@ -58,6 +66,10 @@ func secretGenerator(ctx context.Context, filter preparation.FilterToQueryReduce
}
func secretGeneratorConfig(ctx context.Context, filter preparation.FilterToQueryReducer, typ domain.SecretGeneratorType) (*crypto.GeneratorConfig, error) {
return secretGeneratorConfigWithDefault(ctx, filter, typ, emptyConfig)
}
func secretGeneratorConfigWithDefault(ctx context.Context, filter preparation.FilterToQueryReducer, typ domain.SecretGeneratorType, defaultConfig *crypto.GeneratorConfig) (*crypto.GeneratorConfig, error) {
wm := NewInstanceSecretGeneratorConfigWriteModel(ctx, typ)
events, err := filter(ctx, wm.Query())
if err != nil {
@ -67,6 +79,9 @@ func secretGeneratorConfig(ctx context.Context, filter preparation.FilterToQuery
if err := wm.Reduce(); err != nil {
return nil, err
}
if wm.State != domain.SecretGeneratorStateActive {
return defaultConfig, nil
}
return &crypto.GeneratorConfig{
Length: wm.Length,
Expiry: wm.Expiry,

View File

@ -33,6 +33,21 @@ func mockCode(code string, exp time.Duration) cryptoCodeFunc {
}
}
func mockCodeWithDefault(code string, exp time.Duration) cryptoCodeWithDefaultFunc {
return func(ctx context.Context, filter preparation.FilterToQueryReducer, _ domain.SecretGeneratorType, alg crypto.Crypto, _ *crypto.GeneratorConfig) (*CryptoCode, error) {
return &CryptoCode{
Crypted: &crypto.CryptoValue{
CryptoType: crypto.TypeEncryption,
Algorithm: "enc",
KeyID: "id",
Crypted: []byte(code),
},
Plain: code,
Expiry: exp,
}, nil
}
}
var (
testGeneratorConfig = crypto.GeneratorConfig{
Length: 12,
@ -175,8 +190,9 @@ func Test_verifyCryptoCode(t *testing.T) {
func Test_secretGenerator(t *testing.T) {
type args struct {
typ domain.SecretGeneratorType
alg crypto.Crypto
typ domain.SecretGeneratorType
alg crypto.Crypto
defaultConfig *crypto.GeneratorConfig
}
tests := []struct {
name string
@ -190,8 +206,9 @@ func Test_secretGenerator(t *testing.T) {
name: "filter config error",
eventsore: eventstoreExpect(t, expectFilterError(io.ErrClosedPipe)),
args: args{
typ: domain.SecretGeneratorTypeVerifyEmailCode,
alg: crypto.CreateMockHashAlg(gomock.NewController(t)),
typ: domain.SecretGeneratorTypeVerifyEmailCode,
alg: crypto.CreateMockHashAlg(gomock.NewController(t)),
defaultConfig: emptyConfig,
},
wantErr: io.ErrClosedPipe,
},
@ -201,8 +218,9 @@ func Test_secretGenerator(t *testing.T) {
eventFromEventPusher(testSecretGeneratorAddedEvent(domain.SecretGeneratorTypeVerifyEmailCode)),
)),
args: args{
typ: domain.SecretGeneratorTypeVerifyEmailCode,
alg: crypto.CreateMockHashAlg(gomock.NewController(t)),
typ: domain.SecretGeneratorTypeVerifyEmailCode,
alg: crypto.CreateMockHashAlg(gomock.NewController(t)),
defaultConfig: emptyConfig,
},
want: crypto.NewHashGenerator(testGeneratorConfig, crypto.CreateMockHashAlg(gomock.NewController(t))),
wantConf: &testGeneratorConfig,
@ -213,8 +231,31 @@ func Test_secretGenerator(t *testing.T) {
eventFromEventPusher(testSecretGeneratorAddedEvent(domain.SecretGeneratorTypeVerifyEmailCode)),
)),
args: args{
typ: domain.SecretGeneratorTypeVerifyEmailCode,
alg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
typ: domain.SecretGeneratorTypeVerifyEmailCode,
alg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
defaultConfig: emptyConfig,
},
want: crypto.NewEncryptionGenerator(testGeneratorConfig, crypto.CreateMockEncryptionAlg(gomock.NewController(t))),
wantConf: &testGeneratorConfig,
},
{
name: "hash generator with default config",
eventsore: eventstoreExpect(t, expectFilter()),
args: args{
typ: domain.SecretGeneratorTypeVerifyEmailCode,
alg: crypto.CreateMockHashAlg(gomock.NewController(t)),
defaultConfig: &testGeneratorConfig,
},
want: crypto.NewHashGenerator(testGeneratorConfig, crypto.CreateMockHashAlg(gomock.NewController(t))),
wantConf: &testGeneratorConfig,
},
{
name: "encryption generator with default config",
eventsore: eventstoreExpect(t, expectFilter()),
args: args{
typ: domain.SecretGeneratorTypeVerifyEmailCode,
alg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
defaultConfig: &testGeneratorConfig,
},
want: crypto.NewEncryptionGenerator(testGeneratorConfig, crypto.CreateMockEncryptionAlg(gomock.NewController(t))),
wantConf: &testGeneratorConfig,
@ -225,15 +266,16 @@ func Test_secretGenerator(t *testing.T) {
eventFromEventPusher(testSecretGeneratorAddedEvent(domain.SecretGeneratorTypeVerifyEmailCode)),
)),
args: args{
typ: domain.SecretGeneratorTypeVerifyEmailCode,
alg: nil,
typ: domain.SecretGeneratorTypeVerifyEmailCode,
alg: nil,
defaultConfig: emptyConfig,
},
wantErr: errors.ThrowInternalf(nil, "COMMA-RreV6", "Errors.Internal unsupported crypto algorithm type %T", nil),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, gotConf, err := secretGenerator(context.Background(), tt.eventsore.Filter, tt.args.typ, tt.args.alg)
got, gotConf, err := secretGenerator(context.Background(), tt.eventsore.Filter, tt.args.typ, tt.args.alg, tt.args.defaultConfig)
require.ErrorIs(t, err, tt.wantErr)
assert.IsType(t, tt.want, got)
assert.Equal(t, tt.wantConf, gotConf)

View File

@ -110,6 +110,16 @@ type LDAPProvider struct {
IDPOptions idp.Options
}
type AppleProvider struct {
Name string
ClientID string
TeamID string
KeyID string
PrivateKey []byte
Scopes []string
IDPOptions idp.Options
}
func ExistsIDP(ctx context.Context, filter preparation.FilterToQueryReducer, id, orgID string) (exists bool, err error) {
writeModel := NewOrgIDPRemoveWriteModel(orgID, id)
events, err := filter(ctx, writeModel.Query())

View File

@ -14,6 +14,7 @@ import (
"github.com/zitadel/zitadel/internal/errors"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/idp"
"github.com/zitadel/zitadel/internal/idp/providers/apple"
"github.com/zitadel/zitadel/internal/idp/providers/azuread"
"github.com/zitadel/zitadel/internal/idp/providers/jwt"
"github.com/zitadel/zitadel/internal/idp/providers/oauth"
@ -215,6 +216,8 @@ func tokensForSucceededIDPIntent(session idp.Session, encryptionAlg crypto.Encry
tokens = s.Tokens
case *azuread.Session:
tokens = s.Tokens
case *apple.Session:
tokens = s.Tokens
default:
return nil, "", nil
}

View File

@ -3,6 +3,7 @@ package command
import (
"net/http"
"reflect"
"slices"
"time"
"github.com/zitadel/logging"
@ -14,6 +15,7 @@ import (
"github.com/zitadel/zitadel/internal/errors"
"github.com/zitadel/zitadel/internal/eventstore"
providers "github.com/zitadel/zitadel/internal/idp"
"github.com/zitadel/zitadel/internal/idp/providers/apple"
"github.com/zitadel/zitadel/internal/idp/providers/azuread"
"github.com/zitadel/zitadel/internal/idp/providers/github"
"github.com/zitadel/zitadel/internal/idp/providers/gitlab"
@ -1587,6 +1589,138 @@ func (wm *LDAPIDPWriteModel) GetProviderOptions() idp.Options {
return wm.Options
}
type AppleIDPWriteModel struct {
eventstore.WriteModel
ID string
Name string
ClientID string
TeamID string
KeyID string
PrivateKey *crypto.CryptoValue
Scopes []string
idp.Options
State domain.IDPState
}
func (wm *AppleIDPWriteModel) Reduce() error {
for _, event := range wm.Events {
switch e := event.(type) {
case *idp.AppleIDPAddedEvent:
wm.reduceAddedEvent(e)
case *idp.AppleIDPChangedEvent:
wm.reduceChangedEvent(e)
case *idp.RemovedEvent:
wm.State = domain.IDPStateRemoved
}
}
return wm.WriteModel.Reduce()
}
func (wm *AppleIDPWriteModel) reduceAddedEvent(e *idp.AppleIDPAddedEvent) {
wm.Name = e.Name
wm.ClientID = e.ClientID
wm.TeamID = e.TeamID
wm.KeyID = e.KeyID
wm.PrivateKey = e.PrivateKey
wm.Scopes = e.Scopes
wm.Options = e.Options
wm.State = domain.IDPStateActive
}
func (wm *AppleIDPWriteModel) reduceChangedEvent(e *idp.AppleIDPChangedEvent) {
if e.Name != nil {
wm.Name = *e.Name
}
if e.ClientID != nil {
wm.ClientID = *e.ClientID
}
if e.PrivateKey != nil {
wm.PrivateKey = e.PrivateKey
}
if e.Scopes != nil {
wm.Scopes = e.Scopes
}
wm.Options.ReduceChanges(e.OptionChanges)
}
func (wm *AppleIDPWriteModel) NewChanges(
name string,
clientID string,
teamID string,
keyID string,
privateKey []byte,
secretCrypto crypto.Crypto,
scopes []string,
options idp.Options,
) ([]idp.AppleIDPChanges, error) {
changes := make([]idp.AppleIDPChanges, 0)
var encryptedKey *crypto.CryptoValue
var err error
if len(privateKey) != 0 {
encryptedKey, err = crypto.Crypt(privateKey, secretCrypto)
if err != nil {
return nil, err
}
changes = append(changes, idp.ChangeApplePrivateKey(encryptedKey))
}
if wm.Name != name {
changes = append(changes, idp.ChangeAppleName(name))
}
if wm.ClientID != clientID {
changes = append(changes, idp.ChangeAppleClientID(clientID))
}
if wm.TeamID != teamID {
changes = append(changes, idp.ChangeAppleTeamID(teamID))
}
if wm.KeyID != keyID {
changes = append(changes, idp.ChangeAppleKeyID(keyID))
}
if slices.Compare(wm.Scopes, scopes) != 0 {
changes = append(changes, idp.ChangeAppleScopes(scopes))
}
opts := wm.Options.Changes(options)
if !opts.IsZero() {
changes = append(changes, idp.ChangeAppleOptions(opts))
}
return changes, nil
}
func (wm *AppleIDPWriteModel) ToProvider(callbackURL string, idpAlg crypto.EncryptionAlgorithm) (providers.Provider, error) {
privateKey, err := crypto.Decrypt(wm.PrivateKey, idpAlg)
if err != nil {
return nil, err
}
opts := make([]oidc.ProviderOpts, 0, 4)
if wm.IsCreationAllowed {
opts = append(opts, oidc.WithCreationAllowed())
}
if wm.IsLinkingAllowed {
opts = append(opts, oidc.WithLinkingAllowed())
}
if wm.IsAutoCreation {
opts = append(opts, oidc.WithAutoCreation())
}
if wm.IsAutoUpdate {
opts = append(opts, oidc.WithAutoUpdate())
}
return apple.New(
wm.ClientID,
wm.TeamID,
wm.KeyID,
callbackURL,
privateKey,
wm.Scopes,
opts...,
)
}
func (wm *AppleIDPWriteModel) GetProviderOptions() idp.Options {
return wm.Options
}
type IDPRemoveWriteModel struct {
eventstore.WriteModel
@ -1617,6 +1751,8 @@ func (wm *IDPRemoveWriteModel) Reduce() error {
wm.reduceAdded(e.ID)
case *idp.LDAPIDPAddedEvent:
wm.reduceAdded(e.ID)
case *idp.AppleIDPAddedEvent:
wm.reduceAdded(e.ID)
case *idp.RemovedEvent:
wm.reduceRemoved(e.ID)
case *idpconfig.IDPConfigAddedEvent:
@ -1699,6 +1835,10 @@ func (wm *IDPTypeWriteModel) Reduce() error {
wm.reduceAdded(e.ID, domain.IDPTypeLDAP, e.Aggregate())
case *org.LDAPIDPAddedEvent:
wm.reduceAdded(e.ID, domain.IDPTypeLDAP, e.Aggregate())
case *instance.AppleIDPAddedEvent:
wm.reduceAdded(e.ID, domain.IDPTypeApple, e.Aggregate())
case *org.AppleIDPAddedEvent:
wm.reduceAdded(e.ID, domain.IDPTypeApple, e.Aggregate())
case *instance.OIDCIDPMigratedAzureADEvent:
wm.reduceChanged(e.ID, domain.IDPTypeAzureAD)
case *org.OIDCIDPMigratedAzureADEvent:
@ -1774,6 +1914,7 @@ func (wm *IDPTypeWriteModel) Query() *eventstore.SearchQueryBuilder {
instance.GitLabSelfHostedIDPAddedEventType,
instance.GoogleIDPAddedEventType,
instance.LDAPIDPAddedEventType,
instance.AppleIDPAddedEventType,
instance.OIDCIDPMigratedAzureADEventType,
instance.OIDCIDPMigratedGoogleEventType,
instance.IDPRemovedEventType,
@ -1792,6 +1933,7 @@ func (wm *IDPTypeWriteModel) Query() *eventstore.SearchQueryBuilder {
org.GitLabSelfHostedIDPAddedEventType,
org.GoogleIDPAddedEventType,
org.LDAPIDPAddedEventType,
org.AppleIDPAddedEventType,
org.OIDCIDPMigratedAzureADEventType,
org.OIDCIDPMigratedGoogleEventType,
org.IDPRemovedEventType,
@ -1859,6 +2001,8 @@ func NewAllIDPWriteModel(resourceOwner string, instanceBool bool, id string, idp
writeModel.model = NewGitLabSelfHostedInstanceIDPWriteModel(resourceOwner, id)
case domain.IDPTypeGoogle:
writeModel.model = NewGoogleInstanceIDPWriteModel(resourceOwner, id)
case domain.IDPTypeApple:
writeModel.model = NewAppleInstanceIDPWriteModel(resourceOwner, id)
case domain.IDPTypeUnspecified:
fallthrough
default:
@ -1886,6 +2030,8 @@ func NewAllIDPWriteModel(resourceOwner string, instanceBool bool, id string, idp
writeModel.model = NewGitLabSelfHostedOrgIDPWriteModel(resourceOwner, id)
case domain.IDPTypeGoogle:
writeModel.model = NewGoogleOrgIDPWriteModel(resourceOwner, id)
case domain.IDPTypeApple:
writeModel.model = NewAppleOrgIDPWriteModel(resourceOwner, id)
case domain.IDPTypeUnspecified:
fallthrough
default:

View File

@ -414,6 +414,7 @@ func (c *Commands) SetUpInstance(ctx context.Context, setup *InstanceSetup) (str
instanceAgg,
setup.SMTPConfiguration.From,
setup.SMTPConfiguration.FromName,
setup.SMTPConfiguration.ReplyToAddress,
setup.SMTPConfiguration.SMTP.Host,
setup.SMTPConfiguration.SMTP.User,
[]byte(setup.SMTPConfiguration.SMTP.Password),

View File

@ -467,6 +467,48 @@ func (c *Commands) UpdateInstanceLDAPProvider(ctx context.Context, id string, pr
return pushedEventsToObjectDetails(pushedEvents), nil
}
func (c *Commands) AddInstanceAppleProvider(ctx context.Context, provider AppleProvider) (string, *domain.ObjectDetails, error) {
instanceID := authz.GetInstance(ctx).InstanceID()
instanceAgg := instance.NewAggregate(instanceID)
id, err := c.idGenerator.Next()
if err != nil {
return "", nil, err
}
writeModel := NewAppleInstanceIDPWriteModel(instanceID, id)
cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, c.prepareAddInstanceAppleProvider(instanceAgg, writeModel, provider))
if err != nil {
return "", nil, err
}
pushedEvents, err := c.eventstore.Push(ctx, cmds...)
if err != nil {
return "", nil, err
}
return id, pushedEventsToObjectDetails(pushedEvents), nil
}
func (c *Commands) UpdateInstanceAppleProvider(ctx context.Context, id string, provider AppleProvider) (*domain.ObjectDetails, error) {
instanceID := authz.GetInstance(ctx).InstanceID()
instanceAgg := instance.NewAggregate(instanceID)
writeModel := NewAppleInstanceIDPWriteModel(instanceID, id)
cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, c.prepareUpdateInstanceAppleProvider(instanceAgg, writeModel, provider))
if err != nil {
return nil, err
}
if len(cmds) == 0 {
// no change, so return directly
return &domain.ObjectDetails{
Sequence: writeModel.ProcessedSequence,
EventDate: writeModel.ChangeDate,
ResourceOwner: writeModel.ResourceOwner,
}, nil
}
pushedEvents, err := c.eventstore.Push(ctx, cmds...)
if err != nil {
return nil, err
}
return pushedEventsToObjectDetails(pushedEvents), nil
}
func (c *Commands) DeleteInstanceProvider(ctx context.Context, id string) (*domain.ObjectDetails, error) {
instanceAgg := instance.NewAggregate(authz.GetInstance(ctx).InstanceID())
cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, c.prepareDeleteInstanceProvider(instanceAgg, id))
@ -1518,6 +1560,98 @@ func (c *Commands) prepareUpdateInstanceLDAPProvider(a *instance.Aggregate, writ
}
}
func (c *Commands) prepareAddInstanceAppleProvider(a *instance.Aggregate, writeModel *InstanceAppleIDPWriteModel, provider AppleProvider) preparation.Validation {
return func() (preparation.CreateCommands, error) {
if provider.ClientID = strings.TrimSpace(provider.ClientID); provider.ClientID == "" {
return nil, caos_errs.ThrowInvalidArgument(nil, "INST-jkn3w", "Errors.IDP.ClientIDMissing")
}
if provider.TeamID = strings.TrimSpace(provider.TeamID); provider.TeamID == "" {
return nil, caos_errs.ThrowInvalidArgument(nil, "INST-Ffg32", "Errors.IDP.TeamIDMissing")
}
if provider.KeyID = strings.TrimSpace(provider.KeyID); provider.KeyID == "" {
return nil, caos_errs.ThrowInvalidArgument(nil, "INST-GDjm5", "Errors.IDP.KeyIDMissing")
}
if len(provider.PrivateKey) == 0 {
return nil, caos_errs.ThrowInvalidArgument(nil, "INST-GVD4n", "Errors.IDP.PrivateKeyMissing")
}
return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) {
events, err := filter(ctx, writeModel.Query())
if err != nil {
return nil, err
}
writeModel.AppendEvents(events...)
if err = writeModel.Reduce(); err != nil {
return nil, err
}
privateKey, err := crypto.Encrypt(provider.PrivateKey, c.idpConfigEncryption)
if err != nil {
return nil, err
}
return []eventstore.Command{
instance.NewAppleIDPAddedEvent(
ctx,
&a.Aggregate,
writeModel.ID,
provider.Name,
provider.ClientID,
provider.TeamID,
provider.KeyID,
privateKey,
provider.Scopes,
provider.IDPOptions,
),
}, nil
}, nil
}
}
func (c *Commands) prepareUpdateInstanceAppleProvider(a *instance.Aggregate, writeModel *InstanceAppleIDPWriteModel, provider AppleProvider) preparation.Validation {
return func() (preparation.CreateCommands, error) {
if writeModel.ID = strings.TrimSpace(writeModel.ID); writeModel.ID == "" {
return nil, caos_errs.ThrowInvalidArgument(nil, "INST-FRHBH", "Errors.IDMissing")
}
if provider.ClientID = strings.TrimSpace(provider.ClientID); provider.ClientID == "" {
return nil, caos_errs.ThrowInvalidArgument(nil, "INST-SFm4l", "Errors.IDP.ClientIDMissing")
}
if provider.TeamID = strings.TrimSpace(provider.TeamID); provider.TeamID == "" {
return nil, caos_errs.ThrowInvalidArgument(nil, "INST-SG34t", "Errors.IDP.TeamIDMissing")
}
if provider.KeyID = strings.TrimSpace(provider.KeyID); provider.KeyID == "" {
return nil, caos_errs.ThrowInvalidArgument(nil, "INST-Gh4z2", "Errors.IDP.KeyIDMissing")
}
return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) {
events, err := filter(ctx, writeModel.Query())
if err != nil {
return nil, err
}
writeModel.AppendEvents(events...)
if err = writeModel.Reduce(); err != nil {
return nil, err
}
if !writeModel.State.Exists() {
return nil, caos_errs.ThrowNotFound(nil, "INST-SG3bh", "Errors.IDPConfig.NotExisting")
}
event, err := writeModel.NewChangedEvent(
ctx,
&a.Aggregate,
writeModel.ID,
provider.Name,
provider.ClientID,
provider.TeamID,
provider.KeyID,
provider.PrivateKey,
c.idpConfigEncryption,
provider.Scopes,
provider.IDPOptions,
)
if err != nil || event == nil {
return nil, err
}
return []eventstore.Command{event}, nil
}, nil
}
}
func (c *Commands) prepareDeleteInstanceProvider(a *instance.Aggregate, id string) preparation.Validation {
return func() (preparation.CreateCommands, error) {
return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) {

View File

@ -793,6 +793,73 @@ func (wm *InstanceLDAPIDPWriteModel) NewChangedEvent(
return instance.NewLDAPIDPChangedEvent(ctx, aggregate, id, changes)
}
type InstanceAppleIDPWriteModel struct {
AppleIDPWriteModel
}
func NewAppleInstanceIDPWriteModel(instanceID, id string) *InstanceAppleIDPWriteModel {
return &InstanceAppleIDPWriteModel{
AppleIDPWriteModel{
WriteModel: eventstore.WriteModel{
AggregateID: instanceID,
ResourceOwner: instanceID,
},
ID: id,
},
}
}
func (wm *InstanceAppleIDPWriteModel) AppendEvents(events ...eventstore.Event) {
for _, event := range events {
switch e := event.(type) {
case *instance.AppleIDPAddedEvent:
wm.AppleIDPWriteModel.AppendEvents(&e.AppleIDPAddedEvent)
case *instance.AppleIDPChangedEvent:
wm.AppleIDPWriteModel.AppendEvents(&e.AppleIDPChangedEvent)
case *instance.IDPRemovedEvent:
wm.AppleIDPWriteModel.AppendEvents(&e.RemovedEvent)
default:
wm.AppleIDPWriteModel.AppendEvents(e)
}
}
}
func (wm *InstanceAppleIDPWriteModel) Query() *eventstore.SearchQueryBuilder {
return eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent).
ResourceOwner(wm.ResourceOwner).
AddQuery().
AggregateTypes(instance.AggregateType).
AggregateIDs(wm.AggregateID).
EventTypes(
instance.AppleIDPAddedEventType,
instance.AppleIDPChangedEventType,
instance.IDPRemovedEventType,
).
EventData(map[string]interface{}{"id": wm.ID}).
Builder()
}
func (wm *InstanceAppleIDPWriteModel) NewChangedEvent(
ctx context.Context,
aggregate *eventstore.Aggregate,
id,
name,
clientID,
teamID,
keyID string,
privateKey []byte,
secretCrypto crypto.Crypto,
scopes []string,
options idp.Options,
) (*instance.AppleIDPChangedEvent, error) {
changes, err := wm.AppleIDPWriteModel.NewChanges(name, clientID, teamID, keyID, privateKey, secretCrypto, scopes, options)
if err != nil || len(changes) == 0 {
return nil, err
}
return instance.NewAppleIDPChangedEvent(ctx, aggregate, id, changes)
}
type InstanceIDPRemoveWriteModel struct {
IDPRemoveWriteModel
}
@ -832,6 +899,8 @@ func (wm *InstanceIDPRemoveWriteModel) AppendEvents(events ...eventstore.Event)
wm.IDPRemoveWriteModel.AppendEvents(&e.GoogleIDPAddedEvent)
case *instance.LDAPIDPAddedEvent:
wm.IDPRemoveWriteModel.AppendEvents(&e.LDAPIDPAddedEvent)
case *instance.AppleIDPAddedEvent:
wm.IDPRemoveWriteModel.AppendEvents(&e.AppleIDPAddedEvent)
case *instance.IDPRemovedEvent:
wm.IDPRemoveWriteModel.AppendEvents(&e.RemovedEvent)
case *instance.IDPConfigAddedEvent:
@ -861,6 +930,7 @@ func (wm *InstanceIDPRemoveWriteModel) Query() *eventstore.SearchQueryBuilder {
instance.GitLabSelfHostedIDPAddedEventType,
instance.GoogleIDPAddedEventType,
instance.LDAPIDPAddedEventType,
instance.AppleIDPAddedEventType,
instance.IDPRemovedEventType,
).
EventData(map[string]interface{}{"id": wm.ID}).

View File

@ -4857,3 +4857,464 @@ func TestCommandSide_UpdateInstanceLDAPIDP(t *testing.T) {
})
}
}
func TestCommandSide_AddInstanceAppleIDP(t *testing.T) {
type fields struct {
eventstore *eventstore.Eventstore
idGenerator id.Generator
secretCrypto crypto.EncryptionAlgorithm
}
type args struct {
ctx context.Context
provider AppleProvider
}
type res struct {
id string
want *domain.ObjectDetails
err func(error) bool
}
tests := []struct {
name string
fields fields
args args
res res
}{
{
"invalid clientID",
fields{
eventstore: eventstoreExpect(t),
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "id1"),
},
args{
ctx: authz.WithInstanceID(context.Background(), "instance1"),
provider: AppleProvider{},
},
res{
err: func(err error) bool {
return errors.Is(err, caos_errors.ThrowInvalidArgument(nil, "INST-jkn3w", "Errors.IDP.ClientIDMissing"))
},
},
},
{
"invalid teamID",
fields{
eventstore: eventstoreExpect(t),
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "id1"),
},
args{
ctx: authz.WithInstanceID(context.Background(), "instance1"),
provider: AppleProvider{
ClientID: "clientID",
},
},
res{
err: func(err error) bool {
return errors.Is(err, caos_errors.ThrowInvalidArgument(nil, "INST-Ffg32", "Errors.IDP.TeamIDMissing"))
},
},
},
{
"invalid keyID",
fields{
eventstore: eventstoreExpect(t),
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "id1"),
},
args{
ctx: authz.WithInstanceID(context.Background(), "instance1"),
provider: AppleProvider{
ClientID: "clientID",
TeamID: "teamID",
},
},
res{
err: func(err error) bool {
return errors.Is(err, caos_errors.ThrowInvalidArgument(nil, "INST-GDjm5", "Errors.IDP.KeyIDMissing"))
},
},
},
{
"invalid privateKey",
fields{
eventstore: eventstoreExpect(t),
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "id1"),
},
args{
ctx: authz.WithInstanceID(context.Background(), "instance1"),
provider: AppleProvider{
ClientID: "clientID",
TeamID: "teamID",
KeyID: "keyID",
},
},
res{
err: func(err error) bool {
return errors.Is(err, caos_errors.ThrowInvalidArgument(nil, "INST-GVD4n", "Errors.IDP.PrivateKeyMissing"))
},
},
},
{
name: "ok",
fields: fields{
eventstore: eventstoreExpect(t,
expectFilter(),
expectPush(
[]*repository.Event{
eventFromEventPusherWithInstanceID(
"instance1",
instance.NewAppleIDPAddedEvent(context.Background(), &instance.NewAggregate("instance1").Aggregate,
"id1",
"",
"clientID",
"teamID",
"keyID",
&crypto.CryptoValue{
CryptoType: crypto.TypeEncryption,
Algorithm: "enc",
KeyID: "id",
Crypted: []byte("privateKey"),
},
nil,
idp.Options{},
)),
},
),
),
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "id1"),
secretCrypto: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
},
args: args{
ctx: authz.WithInstanceID(context.Background(), "instance1"),
provider: AppleProvider{
ClientID: "clientID",
TeamID: "teamID",
KeyID: "keyID",
PrivateKey: []byte("privateKey"),
},
},
res: res{
id: "id1",
want: &domain.ObjectDetails{ResourceOwner: "instance1"},
},
},
{
name: "ok all set",
fields: fields{
eventstore: eventstoreExpect(t,
expectFilter(),
expectPush(
[]*repository.Event{
eventFromEventPusherWithInstanceID(
"instance1",
instance.NewAppleIDPAddedEvent(context.Background(), &instance.NewAggregate("instance1").Aggregate,
"id1",
"",
"clientID",
"teamID",
"keyID",
&crypto.CryptoValue{
CryptoType: crypto.TypeEncryption,
Algorithm: "enc",
KeyID: "id",
Crypted: []byte("privateKey"),
},
[]string{"name", "email"},
idp.Options{
IsCreationAllowed: true,
IsLinkingAllowed: true,
IsAutoCreation: true,
IsAutoUpdate: true,
},
)),
},
),
),
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "id1"),
secretCrypto: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
},
args: args{
ctx: authz.WithInstanceID(context.Background(), "instance1"),
provider: AppleProvider{
ClientID: "clientID",
TeamID: "teamID",
KeyID: "keyID",
PrivateKey: []byte("privateKey"),
Scopes: []string{"name", "email"},
IDPOptions: idp.Options{
IsCreationAllowed: true,
IsLinkingAllowed: true,
IsAutoCreation: true,
IsAutoUpdate: true,
},
},
},
res: res{
id: "id1",
want: &domain.ObjectDetails{ResourceOwner: "instance1"},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := &Commands{
eventstore: tt.fields.eventstore,
idGenerator: tt.fields.idGenerator,
idpConfigEncryption: tt.fields.secretCrypto,
}
id, got, err := c.AddInstanceAppleProvider(tt.args.ctx, tt.args.provider)
if tt.res.err == nil {
assert.NoError(t, err)
}
if tt.res.err != nil && !tt.res.err(err) {
t.Errorf("got wrong err: %v ", err)
}
if tt.res.err == nil {
assert.Equal(t, tt.res.id, id)
assert.Equal(t, tt.res.want, got)
}
})
}
}
func TestCommandSide_UpdateInstanceAppleIDP(t *testing.T) {
type fields struct {
eventstore *eventstore.Eventstore
secretCrypto crypto.EncryptionAlgorithm
}
type args struct {
ctx context.Context
id string
provider AppleProvider
}
type res struct {
want *domain.ObjectDetails
err func(error) bool
}
tests := []struct {
name string
fields fields
args args
res res
}{
{
"invalid id",
fields{
eventstore: eventstoreExpect(t),
},
args{
ctx: authz.WithInstanceID(context.Background(), "instance1"),
provider: AppleProvider{},
},
res{
err: func(err error) bool {
return errors.Is(err, caos_errors.ThrowInvalidArgument(nil, "INST-FRHBH", "Errors.IDMissing"))
},
},
},
{
"invalid clientID",
fields{
eventstore: eventstoreExpect(t),
},
args{
ctx: authz.WithInstanceID(context.Background(), "instance1"),
id: "id1",
provider: AppleProvider{},
},
res{
err: func(err error) bool {
return errors.Is(err, caos_errors.ThrowInvalidArgument(nil, "INST-SFm4l", "Errors.IDP.ClientIDMissing"))
},
},
},
{
"invalid teamID",
fields{
eventstore: eventstoreExpect(t),
},
args{
ctx: authz.WithInstanceID(context.Background(), "instance1"),
id: "id1",
provider: AppleProvider{
ClientID: "clientID",
},
},
res{
err: func(err error) bool {
return errors.Is(err, caos_errors.ThrowInvalidArgument(nil, "INST-SG34t", "Errors.IDP.TeamIDMissing"))
},
},
},
{
"invalid keyID",
fields{
eventstore: eventstoreExpect(t),
},
args{
ctx: authz.WithInstanceID(context.Background(), "instance1"),
id: "id1",
provider: AppleProvider{
ClientID: "clientID",
TeamID: "teamID",
},
},
res{
err: func(err error) bool {
return errors.Is(err, caos_errors.ThrowInvalidArgument(nil, "INST-Gh4z2", "Errors.IDP.KeyIDMissing"))
},
},
},
{
name: "not found",
fields: fields{
eventstore: eventstoreExpect(t,
expectFilter(),
),
},
args: args{
ctx: authz.WithInstanceID(context.Background(), "instance1"),
id: "id1",
provider: AppleProvider{
ClientID: "clientID",
TeamID: "teamID",
KeyID: "keyID",
},
},
res: res{
err: caos_errors.IsNotFound,
},
},
{
name: "no changes",
fields: fields{
eventstore: eventstoreExpect(t,
expectFilter(
eventFromEventPusher(
instance.NewAppleIDPAddedEvent(context.Background(), &instance.NewAggregate("instance1").Aggregate,
"id1",
"",
"clientID",
"teamID",
"keyID",
&crypto.CryptoValue{
CryptoType: crypto.TypeEncryption,
Algorithm: "enc",
KeyID: "id",
Crypted: []byte("privateKey"),
},
nil,
idp.Options{},
)),
),
),
},
args: args{
ctx: authz.WithInstanceID(context.Background(), "instance1"),
id: "id1",
provider: AppleProvider{
ClientID: "clientID",
TeamID: "teamID",
KeyID: "keyID",
},
},
res: res{
want: &domain.ObjectDetails{ResourceOwner: "instance1"},
},
},
{
name: "change ok",
fields: fields{
eventstore: eventstoreExpect(t,
expectFilter(
eventFromEventPusher(
instance.NewAppleIDPAddedEvent(context.Background(), &instance.NewAggregate("instance1").Aggregate,
"id1",
"",
"clientID",
"teamID",
"keyID",
&crypto.CryptoValue{
CryptoType: crypto.TypeEncryption,
Algorithm: "enc",
KeyID: "id",
Crypted: []byte("privateKey"),
},
nil,
idp.Options{},
)),
),
expectPush(
[]*repository.Event{
eventFromEventPusherWithInstanceID(
"instance1",
func() eventstore.Command {
t := true
event, _ := instance.NewAppleIDPChangedEvent(context.Background(), &instance.NewAggregate("instance1").Aggregate,
"id1",
[]idp.AppleIDPChanges{
idp.ChangeAppleClientID("clientID2"),
idp.ChangeAppleTeamID("teamID2"),
idp.ChangeAppleKeyID("keyID2"),
idp.ChangeApplePrivateKey(&crypto.CryptoValue{
CryptoType: crypto.TypeEncryption,
Algorithm: "enc",
KeyID: "id",
Crypted: []byte("newPrivateKey"),
}),
idp.ChangeAppleScopes([]string{"name", "email"}),
idp.ChangeAppleOptions(idp.OptionChanges{
IsCreationAllowed: &t,
IsLinkingAllowed: &t,
IsAutoCreation: &t,
IsAutoUpdate: &t,
}),
},
)
return event
}(),
),
},
),
),
secretCrypto: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
},
args: args{
ctx: authz.WithInstanceID(context.Background(), "instance1"),
id: "id1",
provider: AppleProvider{
ClientID: "clientID2",
TeamID: "teamID2",
KeyID: "keyID2",
PrivateKey: []byte("newPrivateKey"),
Scopes: []string{"name", "email"},
IDPOptions: idp.Options{
IsCreationAllowed: true,
IsLinkingAllowed: true,
IsAutoCreation: true,
IsAutoUpdate: true,
},
},
},
res: res{
want: &domain.ObjectDetails{ResourceOwner: "instance1"},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := &Commands{
eventstore: tt.fields.eventstore,
idpConfigEncryption: tt.fields.secretCrypto,
}
got, err := c.UpdateInstanceAppleProvider(tt.args.ctx, tt.args.id, tt.args.provider)
if tt.res.err == nil {
assert.NoError(t, err)
}
if tt.res.err != nil && !tt.res.err(err) {
t.Errorf("got wrong err: %v ", err)
}
if tt.res.err == nil {
assert.Equal(t, tt.res.want, got)
}
})
}
}

View File

@ -12,13 +12,14 @@ import (
type InstanceSMTPConfigWriteModel struct {
eventstore.WriteModel
SenderAddress string
SenderName string
TLS bool
Host string
User string
Password *crypto.CryptoValue
State domain.SMTPConfigState
SenderAddress string
SenderName string
ReplyToAddress string
TLS bool
Host string
User string
Password *crypto.CryptoValue
State domain.SMTPConfigState
domain string
domainState domain.InstanceDomainState
@ -62,6 +63,7 @@ func (wm *InstanceSMTPConfigWriteModel) Reduce() error {
wm.TLS = e.TLS
wm.SenderAddress = e.SenderAddress
wm.SenderName = e.SenderName
wm.ReplyToAddress = e.ReplyToAddress
wm.Host = e.Host
wm.User = e.User
wm.Password = e.Password
@ -76,6 +78,9 @@ func (wm *InstanceSMTPConfigWriteModel) Reduce() error {
if e.FromName != nil {
wm.SenderName = *e.FromName
}
if e.ReplyToAddress != nil {
wm.ReplyToAddress = *e.ReplyToAddress
}
if e.Host != nil {
wm.Host = *e.Host
}
@ -87,6 +92,7 @@ func (wm *InstanceSMTPConfigWriteModel) Reduce() error {
wm.TLS = false
wm.SenderName = ""
wm.SenderAddress = ""
wm.ReplyToAddress = ""
wm.Host = ""
wm.User = ""
wm.Password = nil
@ -122,7 +128,7 @@ func (wm *InstanceSMTPConfigWriteModel) Query() *eventstore.SearchQueryBuilder {
Builder()
}
func (wm *InstanceSMTPConfigWriteModel) NewChangedEvent(ctx context.Context, aggregate *eventstore.Aggregate, tls bool, fromAddress, fromName, smtpHost, smtpUser string) (*instance.SMTPConfigChangedEvent, bool, error) {
func (wm *InstanceSMTPConfigWriteModel) NewChangedEvent(ctx context.Context, aggregate *eventstore.Aggregate, tls bool, fromAddress, fromName, replyToAddress, smtpHost, smtpUser string) (*instance.SMTPConfigChangedEvent, bool, error) {
changes := make([]instance.SMTPConfigChanges, 0)
var err error
@ -135,6 +141,9 @@ func (wm *InstanceSMTPConfigWriteModel) NewChangedEvent(ctx context.Context, agg
if wm.SenderName != fromName {
changes = append(changes, instance.ChangeSMTPConfigFromName(fromName))
}
if wm.ReplyToAddress != replyToAddress {
changes = append(changes, instance.ChangeSMTPConfigReplyToAddress(replyToAddress))
}
if wm.Host != smtpHost {
changes = append(changes, instance.ChangeSMTPConfigSMTPHost(smtpHost))
}

Some files were not shown because too many files have changed in this diff Show More