feat: add apple as idp (#6442)

* feat: manage apple idp

* handle apple idp callback

* add tests for provider

* basic console implementation

* implement flow for login UI and add logos / styling

* tests

* cleanup

* add upload button

* begin i18n

* apple logo positioning, file upload component

* fix add apple instance idp

* add missing apple logos for login

* update to go 1.21

* fix slice compare

* revert permission changes

* concrete error messages

* translate login apple logo -y-2px

* change form parsing

* sign in button

* fix tests

* lint console

---------

Co-authored-by: peintnermax <max@caos.ch>
This commit is contained in:
Livio Spring 2023-08-31 08:39:16 +02:00 committed by GitHub
parent 0d94947d3c
commit e17b49e4ca
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
89 changed files with 4384 additions and 64 deletions

View File

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

View File

@ -8,7 +8,7 @@ issues:
run: run:
concurrency: 4 concurrency: 4
timeout: 10m timeout: 10m
go: '1.19' go: '1.21'
skip-dirs: skip-dirs:
- .artifacts - .artifacts
- .backups - .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: The commands in this section are tested against the following software versions:
- [Docker version 20.10.17](https://docs.docker.com/engine/install/) - [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) - [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. Make some changes to the source code, then run the database locally.

View File

@ -380,7 +380,7 @@ func startAPIs(
apis.RegisterHandlerOnPrefix(idp.HandlerPrefix, idp.NewHandler(commands, queries, keys.IDPConfig, config.ExternalSecure, instanceInterceptor.Handler)) 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 { if err != nil {
return err return err
} }

View File

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

View File

@ -80,6 +80,11 @@
<i class="idp-icon las la-building"></i> <i class="idp-icon las la-building"></i>
Active Directory / LDAP Active Directory / LDAP
</div> </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 class="idp-table-provider-type" *ngSwitchDefault>coming soon</div>
</div> </div>
</td> </td>

View File

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

View File

@ -261,6 +261,8 @@ export class IdpTableComponent implements OnInit, OnDestroy {
]; ];
case ProviderType.PROVIDER_TYPE_GITHUB: case ProviderType.PROVIDER_TYPE_GITHUB:
return [row.owner === IDPOwnerType.IDP_OWNER_TYPE_SYSTEM ? '/instance' : '/org', 'provider', 'github', row.id]; 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> </div>
</a> </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 <a
class="item card" class="item card"
[routerLink]=" [routerLink]="

View File

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

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 { RouterModule, Routes } from '@angular/router';
import { ProviderType } from 'src/app/proto/generated/zitadel/idp_pb'; 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 { ProviderAzureADComponent } from './provider-azure-ad/provider-azure-ad.component';
import { ProviderGithubESComponent } from './provider-github-es/provider-github-es.component'; import { ProviderGithubESComponent } from './provider-github-es/provider-github-es.component';
import { ProviderGithubComponent } from './provider-github/provider-github.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_OAUTH]: { path: 'oauth', component: ProviderOAuthComponent },
[ProviderType.PROVIDER_TYPE_OIDC]: { path: 'oidc', component: ProviderOIDCComponent }, [ProviderType.PROVIDER_TYPE_OIDC]: { path: 'oidc', component: ProviderOIDCComponent },
[ProviderType.PROVIDER_TYPE_LDAP]: { path: 'ldap', component: ProviderLDAPComponent }, [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]) => { 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 { ProviderOptionsModule } from '../provider-options/provider-options.module';
import { StringListModule } from '../string-list/string-list.module'; import { StringListModule } from '../string-list/string-list.module';
import { LDAPAttributesComponent } from './ldap-attributes/ldap-attributes.component'; 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 { ProviderAzureADComponent } from './provider-azure-ad/provider-azure-ad.component';
import { ProviderGithubESComponent } from './provider-github-es/provider-github-es.component'; import { ProviderGithubESComponent } from './provider-github-es/provider-github-es.component';
import { ProviderGithubComponent } from './provider-github/provider-github.component'; import { ProviderGithubComponent } from './provider-github/provider-github.component';
@ -43,6 +44,7 @@ import { ProvidersRoutingModule } from './providers-routing.module';
ProviderOIDCComponent, ProviderOIDCComponent,
ProviderOAuthComponent, ProviderOAuthComponent,
ProviderLDAPComponent, ProviderLDAPComponent,
ProviderAppleComponent,
], ],
imports: [ imports: [
ProvidersRoutingModule, ProvidersRoutingModule,

View File

@ -3,6 +3,7 @@
@mixin identity-provider-theme($theme) { @mixin identity-provider-theme($theme) {
$is-dark-theme: map-get($theme, is-dark); $is-dark-theme: map-get($theme, is-dark);
$background: map-get($theme, background); $background: map-get($theme, background);
$foreground: map-get($theme, foreground);
.identity-provider-desc { .identity-provider-desc {
font-size: 14px; font-size: 14px;
@ -19,6 +20,10 @@
margin-right: 1rem; margin-right: 1rem;
flex-shrink: 0; flex-shrink: 0;
&.apple {
margin-bottom: 4px;
}
&.dark { &.dark {
display: if($is-dark-theme, block, none); 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 { .string-list-component-wrapper {
max-width: 400px; max-width: 400px;
} }

View File

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

View File

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

View File

@ -1706,6 +1706,10 @@
"LDAP": { "LDAP": {
"TITLE": "Active Directory / LDAP", "TITLE": "Active Directory / LDAP",
"DESCRIPTION": "Enter the credentials for your LDAP Provider" "DESCRIPTION": "Enter the credentials for your LDAP Provider"
},
"APPLE": {
"TITLE": "Sign in with Apple",
"DESCRIPTION": "Enter the credentials for your Apple Provider"
} }
}, },
"DETAIL": { "DETAIL": {
@ -1819,6 +1823,14 @@
"JWTENDPOINT": "JWT Endpoint", "JWTENDPOINT": "JWT Endpoint",
"JWTKEYSENDPOINT": "JWT Keys 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": { "TOAST": {
"SAVED": "Successfully saved.", "SAVED": "Successfully saved.",
"REACTIVATED": "Idp reactivated.", "REACTIVATED": "Idp reactivated.",

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

2
go.mod
View File

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

View File

@ -405,6 +405,27 @@ func (s *Server) UpdateLDAPProvider(ctx context.Context, req *admin_pb.UpdateLDA
}, nil }, 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) { func (s *Server) DeleteProvider(ctx context.Context, req *admin_pb.DeleteProviderRequest) (*admin_pb.DeleteProviderResponse, error) {
details, err := s.command.DeleteInstanceProvider(ctx, req.Id) details, err := s.command.DeleteInstanceProvider(ctx, req.Id)
if err != nil { if err != nil {

View File

@ -440,3 +440,27 @@ func updateLDAPProviderToCommand(req *admin_pb.UpdateLDAPProviderRequest) comman
IDPOptions: idp_grpc.OptionsToCommand(req.ProviderOptions), 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 return idp_pb.ProviderType_PROVIDER_TYPE_GITLAB_SELF_HOSTED
case domain.IDPTypeGoogle: case domain.IDPTypeGoogle:
return idp_pb.ProviderType_PROVIDER_TYPE_GOOGLE return idp_pb.ProviderType_PROVIDER_TYPE_GOOGLE
case domain.IDPTypeApple:
return idp_pb.ProviderType_PROVIDER_TYPE_APPLE
case domain.IDPTypeUnspecified: case domain.IDPTypeUnspecified:
return idp_pb.ProviderType_PROVIDER_TYPE_UNSPECIFIED return idp_pb.ProviderType_PROVIDER_TYPE_UNSPECIFIED
default: default:
@ -470,6 +472,10 @@ func configToPb(config *query.IDPTemplate) *idp_pb.ProviderConfig {
ldapConfigToPb(providerConfig, config.LDAPIDPTemplate) ldapConfigToPb(providerConfig, config.LDAPIDPTemplate)
return providerConfig return providerConfig
} }
if config.AppleIDPTemplate != nil {
appleConfigToPb(providerConfig, config.AppleIDPTemplate)
return providerConfig
}
return providerConfig return providerConfig
} }
@ -620,3 +626,14 @@ func ldapAttributesToPb(attributes idp.LDAPAttributes) *idp_pb.LDAPAttributes {
ProfileAttribute: attributes.ProfileAttribute, 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 }, 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) { 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) details, err := s.command.DeleteOrgProvider(ctx, authz.GetCtxData(ctx).OrgID, req.Id)
if err != nil { if err != nil {

View File

@ -457,3 +457,27 @@ func updateLDAPProviderToCommand(req *mgmt_pb.UpdateLDAPProviderRequest) command
IDPOptions: idp_grpc.OptionsToCommand(req.ProviderOptions), 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

@ -16,6 +16,7 @@ import (
z_errs "github.com/zitadel/zitadel/internal/errors" z_errs "github.com/zitadel/zitadel/internal/errors"
"github.com/zitadel/zitadel/internal/form" "github.com/zitadel/zitadel/internal/form"
"github.com/zitadel/zitadel/internal/idp" "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/azuread"
"github.com/zitadel/zitadel/internal/idp/providers/github" "github.com/zitadel/zitadel/internal/idp/providers/github"
"github.com/zitadel/zitadel/internal/idp/providers/gitlab" "github.com/zitadel/zitadel/internal/idp/providers/gitlab"
@ -52,6 +53,9 @@ type externalIDPCallbackData struct {
Code string `schema:"code"` Code string `schema:"code"`
Error string `schema:"error"` Error string `schema:"error"`
ErrorDescription string `schema:"error_description"` 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 // 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 return
} }
idpUser, idpSession, err := h.fetchIDPUser(ctx, provider, data.Code) idpUser, idpSession, err := h.fetchIDPUser(ctx, provider, data.Code, data.User)
if err != nil { if err != nil {
cmdErr := h.commands.FailIDPIntent(ctx, intent, err.Error()) cmdErr := h.commands.FailIDPIntent(ctx, intent, err.Error())
logging.WithFields("intent", intent.AggregateID).OnError(cmdErr).Error("failed to push failed event on idp intent") 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) 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 var session idp.Session
switch provider := identityProvider.(type) { switch provider := identityProvider.(type) {
case *oauth.Provider: 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} session = &openid.Session{Provider: provider.Provider, Code: code}
case *google.Provider: case *google.Provider:
session = &openid.Session{Provider: provider.Provider, Code: code} 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: case *jwt.Provider, *ldap.Provider:
return nil, nil, z_errs.ThrowInvalidArgument(nil, "IDP-52jmn", "Errors.ExternalIDP.IDPTypeNotImplemented") return nil, nil, z_errs.ThrowInvalidArgument(nil, "IDP-52jmn", "Errors.ExternalIDP.IDPTypeNotImplemented")
default: default:

View File

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

View File

@ -18,6 +18,7 @@ import (
"github.com/zitadel/zitadel/internal/errors" "github.com/zitadel/zitadel/internal/errors"
"github.com/zitadel/zitadel/internal/eventstore/v1/models" "github.com/zitadel/zitadel/internal/eventstore/v1/models"
"github.com/zitadel/zitadel/internal/idp" "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/azuread"
"github.com/zitadel/zitadel/internal/idp/providers/github" "github.com/zitadel/zitadel/internal/idp/providers/github"
"github.com/zitadel/zitadel/internal/idp/providers/gitlab" "github.com/zitadel/zitadel/internal/idp/providers/gitlab"
@ -41,6 +42,9 @@ type externalIDPData struct {
type externalIDPCallbackData struct { type externalIDPCallbackData struct {
State string `schema:"state"` State string `schema:"state"`
Code string `schema:"code"` Code string `schema:"code"`
// Apple returns a user on first registration
User string `schema:"user"`
} }
type externalNotFoundOptionFormData struct { 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) provider, err = l.gitlabSelfHostedProvider(r.Context(), identityProvider)
case domain.IDPTypeGoogle: case domain.IDPTypeGoogle:
provider, err = l.googleProvider(r.Context(), identityProvider) provider, err = l.googleProvider(r.Context(), identityProvider)
case domain.IDPTypeApple:
provider, err = l.appleProvider(r.Context(), identityProvider)
case domain.IDPTypeLDAP: case domain.IDPTypeLDAP:
provider, err = l.ldapProvider(r.Context(), identityProvider) provider, err = l.ldapProvider(r.Context(), identityProvider)
case domain.IDPTypeUnspecified: 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) 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 // handleExternalLoginCallback handles the callback from a IDP
// and tries to extract the user with the provided data // and tries to extract the user with the provided data
func (l *Login) handleExternalLoginCallback(w http.ResponseWriter, r *http.Request) { 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 return
} }
session = &openid.Session{Provider: provider.(*google.Provider).Provider, Code: data.Code} 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, case domain.IDPTypeJWT,
domain.IDPTypeLDAP, domain.IDPTypeLDAP,
domain.IDPTypeUnspecified: 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 { func (l *Login) appendUserGrants(ctx context.Context, userGrants []*domain.UserGrant, resourceOwner string) error {
if len(userGrants) == 0 { if len(userGrants) == 0 {
return nil return nil
@ -971,6 +1011,8 @@ func tokens(session idp.Session) *oidc.Tokens[*oidc.IDTokenClaims] {
return s.Tokens return s.Tokens
case *azuread.Session: case *azuread.Session:
return s.Tokens return s.Tokens
case *apple.Session:
return s.Tokens
} }
return nil return nil
} }

View File

@ -120,6 +120,12 @@ func createCSRFInterceptor(cookieName string, csrfCookieKey []byte, externalSecu
handler.ServeHTTP(w, r) handler.ServeHTTP(w, r)
return 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.Protect(csrfCookieKey,
csrf.Secure(externalSecure), csrf.Secure(externalSecure),
csrf.CookieName(http_utils.SetCookiePrefix(cookieName, "", path, externalSecure)), csrf.CookieName(http_utils.SetCookiePrefix(cookieName, "", path, externalSecure)),

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -372,6 +372,8 @@ Footer:
Help: 帮助 Help: 帮助
SupportEmail: 支持邮箱 SupportEmail: 支持邮箱
SignIn: 通过 {{.Provider}} 登录
Errors: Errors:
Internal: 发生了内部错误 Internal: 发生了内部错误
AuthRequest: 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-padding: 0 1px;
$lgn-idp-provider-name-line-height: 36px; $lgn-idp-provider-name-line-height: 36px;
$lgn-idp-border-radius: 0.5rem; $lgn-idp-border-radius: 0.5rem;
$lgn-idp-logo-size: 46px;
@mixin lgn-idp-base { @mixin lgn-idp-base {
display: block; display: block;
@ -17,14 +18,14 @@ $lgn-idp-border-radius: 0.5rem;
transition: border-color 0.2s ease-in-out; transition: border-color 0.2s ease-in-out;
span.logo { span.logo {
height: 46px; height: $lgn-idp-logo-size;
width: 46px; width: $lgn-idp-logo-size;
} }
span.provider-name { span.provider-name {
line-height: $lgn-idp-provider-name-line-height; line-height: $lgn-idp-provider-name-line-height;
position: absolute; position: relative;
left: 50%; left: calc(50% - $lgn-idp-logo-size);
transform: translateX(-50%); transform: translateX(-50%);
} }
@ -75,4 +76,17 @@ $lgn-idp-border-radius: 0.5rem;
border-radius: 5px; 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-github-background: #ffffff;
--zitadel-color-gitlab-text: #8b8d8d; --zitadel-color-gitlab-text: #8b8d8d;
--zitadel-color-gitlab-background: #ffffff; --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: var(--zitadel-color-black);
--zitadel-color-qr-background: var(--zitadel-color-white); --zitadel-color-qr-background: var(--zitadel-color-white);
@ -125,6 +127,7 @@
--github-image-src: url(../../../images/idp/github.png); --github-image-src: url(../../../images/idp/github.png);
--gitlab-image-src: url(../../../images/idp/gitlab.png); --gitlab-image-src: url(../../../images/idp/gitlab.png);
--azure-image-src: url(../../../images/idp/ms.svg); --azure-image-src: url(../../../images/idp/ms.svg);
--apple-image-src: url(../../../images/idp/apple.svg);
} }
.lgn-dark-theme { .lgn-dark-theme {
@ -227,9 +230,12 @@
--zitadel-color-github-background: #ffffff; --zitadel-color-github-background: #ffffff;
--zitadel-color-gitlab-text: #8b8d8d; --zitadel-color-gitlab-text: #8b8d8d;
--zitadel-color-gitlab-background: #ffffff; --zitadel-color-gitlab-background: #ffffff;
--zitadel-color-apple-text: #8b8d8d;
--zitadel-color-apple-background: #ffffff;
--google-image-src: url(../../../images/idp/google.png); --google-image-src: url(../../../images/idp/google.png);
--github-image-src: url(../../../images/idp/github-white.png); --github-image-src: url(../../../images/idp/github-white.png);
--gitlab-image-src: url(../../../images/idp/gitlab.png); --gitlab-image-src: url(../../../images/idp/gitlab.png);
--azure-image-src: url(../../../images/idp/ms.svg); --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}}" <a href="{{ externalIDPAuthURL $reqid $provider.IDPConfigID}}"
class="lgn-idp {{idpProviderClass $provider.IDPType}}"> class="lgn-idp {{idpProviderClass $provider.IDPType}}">
<span class="logo"></span> <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> <span class="provider-name">{{$provider.DisplayName}}</span>
{{end}}
</a> </a>
{{end}} {{end}}
</div> </div>

View File

@ -29,7 +29,11 @@
<a href="{{ externalIDPRegisterURL $reqid $provider.IDPConfigID}}" <a href="{{ externalIDPRegisterURL $reqid $provider.IDPConfigID}}"
class="lgn-idp {{idpProviderClass $provider.IDPType}}"> class="lgn-idp {{idpProviderClass $provider.IDPType}}">
<span class="logo"></span> <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> <span class="provider-name">{{$provider.DisplayName}}</span>
{{end}}
</a> </a>
{{end}} {{end}}
{{end}} {{end}}

View File

@ -110,6 +110,16 @@ type LDAPProvider struct {
IDPOptions idp.Options 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) { func ExistsIDP(ctx context.Context, filter preparation.FilterToQueryReducer, id, orgID string) (exists bool, err error) {
writeModel := NewOrgIDPRemoveWriteModel(orgID, id) writeModel := NewOrgIDPRemoveWriteModel(orgID, id)
events, err := filter(ctx, writeModel.Query()) events, err := filter(ctx, writeModel.Query())

View File

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

View File

@ -3,6 +3,7 @@ package command
import ( import (
"net/http" "net/http"
"reflect" "reflect"
"slices"
"time" "time"
"github.com/zitadel/logging" "github.com/zitadel/logging"
@ -14,6 +15,7 @@ import (
"github.com/zitadel/zitadel/internal/errors" "github.com/zitadel/zitadel/internal/errors"
"github.com/zitadel/zitadel/internal/eventstore" "github.com/zitadel/zitadel/internal/eventstore"
providers "github.com/zitadel/zitadel/internal/idp" 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/azuread"
"github.com/zitadel/zitadel/internal/idp/providers/github" "github.com/zitadel/zitadel/internal/idp/providers/github"
"github.com/zitadel/zitadel/internal/idp/providers/gitlab" "github.com/zitadel/zitadel/internal/idp/providers/gitlab"
@ -1587,6 +1589,138 @@ func (wm *LDAPIDPWriteModel) GetProviderOptions() idp.Options {
return wm.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 { type IDPRemoveWriteModel struct {
eventstore.WriteModel eventstore.WriteModel
@ -1617,6 +1751,8 @@ func (wm *IDPRemoveWriteModel) Reduce() error {
wm.reduceAdded(e.ID) wm.reduceAdded(e.ID)
case *idp.LDAPIDPAddedEvent: case *idp.LDAPIDPAddedEvent:
wm.reduceAdded(e.ID) wm.reduceAdded(e.ID)
case *idp.AppleIDPAddedEvent:
wm.reduceAdded(e.ID)
case *idp.RemovedEvent: case *idp.RemovedEvent:
wm.reduceRemoved(e.ID) wm.reduceRemoved(e.ID)
case *idpconfig.IDPConfigAddedEvent: case *idpconfig.IDPConfigAddedEvent:
@ -1699,6 +1835,10 @@ func (wm *IDPTypeWriteModel) Reduce() error {
wm.reduceAdded(e.ID, domain.IDPTypeLDAP, e.Aggregate()) wm.reduceAdded(e.ID, domain.IDPTypeLDAP, e.Aggregate())
case *org.LDAPIDPAddedEvent: case *org.LDAPIDPAddedEvent:
wm.reduceAdded(e.ID, domain.IDPTypeLDAP, e.Aggregate()) 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: case *instance.OIDCIDPMigratedAzureADEvent:
wm.reduceChanged(e.ID, domain.IDPTypeAzureAD) wm.reduceChanged(e.ID, domain.IDPTypeAzureAD)
case *org.OIDCIDPMigratedAzureADEvent: case *org.OIDCIDPMigratedAzureADEvent:
@ -1774,6 +1914,7 @@ func (wm *IDPTypeWriteModel) Query() *eventstore.SearchQueryBuilder {
instance.GitLabSelfHostedIDPAddedEventType, instance.GitLabSelfHostedIDPAddedEventType,
instance.GoogleIDPAddedEventType, instance.GoogleIDPAddedEventType,
instance.LDAPIDPAddedEventType, instance.LDAPIDPAddedEventType,
instance.AppleIDPAddedEventType,
instance.OIDCIDPMigratedAzureADEventType, instance.OIDCIDPMigratedAzureADEventType,
instance.OIDCIDPMigratedGoogleEventType, instance.OIDCIDPMigratedGoogleEventType,
instance.IDPRemovedEventType, instance.IDPRemovedEventType,
@ -1792,6 +1933,7 @@ func (wm *IDPTypeWriteModel) Query() *eventstore.SearchQueryBuilder {
org.GitLabSelfHostedIDPAddedEventType, org.GitLabSelfHostedIDPAddedEventType,
org.GoogleIDPAddedEventType, org.GoogleIDPAddedEventType,
org.LDAPIDPAddedEventType, org.LDAPIDPAddedEventType,
org.AppleIDPAddedEventType,
org.OIDCIDPMigratedAzureADEventType, org.OIDCIDPMigratedAzureADEventType,
org.OIDCIDPMigratedGoogleEventType, org.OIDCIDPMigratedGoogleEventType,
org.IDPRemovedEventType, org.IDPRemovedEventType,
@ -1859,6 +2001,8 @@ func NewAllIDPWriteModel(resourceOwner string, instanceBool bool, id string, idp
writeModel.model = NewGitLabSelfHostedInstanceIDPWriteModel(resourceOwner, id) writeModel.model = NewGitLabSelfHostedInstanceIDPWriteModel(resourceOwner, id)
case domain.IDPTypeGoogle: case domain.IDPTypeGoogle:
writeModel.model = NewGoogleInstanceIDPWriteModel(resourceOwner, id) writeModel.model = NewGoogleInstanceIDPWriteModel(resourceOwner, id)
case domain.IDPTypeApple:
writeModel.model = NewAppleInstanceIDPWriteModel(resourceOwner, id)
case domain.IDPTypeUnspecified: case domain.IDPTypeUnspecified:
fallthrough fallthrough
default: default:
@ -1886,6 +2030,8 @@ func NewAllIDPWriteModel(resourceOwner string, instanceBool bool, id string, idp
writeModel.model = NewGitLabSelfHostedOrgIDPWriteModel(resourceOwner, id) writeModel.model = NewGitLabSelfHostedOrgIDPWriteModel(resourceOwner, id)
case domain.IDPTypeGoogle: case domain.IDPTypeGoogle:
writeModel.model = NewGoogleOrgIDPWriteModel(resourceOwner, id) writeModel.model = NewGoogleOrgIDPWriteModel(resourceOwner, id)
case domain.IDPTypeApple:
writeModel.model = NewAppleOrgIDPWriteModel(resourceOwner, id)
case domain.IDPTypeUnspecified: case domain.IDPTypeUnspecified:
fallthrough fallthrough
default: default:

View File

@ -467,6 +467,48 @@ func (c *Commands) UpdateInstanceLDAPProvider(ctx context.Context, id string, pr
return pushedEventsToObjectDetails(pushedEvents), nil 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) { func (c *Commands) DeleteInstanceProvider(ctx context.Context, id string) (*domain.ObjectDetails, error) {
instanceAgg := instance.NewAggregate(authz.GetInstance(ctx).InstanceID()) instanceAgg := instance.NewAggregate(authz.GetInstance(ctx).InstanceID())
cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, c.prepareDeleteInstanceProvider(instanceAgg, id)) 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 { func (c *Commands) prepareDeleteInstanceProvider(a *instance.Aggregate, id string) preparation.Validation {
return func() (preparation.CreateCommands, error) { return func() (preparation.CreateCommands, error) {
return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, 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) 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 { type InstanceIDPRemoveWriteModel struct {
IDPRemoveWriteModel IDPRemoveWriteModel
} }
@ -832,6 +899,8 @@ func (wm *InstanceIDPRemoveWriteModel) AppendEvents(events ...eventstore.Event)
wm.IDPRemoveWriteModel.AppendEvents(&e.GoogleIDPAddedEvent) wm.IDPRemoveWriteModel.AppendEvents(&e.GoogleIDPAddedEvent)
case *instance.LDAPIDPAddedEvent: case *instance.LDAPIDPAddedEvent:
wm.IDPRemoveWriteModel.AppendEvents(&e.LDAPIDPAddedEvent) wm.IDPRemoveWriteModel.AppendEvents(&e.LDAPIDPAddedEvent)
case *instance.AppleIDPAddedEvent:
wm.IDPRemoveWriteModel.AppendEvents(&e.AppleIDPAddedEvent)
case *instance.IDPRemovedEvent: case *instance.IDPRemovedEvent:
wm.IDPRemoveWriteModel.AppendEvents(&e.RemovedEvent) wm.IDPRemoveWriteModel.AppendEvents(&e.RemovedEvent)
case *instance.IDPConfigAddedEvent: case *instance.IDPConfigAddedEvent:
@ -861,6 +930,7 @@ func (wm *InstanceIDPRemoveWriteModel) Query() *eventstore.SearchQueryBuilder {
instance.GitLabSelfHostedIDPAddedEventType, instance.GitLabSelfHostedIDPAddedEventType,
instance.GoogleIDPAddedEventType, instance.GoogleIDPAddedEventType,
instance.LDAPIDPAddedEventType, instance.LDAPIDPAddedEventType,
instance.AppleIDPAddedEventType,
instance.IDPRemovedEventType, instance.IDPRemovedEventType,
). ).
EventData(map[string]interface{}{"id": wm.ID}). 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

@ -444,6 +444,46 @@ func (c *Commands) UpdateOrgLDAPProvider(ctx context.Context, resourceOwner, id
return pushedEventsToObjectDetails(pushedEvents), nil return pushedEventsToObjectDetails(pushedEvents), nil
} }
func (c *Commands) AddOrgAppleProvider(ctx context.Context, resourceOwner string, provider AppleProvider) (string, *domain.ObjectDetails, error) {
orgAgg := org.NewAggregate(resourceOwner)
id, err := c.idGenerator.Next()
if err != nil {
return "", nil, err
}
writeModel := NewAppleOrgIDPWriteModel(resourceOwner, id)
cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, c.prepareAddOrgAppleProvider(orgAgg, 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) UpdateOrgAppleProvider(ctx context.Context, resourceOwner, id string, provider AppleProvider) (*domain.ObjectDetails, error) {
orgAgg := org.NewAggregate(resourceOwner)
writeModel := NewAppleOrgIDPWriteModel(resourceOwner, id)
cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, c.prepareUpdateOrgAppleProvider(orgAgg, 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) DeleteOrgProvider(ctx context.Context, resourceOwner, id string) (*domain.ObjectDetails, error) { func (c *Commands) DeleteOrgProvider(ctx context.Context, resourceOwner, id string) (*domain.ObjectDetails, error) {
orgAgg := org.NewAggregate(resourceOwner) orgAgg := org.NewAggregate(resourceOwner)
cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, c.prepareDeleteOrgProvider(orgAgg, resourceOwner, id)) cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, c.prepareDeleteOrgProvider(orgAgg, resourceOwner, id))
@ -1507,6 +1547,98 @@ func (c *Commands) prepareUpdateOrgLDAPProvider(a *org.Aggregate, writeModel *Or
} }
} }
func (c *Commands) prepareAddOrgAppleProvider(a *org.Aggregate, writeModel *OrgAppleIDPWriteModel, provider AppleProvider) preparation.Validation {
return func() (preparation.CreateCommands, error) {
if provider.ClientID = strings.TrimSpace(provider.ClientID); provider.ClientID == "" {
return nil, caos_errs.ThrowInvalidArgument(nil, "ORG-jkn3w", "Errors.IDP.ClientIDMissing")
}
if provider.TeamID = strings.TrimSpace(provider.TeamID); provider.TeamID == "" {
return nil, caos_errs.ThrowInvalidArgument(nil, "ORG-Ffg32", "Errors.IDP.TeamIDMissing")
}
if provider.KeyID = strings.TrimSpace(provider.KeyID); provider.KeyID == "" {
return nil, caos_errs.ThrowInvalidArgument(nil, "ORG-GDjm5", "Errors.IDP.KeyIDMissing")
}
if len(provider.PrivateKey) == 0 {
return nil, caos_errs.ThrowInvalidArgument(nil, "ORG-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{
org.NewAppleIDPAddedEvent(
ctx,
&a.Aggregate,
writeModel.ID,
provider.Name,
provider.ClientID,
provider.TeamID,
provider.KeyID,
privateKey,
provider.Scopes,
provider.IDPOptions,
),
}, nil
}, nil
}
}
func (c *Commands) prepareUpdateOrgAppleProvider(a *org.Aggregate, writeModel *OrgAppleIDPWriteModel, provider AppleProvider) preparation.Validation {
return func() (preparation.CreateCommands, error) {
if writeModel.ID = strings.TrimSpace(writeModel.ID); writeModel.ID == "" {
return nil, caos_errs.ThrowInvalidArgument(nil, "ORG-FRHBH", "Errors.IDMissing")
}
if provider.ClientID = strings.TrimSpace(provider.ClientID); provider.ClientID == "" {
return nil, caos_errs.ThrowInvalidArgument(nil, "ORG-SFm4l", "Errors.IDP.ClientIDMissing")
}
if provider.TeamID = strings.TrimSpace(provider.TeamID); provider.TeamID == "" {
return nil, caos_errs.ThrowInvalidArgument(nil, "ORG-SG34t", "Errors.IDP.TeamIDMissing")
}
if provider.KeyID = strings.TrimSpace(provider.KeyID); provider.KeyID == "" {
return nil, caos_errs.ThrowInvalidArgument(nil, "ORG-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, "ORG-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) prepareDeleteOrgProvider(a *org.Aggregate, resourceOwner, id string) preparation.Validation { func (c *Commands) prepareDeleteOrgProvider(a *org.Aggregate, resourceOwner, id string) preparation.Validation {
return func() (preparation.CreateCommands, error) { return func() (preparation.CreateCommands, error) {
return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) { return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) {

View File

@ -803,6 +803,73 @@ func (wm *OrgLDAPIDPWriteModel) NewChangedEvent(
return org.NewLDAPIDPChangedEvent(ctx, aggregate, id, changes) return org.NewLDAPIDPChangedEvent(ctx, aggregate, id, changes)
} }
type OrgAppleIDPWriteModel struct {
AppleIDPWriteModel
}
func NewAppleOrgIDPWriteModel(orgID, id string) *OrgAppleIDPWriteModel {
return &OrgAppleIDPWriteModel{
AppleIDPWriteModel{
WriteModel: eventstore.WriteModel{
AggregateID: orgID,
ResourceOwner: orgID,
},
ID: id,
},
}
}
func (wm *OrgAppleIDPWriteModel) AppendEvents(events ...eventstore.Event) {
for _, event := range events {
switch e := event.(type) {
case *org.AppleIDPAddedEvent:
wm.AppleIDPWriteModel.AppendEvents(&e.AppleIDPAddedEvent)
case *org.AppleIDPChangedEvent:
wm.AppleIDPWriteModel.AppendEvents(&e.AppleIDPChangedEvent)
case *org.IDPRemovedEvent:
wm.AppleIDPWriteModel.AppendEvents(&e.RemovedEvent)
default:
wm.AppleIDPWriteModel.AppendEvents(e)
}
}
}
func (wm *OrgAppleIDPWriteModel) Query() *eventstore.SearchQueryBuilder {
return eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent).
ResourceOwner(wm.ResourceOwner).
AddQuery().
AggregateTypes(org.AggregateType).
AggregateIDs(wm.AggregateID).
EventTypes(
org.AppleIDPAddedEventType,
org.AppleIDPChangedEventType,
org.IDPRemovedEventType,
).
EventData(map[string]interface{}{"id": wm.ID}).
Builder()
}
func (wm *OrgAppleIDPWriteModel) NewChangedEvent(
ctx context.Context,
aggregate *eventstore.Aggregate,
id,
name,
clientID,
teamID,
keyID string,
privateKey []byte,
secretCrypto crypto.Crypto,
scopes []string,
options idp.Options,
) (*org.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 org.NewAppleIDPChangedEvent(ctx, aggregate, id, changes)
}
type OrgIDPRemoveWriteModel struct { type OrgIDPRemoveWriteModel struct {
IDPRemoveWriteModel IDPRemoveWriteModel
} }
@ -842,6 +909,8 @@ func (wm *OrgIDPRemoveWriteModel) AppendEvents(events ...eventstore.Event) {
wm.IDPRemoveWriteModel.AppendEvents(&e.GoogleIDPAddedEvent) wm.IDPRemoveWriteModel.AppendEvents(&e.GoogleIDPAddedEvent)
case *org.LDAPIDPAddedEvent: case *org.LDAPIDPAddedEvent:
wm.IDPRemoveWriteModel.AppendEvents(&e.LDAPIDPAddedEvent) wm.IDPRemoveWriteModel.AppendEvents(&e.LDAPIDPAddedEvent)
case *org.AppleIDPAddedEvent:
wm.IDPRemoveWriteModel.AppendEvents(&e.AppleIDPAddedEvent)
case *org.IDPRemovedEvent: case *org.IDPRemovedEvent:
wm.IDPRemoveWriteModel.AppendEvents(&e.RemovedEvent) wm.IDPRemoveWriteModel.AppendEvents(&e.RemovedEvent)
case *org.IDPConfigAddedEvent: case *org.IDPConfigAddedEvent:
@ -871,6 +940,7 @@ func (wm *OrgIDPRemoveWriteModel) Query() *eventstore.SearchQueryBuilder {
org.GitLabSelfHostedIDPAddedEventType, org.GitLabSelfHostedIDPAddedEventType,
org.GoogleIDPAddedEventType, org.GoogleIDPAddedEventType,
org.LDAPIDPAddedEventType, org.LDAPIDPAddedEventType,
org.AppleIDPAddedEventType,
org.IDPRemovedEventType, org.IDPRemovedEventType,
). ).
EventData(map[string]interface{}{"id": wm.ID}). EventData(map[string]interface{}{"id": wm.ID}).

View File

@ -4926,6 +4926,473 @@ func TestCommandSide_UpdateOrgLDAPIDP(t *testing.T) {
} }
} }
func TestCommandSide_AddOrgAppleIDP(t *testing.T) {
type fields struct {
eventstore *eventstore.Eventstore
idGenerator id.Generator
secretCrypto crypto.EncryptionAlgorithm
}
type args struct {
ctx context.Context
resourceOwner string
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: context.Background(),
resourceOwner: "org1",
provider: AppleProvider{},
},
res{
err: func(err error) bool {
return errors.Is(err, caos_errors.ThrowInvalidArgument(nil, "ORG-jkn3w", "Errors.IDP.ClientIDMissing"))
},
},
},
{
"invalid teamID",
fields{
eventstore: eventstoreExpect(t),
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "id1"),
},
args{
ctx: context.Background(),
resourceOwner: "org1",
provider: AppleProvider{
ClientID: "clientID",
},
},
res{
err: func(err error) bool {
return errors.Is(err, caos_errors.ThrowInvalidArgument(nil, "ORG-Ffg32", "Errors.IDP.TeamIDMissing"))
},
},
},
{
"invalid keyID",
fields{
eventstore: eventstoreExpect(t),
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "id1"),
},
args{
ctx: context.Background(),
resourceOwner: "org1",
provider: AppleProvider{
ClientID: "clientID",
TeamID: "teamID",
},
},
res{
err: func(err error) bool {
return errors.Is(err, caos_errors.ThrowInvalidArgument(nil, "ORG-GDjm5", "Errors.IDP.KeyIDMissing"))
},
},
},
{
"invalid privateKey",
fields{
eventstore: eventstoreExpect(t),
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "id1"),
},
args{
ctx: context.Background(),
resourceOwner: "org1",
provider: AppleProvider{
ClientID: "clientID",
TeamID: "teamID",
KeyID: "keyID",
},
},
res{
err: func(err error) bool {
return errors.Is(err, caos_errors.ThrowInvalidArgument(nil, "ORG-GVD4n", "Errors.IDP.PrivateKeyMissing"))
},
},
},
{
name: "ok",
fields: fields{
eventstore: eventstoreExpect(t,
expectFilter(),
expectPush(
eventPusherToEvents(
org.NewAppleIDPAddedEvent(context.Background(), &org.NewAggregate("org1").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: context.Background(),
resourceOwner: "org1",
provider: AppleProvider{
ClientID: "clientID",
TeamID: "teamID",
KeyID: "keyID",
PrivateKey: []byte("privateKey"),
},
},
res: res{
id: "id1",
want: &domain.ObjectDetails{ResourceOwner: "org1"},
},
},
{
name: "ok all set",
fields: fields{
eventstore: eventstoreExpect(t,
expectFilter(),
expectPush(
eventPusherToEvents(
org.NewAppleIDPAddedEvent(context.Background(), &org.NewAggregate("org1").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: context.Background(),
resourceOwner: "org1",
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: "org1"},
},
},
}
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.AddOrgAppleProvider(tt.args.ctx, tt.args.resourceOwner, 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_UpdateOrgAppleIDP(t *testing.T) {
type fields struct {
eventstore *eventstore.Eventstore
secretCrypto crypto.EncryptionAlgorithm
}
type args struct {
ctx context.Context
resourceOwner string
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: context.Background(),
resourceOwner: "org1",
provider: AppleProvider{},
},
res{
err: func(err error) bool {
return errors.Is(err, caos_errors.ThrowInvalidArgument(nil, "ORG-FRHBH", "Errors.IDMissing"))
},
},
},
{
"invalid clientID",
fields{
eventstore: eventstoreExpect(t),
},
args{
ctx: context.Background(),
resourceOwner: "org1",
id: "id1",
provider: AppleProvider{},
},
res{
err: func(err error) bool {
return errors.Is(err, caos_errors.ThrowInvalidArgument(nil, "ORG-SFm4l", "Errors.IDP.ClientIDMissing"))
},
},
},
{
"invalid teamID",
fields{
eventstore: eventstoreExpect(t),
},
args{
ctx: context.Background(),
resourceOwner: "org1",
id: "id1",
provider: AppleProvider{
ClientID: "clientID",
},
},
res{
err: func(err error) bool {
return errors.Is(err, caos_errors.ThrowInvalidArgument(nil, "ORG-SG34t", "Errors.IDP.TeamIDMissing"))
},
},
},
{
"invalid keyID",
fields{
eventstore: eventstoreExpect(t),
},
args{
ctx: context.Background(),
resourceOwner: "org1",
id: "id1",
provider: AppleProvider{
ClientID: "clientID",
TeamID: "teamID",
},
},
res{
err: func(err error) bool {
return errors.Is(err, caos_errors.ThrowInvalidArgument(nil, "ORG-Gh4z2", "Errors.IDP.KeyIDMissing"))
},
},
},
{
name: "not found",
fields: fields{
eventstore: eventstoreExpect(t,
expectFilter(),
),
},
args: args{
ctx: context.Background(),
resourceOwner: "org1",
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(
org.NewAppleIDPAddedEvent(context.Background(), &org.NewAggregate("org1").Aggregate,
"id1",
"",
"clientID",
"teamID",
"keyID",
&crypto.CryptoValue{
CryptoType: crypto.TypeEncryption,
Algorithm: "enc",
KeyID: "id",
Crypted: []byte("privateKey"),
},
nil,
idp.Options{},
)),
),
),
},
args: args{
ctx: context.Background(),
resourceOwner: "org1",
id: "id1",
provider: AppleProvider{
ClientID: "clientID",
TeamID: "teamID",
KeyID: "keyID",
},
},
res: res{
want: &domain.ObjectDetails{ResourceOwner: "org1"},
},
},
{
name: "change ok",
fields: fields{
eventstore: eventstoreExpect(t,
expectFilter(
eventFromEventPusher(
org.NewAppleIDPAddedEvent(context.Background(), &org.NewAggregate("org1").Aggregate,
"id1",
"",
"clientID",
"teamID",
"keyID",
&crypto.CryptoValue{
CryptoType: crypto.TypeEncryption,
Algorithm: "enc",
KeyID: "id",
Crypted: []byte("privateKey"),
},
nil,
idp.Options{},
)),
),
expectPush(
eventPusherToEvents(
func() eventstore.Command {
t := true
event, _ := org.NewAppleIDPChangedEvent(context.Background(), &org.NewAggregate("org1").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: context.Background(),
resourceOwner: "org1",
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: "org1"},
},
},
}
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.UpdateOrgAppleProvider(tt.args.ctx, tt.args.resourceOwner, 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)
}
})
}
}
func stringPointer(s string) *string { func stringPointer(s string) *string {
return &s return &s
} }

View File

@ -36,6 +36,7 @@ const (
IDPTypeGitLab IDPTypeGitLab
IDPTypeGitLabSelfHosted IDPTypeGitLabSelfHosted
IDPTypeGoogle IDPTypeGoogle
IDPTypeApple
) )
func (t IDPType) GetCSSClass() string { func (t IDPType) GetCSSClass() string {
@ -50,6 +51,8 @@ func (t IDPType) GetCSSClass() string {
return "gitlab" return "gitlab"
case IDPTypeAzureAD: case IDPTypeAzureAD:
return "azure" return "azure"
case IDPTypeApple:
return "apple"
case IDPTypeUnspecified, case IDPTypeUnspecified,
IDPTypeOIDC, IDPTypeOIDC,
IDPTypeJWT, IDPTypeJWT,
@ -78,6 +81,8 @@ func (t IDPType) DisplayName() string {
return "GitLab" return "GitLab"
case IDPTypeGoogle: case IDPTypeGoogle:
return "Google" return "Google"
case IDPTypeApple:
return "Apple"
case IDPTypeUnspecified, case IDPTypeUnspecified,
IDPTypeOIDC, IDPTypeOIDC,
IDPTypeJWT, IDPTypeJWT,
@ -94,6 +99,12 @@ func (t IDPType) DisplayName() string {
} }
} }
// IsSignInButton returns if the button should be displayed with a translated
// "Sign in with {{.DisplayName}}", e.g. "Sign in with Apple"
func (t IDPType) IsSignInButton() bool {
return t == IDPTypeApple
}
type IDPIntentState int32 type IDPIntentState int32
const ( const (

View File

@ -3,9 +3,9 @@ package form
import ( import (
"net/http" "net/http"
"github.com/zitadel/zitadel/internal/errors"
"github.com/gorilla/schema" "github.com/gorilla/schema"
"github.com/zitadel/zitadel/internal/errors"
) )
type Parser struct { type Parser struct {

View File

@ -0,0 +1,68 @@
package apple
import (
"crypto/x509"
"encoding/pem"
"time"
"github.com/zitadel/oidc/v2/pkg/crypto"
openid "github.com/zitadel/oidc/v2/pkg/oidc"
"gopkg.in/square/go-jose.v2"
"github.com/zitadel/zitadel/internal/idp"
"github.com/zitadel/zitadel/internal/idp/providers/oidc"
)
const (
name = "Apple"
issuer = "https://appleid.apple.com"
)
var _ idp.Provider = (*Provider)(nil)
// Provider is the [idp.Provider] implementation for Apple
type Provider struct {
*oidc.Provider
}
func New(clientID, teamID, keyID, callbackURL string, key []byte, scopes []string, options ...oidc.ProviderOpts) (*Provider, error) {
secret, err := clientSecretFromPrivateKey(key, teamID, clientID, keyID)
if err != nil {
return nil, err
}
options = append(options, oidc.WithResponseMode("form_post"))
rp, err := oidc.New(name, issuer, clientID, secret, callbackURL, scopes, oidc.DefaultMapper, options...)
if err != nil {
return nil, err
}
return &Provider{
Provider: rp,
}, nil
}
// clientSecretFromPrivateKey uses the private key to create and sign a JWT, which has to be used as client_secret at Apple.
func clientSecretFromPrivateKey(key []byte, teamID, clientID, keyID string) (string, error) {
block, _ := pem.Decode(key)
b := block.Bytes
pk, err := x509.ParsePKCS8PrivateKey(b)
if err != nil {
return "", err
}
signingKey := jose.SigningKey{
Algorithm: jose.ES256,
Key: &jose.JSONWebKey{Key: pk, KeyID: keyID},
}
signer, err := jose.NewSigner(signingKey, &jose.SignerOptions{})
if err != nil {
return "", err
}
iat := time.Now()
exp := iat.Add(time.Hour)
return crypto.Sign(&openid.JWTTokenRequest{
Issuer: teamID,
Subject: clientID,
Audience: []string{issuer},
ExpiresAt: openid.FromTime(exp),
IssuedAt: openid.FromTime(iat),
}, signer)
}

View File

@ -0,0 +1,69 @@
package apple
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/zitadel/zitadel/internal/idp"
"github.com/zitadel/zitadel/internal/idp/providers/oidc"
)
const (
privateKey = `-----BEGIN PRIVATE KEY-----
MIGTAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBHkwdwIBAQQgXn/LDURaetCoymSj
fRslBiBwzBSa8ifiyfYGIWNStYGgCgYIKoZIzj0DAQehRANCAATymZXIsGrXnl6b
+80miSiVOCcLnyaYa2uQBQvQwgB7GibXhrzF+D/MRTV4P7P8+Lg1K9Khkjc59eNK
4RrQP4g7
-----END PRIVATE KEY-----
`
)
func TestProvider_BeginAuth(t *testing.T) {
type fields struct {
clientID string
teamID string
keyID string
privateKey []byte
redirectURI string
scopes []string
}
tests := []struct {
name string
fields fields
want idp.Session
}{
{
name: "successful auth",
fields: fields{
clientID: "clientID",
teamID: "teamID",
keyID: "keyID",
privateKey: []byte(privateKey),
redirectURI: "redirectURI",
scopes: []string{"openid"},
},
want: &Session{
Session: &oidc.Session{
AuthURL: "https://appleid.apple.com/auth/authorize?client_id=clientID&redirect_uri=redirectURI&response_mode=form_post&response_type=code&scope=openid&state=testState",
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
a := assert.New(t)
r := require.New(t)
provider, err := New(tt.fields.clientID, tt.fields.teamID, tt.fields.keyID, tt.fields.redirectURI, tt.fields.privateKey, tt.fields.scopes)
r.NoError(err)
session, err := provider.BeginAuth(context.Background(), "testState")
r.NoError(err)
a.Equal(tt.want.GetAuthURL(), session.GetAuthURL())
})
}
}

View File

@ -0,0 +1,64 @@
package apple
import (
"context"
"encoding/json"
openid "github.com/zitadel/oidc/v2/pkg/oidc"
"github.com/zitadel/zitadel/internal/idp"
"github.com/zitadel/zitadel/internal/idp/providers/oidc"
)
// Session extends the [oidc.Session] with the formValues returned from the callback.
// This enables to parse the user (name and email), which Apple only returns as form params on registration
type Session struct {
*oidc.Session
UserFormValue string
}
type userFormValue struct {
Name userNamesFormValue `json:"name,omitempty" schema:"name"`
}
type userNamesFormValue struct {
FirstName string `json:"firstName,omitempty" schema:"firstName"`
LastName string `json:"lastName,omitempty" schema:"lastName"`
}
// FetchUser implements the [idp.Session] interface.
// It will execute an OIDC code exchange if needed to retrieve the tokens,
// extract the information from the id_token and if available also from the `user` form value.
// The information will be mapped into an [idp.User].
func (s *Session) FetchUser(ctx context.Context) (user idp.User, err error) {
if s.Tokens == nil {
if err = s.Authorize(ctx); err != nil {
return nil, err
}
}
info := s.Tokens.IDTokenClaims.GetUserInfo()
userName := userFormValue{}
if s.UserFormValue != "" {
if err = json.Unmarshal([]byte(s.UserFormValue), &userName); err != nil {
return nil, err
}
}
return NewUser(info, userName.Name), nil
}
func NewUser(info *openid.UserInfo, names userNamesFormValue) *User {
user := oidc.NewUser(info)
user.GivenName = names.FirstName
user.FamilyName = names.LastName
return &User{User: user}
}
// User extends the [oidc.User] by returning the email as preferred_username, since Apple does not return the latter.
type User struct {
*oidc.User
}
func (u *User) GetPreferredUsername() string {
return u.Email
}

View File

@ -0,0 +1,217 @@
package apple
import (
"context"
"errors"
"testing"
"time"
"github.com/h2non/gock"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
openid "github.com/zitadel/oidc/v2/pkg/oidc"
"golang.org/x/oauth2"
"golang.org/x/text/language"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/idp/providers/oidc"
)
func TestSession_FetchUser(t *testing.T) {
type fields struct {
clientID string
teamID string
keyID string
privateKey []byte
redirectURI string
scopes []string
httpMock func()
authURL string
code string
tokens *openid.Tokens[*openid.IDTokenClaims]
userFormValue string
}
type want struct {
err error
id string
firstName string
lastName string
displayName string
nickName string
preferredUsername string
email string
isEmailVerified bool
phone string
isPhoneVerified bool
preferredLanguage language.Tag
avatarURL string
profile string
nonceSupported bool
isPrivateEmail bool
}
tests := []struct {
name string
fields fields
want want
}{
{
name: "unauthenticated session, error",
fields: fields{
clientID: "clientID",
teamID: "teamID",
keyID: "keyID",
privateKey: []byte(privateKey),
redirectURI: "redirectURI",
scopes: []string{"openid"},
httpMock: func() {},
authURL: "https://appleid.apple.com/auth/authorize?client_id=clientID&redirect_uri=redirectURI&response_mode=form_post&response_type=code&scope=openid&state=testState",
tokens: nil,
},
want: want{
err: oidc.ErrCodeMissing,
},
},
{
name: "no user param",
fields: fields{
clientID: "clientID",
teamID: "teamID",
keyID: "keyID",
privateKey: []byte(privateKey),
redirectURI: "redirectURI",
scopes: []string{"openid"},
httpMock: func() {},
authURL: "https://appleid.apple.com/auth/authorize?client_id=clientID&redirect_uri=redirectURI&response_mode=form_post&response_type=code&scope=openid&state=testState",
tokens: &openid.Tokens[*openid.IDTokenClaims]{
Token: &oauth2.Token{
AccessToken: "accessToken",
TokenType: openid.BearerToken,
},
IDTokenClaims: id_token(),
},
userFormValue: "",
},
want: want{
id: "sub",
firstName: "",
lastName: "",
displayName: "",
nickName: "",
preferredUsername: "email",
email: "email",
isEmailVerified: true,
phone: "",
isPhoneVerified: false,
preferredLanguage: language.Und,
avatarURL: "",
profile: "",
nonceSupported: true,
isPrivateEmail: true,
},
},
{
name: "with user param",
fields: fields{
clientID: "clientID",
teamID: "teamID",
keyID: "keyID",
privateKey: []byte(privateKey),
redirectURI: "redirectURI",
scopes: []string{"openid"},
httpMock: func() {},
authURL: "https://appleid.apple.com/auth/authorize?client_id=clientID&redirect_uri=redirectURI&response_mode=form_post&response_type=code&scope=openid&state=testState",
tokens: &openid.Tokens[*openid.IDTokenClaims]{
Token: &oauth2.Token{
AccessToken: "accessToken",
TokenType: openid.BearerToken,
},
IDTokenClaims: id_token(),
},
userFormValue: `{"name": {"firstName": "firstName", "lastName": "lastName"}}`,
},
want: want{
id: "sub",
firstName: "firstName",
lastName: "lastName",
displayName: "",
nickName: "",
preferredUsername: "email",
email: "email",
isEmailVerified: true,
phone: "",
isPhoneVerified: false,
preferredLanguage: language.Und,
avatarURL: "",
profile: "",
nonceSupported: true,
isPrivateEmail: true,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
defer gock.Off()
tt.fields.httpMock()
a := assert.New(t)
// call the real discovery endpoint
gock.New(issuer).Get(openid.DiscoveryEndpoint).EnableNetworking()
provider, err := New(tt.fields.clientID, tt.fields.teamID, tt.fields.keyID, tt.fields.redirectURI, tt.fields.privateKey, tt.fields.scopes)
require.NoError(t, err)
session := &Session{
Session: &oidc.Session{
Provider: provider.Provider,
AuthURL: tt.fields.authURL,
Code: tt.fields.code,
Tokens: tt.fields.tokens,
},
UserFormValue: tt.fields.userFormValue,
}
user, err := session.FetchUser(context.Background())
if tt.want.err != nil && !errors.Is(err, tt.want.err) {
a.Fail("invalid error", "expected %v, got %v", tt.want.err, err)
}
if tt.want.err == nil {
a.NoError(err)
a.Equal(tt.want.id, user.GetID())
a.Equal(tt.want.firstName, user.GetFirstName())
a.Equal(tt.want.lastName, user.GetLastName())
a.Equal(tt.want.displayName, user.GetDisplayName())
a.Equal(tt.want.nickName, user.GetNickname())
a.Equal(tt.want.preferredUsername, user.GetPreferredUsername())
a.Equal(domain.EmailAddress(tt.want.email), user.GetEmail())
a.Equal(tt.want.isEmailVerified, user.IsEmailVerified())
a.Equal(domain.PhoneNumber(tt.want.phone), user.GetPhone())
a.Equal(tt.want.isPhoneVerified, user.IsPhoneVerified())
a.Equal(tt.want.preferredLanguage, user.GetPreferredLanguage())
a.Equal(tt.want.avatarURL, user.GetAvatarURL())
a.Equal(tt.want.profile, user.GetProfile())
}
})
}
}
func id_token() *openid.IDTokenClaims {
return &openid.IDTokenClaims{
TokenClaims: openid.TokenClaims{
Issuer: issuer,
Subject: "sub",
Audience: []string{"clientID"},
Expiration: openid.FromTime(time.Now().Add(1 * time.Hour)),
IssuedAt: openid.FromTime(time.Now().Add(-1 * time.Second)),
AuthTime: openid.FromTime(time.Now().Add(-1 * time.Second)),
Nonce: "nonce",
ClientID: "clientID",
},
UserInfoEmail: openid.UserInfoEmail{
Email: "email",
EmailVerified: true,
},
Claims: map[string]any{
"nonce_supported": true,
"is_private_email": true,
},
}
}

View File

@ -77,6 +77,14 @@ func WithSelectAccount() ProviderOpts {
} }
} }
// WithResponseMode sets the `response_mode` params in the auth request
func WithResponseMode(mode oidc.ResponseMode) ProviderOpts {
return func(p *Provider) {
paramOpt := rp.WithResponseModeURLParam(mode)
p.authOptions = append(p.authOptions, rp.AuthURLOpt(paramOpt))
}
}
type UserInfoMapper func(info *oidc.UserInfo) idp.User type UserInfoMapper func(info *oidc.UserInfo) idp.User
var DefaultMapper UserInfoMapper = func(info *oidc.UserInfo) idp.User { var DefaultMapper UserInfoMapper = func(info *oidc.UserInfo) idp.User {

View File

@ -34,7 +34,7 @@ func (s *Session) GetAuthURL() string {
// call the userinfo endpoint and map the received information into an [idp.User]. // call the userinfo endpoint and map the received information into an [idp.User].
func (s *Session) FetchUser(ctx context.Context) (user idp.User, err error) { func (s *Session) FetchUser(ctx context.Context) (user idp.User, err error) {
if s.Tokens == nil { if s.Tokens == nil {
if err = s.authorize(ctx); err != nil { if err = s.Authorize(ctx); err != nil {
return nil, err return nil, err
} }
} }
@ -54,7 +54,7 @@ func (s *Session) FetchUser(ctx context.Context) (user idp.User, err error) {
return u, nil return u, nil
} }
func (s *Session) authorize(ctx context.Context) (err error) { func (s *Session) Authorize(ctx context.Context) (err error) {
if s.Code == "" { if s.Code == "" {
return ErrCodeMissing return ErrCodeMissing
} }

View File

@ -43,6 +43,7 @@ type IDPTemplate struct {
*GitLabSelfHostedIDPTemplate *GitLabSelfHostedIDPTemplate
*GoogleIDPTemplate *GoogleIDPTemplate
*LDAPIDPTemplate *LDAPIDPTemplate
*AppleIDPTemplate
} }
type IDPTemplates struct { type IDPTemplates struct {
@ -140,6 +141,15 @@ type LDAPIDPTemplate struct {
idp.LDAPAttributes idp.LDAPAttributes
} }
type AppleIDPTemplate struct {
IDPID string
ClientID string
TeamID string
KeyID string
PrivateKey *crypto.CryptoValue
Scopes database.StringArray
}
var ( var (
idpTemplateTable = table{ idpTemplateTable = table{
name: projection.IDPTemplateTable, name: projection.IDPTemplateTable,
@ -605,6 +615,41 @@ var (
} }
) )
var (
appleIdpTemplateTable = table{
name: projection.IDPTemplateAppleTable,
instanceIDCol: projection.AppleInstanceIDCol,
}
AppleIDCol = Column{
name: projection.AppleIDCol,
table: appleIdpTemplateTable,
}
AppleInstanceIDCol = Column{
name: projection.AppleInstanceIDCol,
table: appleIdpTemplateTable,
}
AppleClientIDCol = Column{
name: projection.AppleClientIDCol,
table: appleIdpTemplateTable,
}
AppleTeamIDCol = Column{
name: projection.AppleTeamIDCol,
table: appleIdpTemplateTable,
}
AppleKeyIDCol = Column{
name: projection.AppleKeyIDCol,
table: appleIdpTemplateTable,
}
ApplePrivateKeyCol = Column{
name: projection.ApplePrivateKeyCol,
table: appleIdpTemplateTable,
}
AppleScopesCol = Column{
name: projection.AppleScopesCol,
table: appleIdpTemplateTable,
}
)
// IDPTemplateByID searches for the requested id // IDPTemplateByID searches for the requested id
func (q *Queries) IDPTemplateByID(ctx context.Context, shouldTriggerBulk bool, id string, withOwnerRemoved bool, queries ...SearchQuery) (template *IDPTemplate, err error) { func (q *Queries) IDPTemplateByID(ctx context.Context, shouldTriggerBulk bool, id string, withOwnerRemoved bool, queries ...SearchQuery) (template *IDPTemplate, err error) {
ctx, span := tracing.NewSpan(ctx) ctx, span := tracing.NewSpan(ctx)
@ -799,6 +844,13 @@ func prepareIDPTemplateByIDQuery(ctx context.Context, db prepareDatabase) (sq.Se
LDAPPreferredLanguageAttributeCol.identifier(), LDAPPreferredLanguageAttributeCol.identifier(),
LDAPAvatarURLAttributeCol.identifier(), LDAPAvatarURLAttributeCol.identifier(),
LDAPProfileAttributeCol.identifier(), LDAPProfileAttributeCol.identifier(),
// apple
AppleIDCol.identifier(),
AppleClientIDCol.identifier(),
AppleTeamIDCol.identifier(),
AppleKeyIDCol.identifier(),
ApplePrivateKeyCol.identifier(),
AppleScopesCol.identifier(),
).From(idpTemplateTable.identifier()). ).From(idpTemplateTable.identifier()).
LeftJoin(join(OAuthIDCol, IDPTemplateIDCol)). LeftJoin(join(OAuthIDCol, IDPTemplateIDCol)).
LeftJoin(join(OIDCIDCol, IDPTemplateIDCol)). LeftJoin(join(OIDCIDCol, IDPTemplateIDCol)).
@ -809,7 +861,8 @@ func prepareIDPTemplateByIDQuery(ctx context.Context, db prepareDatabase) (sq.Se
LeftJoin(join(GitLabIDCol, IDPTemplateIDCol)). LeftJoin(join(GitLabIDCol, IDPTemplateIDCol)).
LeftJoin(join(GitLabSelfHostedIDCol, IDPTemplateIDCol)). LeftJoin(join(GitLabSelfHostedIDCol, IDPTemplateIDCol)).
LeftJoin(join(GoogleIDCol, IDPTemplateIDCol)). LeftJoin(join(GoogleIDCol, IDPTemplateIDCol)).
LeftJoin(join(LDAPIDCol, IDPTemplateIDCol) + db.Timetravel(call.Took(ctx))). LeftJoin(join(LDAPIDCol, IDPTemplateIDCol)).
LeftJoin(join(AppleIDCol, IDPTemplateIDCol) + db.Timetravel(call.Took(ctx))).
PlaceholderFormat(sq.Dollar), PlaceholderFormat(sq.Dollar),
func(row *sql.Row) (*IDPTemplate, error) { func(row *sql.Row) (*IDPTemplate, error) {
idpTemplate := new(IDPTemplate) idpTemplate := new(IDPTemplate)
@ -898,6 +951,13 @@ func prepareIDPTemplateByIDQuery(ctx context.Context, db prepareDatabase) (sq.Se
ldapAvatarURLAttribute := sql.NullString{} ldapAvatarURLAttribute := sql.NullString{}
ldapProfileAttribute := sql.NullString{} ldapProfileAttribute := sql.NullString{}
appleID := sql.NullString{}
appleClientID := sql.NullString{}
appleTeamID := sql.NullString{}
appleKeyID := sql.NullString{}
applePrivateKey := new(crypto.CryptoValue)
appleScopes := database.StringArray{}
err := row.Scan( err := row.Scan(
&idpTemplate.ID, &idpTemplate.ID,
&idpTemplate.ResourceOwner, &idpTemplate.ResourceOwner,
@ -994,6 +1054,13 @@ func prepareIDPTemplateByIDQuery(ctx context.Context, db prepareDatabase) (sq.Se
&ldapPreferredLanguageAttribute, &ldapPreferredLanguageAttribute,
&ldapAvatarURLAttribute, &ldapAvatarURLAttribute,
&ldapProfileAttribute, &ldapProfileAttribute,
// apple
&appleID,
&appleClientID,
&appleTeamID,
&appleKeyID,
&applePrivateKey,
&appleScopes,
) )
if err != nil { if err != nil {
if errs.Is(err, sql.ErrNoRows) { if errs.Is(err, sql.ErrNoRows) {
@ -1118,6 +1185,16 @@ func prepareIDPTemplateByIDQuery(ctx context.Context, db prepareDatabase) (sq.Se
}, },
} }
} }
if appleID.Valid {
idpTemplate.AppleIDPTemplate = &AppleIDPTemplate{
IDPID: appleID.String,
ClientID: appleClientID.String,
TeamID: appleTeamID.String,
KeyID: appleKeyID.String,
PrivateKey: applePrivateKey,
Scopes: appleScopes,
}
}
return idpTemplate, nil return idpTemplate, nil
} }
@ -1220,6 +1297,14 @@ func prepareIDPTemplatesQuery(ctx context.Context, db prepareDatabase) (sq.Selec
LDAPPreferredLanguageAttributeCol.identifier(), LDAPPreferredLanguageAttributeCol.identifier(),
LDAPAvatarURLAttributeCol.identifier(), LDAPAvatarURLAttributeCol.identifier(),
LDAPProfileAttributeCol.identifier(), LDAPProfileAttributeCol.identifier(),
// apple
AppleIDCol.identifier(),
AppleClientIDCol.identifier(),
AppleTeamIDCol.identifier(),
AppleKeyIDCol.identifier(),
ApplePrivateKeyCol.identifier(),
AppleScopesCol.identifier(),
// count
countColumn.identifier(), countColumn.identifier(),
).From(idpTemplateTable.identifier()). ).From(idpTemplateTable.identifier()).
LeftJoin(join(OAuthIDCol, IDPTemplateIDCol)). LeftJoin(join(OAuthIDCol, IDPTemplateIDCol)).
@ -1231,7 +1316,8 @@ func prepareIDPTemplatesQuery(ctx context.Context, db prepareDatabase) (sq.Selec
LeftJoin(join(GitLabIDCol, IDPTemplateIDCol)). LeftJoin(join(GitLabIDCol, IDPTemplateIDCol)).
LeftJoin(join(GitLabSelfHostedIDCol, IDPTemplateIDCol)). LeftJoin(join(GitLabSelfHostedIDCol, IDPTemplateIDCol)).
LeftJoin(join(GoogleIDCol, IDPTemplateIDCol)). LeftJoin(join(GoogleIDCol, IDPTemplateIDCol)).
LeftJoin(join(LDAPIDCol, IDPTemplateIDCol) + db.Timetravel(call.Took(ctx))). LeftJoin(join(LDAPIDCol, IDPTemplateIDCol)).
LeftJoin(join(AppleIDCol, IDPTemplateIDCol) + db.Timetravel(call.Took(ctx))).
PlaceholderFormat(sq.Dollar), PlaceholderFormat(sq.Dollar),
func(rows *sql.Rows) (*IDPTemplates, error) { func(rows *sql.Rows) (*IDPTemplates, error) {
templates := make([]*IDPTemplate, 0) templates := make([]*IDPTemplate, 0)
@ -1323,6 +1409,13 @@ func prepareIDPTemplatesQuery(ctx context.Context, db prepareDatabase) (sq.Selec
ldapAvatarURLAttribute := sql.NullString{} ldapAvatarURLAttribute := sql.NullString{}
ldapProfileAttribute := sql.NullString{} ldapProfileAttribute := sql.NullString{}
appleID := sql.NullString{}
appleClientID := sql.NullString{}
appleTeamID := sql.NullString{}
appleKeyID := sql.NullString{}
applePrivateKey := new(crypto.CryptoValue)
appleScopes := database.StringArray{}
err := rows.Scan( err := rows.Scan(
&idpTemplate.ID, &idpTemplate.ID,
&idpTemplate.ResourceOwner, &idpTemplate.ResourceOwner,
@ -1419,6 +1512,13 @@ func prepareIDPTemplatesQuery(ctx context.Context, db prepareDatabase) (sq.Selec
&ldapPreferredLanguageAttribute, &ldapPreferredLanguageAttribute,
&ldapAvatarURLAttribute, &ldapAvatarURLAttribute,
&ldapProfileAttribute, &ldapProfileAttribute,
// apple
&appleID,
&appleClientID,
&appleTeamID,
&appleKeyID,
&applePrivateKey,
&appleScopes,
&count, &count,
) )
@ -1542,6 +1642,16 @@ func prepareIDPTemplatesQuery(ctx context.Context, db prepareDatabase) (sq.Selec
}, },
} }
} }
if appleID.Valid {
idpTemplate.AppleIDPTemplate = &AppleIDPTemplate{
IDPID: appleID.String,
ClientID: appleClientID.String,
TeamID: appleTeamID.String,
KeyID: appleKeyID.String,
PrivateKey: applePrivateKey,
Scopes: appleScopes,
}
}
templates = append(templates, idpTemplate) templates = append(templates, idpTemplate)
} }

View File

@ -110,7 +110,14 @@ var (
` projections.idp_templates5_ldap2.phone_verified_attribute,` + ` projections.idp_templates5_ldap2.phone_verified_attribute,` +
` projections.idp_templates5_ldap2.preferred_language_attribute,` + ` projections.idp_templates5_ldap2.preferred_language_attribute,` +
` projections.idp_templates5_ldap2.avatar_url_attribute,` + ` projections.idp_templates5_ldap2.avatar_url_attribute,` +
` projections.idp_templates5_ldap2.profile_attribute` + ` projections.idp_templates5_ldap2.profile_attribute,` +
// apple
` projections.idp_templates5_apple.idp_id,` +
` projections.idp_templates5_apple.client_id,` +
` projections.idp_templates5_apple.team_id,` +
` projections.idp_templates5_apple.key_id,` +
` projections.idp_templates5_apple.private_key,` +
` projections.idp_templates5_apple.scopes` +
` FROM projections.idp_templates5` + ` FROM projections.idp_templates5` +
` LEFT JOIN projections.idp_templates5_oauth2 ON projections.idp_templates5.id = projections.idp_templates5_oauth2.idp_id AND projections.idp_templates5.instance_id = projections.idp_templates5_oauth2.instance_id` + ` LEFT JOIN projections.idp_templates5_oauth2 ON projections.idp_templates5.id = projections.idp_templates5_oauth2.idp_id AND projections.idp_templates5.instance_id = projections.idp_templates5_oauth2.instance_id` +
` LEFT JOIN projections.idp_templates5_oidc ON projections.idp_templates5.id = projections.idp_templates5_oidc.idp_id AND projections.idp_templates5.instance_id = projections.idp_templates5_oidc.instance_id` + ` LEFT JOIN projections.idp_templates5_oidc ON projections.idp_templates5.id = projections.idp_templates5_oidc.idp_id AND projections.idp_templates5.instance_id = projections.idp_templates5_oidc.instance_id` +
@ -122,6 +129,7 @@ var (
` LEFT JOIN projections.idp_templates5_gitlab_self_hosted ON projections.idp_templates5.id = projections.idp_templates5_gitlab_self_hosted.idp_id AND projections.idp_templates5.instance_id = projections.idp_templates5_gitlab_self_hosted.instance_id` + ` LEFT JOIN projections.idp_templates5_gitlab_self_hosted ON projections.idp_templates5.id = projections.idp_templates5_gitlab_self_hosted.idp_id AND projections.idp_templates5.instance_id = projections.idp_templates5_gitlab_self_hosted.instance_id` +
` LEFT JOIN projections.idp_templates5_google ON projections.idp_templates5.id = projections.idp_templates5_google.idp_id AND projections.idp_templates5.instance_id = projections.idp_templates5_google.instance_id` + ` LEFT JOIN projections.idp_templates5_google ON projections.idp_templates5.id = projections.idp_templates5_google.idp_id AND projections.idp_templates5.instance_id = projections.idp_templates5_google.instance_id` +
` LEFT JOIN projections.idp_templates5_ldap2 ON projections.idp_templates5.id = projections.idp_templates5_ldap2.idp_id AND projections.idp_templates5.instance_id = projections.idp_templates5_ldap2.instance_id` + ` LEFT JOIN projections.idp_templates5_ldap2 ON projections.idp_templates5.id = projections.idp_templates5_ldap2.idp_id AND projections.idp_templates5.instance_id = projections.idp_templates5_ldap2.instance_id` +
` LEFT JOIN projections.idp_templates5_apple ON projections.idp_templates5.id = projections.idp_templates5_apple.idp_id AND projections.idp_templates5.instance_id = projections.idp_templates5_apple.instance_id` +
` AS OF SYSTEM TIME '-1 ms'` ` AS OF SYSTEM TIME '-1 ms'`
idpTemplateCols = []string{ idpTemplateCols = []string{
"id", "id",
@ -219,6 +227,13 @@ var (
"preferred_language_attribute", "preferred_language_attribute",
"avatar_url_attribute", "avatar_url_attribute",
"profile_attribute", "profile_attribute",
// apple config
"idp_id",
"client_id",
"team_id",
"key_id",
"private_key",
"scopes",
} }
idpTemplatesQuery = `SELECT projections.idp_templates5.id,` + idpTemplatesQuery = `SELECT projections.idp_templates5.id,` +
` projections.idp_templates5.resource_owner,` + ` projections.idp_templates5.resource_owner,` +
@ -315,6 +330,13 @@ var (
` projections.idp_templates5_ldap2.preferred_language_attribute,` + ` projections.idp_templates5_ldap2.preferred_language_attribute,` +
` projections.idp_templates5_ldap2.avatar_url_attribute,` + ` projections.idp_templates5_ldap2.avatar_url_attribute,` +
` projections.idp_templates5_ldap2.profile_attribute,` + ` projections.idp_templates5_ldap2.profile_attribute,` +
// apple
` projections.idp_templates5_apple.idp_id,` +
` projections.idp_templates5_apple.client_id,` +
` projections.idp_templates5_apple.team_id,` +
` projections.idp_templates5_apple.key_id,` +
` projections.idp_templates5_apple.private_key,` +
` projections.idp_templates5_apple.scopes,` +
` COUNT(*) OVER ()` + ` COUNT(*) OVER ()` +
` FROM projections.idp_templates5` + ` FROM projections.idp_templates5` +
` LEFT JOIN projections.idp_templates5_oauth2 ON projections.idp_templates5.id = projections.idp_templates5_oauth2.idp_id AND projections.idp_templates5.instance_id = projections.idp_templates5_oauth2.instance_id` + ` LEFT JOIN projections.idp_templates5_oauth2 ON projections.idp_templates5.id = projections.idp_templates5_oauth2.idp_id AND projections.idp_templates5.instance_id = projections.idp_templates5_oauth2.instance_id` +
@ -327,6 +349,7 @@ var (
` LEFT JOIN projections.idp_templates5_gitlab_self_hosted ON projections.idp_templates5.id = projections.idp_templates5_gitlab_self_hosted.idp_id AND projections.idp_templates5.instance_id = projections.idp_templates5_gitlab_self_hosted.instance_id` + ` LEFT JOIN projections.idp_templates5_gitlab_self_hosted ON projections.idp_templates5.id = projections.idp_templates5_gitlab_self_hosted.idp_id AND projections.idp_templates5.instance_id = projections.idp_templates5_gitlab_self_hosted.instance_id` +
` LEFT JOIN projections.idp_templates5_google ON projections.idp_templates5.id = projections.idp_templates5_google.idp_id AND projections.idp_templates5.instance_id = projections.idp_templates5_google.instance_id` + ` LEFT JOIN projections.idp_templates5_google ON projections.idp_templates5.id = projections.idp_templates5_google.idp_id AND projections.idp_templates5.instance_id = projections.idp_templates5_google.instance_id` +
` LEFT JOIN projections.idp_templates5_ldap2 ON projections.idp_templates5.id = projections.idp_templates5_ldap2.idp_id AND projections.idp_templates5.instance_id = projections.idp_templates5_ldap2.instance_id` + ` LEFT JOIN projections.idp_templates5_ldap2 ON projections.idp_templates5.id = projections.idp_templates5_ldap2.idp_id AND projections.idp_templates5.instance_id = projections.idp_templates5_ldap2.instance_id` +
` LEFT JOIN projections.idp_templates5_apple ON projections.idp_templates5.id = projections.idp_templates5_apple.idp_id AND projections.idp_templates5.instance_id = projections.idp_templates5_apple.instance_id` +
` AS OF SYSTEM TIME '-1 ms'` ` AS OF SYSTEM TIME '-1 ms'`
idpTemplatesCols = []string{ idpTemplatesCols = []string{
"id", "id",
@ -424,6 +447,13 @@ var (
"preferred_language_attribute", "preferred_language_attribute",
"avatar_url_attribute", "avatar_url_attribute",
"profile_attribute", "profile_attribute",
// apple config
"idp_id",
"client_id",
"team_id",
"key_id",
"private_key",
"scopes",
"count", "count",
} }
) )
@ -560,6 +590,13 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) {
nil, nil,
nil, nil,
nil, nil,
// apple
nil,
nil,
nil,
nil,
nil,
nil,
}, },
), ),
}, },
@ -692,6 +729,13 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) {
nil, nil,
nil, nil,
nil, nil,
// apple
nil,
nil,
nil,
nil,
nil,
nil,
}, },
), ),
}, },
@ -822,6 +866,13 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) {
nil, nil,
nil, nil,
nil, nil,
// apple
nil,
nil,
nil,
nil,
nil,
nil,
}, },
), ),
}, },
@ -951,6 +1002,13 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) {
nil, nil,
nil, nil,
nil, nil,
// apple
nil,
nil,
nil,
nil,
nil,
nil,
}, },
), ),
}, },
@ -1079,6 +1137,13 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) {
nil, nil,
nil, nil,
nil, nil,
// apple
nil,
nil,
nil,
nil,
nil,
nil,
}, },
), ),
}, },
@ -1207,6 +1272,13 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) {
nil, nil,
nil, nil,
nil, nil,
// apple
nil,
nil,
nil,
nil,
nil,
nil,
}, },
), ),
}, },
@ -1336,6 +1408,13 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) {
nil, nil,
nil, nil,
nil, nil,
// apple
nil,
nil,
nil,
nil,
nil,
nil,
}, },
), ),
}, },
@ -1464,6 +1543,13 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) {
"lang", "lang",
"avatar", "avatar",
"profile", "profile",
// apple
nil,
nil,
nil,
nil,
nil,
nil,
}, },
), ),
}, },
@ -1509,6 +1595,143 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) {
}, },
}, },
}, },
{
name: "prepareIDPTemplateByIDQuery apple idp",
prepare: prepareIDPTemplateByIDQuery,
want: want{
sqlExpectations: mockQuery(
regexp.QuoteMeta(idpTemplateQuery),
idpTemplateCols,
[]driver.Value{
"idp-id",
"ro",
testNow,
testNow,
uint64(20211109),
domain.IDPConfigStateActive,
"idp-name",
domain.IDPTypeApple,
domain.IdentityProviderTypeOrg,
true,
true,
true,
true,
// oauth
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
// oidc
nil,
nil,
nil,
nil,
nil,
nil,
// jwt
nil,
nil,
nil,
nil,
nil,
// azure
nil,
nil,
nil,
nil,
nil,
nil,
// github
nil,
nil,
nil,
nil,
// github enterprise
nil,
nil,
nil,
nil,
nil,
nil,
nil,
// gitlab
nil,
nil,
nil,
nil,
// gitlab self hosted
nil,
nil,
nil,
nil,
nil,
// google
nil,
nil,
nil,
nil,
// ldap config
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
// apple
"idp-id",
"client_id",
"team_id",
"key_id",
nil,
database.StringArray{"profile"},
},
),
},
object: &IDPTemplate{
CreationDate: testNow,
ChangeDate: testNow,
Sequence: 20211109,
ResourceOwner: "ro",
ID: "idp-id",
State: domain.IDPStateActive,
Name: "idp-name",
Type: domain.IDPTypeApple,
OwnerType: domain.IdentityProviderTypeOrg,
IsCreationAllowed: true,
IsLinkingAllowed: true,
IsAutoCreation: true,
IsAutoUpdate: true,
AppleIDPTemplate: &AppleIDPTemplate{
IDPID: "idp-id",
ClientID: "client_id",
TeamID: "team_id",
KeyID: "key_id",
PrivateKey: nil,
Scopes: []string{"profile"},
},
},
},
{ {
name: "prepareIDPTemplateByIDQuery no config", name: "prepareIDPTemplateByIDQuery no config",
prepare: prepareIDPTemplateByIDQuery, prepare: prepareIDPTemplateByIDQuery,
@ -1612,6 +1835,13 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) {
nil, nil,
nil, nil,
nil, nil,
// apple
nil,
nil,
nil,
nil,
nil,
nil,
}, },
), ),
}, },
@ -1770,6 +2000,13 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) {
"lang", "lang",
"avatar", "avatar",
"profile", "profile",
// apple
nil,
nil,
nil,
nil,
nil,
nil,
}, },
}, },
), ),
@ -1927,6 +2164,13 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) {
nil, nil,
nil, nil,
nil, nil,
// apple
nil,
nil,
nil,
nil,
nil,
nil,
}, },
}, },
), ),
@ -2058,6 +2302,13 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) {
"lang", "lang",
"avatar", "avatar",
"profile", "profile",
// apple
nil,
nil,
nil,
nil,
nil,
nil,
}, },
{ {
"idp-id-google", "idp-id-google",
@ -2155,6 +2406,13 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) {
nil, nil,
nil, nil,
nil, nil,
// apple
nil,
nil,
nil,
nil,
nil,
nil,
}, },
{ {
"idp-id-oauth", "idp-id-oauth",
@ -2252,6 +2510,13 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) {
nil, nil,
nil, nil,
nil, nil,
// apple
nil,
nil,
nil,
nil,
nil,
nil,
}, },
{ {
"idp-id-oidc", "idp-id-oidc",
@ -2349,6 +2614,13 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) {
nil, nil,
nil, nil,
nil, nil,
// apple
nil,
nil,
nil,
nil,
nil,
nil,
}, },
{ {
"idp-id-jwt", "idp-id-jwt",
@ -2446,6 +2718,13 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) {
nil, nil,
nil, nil,
nil, nil,
// apple
nil,
nil,
nil,
nil,
nil,
nil,
}, },
}, },
), ),

