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

View File

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

View File

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

View File

@ -380,7 +380,7 @@ func startAPIs(
apis.RegisterHandlerOnPrefix(idp.HandlerPrefix, idp.NewHandler(commands, queries, keys.IDPConfig, config.ExternalSecure, instanceInterceptor.Handler))
userAgentInterceptor, err := middleware.NewUserAgentHandler(config.UserAgentCookie, keys.UserAgentCookieKey, id.SonyFlakeGenerator(), config.ExternalSecure, login.EndpointResources)
userAgentInterceptor, err := middleware.NewUserAgentHandler(config.UserAgentCookie, keys.UserAgentCookieKey, id.SonyFlakeGenerator(), config.ExternalSecure, login.EndpointResources, login.EndpointExternalLoginCallbackFormPost)
if err != nil {
return err
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1706,6 +1706,10 @@
"LDAP": {
"TITLE": "Active Directory / LDAP",
"DESCRIPTION": "Enter the credentials for your LDAP Provider"
},
"APPLE": {
"TITLE": "Sign in with Apple",
"DESCRIPTION": "Enter the credentials for your Apple Provider"
}
},
"DETAIL": {
@ -1819,6 +1823,14 @@
"JWTENDPOINT": "JWT Endpoint",
"JWTKEYSENDPOINT": "JWT Keys Endpoint"
},
"APPLE": {
"TEAMID": "Team ID",
"KEYID": "Key ID",
"PRIVATEKEY": "Private Key",
"UPDATEPRIVATEKEY": "Update Private Key",
"UPLOADPRIVATEKEY": "Upload Private Key",
"KEYMAXSIZEEXCEEDED": "Maximum size of 5kB exceeded."
},
"TOAST": {
"SAVED": "Successfully saved.",
"REACTIVATED": "Idp reactivated.",

View File

@ -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
go 1.20
go 1.21
require (
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
}
func (s *Server) AddAppleProvider(ctx context.Context, req *admin_pb.AddAppleProviderRequest) (*admin_pb.AddAppleProviderResponse, error) {
id, details, err := s.command.AddInstanceAppleProvider(ctx, addAppleProviderToCommand(req))
if err != nil {
return nil, err
}
return &admin_pb.AddAppleProviderResponse{
Id: id,
Details: object_pb.DomainToAddDetailsPb(details),
}, nil
}
func (s *Server) UpdateAppleProvider(ctx context.Context, req *admin_pb.UpdateAppleProviderRequest) (*admin_pb.UpdateAppleProviderResponse, error) {
details, err := s.command.UpdateInstanceAppleProvider(ctx, req.Id, updateAppleProviderToCommand(req))
if err != nil {
return nil, err
}
return &admin_pb.UpdateAppleProviderResponse{
Details: object_pb.DomainToChangeDetailsPb(details),
}, nil
}
func (s *Server) DeleteProvider(ctx context.Context, req *admin_pb.DeleteProviderRequest) (*admin_pb.DeleteProviderResponse, error) {
details, err := s.command.DeleteInstanceProvider(ctx, req.Id)
if err != nil {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

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

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -444,6 +444,46 @@ func (c *Commands) UpdateOrgLDAPProvider(ctx context.Context, resourceOwner, id
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) {
orgAgg := org.NewAggregate(resourceOwner)
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 {
return func() (preparation.CreateCommands, 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)
}
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 {
IDPRemoveWriteModel
}
@ -842,6 +909,8 @@ func (wm *OrgIDPRemoveWriteModel) AppendEvents(events ...eventstore.Event) {
wm.IDPRemoveWriteModel.AppendEvents(&e.GoogleIDPAddedEvent)
case *org.LDAPIDPAddedEvent:
wm.IDPRemoveWriteModel.AppendEvents(&e.LDAPIDPAddedEvent)
case *org.AppleIDPAddedEvent:
wm.IDPRemoveWriteModel.AppendEvents(&e.AppleIDPAddedEvent)
case *org.IDPRemovedEvent:
wm.IDPRemoveWriteModel.AppendEvents(&e.RemovedEvent)
case *org.IDPConfigAddedEvent:
@ -871,6 +940,7 @@ func (wm *OrgIDPRemoveWriteModel) Query() *eventstore.SearchQueryBuilder {
org.GitLabSelfHostedIDPAddedEventType,
org.GoogleIDPAddedEventType,
org.LDAPIDPAddedEventType,
org.AppleIDPAddedEventType,
org.IDPRemovedEventType,
).
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 {
return &s
}

View File

@ -36,6 +36,7 @@ const (
IDPTypeGitLab
IDPTypeGitLabSelfHosted
IDPTypeGoogle
IDPTypeApple
)
func (t IDPType) GetCSSClass() string {
@ -50,6 +51,8 @@ func (t IDPType) GetCSSClass() string {
return "gitlab"
case IDPTypeAzureAD:
return "azure"
case IDPTypeApple:
return "apple"
case IDPTypeUnspecified,
IDPTypeOIDC,
IDPTypeJWT,
@ -78,6 +81,8 @@ func (t IDPType) DisplayName() string {
return "GitLab"
case IDPTypeGoogle:
return "Google"
case IDPTypeApple:
return "Apple"
case IDPTypeUnspecified,
IDPTypeOIDC,
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
const (

View File

@ -3,9 +3,9 @@ package form
import (
"net/http"
"github.com/zitadel/zitadel/internal/errors"
"github.com/gorilla/schema"
"github.com/zitadel/zitadel/internal/errors"
)
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
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].
func (s *Session) FetchUser(ctx context.Context) (user idp.User, err error) {
if s.Tokens == nil {
if err = s.authorize(ctx); err != nil {
if err = s.Authorize(ctx); err != nil {
return nil, err
}
}
@ -54,7 +54,7 @@ func (s *Session) FetchUser(ctx context.Context) (user idp.User, err error) {
return u, nil
}
func (s *Session) authorize(ctx context.Context) (err error) {
func (s *Session) Authorize(ctx context.Context) (err error) {
if s.Code == "" {
return ErrCodeMissing
}

View File

@ -43,6 +43,7 @@ type IDPTemplate struct {
*GitLabSelfHostedIDPTemplate
*GoogleIDPTemplate
*LDAPIDPTemplate
*AppleIDPTemplate
}
type IDPTemplates struct {
@ -140,6 +141,15 @@ type LDAPIDPTemplate struct {
idp.LDAPAttributes
}
type AppleIDPTemplate struct {
IDPID string
ClientID string
TeamID string
KeyID string
PrivateKey *crypto.CryptoValue
Scopes database.StringArray
}
var (
idpTemplateTable = table{
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
func (q *Queries) IDPTemplateByID(ctx context.Context, shouldTriggerBulk bool, id string, withOwnerRemoved bool, queries ...SearchQuery) (template *IDPTemplate, err error) {
ctx, span := tracing.NewSpan(ctx)
@ -799,6 +844,13 @@ func prepareIDPTemplateByIDQuery(ctx context.Context, db prepareDatabase) (sq.Se
LDAPPreferredLanguageAttributeCol.identifier(),
LDAPAvatarURLAttributeCol.identifier(),
LDAPProfileAttributeCol.identifier(),
// apple
AppleIDCol.identifier(),
AppleClientIDCol.identifier(),
AppleTeamIDCol.identifier(),
AppleKeyIDCol.identifier(),
ApplePrivateKeyCol.identifier(),
AppleScopesCol.identifier(),
).From(idpTemplateTable.identifier()).
LeftJoin(join(OAuthIDCol, IDPTemplateIDCol)).
LeftJoin(join(OIDCIDCol, IDPTemplateIDCol)).
@ -809,7 +861,8 @@ func prepareIDPTemplateByIDQuery(ctx context.Context, db prepareDatabase) (sq.Se
LeftJoin(join(GitLabIDCol, IDPTemplateIDCol)).
LeftJoin(join(GitLabSelfHostedIDCol, 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),
func(row *sql.Row) (*IDPTemplate, error) {
idpTemplate := new(IDPTemplate)
@ -898,6 +951,13 @@ func prepareIDPTemplateByIDQuery(ctx context.Context, db prepareDatabase) (sq.Se
ldapAvatarURLAttribute := 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(
&idpTemplate.ID,
&idpTemplate.ResourceOwner,
@ -994,6 +1054,13 @@ func prepareIDPTemplateByIDQuery(ctx context.Context, db prepareDatabase) (sq.Se
&ldapPreferredLanguageAttribute,
&ldapAvatarURLAttribute,
&ldapProfileAttribute,
// apple
&appleID,
&appleClientID,
&appleTeamID,
&appleKeyID,
&applePrivateKey,
&appleScopes,
)
if err != nil {
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
}
@ -1220,6 +1297,14 @@ func prepareIDPTemplatesQuery(ctx context.Context, db prepareDatabase) (sq.Selec
LDAPPreferredLanguageAttributeCol.identifier(),
LDAPAvatarURLAttributeCol.identifier(),
LDAPProfileAttributeCol.identifier(),
// apple
AppleIDCol.identifier(),
AppleClientIDCol.identifier(),
AppleTeamIDCol.identifier(),
AppleKeyIDCol.identifier(),
ApplePrivateKeyCol.identifier(),
AppleScopesCol.identifier(),
// count
countColumn.identifier(),
).From(idpTemplateTable.identifier()).
LeftJoin(join(OAuthIDCol, IDPTemplateIDCol)).
@ -1231,7 +1316,8 @@ func prepareIDPTemplatesQuery(ctx context.Context, db prepareDatabase) (sq.Selec
LeftJoin(join(GitLabIDCol, IDPTemplateIDCol)).
LeftJoin(join(GitLabSelfHostedIDCol, 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),
func(rows *sql.Rows) (*IDPTemplates, error) {
templates := make([]*IDPTemplate, 0)
@ -1323,6 +1409,13 @@ func prepareIDPTemplatesQuery(ctx context.Context, db prepareDatabase) (sq.Selec
ldapAvatarURLAttribute := 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(
&idpTemplate.ID,
&idpTemplate.ResourceOwner,
@ -1419,6 +1512,13 @@ func prepareIDPTemplatesQuery(ctx context.Context, db prepareDatabase) (sq.Selec
&ldapPreferredLanguageAttribute,
&ldapAvatarURLAttribute,
&ldapProfileAttribute,
// apple
&appleID,
&appleClientID,
&appleTeamID,
&appleKeyID,
&applePrivateKey,
&appleScopes,
&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)
}

View File

@ -110,7 +110,14 @@ var (
` projections.idp_templates5_ldap2.phone_verified_attribute,` +
` projections.idp_templates5_ldap2.preferred_language_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` +
` 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` +
@ -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_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_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'`
idpTemplateCols = []string{
"id",
@ -219,6 +227,13 @@ var (
"preferred_language_attribute",
"avatar_url_attribute",
"profile_attribute",
// apple config
"idp_id",
"client_id",
"team_id",
"key_id",
"private_key",
"scopes",
}
idpTemplatesQuery = `SELECT projections.idp_templates5.id,` +
` projections.idp_templates5.resource_owner,` +
@ -315,6 +330,13 @@ var (
` projections.idp_templates5_ldap2.preferred_language_attribute,` +
` projections.idp_templates5_ldap2.avatar_url_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 ()` +
` 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` +
@ -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_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_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'`
idpTemplatesCols = []string{
"id",
@ -424,6 +447,13 @@ var (
"preferred_language_attribute",
"avatar_url_attribute",
"profile_attribute",
// apple config
"idp_id",
"client_id",
"team_id",
"key_id",
"private_key",
"scopes",
"count",
}
)
@ -560,6 +590,13 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) {
nil,
nil,
nil,
// apple
nil,
nil,
nil,
nil,
nil,
nil,
},
),
},
@ -692,6 +729,13 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) {
nil,
nil,
nil,
// apple
nil,
nil,
nil,
nil,
nil,
nil,
},
),
},
@ -822,6 +866,13 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) {
nil,
nil,
nil,
// apple
nil,
nil,
nil,
nil,
nil,
nil,
},
),
},
@ -951,6 +1002,13 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) {
nil,
nil,
nil,
// apple
nil,
nil,
nil,
nil,
nil,
nil,
},
),
},
@ -1079,6 +1137,13 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) {
nil,
nil,
nil,
// apple
nil,
nil,
nil,
nil,
nil,
nil,
},
),
},
@ -1207,6 +1272,13 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) {
nil,
nil,
nil,
// apple
nil,
nil,
nil,
nil,
nil,
nil,
},
),
},
@ -1336,6 +1408,13 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) {
nil,
nil,
nil,
// apple
nil,
nil,
nil,
nil,
nil,
nil,
},
),
},
@ -1464,6 +1543,13 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) {
"lang",
"avatar",
"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",
prepare: prepareIDPTemplateByIDQuery,
@ -1612,6 +1835,13 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) {
nil,
nil,
nil,
// apple
nil,
nil,
nil,
nil,
nil,
nil,
},
),
},
@ -1770,6 +2000,13 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) {
"lang",
"avatar",
"profile",
// apple
nil,
nil,
nil,
nil,
nil,
nil,
},
},
),
@ -1927,6 +2164,13 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) {
nil,
nil,
nil,
// apple
nil,
nil,
nil,
nil,
nil,
nil,
},
},
),
@ -2058,6 +2302,13 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) {
"lang",
"avatar",
"profile",
// apple
nil,
nil,
nil,
nil,
nil,
nil,
},
{
"idp-id-google",
@ -2155,6 +2406,13 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) {
nil,
nil,
nil,
// apple
nil,
nil,
nil,
nil,
nil,
nil,
},
{
"idp-id-oauth",
@ -2252,6 +2510,13 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) {
nil,
nil,
nil,
// apple
nil,
nil,
nil,
nil,
nil,
nil,
},
{
"idp-id-oidc",
@ -2349,6 +2614,13 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) {
nil,
nil,
nil,
// apple
nil,
nil,
nil,
nil,
nil,
nil,
},
{
"idp-id-jwt",
@ -2446,6 +2718,13 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) {
nil,
nil,
nil,
// apple
nil,
nil,
nil,
nil,
nil,
nil,
},
},
),