View File

@ -28,6 +28,7 @@ const (
IDPTemplateGitLabSelfHostedTable = IDPTemplateTable + "_" + IDPTemplateGitLabSelfHostedSuffix IDPTemplateGitLabSelfHostedTable = IDPTemplateTable + "_" + IDPTemplateGitLabSelfHostedSuffix
IDPTemplateGoogleTable = IDPTemplateTable + "_" + IDPTemplateGoogleSuffix IDPTemplateGoogleTable = IDPTemplateTable + "_" + IDPTemplateGoogleSuffix
IDPTemplateLDAPTable = IDPTemplateTable + "_" + IDPTemplateLDAPSuffix IDPTemplateLDAPTable = IDPTemplateTable + "_" + IDPTemplateLDAPSuffix
IDPTemplateAppleTable = IDPTemplateTable + "_" + IDPTemplateAppleSuffix
IDPTemplateOAuthSuffix = "oauth2" IDPTemplateOAuthSuffix = "oauth2"
IDPTemplateOIDCSuffix = "oidc" IDPTemplateOIDCSuffix = "oidc"
@ -39,6 +40,7 @@ const (
IDPTemplateGitLabSelfHostedSuffix = "gitlab_self_hosted" IDPTemplateGitLabSelfHostedSuffix = "gitlab_self_hosted"
IDPTemplateGoogleSuffix = "google" IDPTemplateGoogleSuffix = "google"
IDPTemplateLDAPSuffix = "ldap2" IDPTemplateLDAPSuffix = "ldap2"
IDPTemplateAppleSuffix = "apple"
IDPTemplateIDCol = "id" IDPTemplateIDCol = "id"
IDPTemplateCreationDateCol = "creation_date" IDPTemplateCreationDateCol = "creation_date"
@ -147,6 +149,14 @@ const (
LDAPPreferredLanguageAttributeCol = "preferred_language_attribute" LDAPPreferredLanguageAttributeCol = "preferred_language_attribute"
LDAPAvatarURLAttributeCol = "avatar_url_attribute" LDAPAvatarURLAttributeCol = "avatar_url_attribute"
LDAPProfileAttributeCol = "profile_attribute" LDAPProfileAttributeCol = "profile_attribute"
AppleIDCol = "idp_id"
AppleInstanceIDCol = "instance_id"
AppleClientIDCol = "client_id"
AppleTeamIDCol = "team_id"
AppleKeyIDCol = "key_id"
ApplePrivateKeyCol = "private_key"
AppleScopesCol = "scopes"
) )
type idpTemplateProjection struct { type idpTemplateProjection struct {
@ -321,6 +331,19 @@ func newIDPTemplateProjection(ctx context.Context, config crdb.StatementHandlerC
IDPTemplateLDAPSuffix, IDPTemplateLDAPSuffix,
crdb.WithForeignKey(crdb.NewForeignKeyOfPublicKeys()), crdb.WithForeignKey(crdb.NewForeignKeyOfPublicKeys()),
), ),
crdb.NewSuffixedTable([]*crdb.Column{
crdb.NewColumn(AppleIDCol, crdb.ColumnTypeText),
crdb.NewColumn(AppleInstanceIDCol, crdb.ColumnTypeText),
crdb.NewColumn(AppleClientIDCol, crdb.ColumnTypeText),
crdb.NewColumn(AppleTeamIDCol, crdb.ColumnTypeText),
crdb.NewColumn(AppleKeyIDCol, crdb.ColumnTypeText),
crdb.NewColumn(ApplePrivateKeyCol, crdb.ColumnTypeJSONB),
crdb.NewColumn(AppleScopesCol, crdb.ColumnTypeTextArray, crdb.Nullable()),
},
crdb.NewPrimaryKey(AppleInstanceIDCol, AppleIDCol),
IDPTemplateAppleSuffix,
crdb.WithForeignKey(crdb.NewForeignKeyOfPublicKeys()),
),
) )
p.StatementHandler = crdb.NewStatementHandler(ctx, config) p.StatementHandler = crdb.NewStatementHandler(ctx, config)
return p return p
@ -443,6 +466,14 @@ func (p *idpTemplateProjection) reducers() []handler.AggregateReducer {
Event: instance.LDAPIDPChangedEventType, Event: instance.LDAPIDPChangedEventType,
Reduce: p.reduceLDAPIDPChanged, Reduce: p.reduceLDAPIDPChanged,
}, },
{
Event: instance.AppleIDPAddedEventType,
Reduce: p.reduceAppleIDPAdded,
},
{
Event: instance.AppleIDPChangedEventType,
Reduce: p.reduceAppleIDPChanged,
},
{ {
Event: instance.IDPConfigRemovedEventType, Event: instance.IDPConfigRemovedEventType,
Reduce: p.reduceIDPConfigRemoved, Reduce: p.reduceIDPConfigRemoved,
@ -572,6 +603,14 @@ func (p *idpTemplateProjection) reducers() []handler.AggregateReducer {
Event: org.LDAPIDPChangedEventType, Event: org.LDAPIDPChangedEventType,
Reduce: p.reduceLDAPIDPChanged, Reduce: p.reduceLDAPIDPChanged,
}, },
{
Event: org.AppleIDPAddedEventType,
Reduce: p.reduceAppleIDPAdded,
},
{
Event: org.AppleIDPChangedEventType,
Reduce: p.reduceAppleIDPChanged,
},
{ {
Event: org.IDPConfigRemovedEventType, Event: org.IDPConfigRemovedEventType,
Reduce: p.reduceIDPConfigRemoved, Reduce: p.reduceIDPConfigRemoved,
@ -1858,6 +1897,97 @@ func (p *idpTemplateProjection) reduceLDAPIDPChanged(event eventstore.Event) (*h
ops..., ops...,
), nil ), nil
} }
func (p *idpTemplateProjection) reduceAppleIDPAdded(event eventstore.Event) (*handler.Statement, error) {
var idpEvent idp.AppleIDPAddedEvent
var idpOwnerType domain.IdentityProviderType
switch e := event.(type) {
case *org.AppleIDPAddedEvent:
idpEvent = e.AppleIDPAddedEvent
idpOwnerType = domain.IdentityProviderTypeOrg
case *instance.AppleIDPAddedEvent:
idpEvent = e.AppleIDPAddedEvent
idpOwnerType = domain.IdentityProviderTypeSystem
default:
return nil, errors.ThrowInvalidArgumentf(nil, "HANDL-SFvg3", "reduce.wrong.event.type %v", []eventstore.EventType{org.AppleIDPAddedEventType /*, instance.AppleIDPAddedEventType*/})
}
return crdb.NewMultiStatement(
&idpEvent,
crdb.AddCreateStatement(
[]handler.Column{
handler.NewCol(IDPTemplateIDCol, idpEvent.ID),
handler.NewCol(IDPTemplateCreationDateCol, idpEvent.CreationDate()),
handler.NewCol(IDPTemplateChangeDateCol, idpEvent.CreationDate()),
handler.NewCol(IDPTemplateSequenceCol, idpEvent.Sequence()),
handler.NewCol(IDPTemplateResourceOwnerCol, idpEvent.Aggregate().ResourceOwner),
handler.NewCol(IDPTemplateInstanceIDCol, idpEvent.Aggregate().InstanceID),
handler.NewCol(IDPTemplateStateCol, domain.IDPStateActive),
handler.NewCol(IDPTemplateNameCol, idpEvent.Name),
handler.NewCol(IDPTemplateOwnerTypeCol, idpOwnerType),
handler.NewCol(IDPTemplateTypeCol, domain.IDPTypeApple),
handler.NewCol(IDPTemplateIsCreationAllowedCol, idpEvent.IsCreationAllowed),
handler.NewCol(IDPTemplateIsLinkingAllowedCol, idpEvent.IsLinkingAllowed),
handler.NewCol(IDPTemplateIsAutoCreationCol, idpEvent.IsAutoCreation),
handler.NewCol(IDPTemplateIsAutoUpdateCol, idpEvent.IsAutoUpdate),
},
),
crdb.AddCreateStatement(
[]handler.Column{
handler.NewCol(AppleIDCol, idpEvent.ID),
handler.NewCol(AppleInstanceIDCol, idpEvent.Aggregate().InstanceID),
handler.NewCol(AppleClientIDCol, idpEvent.ClientID),
handler.NewCol(AppleTeamIDCol, idpEvent.TeamID),
handler.NewCol(AppleKeyIDCol, idpEvent.KeyID),
handler.NewCol(ApplePrivateKeyCol, idpEvent.PrivateKey),
handler.NewCol(AppleScopesCol, database.StringArray(idpEvent.Scopes)),
},
crdb.WithTableSuffix(IDPTemplateAppleSuffix),
),
), nil
}
func (p *idpTemplateProjection) reduceAppleIDPChanged(event eventstore.Event) (*handler.Statement, error) {
var idpEvent idp.AppleIDPChangedEvent
switch e := event.(type) {
case *org.AppleIDPChangedEvent:
idpEvent = e.AppleIDPChangedEvent
case *instance.AppleIDPChangedEvent:
idpEvent = e.AppleIDPChangedEvent
default:
return nil, errors.ThrowInvalidArgumentf(nil, "HANDL-GBez3", "reduce.wrong.event.type %v", []eventstore.EventType{org.AppleIDPChangedEventType /*, instance.AppleIDPChangedEventType*/})
}
ops := make([]func(eventstore.Event) crdb.Exec, 0, 2)
ops = append(ops,
crdb.AddUpdateStatement(
reduceIDPChangedTemplateColumns(idpEvent.Name, idpEvent.CreationDate(), idpEvent.Sequence(), idpEvent.OptionChanges),
[]handler.Condition{
handler.NewCond(IDPTemplateIDCol, idpEvent.ID),
handler.NewCond(IDPTemplateInstanceIDCol, idpEvent.Aggregate().InstanceID),
},
),
)
appleCols := reduceAppleIDPChangedColumns(idpEvent)
if len(appleCols) > 0 {
ops = append(ops,
crdb.AddUpdateStatement(
appleCols,
[]handler.Condition{
handler.NewCond(AppleIDCol, idpEvent.ID),
handler.NewCond(AppleInstanceIDCol, idpEvent.Aggregate().InstanceID),
},
crdb.WithTableSuffix(IDPTemplateAppleSuffix),
),
)
}
return crdb.NewMultiStatement(
&idpEvent,
ops...,
), nil
}
func (p *idpTemplateProjection) reduceIDPConfigRemoved(event eventstore.Event) (*handler.Statement, error) { func (p *idpTemplateProjection) reduceIDPConfigRemoved(event eventstore.Event) (*handler.Statement, error) {
var idpEvent idpconfig.IDPConfigRemovedEvent var idpEvent idpconfig.IDPConfigRemovedEvent
switch e := event.(type) { switch e := event.(type) {
@ -2176,3 +2306,23 @@ func reduceLDAPIDPChangedColumns(idpEvent idp.LDAPIDPChangedEvent) []handler.Col
} }
return ldapCols return ldapCols
} }
func reduceAppleIDPChangedColumns(idpEvent idp.AppleIDPChangedEvent) []handler.Column {
appleCols := make([]handler.Column, 0, 5)
if idpEvent.ClientID != nil {
appleCols = append(appleCols, handler.NewCol(AppleClientIDCol, *idpEvent.ClientID))
}
if idpEvent.TeamID != nil {
appleCols = append(appleCols, handler.NewCol(AppleTeamIDCol, *idpEvent.TeamID))
}
if idpEvent.KeyID != nil {
appleCols = append(appleCols, handler.NewCol(AppleKeyIDCol, *idpEvent.KeyID))
}
if idpEvent.PrivateKey != nil {
appleCols = append(appleCols, handler.NewCol(ApplePrivateKeyCol, *idpEvent.PrivateKey))
}
if idpEvent.Scopes != nil {
appleCols = append(appleCols, handler.NewCol(AppleScopesCol, database.StringArray(idpEvent.Scopes)))
}
return appleCols
}

View File

@ -2440,6 +2440,268 @@ func TestIDPTemplateProjection_reducesLDAP(t *testing.T) {
} }
} }
func TestIDPTemplateProjection_reducesApple(t *testing.T) {
type args struct {
event func(t *testing.T) eventstore.Event
}
tests := []struct {
name string
args args
reduce func(event eventstore.Event) (*handler.Statement, error)
want wantReduce
}{
{
name: "instance reduceAppleIDPAdded",
args: args{
event: getEvent(testEvent(
repository.EventType(instance.AppleIDPAddedEventType),
instance.AggregateType,
[]byte(`{
"id": "idp-id",
"clientId": "client_id",
"teamId": "team_id",
"keyId": "key_id",
"privateKey": {
"cryptoType": 0,
"algorithm": "RSA-265",
"keyId": "key-id"
},
"scopes": ["name"],
"isCreationAllowed": true,
"isLinkingAllowed": true,
"isAutoCreation": true,
"isAutoUpdate": true
}`),
), instance.AppleIDPAddedEventMapper),
},
reduce: (&idpTemplateProjection{}).reduceAppleIDPAdded,
want: wantReduce{
aggregateType: eventstore.AggregateType("instance"),
sequence: 15,
previousSequence: 10,
executer: &testExecuter{
executions: []execution{
{
expectedStmt: idpTemplateInsertStmt,
expectedArgs: []interface{}{
"idp-id",
anyArg{},
anyArg{},
uint64(15),
"ro-id",
"instance-id",
domain.IDPStateActive,
"",
domain.IdentityProviderTypeSystem,
domain.IDPTypeApple,
true,
true,
true,
true,
},
},
{
expectedStmt: "INSERT INTO projections.idp_templates5_apple (idp_id, instance_id, client_id, team_id, key_id, private_key, scopes) VALUES ($1, $2, $3, $4, $5, $6, $7)",
expectedArgs: []interface{}{
"idp-id",
"instance-id",
"client_id",
"team_id",
"key_id",
anyArg{},
database.StringArray{"name"},
},
},
},
},
},
},
{
name: "org reduceAppleIDPAdded",
args: args{
event: getEvent(testEvent(
repository.EventType(org.AppleIDPAddedEventType),
org.AggregateType,
[]byte(`{
"id": "idp-id",
"clientId": "client_id",
"teamId": "team_id",
"keyId": "key_id",
"privateKey": {
"cryptoType": 0,
"algorithm": "RSA-265",
"keyId": "key-id"
},
"scopes": ["name"],
"isCreationAllowed": true,
"isLinkingAllowed": true,
"isAutoCreation": true,
"isAutoUpdate": true
}`),
), org.AppleIDPAddedEventMapper),
},
reduce: (&idpTemplateProjection{}).reduceAppleIDPAdded,
want: wantReduce{
aggregateType: eventstore.AggregateType("org"),
sequence: 15,
previousSequence: 10,
executer: &testExecuter{
executions: []execution{
{
expectedStmt: idpTemplateInsertStmt,
expectedArgs: []interface{}{
"idp-id",
anyArg{},
anyArg{},
uint64(15),
"ro-id",
"instance-id",
domain.IDPStateActive,
"",
domain.IdentityProviderTypeOrg,
domain.IDPTypeApple,
true,
true,
true,
true,
},
},
{
expectedStmt: "INSERT INTO projections.idp_templates5_apple (idp_id, instance_id, client_id, team_id, key_id, private_key, scopes) VALUES ($1, $2, $3, $4, $5, $6, $7)",
expectedArgs: []interface{}{
"idp-id",
"instance-id",
"client_id",
"team_id",
"key_id",
anyArg{},
database.StringArray{"name"},
},
},
},
},
},
},
{
name: "instance reduceAppleIDPChanged minimal",
args: args{
event: getEvent(testEvent(
repository.EventType(instance.AppleIDPChangedEventType),
instance.AggregateType,
[]byte(`{
"id": "idp-id",
"isCreationAllowed": true,
"clientId": "id"
}`),
), instance.AppleIDPChangedEventMapper),
},
reduce: (&idpTemplateProjection{}).reduceAppleIDPChanged,
want: wantReduce{
aggregateType: eventstore.AggregateType("instance"),
sequence: 15,
previousSequence: 10,
executer: &testExecuter{
executions: []execution{
{
expectedStmt: idpTemplateUpdateMinimalStmt,
expectedArgs: []interface{}{
true,
anyArg{},
uint64(15),
"idp-id",
"instance-id",
},
},
{
expectedStmt: "UPDATE projections.idp_templates5_apple SET client_id = $1 WHERE (idp_id = $2) AND (instance_id = $3)",
expectedArgs: []interface{}{
"id",
"idp-id",
"instance-id",
},
},
},
},
},
},
{
name: "instance reduceAppleIDPChanged",
args: args{
event: getEvent(testEvent(
repository.EventType(instance.AppleIDPChangedEventType),
instance.AggregateType,
[]byte(`{
"id": "idp-id",
"name": "name",
"clientId": "client_id",
"teamId": "team_id",
"keyId": "key_id",
"privateKey": {
"cryptoType": 0,
"algorithm": "RSA-265",
"keyId": "key-id"
},
"scopes": ["name"],
"isCreationAllowed": true,
"isLinkingAllowed": true,
"isAutoCreation": true,
"isAutoUpdate": true
}`),
), instance.AppleIDPChangedEventMapper),
},
reduce: (&idpTemplateProjection{}).reduceAppleIDPChanged,
want: wantReduce{
aggregateType: eventstore.AggregateType("instance"),
sequence: 15,
previousSequence: 10,
executer: &testExecuter{
executions: []execution{
{
expectedStmt: idpTemplateUpdateStmt,
expectedArgs: []interface{}{
"name",
true,
true,
true,
true,
anyArg{},
uint64(15),
"idp-id",
"instance-id",
},
},
{
expectedStmt: "UPDATE projections.idp_templates5_apple SET (client_id, team_id, key_id, private_key, scopes) = ($1, $2, $3, $4, $5) WHERE (idp_id = $6) AND (instance_id = $7)",
expectedArgs: []interface{}{
"client_id",
"team_id",
"key_id",
anyArg{},
database.StringArray{"name"},
"idp-id",
"instance-id",
},
},
},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
event := baseEvent(t)
got, err := tt.reduce(event)
if !errors.IsErrorInvalidArgument(err) {
t.Errorf("no wrong event mapping: %v, got: %v", err, got)
}
event = tt.args.event(t)
got, err = tt.reduce(event)
assertReduce(t, got, err, IDPTemplateTable, tt.want)
})
}
}
func TestIDPTemplateProjection_reducesOIDC(t *testing.T) { func TestIDPTemplateProjection_reducesOIDC(t *testing.T) {
type args struct { type args struct {
event func(t *testing.T) eventstore.Event event func(t *testing.T) eventstore.Event

View File

@ -0,0 +1,164 @@
package idp
import (
"encoding/json"
"github.com/zitadel/zitadel/internal/crypto"
"github.com/zitadel/zitadel/internal/errors"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/eventstore/repository"
)
type AppleIDPAddedEvent struct {
eventstore.BaseEvent `json:"-"`
ID string `json:"id"`
Name string `json:"name,omitempty"`
ClientID string `json:"clientId"`
TeamID string `json:"teamId"`
KeyID string `json:"keyId"`
PrivateKey *crypto.CryptoValue `json:"privateKey"`
Scopes []string `json:"scopes,omitempty"`
Options
}
func NewAppleIDPAddedEvent(
base *eventstore.BaseEvent,
id,
name,
clientID,
teamID,
keyID string,
privateKey *crypto.CryptoValue,
scopes []string,
options Options,
) *AppleIDPAddedEvent {
return &AppleIDPAddedEvent{
BaseEvent: *base,
ID: id,
Name: name,
ClientID: clientID,
TeamID: teamID,
KeyID: keyID,
PrivateKey: privateKey,
Scopes: scopes,
Options: options,
}
}
func (e *AppleIDPAddedEvent) Data() interface{} {
return e
}
func (e *AppleIDPAddedEvent) UniqueConstraints() []*eventstore.EventUniqueConstraint {
return nil
}
func AppleIDPAddedEventMapper(event *repository.Event) (eventstore.Event, error) {
e := &AppleIDPAddedEvent{
BaseEvent: *eventstore.BaseEventFromRepo(event),
}
err := json.Unmarshal(event.Data, e)
if err != nil {
return nil, errors.ThrowInternal(err, "IDP-Beqss", "unable to unmarshal event")
}
return e, nil
}
type AppleIDPChangedEvent struct {
eventstore.BaseEvent `json:"-"`
ID string `json:"id"`
Name *string `json:"name,omitempty"`
ClientID *string `json:"clientId,omitempty"`
TeamID *string `json:"teamId,omitempty"`
KeyID *string `json:"keyId,omitempty"`
PrivateKey *crypto.CryptoValue `json:"privateKey,omitempty"`
Scopes []string `json:"scopes,omitempty"`
OptionChanges
}
func NewAppleIDPChangedEvent(
base *eventstore.BaseEvent,
id string,
changes []AppleIDPChanges,
) (*AppleIDPChangedEvent, error) {
if len(changes) == 0 {
return nil, errors.ThrowPreconditionFailed(nil, "IDP-SF3h2", "Errors.NoChangesFound")
}
changedEvent := &AppleIDPChangedEvent{
BaseEvent: *base,
ID: id,
}
for _, change := range changes {
change(changedEvent)
}
return changedEvent, nil
}
type AppleIDPChanges func(*AppleIDPChangedEvent)
func ChangeAppleName(name string) func(*AppleIDPChangedEvent) {
return func(e *AppleIDPChangedEvent) {
e.Name = &name
}
}
func ChangeAppleClientID(clientID string) func(*AppleIDPChangedEvent) {
return func(e *AppleIDPChangedEvent) {
e.ClientID = &clientID
}
}
func ChangeAppleTeamID(teamID string) func(*AppleIDPChangedEvent) {
return func(e *AppleIDPChangedEvent) {
e.TeamID = &teamID
}
}
func ChangeAppleKeyID(keyID string) func(*AppleIDPChangedEvent) {
return func(e *AppleIDPChangedEvent) {
e.KeyID = &keyID
}
}
func ChangeApplePrivateKey(privateKey *crypto.CryptoValue) func(*AppleIDPChangedEvent) {
return func(e *AppleIDPChangedEvent) {
e.PrivateKey = privateKey
}
}
func ChangeAppleScopes(scopes []string) func(*AppleIDPChangedEvent) {
return func(e *AppleIDPChangedEvent) {
e.Scopes = scopes
}
}
func ChangeAppleOptions(options OptionChanges) func(*AppleIDPChangedEvent) {
return func(e *AppleIDPChangedEvent) {
e.OptionChanges = options
}
}
func (e *AppleIDPChangedEvent) Data() interface{} {
return e
}
func (e *AppleIDPChangedEvent) UniqueConstraints() []*eventstore.EventUniqueConstraint {
return nil
}
func AppleIDPChangedEventMapper(event *repository.Event) (eventstore.Event, error) {
e := &AppleIDPChangedEvent{
BaseEvent: *eventstore.BaseEventFromRepo(event),
}
err := json.Unmarshal(event.Data, e)
if err != nil {
return nil, errors.ThrowInternal(err, "IDP-NBe1s", "unable to unmarshal event")
}
return e, nil
}

View File

@ -92,6 +92,8 @@ func RegisterEventMappers(es *eventstore.Eventstore) {
RegisterFilterEventMapper(AggregateType, GoogleIDPChangedEventType, GoogleIDPChangedEventMapper). RegisterFilterEventMapper(AggregateType, GoogleIDPChangedEventType, GoogleIDPChangedEventMapper).
RegisterFilterEventMapper(AggregateType, LDAPIDPAddedEventType, LDAPIDPAddedEventMapper). RegisterFilterEventMapper(AggregateType, LDAPIDPAddedEventType, LDAPIDPAddedEventMapper).
RegisterFilterEventMapper(AggregateType, LDAPIDPChangedEventType, LDAPIDPChangedEventMapper). RegisterFilterEventMapper(AggregateType, LDAPIDPChangedEventType, LDAPIDPChangedEventMapper).
RegisterFilterEventMapper(AggregateType, AppleIDPAddedEventType, AppleIDPAddedEventMapper).
RegisterFilterEventMapper(AggregateType, AppleIDPChangedEventType, AppleIDPChangedEventMapper).
RegisterFilterEventMapper(AggregateType, IDPRemovedEventType, IDPRemovedEventMapper). RegisterFilterEventMapper(AggregateType, IDPRemovedEventType, IDPRemovedEventMapper).
RegisterFilterEventMapper(AggregateType, LoginPolicyIDPProviderAddedEventType, IdentityProviderAddedEventMapper). RegisterFilterEventMapper(AggregateType, LoginPolicyIDPProviderAddedEventType, IdentityProviderAddedEventMapper).
RegisterFilterEventMapper(AggregateType, LoginPolicyIDPProviderRemovedEventType, IdentityProviderRemovedEventMapper). RegisterFilterEventMapper(AggregateType, LoginPolicyIDPProviderRemovedEventType, IdentityProviderRemovedEventMapper).

View File

@ -33,6 +33,8 @@ const (
GoogleIDPChangedEventType eventstore.EventType = "instance.idp.google.changed" GoogleIDPChangedEventType eventstore.EventType = "instance.idp.google.changed"
LDAPIDPAddedEventType eventstore.EventType = "instance.idp.ldap.v2.added" LDAPIDPAddedEventType eventstore.EventType = "instance.idp.ldap.v2.added"
LDAPIDPChangedEventType eventstore.EventType = "instance.idp.ldap.v2.changed" LDAPIDPChangedEventType eventstore.EventType = "instance.idp.ldap.v2.changed"
AppleIDPAddedEventType eventstore.EventType = "instance.idp.apple.added"
AppleIDPChangedEventType eventstore.EventType = "instance.idp.apple.changed"
IDPRemovedEventType eventstore.EventType = "instance.idp.removed" IDPRemovedEventType eventstore.EventType = "instance.idp.removed"
) )
@ -920,6 +922,86 @@ func LDAPIDPChangedEventMapper(event *repository.Event) (eventstore.Event, error
return &LDAPIDPChangedEvent{LDAPIDPChangedEvent: *e.(*idp.LDAPIDPChangedEvent)}, nil return &LDAPIDPChangedEvent{LDAPIDPChangedEvent: *e.(*idp.LDAPIDPChangedEvent)}, nil
} }
type AppleIDPAddedEvent struct {
idp.AppleIDPAddedEvent
}
func NewAppleIDPAddedEvent(
ctx context.Context,
aggregate *eventstore.Aggregate,
id,
name,
clientID,
teamID,
keyID string,
privateKey *crypto.CryptoValue,
scopes []string,
options idp.Options,
) *AppleIDPAddedEvent {
return &AppleIDPAddedEvent{
AppleIDPAddedEvent: *idp.NewAppleIDPAddedEvent(
eventstore.NewBaseEventForPush(
ctx,
aggregate,
AppleIDPAddedEventType,
),
id,
name,
clientID,
teamID,
keyID,
privateKey,
scopes,
options,
),
}
}
func AppleIDPAddedEventMapper(event *repository.Event) (eventstore.Event, error) {
e, err := idp.AppleIDPAddedEventMapper(event)
if err != nil {
return nil, err
}
return &AppleIDPAddedEvent{AppleIDPAddedEvent: *e.(*idp.AppleIDPAddedEvent)}, nil
}
type AppleIDPChangedEvent struct {
idp.AppleIDPChangedEvent
}
func NewAppleIDPChangedEvent(
ctx context.Context,
aggregate *eventstore.Aggregate,
id string,
changes []idp.AppleIDPChanges,
) (*AppleIDPChangedEvent, error) {
changedEvent, err := idp.NewAppleIDPChangedEvent(
eventstore.NewBaseEventForPush(
ctx,
aggregate,
AppleIDPChangedEventType,
),
id,
changes,
)
if err != nil {
return nil, err
}
return &AppleIDPChangedEvent{AppleIDPChangedEvent: *changedEvent}, nil
}
func AppleIDPChangedEventMapper(event *repository.Event) (eventstore.Event, error) {
e, err := idp.AppleIDPChangedEventMapper(event)
if err != nil {
return nil, err
}
return &AppleIDPChangedEvent{AppleIDPChangedEvent: *e.(*idp.AppleIDPChangedEvent)}, nil
}
type IDPRemovedEvent struct { type IDPRemovedEvent struct {
idp.RemovedEvent idp.RemovedEvent
} }

View File

@ -101,6 +101,8 @@ func RegisterEventMappers(es *eventstore.Eventstore) {
RegisterFilterEventMapper(AggregateType, GoogleIDPChangedEventType, GoogleIDPChangedEventMapper). RegisterFilterEventMapper(AggregateType, GoogleIDPChangedEventType, GoogleIDPChangedEventMapper).
RegisterFilterEventMapper(AggregateType, LDAPIDPAddedEventType, LDAPIDPAddedEventMapper). RegisterFilterEventMapper(AggregateType, LDAPIDPAddedEventType, LDAPIDPAddedEventMapper).
RegisterFilterEventMapper(AggregateType, LDAPIDPChangedEventType, LDAPIDPChangedEventMapper). RegisterFilterEventMapper(AggregateType, LDAPIDPChangedEventType, LDAPIDPChangedEventMapper).
RegisterFilterEventMapper(AggregateType, AppleIDPAddedEventType, AppleIDPAddedEventMapper).
RegisterFilterEventMapper(AggregateType, AppleIDPChangedEventType, AppleIDPChangedEventMapper).
RegisterFilterEventMapper(AggregateType, IDPRemovedEventType, IDPRemovedEventMapper). RegisterFilterEventMapper(AggregateType, IDPRemovedEventType, IDPRemovedEventMapper).
RegisterFilterEventMapper(AggregateType, TriggerActionsSetEventType, TriggerActionsSetEventMapper). RegisterFilterEventMapper(AggregateType, TriggerActionsSetEventType, TriggerActionsSetEventMapper).
RegisterFilterEventMapper(AggregateType, TriggerActionsCascadeRemovedEventType, TriggerActionsCascadeRemovedEventMapper). RegisterFilterEventMapper(AggregateType, TriggerActionsCascadeRemovedEventType, TriggerActionsCascadeRemovedEventMapper).

View File

@ -33,6 +33,8 @@ const (
GoogleIDPChangedEventType eventstore.EventType = "org.idp.google.changed" GoogleIDPChangedEventType eventstore.EventType = "org.idp.google.changed"
LDAPIDPAddedEventType eventstore.EventType = "org.idp.ldap.added" LDAPIDPAddedEventType eventstore.EventType = "org.idp.ldap.added"
LDAPIDPChangedEventType eventstore.EventType = "org.idp.ldap.changed" LDAPIDPChangedEventType eventstore.EventType = "org.idp.ldap.changed"
AppleIDPAddedEventType eventstore.EventType = "org.idp.apple.added"
AppleIDPChangedEventType eventstore.EventType = "org.idp.apple.changed"
IDPRemovedEventType eventstore.EventType = "org.idp.removed" IDPRemovedEventType eventstore.EventType = "org.idp.removed"
) )
@ -920,6 +922,86 @@ func LDAPIDPChangedEventMapper(event *repository.Event) (eventstore.Event, error
return &LDAPIDPChangedEvent{LDAPIDPChangedEvent: *e.(*idp.LDAPIDPChangedEvent)}, nil return &LDAPIDPChangedEvent{LDAPIDPChangedEvent: *e.(*idp.LDAPIDPChangedEvent)}, nil
} }
type AppleIDPAddedEvent struct {
idp.AppleIDPAddedEvent
}
func NewAppleIDPAddedEvent(
ctx context.Context,
aggregate *eventstore.Aggregate,
id,
name,
clientID,
teamID,
keyID string,
privateKey *crypto.CryptoValue,
scopes []string,
options idp.Options,
) *AppleIDPAddedEvent {
return &AppleIDPAddedEvent{
AppleIDPAddedEvent: *idp.NewAppleIDPAddedEvent(
eventstore.NewBaseEventForPush(
ctx,
aggregate,
AppleIDPAddedEventType,
),
id,
name,
clientID,
teamID,
keyID,
privateKey,
scopes,
options,
),
}
}
func AppleIDPAddedEventMapper(event *repository.Event) (eventstore.Event, error) {
e, err := idp.AppleIDPAddedEventMapper(event)
if err != nil {
return nil, err
}
return &AppleIDPAddedEvent{AppleIDPAddedEvent: *e.(*idp.AppleIDPAddedEvent)}, nil
}
type AppleIDPChangedEvent struct {
idp.AppleIDPChangedEvent
}
func NewAppleIDPChangedEvent(
ctx context.Context,
aggregate *eventstore.Aggregate,
id string,
changes []idp.AppleIDPChanges,
) (*AppleIDPChangedEvent, error) {
changedEvent, err := idp.NewAppleIDPChangedEvent(
eventstore.NewBaseEventForPush(
ctx,
aggregate,
AppleIDPChangedEventType,
),
id,
changes,
)
if err != nil {
return nil, err
}
return &AppleIDPChangedEvent{AppleIDPChangedEvent: *changedEvent}, nil
}
func AppleIDPChangedEventMapper(event *repository.Event) (eventstore.Event, error) {
e, err := idp.AppleIDPChangedEventMapper(event)
if err != nil {
return nil, err
}
return &AppleIDPChangedEvent{AppleIDPChangedEvent: *e.(*idp.AppleIDPChangedEvent)}, nil
}
type IDPRemovedEvent struct { type IDPRemovedEvent struct {
idp.RemovedEvent idp.RemovedEvent
} }

View File

@ -201,6 +201,10 @@ Errors:
InvalidCharacter: 'Само буквено-цифрови знаци, . ' InvalidCharacter: 'Само буквено-цифрови знаци, . '
IDP: IDP:
InvalidSearchQuery: Невалидна заявка за търсене InvalidSearchQuery: Невалидна заявка за търсене
ClientIDMissing: Липсва ClientID
TeamIDMissing: TeamID липсва
KeyIDMissing: Липсва KeyID
PrivateKeyMissing: Липсва частен ключ
LoginPolicy: LoginPolicy:
NotFound: Правилата за влизане не са намерени NotFound: Правилата за влизане не са намерени
Invalid: Правилата за влизане са невалидни Invalid: Правилата за влизане са невалидни

View File

@ -199,6 +199,10 @@ Errors:
InvalidCharacter: Nur alphanumerische Zeichen, . und - sind für eine Domäne erlaubt InvalidCharacter: Nur alphanumerische Zeichen, . und - sind für eine Domäne erlaubt
IDP: IDP:
InvalidSearchQuery: Ungültiger Suchparameter InvalidSearchQuery: Ungültiger Suchparameter
ClientIDMissing: ClientID fehlt
TeamIDMissing: TeamID fehlt
KeyIDMissing: KeyID fehlt
PrivateKeyMissing: Private Key fehlt
LoginPolicy: LoginPolicy:
NotFound: Login Policy konnte nicht gefunden werden NotFound: Login Policy konnte nicht gefunden werden
Invalid: Login Policy ist ungültig Invalid: Login Policy ist ungültig

View File

@ -199,6 +199,10 @@ Errors:
InvalidCharacter: Only alphanumeric characters, . and - are allowed for a domain InvalidCharacter: Only alphanumeric characters, . and - are allowed for a domain
IDP: IDP:
InvalidSearchQuery: Invalid search query InvalidSearchQuery: Invalid search query
ClientIDMissing: ClientID missing
TeamIDMissing: TeamID missing
KeyIDMissing: KeyID missing
PrivateKeyMissing: Private Key missing
LoginPolicy: LoginPolicy:
NotFound: Login Policy not found NotFound: Login Policy not found
Invalid: Login Policy is invalid Invalid: Login Policy is invalid

View File

@ -199,6 +199,10 @@ Errors:
InvalidCharacter: Solo caracteres alfanuméricos, . y - se permiten para un dominio InvalidCharacter: Solo caracteres alfanuméricos, . y - se permiten para un dominio
IDP: IDP:
InvalidSearchQuery: Consulta de búsqueda no válida InvalidSearchQuery: Consulta de búsqueda no válida
ClientIDMissing: Falta ClientID
TeamIDMissing: Falta TeamID
KeyIDMissing: Falta KeyID
PrivateKeyMissing: Falta la clave privada
LoginPolicy: LoginPolicy:
NotFound: Política de inicio de sesión no encontrada NotFound: Política de inicio de sesión no encontrada
Invalid: Política de inicio de sesión no es válida Invalid: Política de inicio de sesión no es válida

View File

@ -199,6 +199,10 @@ Errors:
InvalidCharacter: Seuls les caractères alphanumériques, . et - sont autorisés pour un domaine InvalidCharacter: Seuls les caractères alphanumériques, . et - sont autorisés pour un domaine
IDP: IDP:
InvalidSearchQuery: Paramètre de recherche non valide InvalidSearchQuery: Paramètre de recherche non valide
ClientIDMissing: ID client manquant
TeamIDMissing: TeamID manquant
KeyIDMissing: ID de clé manquant
PrivateKeyMissing: clé privée manquante
LoginPolicy: LoginPolicy:
NotFound: Politique de connexion non trouvée NotFound: Politique de connexion non trouvée
Invalid: La politique de connexion n'est pas valide Invalid: La politique de connexion n'est pas valide

View File

@ -199,6 +199,10 @@ Errors:
IDP: IDP:
InvalidSearchQuery: Parametro di ricerca non valido InvalidSearchQuery: Parametro di ricerca non valido
InvalidCharacter: Per un dominio sono ammessi solo caratteri alfanumerici, . e - InvalidCharacter: Per un dominio sono ammessi solo caratteri alfanumerici, . e -
ClientIDMissing: ClientID mancante
TeamIDMissing: TeamID mancante
KeyIDMissing: ID chiave mancante
PrivateKeyMissing: Chiave privata mancante
LoginPolicy: LoginPolicy:
NotFound: Impostazioni di accesso non trovati NotFound: Impostazioni di accesso non trovati
Invalid: Impostazioni di accesso non sono validi Invalid: Impostazioni di accesso non sono validi

View File

@ -191,6 +191,10 @@ Errors:
InvalidCharacter: ドメインは英数字、'.'、'-'のみ使用可能です。 InvalidCharacter: ドメインは英数字、'.'、'-'のみ使用可能です。
IDP: IDP:
InvalidSearchQuery: 無効な検索クエリです InvalidSearchQuery: 無効な検索クエリです
ClientIDMissing: クライアントIDがありません
TeamIDMissing: チームIDがありません
KeyIDMissing: キーIDがありません
PrivateKeyMissing: 秘密キーがありません
LoginPolicy: LoginPolicy:
NotFound: ログインポリシーが見つかりません NotFound: ログインポリシーが見つかりません
Invalid: 無効なログインポリシーです Invalid: 無効なログインポリシーです

View File

@ -199,6 +199,10 @@ Errors:
InvalidCharacter: Дозволени се само алфанумерички знаци, . и - се дозволени за домен InvalidCharacter: Дозволени се само алфанумерички знаци, . и - се дозволени за домен
IDP: IDP:
InvalidSearchQuery: Невалидно пребарување InvalidSearchQuery: Невалидно пребарување
ClientID Missing: ClientID недостасува
TeamIDMissing: TeamID недостасува
Клучен ID Недостасува: Недостасува ID на клуч
PrivateKeyMissing: Недостасува приватен клуч
LoginPolicy: LoginPolicy:
NotFound: Политиката за најавување не е пронајдена NotFound: Политиката за најавување не е пронајдена
Invalid: Политиката за најавување е невалидна Invalid: Политиката за најавување е невалидна

View File

@ -199,6 +199,10 @@ Errors:
InvalidCharacter: Tylko znaki alfanumeryczne, . i - są dozwolone dla domeny InvalidCharacter: Tylko znaki alfanumeryczne, . i - są dozwolone dla domeny
IDP: IDP:
InvalidSearchQuery: Nieprawidłowe zapytanie wyszukiwania InvalidSearchQuery: Nieprawidłowe zapytanie wyszukiwania
ClientIDMissing: Brak ClientID
TeamIDMissing: Brak TeamID
KeyIDMissing: Brak KeyID
PrivateKeyMissing: Brak klucza prywatnego
LoginPolicy: LoginPolicy:
NotFound: Polityka logowania nie znaleziona NotFound: Polityka logowania nie znaleziona
Invalid: Polityka logowania jest nieprawidłowa Invalid: Polityka logowania jest nieprawidłowa

View File

@ -197,6 +197,10 @@ Errors:
InvalidCharacter: Apenas caracteres alfanuméricos, . e - são permitidos para um domínio InvalidCharacter: Apenas caracteres alfanuméricos, . e - são permitidos para um domínio
IDP: IDP:
InvalidSearchQuery: Consulta de pesquisa inválida InvalidSearchQuery: Consulta de pesquisa inválida
ClientIDMissing: ClientID ausente
TeamIDMissing: TeamID ausente
KeyIDMissing: KeyID ausente
PrivateKeyMissing: Chave privada ausente
LoginPolicy: LoginPolicy:
NotFound: Política de login não encontrada NotFound: Política de login não encontrada
Invalid: Política de login é inválida Invalid: Política de login é inválida

View File

@ -199,6 +199,10 @@ Errors:
InvalidCharacter: 只有字母数字字符,.和 - 允许用于域名中 InvalidCharacter: 只有字母数字字符,.和 - 允许用于域名中
IDP: IDP:
InvalidSearchQuery: 无效的搜索查询 InvalidSearchQuery: 无效的搜索查询
ClientIDMissing: 客户端 ID 丢失
TeamIDMissing: 团队 ID 丢失
KeyIDMissing: 密钥 ID 丢失
PrivateKeyMissing: 私钥丢失
LoginPolicy: LoginPolicy:
NotFound: 未找到登录策略 NotFound: 未找到登录策略
Invalid: 登录策略无效 Invalid: 登录策略无效

View File

@ -1648,6 +1648,42 @@ service AdminService {
}; };
} }
// Add a new Apple identity provider on the instance
rpc AddAppleProvider(AddAppleProviderRequest) returns (AddAppleProviderResponse) {
option (google.api.http) = {
post: "/idps/apple"
body: "*"
};
option (zitadel.v1.auth_option) = {
permission: "iam.idp.write"
};
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
tags: "Identity Providers";
summary: "Add Apple Identity Provider";
description: "";
};
}
// Change an existing Apple identity provider on the instance
rpc UpdateAppleProvider(UpdateAppleProviderRequest) returns (UpdateAppleProviderResponse) {
option (google.api.http) = {
put: "/idps/apple/{id}"
body: "*"
};
option (zitadel.v1.auth_option) = {
permission: "iam.idp.write"
};
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
tags: "Identity Providers";
summary: "Update Apple Identity Provider";
description: "";
};
}
// Remove an identity provider // Remove an identity provider
// Will remove all linked providers of this configuration on the users // Will remove all linked providers of this configuration on the users
rpc DeleteProvider(DeleteProviderRequest) returns (DeleteProviderResponse) { rpc DeleteProvider(DeleteProviderRequest) returns (DeleteProviderResponse) {
@ -5564,6 +5600,134 @@ message UpdateLDAPProviderResponse {
zitadel.v1.ObjectDetails details = 1; zitadel.v1.ObjectDetails details = 1;
} }
message AddAppleProviderRequest {
// Apple will be used as default, if no name is provided
string name = 1 [
(validate.rules).string = {max_len: 200},
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
max_length: 200;
example: "\"Apple\"";
description: "Apple will be used as default, if no name is provided";
}
];
string client_id = 2 [
(validate.rules).string = {min_len: 1, max_len: 200},
(google.api.field_behavior) = REQUIRED,
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
min_length: 1;
max_length: 200;
example: "\"client-id\"";
description: "Client id (App ID or Service ID) provided by Apple";
}
];
string team_id = 3 [
(validate.rules).string = {len: 10},
(google.api.field_behavior) = REQUIRED,
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
min_length: 10;
max_length: 10;
example: "\"ALT03JV3OS\"";
description: "(10-character) Team ID provided by Apple";
}
];
string key_id = 4 [
(validate.rules).string = {len: 10},
(google.api.field_behavior) = REQUIRED,
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
min_length: 10;
max_length: 10;
example: "\"OGKDK25KD\"";
description: "(10-character) ID of the private key generated by Apple";
}
];
bytes private_key = 5 [
(validate.rules).bytes = {min_len: 1, max_len: 5000},
(google.api.field_behavior) = REQUIRED,
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
min_length: 1;
max_length: 5000;
example: "\"LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCk1...\"";
description: "Private Key generated by Apple";
}
];
repeated string scopes = 6 [
(validate.rules).repeated = {max_items: 20, items: {string: {min_len: 1, max_len: 100}}},
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
max_items: 20,
example: "[\"name\", \"email\"]";
description: "The scopes requested by ZITADEL during the request to Apple";
}
];
zitadel.idp.v1.Options provider_options = 7;
}
message AddAppleProviderResponse {
zitadel.v1.ObjectDetails details = 1;
string id = 2;
}
message UpdateAppleProviderRequest {
string id = 1 [(validate.rules).string = {min_len: 1, max_len: 200}];
string name = 2 [
(validate.rules).string = {max_len: 200},
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
max_length: 200,
example: "\"Apple\"";
}
];
string client_id = 3 [
(validate.rules).string = {min_len: 1, max_len: 200},
(google.api.field_behavior) = REQUIRED,
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
min_length: 1;
max_length: 200;
example: "\"client-id\"";
description: "Client id (App ID or Service ID) provided by Apple";
}
];
string team_id = 4 [
(validate.rules).string = {len: 10},
(google.api.field_behavior) = REQUIRED,
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
min_length: 10;
max_length: 10;
example: "\"ALT03JV3OS\"";
description: "(10-character) Team ID provided by Apple";
}
];
string key_id = 5 [
(validate.rules).string = {len: 10},
(google.api.field_behavior) = REQUIRED,
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
min_length: 10;
max_length: 10;
example: "\"OGKDK25KD\"";
description: "(10-character) ID of the private key generated by Apple";
}
];
bytes private_key = 6 [
(validate.rules).bytes = {max_len: 5000},
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
max_length: 5000,
example: "\"LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCk1...\"";
description: "Private Key generated by Apple";
}
];
repeated string scopes = 7 [
(validate.rules).repeated = {max_items: 20, items: {string: {min_len: 1, max_len: 100}}},
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
max_items: 20,
example: "[\"openid\", \"profile\", \"email\"]";
description: "The scopes requested by ZITADEL during the request to Apple";
}
];
zitadel.idp.v1.Options provider_options = 8;
}
message UpdateAppleProviderResponse {
zitadel.v1.ObjectDetails details = 1;
}
message DeleteProviderRequest { message DeleteProviderRequest {
string id = 1 [(validate.rules).string = {min_len: 1, max_len: 200}]; string id = 1 [(validate.rules).string = {min_len: 1, max_len: 200}];
} }