View File

@ -28,6 +28,7 @@ const (
IDPTemplateGitLabSelfHostedTable = IDPTemplateTable + "_" + IDPTemplateGitLabSelfHostedSuffix
IDPTemplateGoogleTable = IDPTemplateTable + "_" + IDPTemplateGoogleSuffix
IDPTemplateLDAPTable = IDPTemplateTable + "_" + IDPTemplateLDAPSuffix
IDPTemplateAppleTable = IDPTemplateTable + "_" + IDPTemplateAppleSuffix
IDPTemplateOAuthSuffix = "oauth2"
IDPTemplateOIDCSuffix = "oidc"
@ -39,6 +40,7 @@ const (
IDPTemplateGitLabSelfHostedSuffix = "gitlab_self_hosted"
IDPTemplateGoogleSuffix = "google"
IDPTemplateLDAPSuffix = "ldap2"
IDPTemplateAppleSuffix = "apple"
IDPTemplateIDCol = "id"
IDPTemplateCreationDateCol = "creation_date"
@ -147,6 +149,14 @@ const (
LDAPPreferredLanguageAttributeCol = "preferred_language_attribute"
LDAPAvatarURLAttributeCol = "avatar_url_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 {
@ -321,6 +331,19 @@ func newIDPTemplateProjection(ctx context.Context, config crdb.StatementHandlerC
IDPTemplateLDAPSuffix,
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)
return p
@ -443,6 +466,14 @@ func (p *idpTemplateProjection) reducers() []handler.AggregateReducer {
Event: instance.LDAPIDPChangedEventType,
Reduce: p.reduceLDAPIDPChanged,
},
{
Event: instance.AppleIDPAddedEventType,
Reduce: p.reduceAppleIDPAdded,
},
{
Event: instance.AppleIDPChangedEventType,
Reduce: p.reduceAppleIDPChanged,
},
{
Event: instance.IDPConfigRemovedEventType,
Reduce: p.reduceIDPConfigRemoved,
@ -572,6 +603,14 @@ func (p *idpTemplateProjection) reducers() []handler.AggregateReducer {
Event: org.LDAPIDPChangedEventType,
Reduce: p.reduceLDAPIDPChanged,
},
{
Event: org.AppleIDPAddedEventType,
Reduce: p.reduceAppleIDPAdded,
},
{
Event: org.AppleIDPChangedEventType,
Reduce: p.reduceAppleIDPChanged,
},
{
Event: org.IDPConfigRemovedEventType,
Reduce: p.reduceIDPConfigRemoved,
@ -1858,6 +1897,97 @@ func (p *idpTemplateProjection) reduceLDAPIDPChanged(event eventstore.Event) (*h
ops...,
), 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) {
var idpEvent idpconfig.IDPConfigRemovedEvent
switch e := event.(type) {
@ -2176,3 +2306,23 @@ func reduceLDAPIDPChangedColumns(idpEvent idp.LDAPIDPChangedEvent) []handler.Col
}
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) {
type args struct {
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, LDAPIDPAddedEventType, LDAPIDPAddedEventMapper).
RegisterFilterEventMapper(AggregateType, LDAPIDPChangedEventType, LDAPIDPChangedEventMapper).
RegisterFilterEventMapper(AggregateType, AppleIDPAddedEventType, AppleIDPAddedEventMapper).
RegisterFilterEventMapper(AggregateType, AppleIDPChangedEventType, AppleIDPChangedEventMapper).
RegisterFilterEventMapper(AggregateType, IDPRemovedEventType, IDPRemovedEventMapper).
RegisterFilterEventMapper(AggregateType, LoginPolicyIDPProviderAddedEventType, IdentityProviderAddedEventMapper).
RegisterFilterEventMapper(AggregateType, LoginPolicyIDPProviderRemovedEventType, IdentityProviderRemovedEventMapper).

View File

@ -33,6 +33,8 @@ const (
GoogleIDPChangedEventType eventstore.EventType = "instance.idp.google.changed"
LDAPIDPAddedEventType eventstore.EventType = "instance.idp.ldap.v2.added"
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"
)
@ -920,6 +922,86 @@ func LDAPIDPChangedEventMapper(event *repository.Event) (eventstore.Event, error
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 {
idp.RemovedEvent
}

View File

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

View File

@ -33,6 +33,8 @@ const (
GoogleIDPChangedEventType eventstore.EventType = "org.idp.google.changed"
LDAPIDPAddedEventType eventstore.EventType = "org.idp.ldap.added"
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"
)
@ -920,6 +922,86 @@ func LDAPIDPChangedEventMapper(event *repository.Event) (eventstore.Event, error
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 {
idp.RemovedEvent
}

View File

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

View File

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

View File

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

View File

@ -199,6 +199,10 @@ Errors:
InvalidCharacter: Solo caracteres alfanuméricos, . y - se permiten para un dominio
IDP:
InvalidSearchQuery: Consulta de búsqueda no válida
ClientIDMissing: Falta ClientID
TeamIDMissing: Falta TeamID
KeyIDMissing: Falta KeyID
PrivateKeyMissing: Falta la clave privada
LoginPolicy:
NotFound: Política de inicio de sesión no encontrada
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
IDP:
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:
NotFound: Politique de connexion non trouvée
Invalid: La politique de connexion n'est pas valide

View File

@ -199,6 +199,10 @@ Errors:
IDP:
InvalidSearchQuery: Parametro di ricerca non valido
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:
NotFound: Impostazioni di accesso non trovati
Invalid: Impostazioni di accesso non sono validi

View File

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

View File

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

View File

@ -199,6 +199,10 @@ Errors:
InvalidCharacter: Tylko znaki alfanumeryczne, . i - są dozwolone dla domeny
IDP:
InvalidSearchQuery: Nieprawidłowe zapytanie wyszukiwania
ClientIDMissing: Brak ClientID
TeamIDMissing: Brak TeamID
KeyIDMissing: Brak KeyID
PrivateKeyMissing: Brak klucza prywatnego
LoginPolicy:
NotFound: Polityka logowania nie znaleziona
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
IDP:
InvalidSearchQuery: Consulta de pesquisa inválida
ClientIDMissing: ClientID ausente
TeamIDMissing: TeamID ausente
KeyIDMissing: KeyID ausente
PrivateKeyMissing: Chave privada ausente
LoginPolicy:
NotFound: Política de login não encontrada
Invalid: Política de login é inválida

View File

@ -199,6 +199,10 @@ Errors:
InvalidCharacter: 只有字母数字字符,.和 - 允许用于域名中
IDP:
InvalidSearchQuery: 无效的搜索查询
ClientIDMissing: 客户端 ID 丢失
TeamIDMissing: 团队 ID 丢失
KeyIDMissing: 密钥 ID 丢失
PrivateKeyMissing: 私钥丢失
LoginPolicy:
NotFound: 未找到登录策略
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
// Will remove all linked providers of this configuration on the users
rpc DeleteProvider(DeleteProviderRequest) returns (DeleteProviderResponse) {
@ -5564,6 +5600,134 @@ message UpdateLDAPProviderResponse {
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 {
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_SELF_HOSTED = 9;
PROVIDER_TYPE_GOOGLE = 10;
PROVIDER_TYPE_APPLE = 11;
}
message ProviderConfig {
@ -281,6 +282,7 @@ message ProviderConfig {
GitLabConfig gitlab = 9;
GitLabSelfHostedConfig gitlab_self_hosted = 10;
AzureADConfig azure_ad = 11;
AppleConfig apple = 12;
}
}
@ -517,3 +519,30 @@ message AzureADTenant {
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
// Will remove all linked providers of this configuration on the users
rpc DeleteProvider(DeleteProviderRequest) returns (DeleteProviderResponse) {
@ -12448,6 +12484,134 @@ message UpdateLDAPProviderResponse {
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 {
string id = 1 [(validate.rules).string = {min_len: 1, max_len: 200}];
}