View File

@ -266,6 +266,7 @@ enum ProviderType {
PROVIDER_TYPE_GITLAB = 8; PROVIDER_TYPE_GITLAB = 8;
PROVIDER_TYPE_GITLAB_SELF_HOSTED = 9; PROVIDER_TYPE_GITLAB_SELF_HOSTED = 9;
PROVIDER_TYPE_GOOGLE = 10; PROVIDER_TYPE_GOOGLE = 10;
PROVIDER_TYPE_APPLE = 11;
} }
message ProviderConfig { message ProviderConfig {
@ -281,6 +282,7 @@ message ProviderConfig {
GitLabConfig gitlab = 9; GitLabConfig gitlab = 9;
GitLabSelfHostedConfig gitlab_self_hosted = 10; GitLabSelfHostedConfig gitlab_self_hosted = 10;
AzureADConfig azure_ad = 11; AzureADConfig azure_ad = 11;
AppleConfig apple = 12;
} }
} }
@ -517,3 +519,30 @@ message AzureADTenant {
string tenant_id = 2; string tenant_id = 2;
} }
} }
message AppleConfig {
string client_id = 1 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "\"com.client.id\"";
description: "Client id (App ID or Service ID) provided by Apple";
}
];
string team_id = 2 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "\"ALT03JV3OS\"";
description: "Team ID provided by Apple";
}
];
string key_id = 3 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "\"OGKDK25KD\"";
description: "ID of the private key generated by Apple";
}
];
repeated string scopes = 4 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "[\"name\", \"email\"]";
description: "the scopes requested by ZITADEL during the request to Apple";
}
];
}

View File

@ -7058,6 +7058,42 @@ service ManagementService {
}; };
} }
// Add a new Apple identity provider in the organization
rpc AddAppleProvider(AddAppleProviderRequest) returns (AddAppleProviderResponse) {
option (google.api.http) = {
post: "/idps/apple"
body: "*"
};
option (zitadel.v1.auth_option) = {
permission: "org.idp.write"
};
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
tags: "Identity Providers";
summary: "Add Apple Identity Provider";
description: "";
};
}
// Change an existing Apple identity provider in the organization
rpc UpdateAppleProvider(UpdateAppleProviderRequest) returns (UpdateAppleProviderResponse) {
option (google.api.http) = {
put: "/idps/apple/{id}"
body: "*"
};
option (zitadel.v1.auth_option) = {
permission: "org.idp.write"
};
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
tags: "Identity Providers";
summary: "Update Apple Identity Provider";
description: "";
};
}
// Remove an identity provider // Remove an identity provider
// Will remove all linked providers of this configuration on the users // Will remove all linked providers of this configuration on the users
rpc DeleteProvider(DeleteProviderRequest) returns (DeleteProviderResponse) { rpc DeleteProvider(DeleteProviderRequest) returns (DeleteProviderResponse) {
@ -12448,6 +12484,134 @@ message UpdateLDAPProviderResponse {
zitadel.v1.ObjectDetails details = 1; zitadel.v1.ObjectDetails details = 1;
} }
message AddAppleProviderRequest {
// Apple will be used as default, if no name is provided
string name = 1 [
(validate.rules).string = {max_len: 200},
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
max_length: 200;
example: "\"Apple\"";
description: "Apple will be used as default, if no name is provided";
}
];
string client_id = 2 [
(validate.rules).string = {min_len: 1, max_len: 200},
(google.api.field_behavior) = REQUIRED,
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
min_length: 1;
max_length: 200;
example: "\"com.client.id\"";
description: "Client id (App ID or Service ID) provided by Apple";
}
];
string team_id = 3 [
(validate.rules).string = {len: 10},
(google.api.field_behavior) = REQUIRED,
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
min_length: 10;
max_length: 10;
example: "\"ALT03JV3OS\"";
description: "(10-character) Team ID provided by Apple";
}
];
string key_id = 4 [
(validate.rules).string = {len: 10},
(google.api.field_behavior) = REQUIRED,
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
min_length: 10;
max_length: 10;
example: "\"OGKDK25KD\"";
description: "(10-character) ID of the private key generated by Apple";
}
];
bytes private_key = 5 [
(validate.rules).bytes = {min_len: 1, max_len: 5000},
(google.api.field_behavior) = REQUIRED,
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
min_length: 1;
max_length: 5000;
example: "\"LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCk1...\"";
description: "Private Key generated by Apple";
}
];
repeated string scopes = 6 [
(validate.rules).repeated = {max_items: 20, items: {string: {min_len: 1, max_len: 100}}},
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
max_items: 20,
example: "[\"name\", \"email\"]";
description: "The scopes requested by ZITADEL during the request to Apple";
}
];
zitadel.idp.v1.Options provider_options = 7;
}
message AddAppleProviderResponse {
zitadel.v1.ObjectDetails details = 1;
string id = 2;
}
message UpdateAppleProviderRequest {
string id = 1 [(validate.rules).string = {min_len: 1, max_len: 200}];
string name = 2 [
(validate.rules).string = {max_len: 200},
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
max_length: 200,
example: "\"Apple\"";
}
];
string client_id = 3 [
(validate.rules).string = {min_len: 1, max_len: 200},
(google.api.field_behavior) = REQUIRED,
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
min_length: 1;
max_length: 200,
example: "\"client-id\"";
description: "Client id (App ID or Service ID) provided by Apple";
}
];
string team_id = 4 [
(validate.rules).string = {len: 10},
(google.api.field_behavior) = REQUIRED,
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
min_length: 10;
max_length: 10;
example: "\"ALT03JV3OS\"";
description: "(10-character) Team ID provided by Apple";
}
];
string key_id = 5 [
(validate.rules).string = {len: 10},
(google.api.field_behavior) = REQUIRED,
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
min_length: 10;
max_length: 10;
example: "\"OGKDK25KD\"";
description: "(10-character) ID of the private key generated by Apple";
}
];
bytes private_key = 6 [
(validate.rules).bytes = {max_len: 5000},
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
max_length: 5000,
example: "\"LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCk1...\"";
description: "Private Key generated by Apple";
}
];
repeated string scopes = 7 [
(validate.rules).repeated = {max_items: 20, items: {string: {min_len: 1, max_len: 100}}},
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
max_items: 20,
example: "[\"openid\", \"profile\", \"email\"]";
description: "The scopes requested by ZITADEL during the request to Apple";
}
];
zitadel.idp.v1.Options provider_options = 8;
}
message UpdateAppleProviderResponse {
zitadel.v1.ObjectDetails details = 1;
}
message DeleteProviderRequest { message DeleteProviderRequest {
string id = 1 [(validate.rules).string = {min_len: 1, max_len: 200}]; string id = 1 [(validate.rules).string = {min_len: 1, max_len: 200}];
} }