feat(saml): implementation of saml for ZITADEL v2 (#3618)
5
.github/workflows/test-code.yml
vendored
@ -46,6 +46,10 @@ jobs:
|
||||
working-directory: e2e
|
||||
env:
|
||||
ZITADEL_IMAGE: zitadel:pr
|
||||
- name: Save ZITADEL Logs
|
||||
if: always()
|
||||
run: docker compose logs zitadel > ../.artifacts/e2e-compose-zitadel.log
|
||||
working-directory: e2e
|
||||
- name: Archive Test Results
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v2
|
||||
@ -55,4 +59,5 @@ jobs:
|
||||
e2e/cypress/results
|
||||
e2e/cypress/videos
|
||||
e2e/cypress/screenshots
|
||||
.artifacts/e2e-compose-zitadel.log
|
||||
retention-days: 30
|
||||
|
@ -1,7 +1,7 @@
|
||||
module.exports = {
|
||||
branches: [
|
||||
{name: 'main'},
|
||||
{name: '1.87.x', range: '1.87.x', channel: '1.87.x'},
|
||||
{name: '1.87.x', range: '1.87.x', channel: '1.87.x'}
|
||||
],
|
||||
plugins: [
|
||||
"@semantic-release/commit-analyzer"
|
||||
|
@ -79,8 +79,8 @@ It's free for up to 25'000 authenticated requests. Learn more about the [pay-as-
|
||||
- Self-registration including verification
|
||||
- User self service
|
||||
- [Service Accounts](https://docs.zitadel.com/docs/guides/integrate/serviceusers)
|
||||
- [OpenID Connect certified](https://openid.net/certification/#OPs)
|
||||
- 🚧 [SAML 2.0](https://github.com/zitadel/zitadel/pull/3618)
|
||||
- [OpenID Connect certified](https://openid.net/certification/#OPs) => [OIDC Endpoints](https://docs.zitadel.com/docs/apis/openidoauth/endpoints), [OIDC Integration Guides](https://docs.zitadel.com/docs/guides/integrate/auth0-oidc)
|
||||
- [SAML 2.0](http://docs.oasis-open.org/security/saml/Post2.0/sstc-saml-tech-overview-2.0.html) => [SAML Endpoints](https://docs.zitadel.com/docs/apis/saml/endpoints), [SAML Integration Guides](https://docs.zitadel.com/docs/guides/integrate/auth0-saml)
|
||||
- [Postgres](https://docs.zitadel.com/docs/guides/manage/self-hosted/database#postgres) (version >= 14) or [CockroachDB](https://docs.zitadel.com/docs/guides/manage/self-hosted/database#cockroach) (version >= 22.0)
|
||||
|
||||
Track upcoming features on our [roadmap](https://zitadel.com/roadmap).
|
||||
|
@ -200,6 +200,25 @@ OIDC:
|
||||
Keys:
|
||||
Path: /oauth/v2/keys
|
||||
|
||||
SAML:
|
||||
ProviderConfig:
|
||||
MetadataConfig:
|
||||
Path: "/metadata"
|
||||
SignatureAlgorithm: "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"
|
||||
IDPConfig:
|
||||
SignatureAlgorithm: "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"
|
||||
WantAuthRequestsSigned: true
|
||||
Endpoints:
|
||||
#Organisation:
|
||||
# Name: ZITADEL
|
||||
# URL: https://zitadel.com
|
||||
#ContactPerson:
|
||||
# ContactType: "technical"
|
||||
# Company: ZITADEL
|
||||
# EmailAddress: hi@zitadel.com
|
||||
|
||||
|
||||
|
||||
Login:
|
||||
LanguageCookieName: zitadel.login.lang
|
||||
CSRFCookieName: zitadel.login.csrf
|
||||
@ -234,6 +253,9 @@ EncryptionKeys:
|
||||
OIDC:
|
||||
EncryptionKeyID: "oidcKey"
|
||||
DecryptionKeyIDs:
|
||||
SAML:
|
||||
EncryptionKeyID: "samlKey"
|
||||
DecryptionKeyIDs:
|
||||
OTP:
|
||||
EncryptionKeyID: "otpKey"
|
||||
DecryptionKeyIDs:
|
||||
@ -277,8 +299,10 @@ SystemDefaults:
|
||||
FileSystemPath: ".notifications/"
|
||||
KeyConfig:
|
||||
Size: 2048
|
||||
CertificateSize: 4096
|
||||
PrivateKeyLifetime: 6h
|
||||
PublicKeyLifetime: 30h
|
||||
CertificateLifetime: 8766h
|
||||
|
||||
DefaultInstance:
|
||||
InstanceName:
|
||||
|
@ -70,7 +70,10 @@ func (mig *FirstInstance) Execute(ctx context.Context) error {
|
||||
nil,
|
||||
userAlg,
|
||||
nil,
|
||||
nil)
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -47,7 +47,10 @@ func (mig *externalConfigChange) Execute(ctx context.Context) error {
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
nil)
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -6,6 +6,7 @@ import (
|
||||
"github.com/mitchellh/mapstructure"
|
||||
"github.com/spf13/viper"
|
||||
"github.com/zitadel/logging"
|
||||
"github.com/zitadel/zitadel/internal/api/saml"
|
||||
|
||||
admin_es "github.com/zitadel/zitadel/internal/admin/repository/eventsourcing"
|
||||
internal_authz "github.com/zitadel/zitadel/internal/api/authz"
|
||||
@ -45,6 +46,7 @@ type Config struct {
|
||||
Admin admin_es.Config
|
||||
UserAgentCookie *middleware.UserAgentCookieConfig
|
||||
OIDC oidc.Config
|
||||
SAML saml.Config
|
||||
Login login.Config
|
||||
Console console.Config
|
||||
AssetStorage static_config.AssetStorageConfig
|
||||
@ -90,6 +92,7 @@ type encryptionKeyConfig struct {
|
||||
DomainVerification *crypto.KeyConfig
|
||||
IDPConfig *crypto.KeyConfig
|
||||
OIDC *crypto.KeyConfig
|
||||
SAML *crypto.KeyConfig
|
||||
OTP *crypto.KeyConfig
|
||||
SMS *crypto.KeyConfig
|
||||
SMTP *crypto.KeyConfig
|
||||
|
@ -10,6 +10,7 @@ var (
|
||||
"domainVerificationKey",
|
||||
"idpConfigKey",
|
||||
"oidcKey",
|
||||
"samlKey",
|
||||
"otpKey",
|
||||
"smsKey",
|
||||
"smtpKey",
|
||||
@ -23,6 +24,7 @@ type encryptionKeys struct {
|
||||
DomainVerification crypto.EncryptionAlgorithm
|
||||
IDPConfig crypto.EncryptionAlgorithm
|
||||
OIDC crypto.EncryptionAlgorithm
|
||||
SAML crypto.EncryptionAlgorithm
|
||||
OTP crypto.EncryptionAlgorithm
|
||||
SMS crypto.EncryptionAlgorithm
|
||||
SMTP crypto.EncryptionAlgorithm
|
||||
@ -49,6 +51,10 @@ func ensureEncryptionKeys(keyConfig *encryptionKeyConfig, keyStorage crypto.KeyS
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
keys.SAML, err = crypto.NewAESCrypto(keyConfig.SAML, keyStorage)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
key, err := crypto.LoadKey(keyConfig.OIDC.EncryptionKeyID, keyStorage)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -13,6 +13,10 @@ import (
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/zitadel/saml/pkg/provider"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/saml"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
@ -100,7 +104,7 @@ func startZitadel(config *Config, masterKey string) error {
|
||||
return fmt.Errorf("cannot start eventstore for queries: %w", err)
|
||||
}
|
||||
|
||||
queries, err := query.StartQueries(ctx, eventstoreClient, dbClient, config.Projections, config.SystemDefaults, keys.IDPConfig, keys.OTP, keys.OIDC, config.InternalAuthZ.RolePermissionMappings)
|
||||
queries, err := query.StartQueries(ctx, eventstoreClient, dbClient, config.Projections, config.SystemDefaults, keys.IDPConfig, keys.OTP, keys.OIDC, keys.SAML, config.InternalAuthZ.RolePermissionMappings)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot start queries: %w", err)
|
||||
}
|
||||
@ -134,6 +138,8 @@ func startZitadel(config *Config, masterKey string) error {
|
||||
keys.User,
|
||||
keys.DomainVerification,
|
||||
keys.OIDC,
|
||||
keys.SAML,
|
||||
&http.Client{},
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot start commands: %w", err)
|
||||
@ -208,13 +214,19 @@ func startAPIs(ctx context.Context, router *mux.Router, commands *command.Comman
|
||||
return fmt.Errorf("unable to start oidc provider: %w", err)
|
||||
}
|
||||
|
||||
samlProvider, err := saml.NewProvider(ctx, config.SAML, config.ExternalSecure, commands, queries, authRepo, keys.OIDC, keys.SAML, eventstore, dbClient, instanceInterceptor.Handler, userAgentInterceptor)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to start saml provider: %w", err)
|
||||
}
|
||||
apis.RegisterHandler(saml.HandlerPrefix, samlProvider.HttpHandler())
|
||||
|
||||
c, err := console.Start(config.Console, config.ExternalSecure, oidcProvider.IssuerFromRequest, instanceInterceptor.Handler, config.CustomerPortal)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to start console: %w", err)
|
||||
}
|
||||
apis.RegisterHandler(console.HandlerPrefix, c)
|
||||
|
||||
l, err := login.CreateLogin(config.Login, commands, queries, authRepo, store, console.HandlerPrefix+"/", op.AuthCallbackURL(oidcProvider), config.ExternalSecure, userAgentInterceptor, op.NewIssuerInterceptor(oidcProvider.IssuerFromRequest).Handler, instanceInterceptor.Handler, assetsCache.Handler, keys.User, keys.IDPConfig, keys.CSRFCookieKey)
|
||||
l, err := login.CreateLogin(config.Login, commands, queries, authRepo, store, console.HandlerPrefix+"/", op.AuthCallbackURL(oidcProvider), provider.AuthCallbackURL(samlProvider), config.ExternalSecure, userAgentInterceptor, op.NewIssuerInterceptor(oidcProvider.IssuerFromRequest).Handler, provider.NewIssuerInterceptor(samlProvider.IssuerFromRequest).Handler, instanceInterceptor.Handler, assetsCache.Handler, keys.User, keys.IDPConfig, keys.CSRFCookieKey)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to start login: %w", err)
|
||||
}
|
||||
|
895
console/package-lock.json
generated
@ -64,8 +64,8 @@
|
||||
"@types/jasminewd2": "~2.0.10",
|
||||
"@types/jsonwebtoken": "^8.5.5",
|
||||
"@types/node": "^17.0.42",
|
||||
"@typescript-eslint/eslint-plugin": "5.35.1",
|
||||
"@typescript-eslint/parser": "5.30.4",
|
||||
"@typescript-eslint/eslint-plugin": "5.36.1",
|
||||
"@typescript-eslint/parser": "5.36.1",
|
||||
"codelyzer": "^6.0.0",
|
||||
"eslint": "^8.18.0",
|
||||
"jasmine-core": "~4.2.0",
|
||||
|
@ -86,6 +86,7 @@ const authConfig: AuthConfig = {
|
||||
scope: 'openid profile email', // offline_access
|
||||
responseType: 'code',
|
||||
oidc: true,
|
||||
requireHttps: false,
|
||||
};
|
||||
|
||||
@NgModule({
|
||||
|
@ -1,5 +1,13 @@
|
||||
<div class="cnsl-app-card" [ngClass]="{'add': type === OIDCAppType.ADD,'web': type === OIDCAppType.OIDC_APP_TYPE_WEB,
|
||||
'useragent': type === OIDCAppType.OIDC_APP_TYPE_USER_AGENT,
|
||||
'native': type === OIDCAppType.OIDC_APP_TYPE_NATIVE, 'api': isApiApp}">
|
||||
<div
|
||||
class="cnsl-app-card"
|
||||
[ngClass]="{
|
||||
add: type === OIDCAppType.ADD,
|
||||
web: type === OIDCAppType.OIDC_APP_TYPE_WEB,
|
||||
useragent: type === OIDCAppType.OIDC_APP_TYPE_USER_AGENT,
|
||||
native: type === OIDCAppType.OIDC_APP_TYPE_NATIVE,
|
||||
api: isApiApp,
|
||||
saml: type === 'SAML'
|
||||
}"
|
||||
>
|
||||
<ng-content></ng-content>
|
||||
</div>
|
@ -55,5 +55,11 @@
|
||||
border: none;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
&.saml {
|
||||
background: linear-gradient(40deg, rgb(110, 56, 124), rgb(88, 37, 103));
|
||||
border: none;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -8,7 +8,7 @@ import { OIDCAppType } from 'src/app/proto/generated/zitadel/app_pb';
|
||||
})
|
||||
export class AppCardComponent {
|
||||
@Input() public outline: boolean = false;
|
||||
@Input() public type: OIDCAppType | undefined = undefined;
|
||||
@Input() public type: OIDCAppType | 'SAML' | undefined = undefined;
|
||||
@Input() public isApiApp: boolean = false;
|
||||
public OIDCAppType: any = OIDCAppType;
|
||||
}
|
||||
|
@ -2,12 +2,13 @@
|
||||
<ng-container *ngFor="let type of types">
|
||||
<input class="app" type="radio" (change)="emitChange()" [value]="type" [(ngModel)]="selected" [id]="type.prefix" />
|
||||
<label class="cnsl-type-radio-button" [for]="type.prefix">
|
||||
<div class="cnsl-type-radio-header" [ngStyle]="{'background': type.background}">
|
||||
<span>{{type.prefix}}</span>
|
||||
<div class="cnsl-type-radio-header" [ngStyle]="{ background: type.background }">
|
||||
<span>{{ type.prefix }}</span>
|
||||
</div>
|
||||
<p>{{type.titleI18nKey | translate}}</p>
|
||||
<p class="type-desc cnsl-secondary-text">{{type.descI18nKey | translate}}</p>
|
||||
<p>{{ type.titleI18nKey | translate }}</p>
|
||||
<p class="type-desc cnsl-secondary-text">{{ type.descI18nKey | translate }}</p>
|
||||
<span class="fill-space"></span>
|
||||
<span class="cnsl-type-protocol state" *ngIf="type.protocol">{{ type.protocol }}</span>
|
||||
</label>
|
||||
</ng-container>
|
||||
</div>
|
@ -60,6 +60,8 @@
|
||||
box-sizing: border-box;
|
||||
border-top-left-radius: 6px;
|
||||
border-top-right-radius: 6px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
span {
|
||||
margin: 2rem;
|
||||
@ -76,5 +78,10 @@
|
||||
.type-desc {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.cnsl-type-protocol {
|
||||
width: fit-content;
|
||||
margin: 0.5rem auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,14 +1,24 @@
|
||||
<cnsl-create-layout
|
||||
title="{{ 'APP.PAGES.CREATE_OIDC' | translate }}"
|
||||
[createSteps]="createSteps"
|
||||
title="{{ 'APP.PAGES.CREATE' | translate }}"
|
||||
[createSteps]="
|
||||
appType?.value?.createType === AppCreateType.OIDC
|
||||
? appType?.value.oidcAppType !== OIDCAppType.OIDC_APP_TYPE_NATIVE
|
||||
? 4
|
||||
: 3
|
||||
: appType?.value?.createType === AppCreateType.API
|
||||
? 3
|
||||
: appType?.value?.createType === AppCreateType.SAML
|
||||
? 3
|
||||
: 0
|
||||
"
|
||||
[currentCreateStep]="currentCreateStep"
|
||||
(closed)="close()"
|
||||
>
|
||||
<h1>{{ 'APP.PAGES.CREATE_OIDC_DESC_TITLE' | translate }}</h1>
|
||||
<h1>{{ 'APP.PAGES.CREATE_DESC_TITLE' | translate }}</h1>
|
||||
<mat-progress-bar class="progress-bar" color="primary" *ngIf="loading" mode="indeterminate"></mat-progress-bar>
|
||||
|
||||
<mat-checkbox class="proswitch" color="primary" [(ngModel)]="devmode">
|
||||
{{ 'APP.OIDC.PROSWITCH' | translate }}
|
||||
{{ 'APP.PROSWITCH' | translate }}
|
||||
</mat-checkbox>
|
||||
|
||||
<mat-horizontal-stepper
|
||||
@ -21,16 +31,16 @@
|
||||
>
|
||||
<mat-step [stepControl]="firstFormGroup" [editable]="true">
|
||||
<form [formGroup]="firstFormGroup">
|
||||
<ng-template matStepLabel>{{ 'APP.OIDC.NAMEANDTYPESECTION' | translate }}</ng-template>
|
||||
<ng-template matStepLabel>{{ 'APP.NAMEANDTYPESECTION' | translate }}</ng-template>
|
||||
|
||||
<p class="step-title">{{ 'APP.OIDC.TITLEFIRST' | translate }}</p>
|
||||
<p class="step-title">{{ 'APP.TITLEFIRST' | translate }}</p>
|
||||
<cnsl-form-field class="name-formfield">
|
||||
<cnsl-label>{{ 'APP.NAME' | translate }}</cnsl-label>
|
||||
<input cnslInput cdkFocusInitial formControlName="name" />
|
||||
<span cnslError *ngIf="name?.errors?.required">{{ 'PROJECT.APP.NAMEREQUIRED' | translate }}</span>
|
||||
</cnsl-form-field>
|
||||
|
||||
<p class="step-title">{{ 'APP.OIDC.TYPETITLE' | translate }}</p>
|
||||
<p class="step-title">{{ 'APP.TYPETITLE' | translate }}</p>
|
||||
|
||||
<cnsl-type-radio [types]="appTypes" (selectedType)="appType?.setValue($event)" [selected]="appType?.value">
|
||||
</cnsl-type-radio>
|
||||
@ -48,9 +58,13 @@
|
||||
</form>
|
||||
</mat-step>
|
||||
|
||||
<!-- skip for native applications -->
|
||||
<!-- skip for native OIDC and SAML applications -->
|
||||
<mat-step
|
||||
*ngIf="oidcAppRequest.appType !== OIDCAppType.OIDC_APP_TYPE_NATIVE"
|
||||
*ngIf="
|
||||
(appType?.value?.createType === AppCreateType.OIDC &&
|
||||
appType?.value.oidcAppType !== OIDCAppType.OIDC_APP_TYPE_NATIVE) ||
|
||||
appType?.value?.createType === AppCreateType.API
|
||||
"
|
||||
[stepControl]="secondFormGroup"
|
||||
[editable]="true"
|
||||
>
|
||||
@ -85,33 +99,39 @@
|
||||
<ng-template matStepLabel>{{ 'APP.OIDC.REDIRECTSECTION' | translate }}</ng-template>
|
||||
|
||||
<p class="step-title">{{ 'APP.OIDC.REDIRECTTITLE' | translate }}</p>
|
||||
<p class="step-description cnsl-secondary-text" *ngIf="oidcAppRequest.appType === OIDCAppType.OIDC_APP_TYPE_NATIVE">
|
||||
<p
|
||||
class="step-description cnsl-secondary-text"
|
||||
*ngIf="appType?.value.oidcAppType === OIDCAppType.OIDC_APP_TYPE_NATIVE"
|
||||
>
|
||||
{{ 'APP.OIDC.REDIRECTDESCRIPTIONNATIVE' | translate }}
|
||||
</p>
|
||||
<p class="step-description cnsl-secondary-text" *ngIf="oidcAppRequest.appType === OIDCAppType.OIDC_APP_TYPE_WEB">
|
||||
<p class="step-description cnsl-secondary-text" *ngIf="appType?.value.oidcAppType === OIDCAppType.OIDC_APP_TYPE_WEB">
|
||||
{{ 'APP.OIDC.REDIRECTDESCRIPTIONWEB' | translate }}
|
||||
</p>
|
||||
|
||||
<cnsl-redirect-uris
|
||||
class="redirect-section"
|
||||
[canWrite]="true"
|
||||
[isNative]="oidcAppRequest.appType === OIDCAppType.OIDC_APP_TYPE_NATIVE"
|
||||
(changedUris)="oidcAppRequest.redirectUrisList = $any($event)"
|
||||
[urisList]="oidcAppRequest.redirectUrisList"
|
||||
[isNative]="appType?.value.oidcAppType === OIDCAppType.OIDC_APP_TYPE_NATIVE"
|
||||
(changedUris)="oidcAppRequest.setRedirectUrisList($any($event))"
|
||||
[urisList]="oidcAppRequest.toObject().redirectUrisList"
|
||||
[getValues]="requestRedirectValuesSubject$"
|
||||
title="{{ 'APP.OIDC.REDIRECT' | translate }}"
|
||||
>
|
||||
</cnsl-redirect-uris>
|
||||
|
||||
<p class="step-title">{{ 'APP.OIDC.POSTREDIRECTTITLE' | translate }}</p>
|
||||
<p class="step-description cnsl-secondary-text" *ngIf="oidcAppRequest.appType === OIDCAppType.OIDC_APP_TYPE_NATIVE">
|
||||
<p
|
||||
class="step-description cnsl-secondary-text"
|
||||
*ngIf="appType?.value.oidcAppType === OIDCAppType.OIDC_APP_TYPE_NATIVE"
|
||||
>
|
||||
{{ 'APP.OIDC.REDIRECTDESCRIPTIONNATIVE' | translate }}
|
||||
</p>
|
||||
<p
|
||||
class="step-description cnsl-secondary-text"
|
||||
*ngIf="
|
||||
oidcAppRequest.appType === OIDCAppType.OIDC_APP_TYPE_WEB ||
|
||||
oidcAppRequest.appType === OIDCAppType.OIDC_APP_TYPE_USER_AGENT
|
||||
appType?.value.oidcAppType === OIDCAppType.OIDC_APP_TYPE_WEB ||
|
||||
appType?.value.oidcAppType === OIDCAppType.OIDC_APP_TYPE_USER_AGENT
|
||||
"
|
||||
>
|
||||
{{ 'APP.OIDC.REDIRECTDESCRIPTIONWEB' | translate }}
|
||||
@ -120,11 +140,11 @@
|
||||
<cnsl-redirect-uris
|
||||
class="redirect-section"
|
||||
[canWrite]="true"
|
||||
(changedUris)="oidcAppRequest.postLogoutRedirectUrisList = $any($event)"
|
||||
[urisList]="oidcAppRequest.postLogoutRedirectUrisList"
|
||||
(changedUris)="oidcAppRequest.setPostLogoutRedirectUrisList($any($event))"
|
||||
[urisList]="oidcAppRequest.toObject().postLogoutRedirectUrisList"
|
||||
title="{{ 'APP.OIDC.POSTLOGOUTREDIRECT' | translate }}"
|
||||
[getValues]="requestRedirectValuesSubject$"
|
||||
[isNative]="oidcAppRequest.appType === OIDCAppType.OIDC_APP_TYPE_NATIVE"
|
||||
[isNative]="appType?.value.oidcAppType === OIDCAppType.OIDC_APP_TYPE_NATIVE"
|
||||
>
|
||||
</cnsl-redirect-uris>
|
||||
|
||||
@ -136,97 +156,173 @@
|
||||
</div>
|
||||
</mat-step>
|
||||
|
||||
<mat-step *ngIf="appType?.value?.createType === AppCreateType.SAML" [editable]="true">
|
||||
<ng-template matStepLabel>{{ 'APP.SAML.CONFIGSECTION' | translate }}</ng-template>
|
||||
|
||||
<form [formGroup]="samlConfigForm">
|
||||
<cnsl-form-field class="formfield">
|
||||
<cnsl-label>{{ 'APP.SAML.URL' | translate }}</cnsl-label>
|
||||
<input cnslInput formControlName="metadataUrl" placeholder="https://" />
|
||||
</cnsl-form-field>
|
||||
</form>
|
||||
|
||||
<span class="cnsl-app-or cnsl-secondary-text">{{ 'APP.SAML.OR' | translate }}</span>
|
||||
|
||||
<input
|
||||
#xmlFileInput
|
||||
style="display: none"
|
||||
class="file-input"
|
||||
type="file"
|
||||
accept="text/xml,application/xml"
|
||||
(change)="onDropXML($any($event.target).files)"
|
||||
/>
|
||||
<button type="button" mat-stroked-button (click)="$event.preventDefault(); xmlFileInput.click()">
|
||||
{{ 'APP.SAML.XML' | translate }}
|
||||
</button>
|
||||
|
||||
<div
|
||||
class="saml-xml"
|
||||
[ngClass]="{ disabled: !!metadataUrl?.value }"
|
||||
*ngIf="decodedBase64 && samlAppRequest.toObject().metadataXml && !samlAppRequest.toObject().metadataUrl"
|
||||
>
|
||||
<!-- <p class="preview-text cnsl-secondary-text">PREVIEW</p> -->
|
||||
<ngx-codemirror
|
||||
[(ngModel)]="decodedBase64"
|
||||
[options]="{
|
||||
lineNumbers: true,
|
||||
theme: 'material',
|
||||
mode: 'application/xml'
|
||||
}"
|
||||
></ngx-codemirror>
|
||||
</div>
|
||||
|
||||
<div class="app-create-actions">
|
||||
<button mat-stroked-button class="bck-button" matStepperPrevious>{{ 'ACTIONS.BACK' | translate }}</button>
|
||||
<button mat-raised-button color="primary" matStepperNext [attr.data-e2e]="'continue-button-redirecturis'">
|
||||
{{ 'ACTIONS.CONTINUE' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
</mat-step>
|
||||
|
||||
<mat-step>
|
||||
<ng-template matStepLabel>{{ 'APP.OIDC.OVERVIEWSECTION' | translate }}</ng-template>
|
||||
<p class="step-title">{{ 'APP.OIDC.OVERVIEWTITLE' | translate }}</p>
|
||||
<div class="row cnsl-secondary-text">
|
||||
<span class="left">
|
||||
<div class="row">
|
||||
<span class="left cnsl-secondary-text">
|
||||
{{ 'APP.NAME' | translate }}
|
||||
</span>
|
||||
<span class="right">
|
||||
{{ oidcAppRequest.name }}
|
||||
{{ name?.value }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<ng-container *ngIf="appType?.value?.createType === AppCreateType.OIDC">
|
||||
<div class="row cnsl-secondary-text">
|
||||
<span class="left">
|
||||
<div class="row">
|
||||
<span class="left cnsl-secondary-text">
|
||||
{{ 'APP.TYPE' | translate }}
|
||||
</span>
|
||||
<span class="right">
|
||||
{{ 'APP.OIDC.APPTYPE.' + oidcAppRequest.appType | translate }}
|
||||
{{ 'APP.OIDC.APPTYPE.' + oidcAppRequest.toObject().appType | translate }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="row cnsl-secondary-text">
|
||||
<span class="left">
|
||||
<div class="row">
|
||||
<span class="left cnsl-secondary-text">
|
||||
{{ 'APP.GRANT' | translate }}
|
||||
</span>
|
||||
<span class="right" *ngIf="oidcAppRequest.grantTypesList && oidcAppRequest.grantTypesList.length > 0">
|
||||
[<span *ngFor="let element of oidcAppRequest.grantTypesList; index as i">
|
||||
<span
|
||||
class="right"
|
||||
*ngIf="oidcAppRequest.toObject().grantTypesList && oidcAppRequest.toObject().grantTypesList.length > 0"
|
||||
>
|
||||
[<span *ngFor="let element of oidcAppRequest.toObject().grantTypesList; index as i">
|
||||
{{ 'APP.OIDC.GRANT.' + element | translate }}
|
||||
{{ i < oidcAppRequest.grantTypesList.length - 1 ? ', ' : '' }} </span
|
||||
{{ i < oidcAppRequest.toObject().grantTypesList.length - 1 ? ', ' : '' }} </span
|
||||
>]
|
||||
</span>
|
||||
</div>
|
||||
<div class="row cnsl-secondary-text">
|
||||
<span class="left">
|
||||
<div class="row">
|
||||
<span class="left cnsl-secondary-text">
|
||||
{{ 'APP.OIDC.RESPONSETYPE' | translate }}
|
||||
</span>
|
||||
<span class="right" *ngIf="oidcAppRequest.responseTypesList && oidcAppRequest.responseTypesList.length > 0">
|
||||
[<span *ngFor="let element of oidcAppRequest.responseTypesList; index as i">
|
||||
<span
|
||||
class="right"
|
||||
*ngIf="oidcAppRequest.toObject().responseTypesList && oidcAppRequest.toObject().responseTypesList.length > 0"
|
||||
>
|
||||
[<span *ngFor="let element of oidcAppRequest.toObject().responseTypesList; index as i">
|
||||
{{ 'APP.OIDC.RESPONSE.' + element | translate }}
|
||||
{{ i < oidcAppRequest.responseTypesList.length - 1 ? ', ' : '' }} </span
|
||||
{{ i < oidcAppRequest.toObject().responseTypesList.length - 1 ? ', ' : '' }} </span
|
||||
>]
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="row cnsl-secondary-text">
|
||||
<span class="left">
|
||||
<div class="row">
|
||||
<span class="left cnsl-secondary-text">
|
||||
{{ 'APP.AUTHMETHOD' | translate }}
|
||||
</span>
|
||||
<span class="right">
|
||||
<span>
|
||||
{{ 'APP.OIDC.AUTHMETHOD.' + oidcAppRequest.authMethodType | translate }}
|
||||
{{ 'APP.OIDC.AUTHMETHOD.' + oidcAppRequest.toObject().authMethodType | translate }}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="row cnsl-secondary-text">
|
||||
<span class="left">
|
||||
<div class="row">
|
||||
<span class="left cnsl-secondary-text">
|
||||
{{ 'APP.OIDC.REDIRECT' | translate }}
|
||||
</span>
|
||||
<span class="right" *ngIf="oidcAppRequest.redirectUrisList && oidcAppRequest.redirectUrisList.length > 0">
|
||||
[<span *ngFor="let redirect of oidcAppRequest.redirectUrisList; index as i">
|
||||
<span
|
||||
class="right"
|
||||
*ngIf="oidcAppRequest.toObject().redirectUrisList && oidcAppRequest.toObject().redirectUrisList.length > 0"
|
||||
>
|
||||
[<span *ngFor="let redirect of oidcAppRequest.toObject().redirectUrisList; index as i">
|
||||
{{ redirect }}
|
||||
{{ i < oidcAppRequest.redirectUrisList.length - 1 ? ', ' : '' }} </span
|
||||
{{ i < oidcAppRequest.toObject().redirectUrisList.length - 1 ? ', ' : '' }} </span
|
||||
>]
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="row cnsl-secondary-text">
|
||||
<span class="left">
|
||||
<div class="row">
|
||||
<span class="left cnsl-secondary-text">
|
||||
{{ 'APP.OIDC.POSTLOGOUTREDIRECT' | translate }}
|
||||
</span>
|
||||
<span
|
||||
class="right"
|
||||
*ngIf="oidcAppRequest.postLogoutRedirectUrisList && oidcAppRequest.postLogoutRedirectUrisList.length > 0"
|
||||
*ngIf="
|
||||
oidcAppRequest.toObject().postLogoutRedirectUrisList &&
|
||||
oidcAppRequest.toObject().postLogoutRedirectUrisList.length > 0
|
||||
"
|
||||
>
|
||||
[<span *ngFor="let redirect of oidcAppRequest.postLogoutRedirectUrisList; index as i">
|
||||
[<span *ngFor="let redirect of oidcAppRequest.toObject().postLogoutRedirectUrisList; index as i">
|
||||
{{ redirect }}
|
||||
{{ i < oidcAppRequest.postLogoutRedirectUrisList.length - 1 ? ', ' : '' }} </span
|
||||
{{ i < oidcAppRequest.toObject().postLogoutRedirectUrisList.length - 1 ? ', ' : '' }} </span
|
||||
>]
|
||||
</span>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="appType?.value?.createType === AppCreateType.API">
|
||||
<div class="row cnsl-secondary-text">
|
||||
<span class="left">
|
||||
<div class="row">
|
||||
<span class="left cnsl-secondary-text">
|
||||
{{ 'APP.AUTHMETHOD' | translate }}
|
||||
</span>
|
||||
<span class="right">
|
||||
<span>
|
||||
{{ 'APP.API.AUTHMETHOD.' + apiAppRequest.authMethodType | translate }}
|
||||
{{ 'APP.API.AUTHMETHOD.' + authMethodType?.value | translate }}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="appType?.value?.createType === AppCreateType.SAML">
|
||||
<div class="row">
|
||||
<span class="left cnsl-secondary-text">
|
||||
{{ 'APP.SAML.METADATA' | translate }}
|
||||
</span>
|
||||
<span class="right">
|
||||
<span *ngIf="metadataUrl?.value">
|
||||
{{ metadataUrl?.value }}
|
||||
</span>
|
||||
<span *ngIf="samlAppRequest.toObject().metadataXml">
|
||||
{{ 'APP.SAML.METADATAFROMFILE' | translate }}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
@ -278,6 +374,7 @@
|
||||
</cnsl-form-field>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="formappType?.value?.createType !== AppCreateType.SAML">
|
||||
<cnsl-form-field class="formfield">
|
||||
<cnsl-label>{{ 'APP.AUTHMETHOD' | translate }}</cnsl-label>
|
||||
<mat-select formControlName="authMethodType">
|
||||
@ -287,6 +384,46 @@
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</cnsl-form-field>
|
||||
</ng-container>
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
<ng-container *ngIf="formappType?.value?.createType === AppCreateType.SAML">
|
||||
<div class="saml">
|
||||
<cnsl-form-field class="formfield">
|
||||
<cnsl-label>{{ 'APP.SAML.URL' | translate }}</cnsl-label>
|
||||
<input cnslInput formControlName="metadataUrl" placeholder="https://" />
|
||||
</cnsl-form-field>
|
||||
|
||||
<span class="cnsl-app-or cnsl-secondary-text">{{ 'APP.SAML.OR' | translate }}</span>
|
||||
|
||||
<input
|
||||
#xmlFileInput
|
||||
style="display: none"
|
||||
class="file-input"
|
||||
type="file"
|
||||
(change)="onDropXML($any($event.target).files)"
|
||||
/>
|
||||
<button type="button" mat-stroked-button (click)="$event.preventDefault(); xmlFileInput.click()">
|
||||
{{ 'APP.SAML.XML' | translate }}
|
||||
</button>
|
||||
|
||||
<div
|
||||
class="saml-xml"
|
||||
[ngClass]="{ disabled: !!formMetadataUrl?.value }"
|
||||
*ngIf="decodedBase64 && samlAppRequest.toObject().metadataXml && !samlAppRequest.toObject().metadataUrl"
|
||||
>
|
||||
<ngx-codemirror
|
||||
[(ngModel)]="decodedBase64"
|
||||
[options]="{
|
||||
lineNumbers: true,
|
||||
theme: 'material',
|
||||
mode: 'application/xml'
|
||||
}"
|
||||
></ngx-codemirror>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
</div>
|
||||
|
||||
<div class="content" *ngIf="formappType?.value?.createType === AppCreateType.OIDC">
|
||||
@ -294,22 +431,22 @@
|
||||
<cnsl-redirect-uris
|
||||
class="redirect-section"
|
||||
[canWrite]="true"
|
||||
(changedUris)="oidcAppRequest.redirectUrisList = $any($event)"
|
||||
[urisList]="oidcAppRequest.redirectUrisList"
|
||||
(changedUris)="oidcAppRequest.setRedirectUrisList($any($event))"
|
||||
[urisList]="oidcAppRequest.toObject().redirectUrisList"
|
||||
title="{{ 'APP.OIDC.REDIRECT' | translate }}"
|
||||
[getValues]="requestRedirectValuesSubject$"
|
||||
[isNative]="oidcAppRequest.appType === OIDCAppType.OIDC_APP_TYPE_NATIVE"
|
||||
[isNative]="appType?.value.oidcAppType === OIDCAppType.OIDC_APP_TYPE_NATIVE"
|
||||
>
|
||||
</cnsl-redirect-uris>
|
||||
|
||||
<cnsl-redirect-uris
|
||||
class="redirect-section"
|
||||
[canWrite]="true"
|
||||
(changedUris)="oidcAppRequest.postLogoutRedirectUrisList = $any($event)"
|
||||
[urisList]="oidcAppRequest.postLogoutRedirectUrisList"
|
||||
(changedUris)="oidcAppRequest.setPostLogoutRedirectUrisList($any($event))"
|
||||
[urisList]="oidcAppRequest.toObject().postLogoutRedirectUrisList"
|
||||
title="{{ 'APP.OIDC.POSTLOGOUTREDIRECT' | translate }}"
|
||||
[getValues]="requestRedirectValuesSubject$"
|
||||
[isNative]="oidcAppRequest.appType === OIDCAppType.OIDC_APP_TYPE_NATIVE"
|
||||
[isNative]="appType?.value.oidcAppType === OIDCAppType.OIDC_APP_TYPE_NATIVE"
|
||||
>
|
||||
</cnsl-redirect-uris>
|
||||
</div>
|
||||
|
@ -61,6 +61,10 @@ p.desc {
|
||||
}
|
||||
}
|
||||
|
||||
.cnsl-app-or {
|
||||
margin-right: 1rem;
|
||||
}
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
@ -72,10 +76,20 @@ p.desc {
|
||||
}
|
||||
}
|
||||
|
||||
.saml-xml {
|
||||
margin-top: 2rem;
|
||||
|
||||
&.disabled {
|
||||
opacity: 0.5;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
.app-create-actions {
|
||||
margin-top: 1rem;
|
||||
margin-top: 2rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
.bck-button {
|
||||
margin-right: 1rem;
|
||||
@ -102,6 +116,12 @@ p.desc {
|
||||
flex-basis: 80%;
|
||||
}
|
||||
}
|
||||
|
||||
.saml {
|
||||
display: block;
|
||||
width: 100%;
|
||||
margin: 0 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.continue-button {
|
||||
|
@ -5,6 +5,7 @@ import { Component, OnDestroy, OnInit } from '@angular/core';
|
||||
import { AbstractControl, UntypedFormBuilder, UntypedFormControl, UntypedFormGroup, Validators } from '@angular/forms';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { ActivatedRoute, Params, Router } from '@angular/router';
|
||||
import { Buffer } from 'buffer';
|
||||
import { Subject, Subscription } from 'rxjs';
|
||||
import { debounceTime, takeUntil } from 'rxjs/operators';
|
||||
import { RadioItemAuthType } from 'src/app/modules/app-radio/app-auth-method-radio/app-auth-method-radio.component';
|
||||
@ -14,12 +15,14 @@ import {
|
||||
OIDCAuthMethodType,
|
||||
OIDCGrantType,
|
||||
OIDCResponseType,
|
||||
SAMLConfig,
|
||||
} from 'src/app/proto/generated/zitadel/app_pb';
|
||||
import {
|
||||
AddAPIAppRequest,
|
||||
AddAPIAppResponse,
|
||||
AddOIDCAppRequest,
|
||||
AddOIDCAppResponse,
|
||||
AddSAMLAppRequest,
|
||||
} from 'src/app/proto/generated/zitadel/management_pb';
|
||||
import { Breadcrumb, BreadcrumbService, BreadcrumbType } from 'src/app/services/breadcrumb.service';
|
||||
import { ManagementService } from 'src/app/services/mgmt.service';
|
||||
@ -35,7 +38,9 @@ import {
|
||||
PKCE_METHOD,
|
||||
POST_METHOD,
|
||||
} from '../authmethods';
|
||||
import { API_TYPE, AppCreateType, NATIVE_TYPE, RadioItemAppType, USER_AGENT_TYPE, WEB_TYPE } from '../authtypes';
|
||||
import { API_TYPE, AppCreateType, NATIVE_TYPE, RadioItemAppType, SAML_TYPE, USER_AGENT_TYPE, WEB_TYPE } from '../authtypes';
|
||||
|
||||
const MAX_ALLOWED_SIZE = 1 * 1024 * 1024;
|
||||
|
||||
@Component({
|
||||
selector: 'cnsl-app-create',
|
||||
@ -49,11 +54,11 @@ export class AppCreateComponent implements OnInit, OnDestroy {
|
||||
public projectId: string = '';
|
||||
public loading: boolean = false;
|
||||
|
||||
public createSteps: number = 4;
|
||||
public currentCreateStep: number = 1;
|
||||
|
||||
public oidcAppRequest: AddOIDCAppRequest.AsObject = new AddOIDCAppRequest().toObject();
|
||||
public apiAppRequest: AddAPIAppRequest.AsObject = new AddAPIAppRequest().toObject();
|
||||
public oidcAppRequest: AddOIDCAppRequest = new AddOIDCAppRequest();
|
||||
public apiAppRequest: AddAPIAppRequest = new AddAPIAppRequest();
|
||||
public samlAppRequest: AddSAMLAppRequest = new AddSAMLAppRequest();
|
||||
|
||||
public oidcResponseTypes: { type: OIDCResponseType; checked: boolean; disabled: boolean }[] = [
|
||||
{ type: OIDCResponseType.OIDC_RESPONSE_TYPE_CODE, checked: false, disabled: false },
|
||||
@ -66,7 +71,7 @@ export class AppCreateComponent implements OnInit, OnDestroy {
|
||||
OIDCAppType.OIDC_APP_TYPE_NATIVE,
|
||||
OIDCAppType.OIDC_APP_TYPE_USER_AGENT,
|
||||
];
|
||||
public appTypes: any = [WEB_TYPE, NATIVE_TYPE, USER_AGENT_TYPE, API_TYPE];
|
||||
public appTypes: any = [WEB_TYPE, NATIVE_TYPE, USER_AGENT_TYPE, API_TYPE, SAML_TYPE];
|
||||
|
||||
public authMethods: RadioItemAuthType[] = [PKCE_METHOD, CODE_METHOD, PK_JWT_METHOD, POST_METHOD];
|
||||
|
||||
@ -84,8 +89,12 @@ export class AppCreateComponent implements OnInit, OnDestroy {
|
||||
];
|
||||
|
||||
// stepper
|
||||
firstFormGroup!: UntypedFormGroup;
|
||||
secondFormGroup!: UntypedFormGroup;
|
||||
public firstFormGroup!: UntypedFormGroup;
|
||||
public secondFormGroup!: UntypedFormGroup;
|
||||
public samlConfigForm!: UntypedFormGroup;
|
||||
|
||||
public redirectUrisList: string[] = [];
|
||||
public postLogoutRedirectUrisList: string[] = [];
|
||||
|
||||
// devmode
|
||||
public form!: UntypedFormGroup;
|
||||
@ -121,10 +130,13 @@ export class AppCreateComponent implements OnInit, OnDestroy {
|
||||
) {
|
||||
this.form = this.fb.group({
|
||||
name: ['', [Validators.required]],
|
||||
responseTypesList: ['', [Validators.required]],
|
||||
grantTypesList: ['', [Validators.required]],
|
||||
appType: ['', [Validators.required]],
|
||||
authMethodType: ['', [Validators.required]],
|
||||
// apptype OIDC
|
||||
responseTypesList: ['', []],
|
||||
grantTypesList: ['', []],
|
||||
authMethodType: ['', []],
|
||||
// apptype SAML
|
||||
metadataUrl: ['', []],
|
||||
});
|
||||
|
||||
this.initForm();
|
||||
@ -134,25 +146,30 @@ export class AppCreateComponent implements OnInit, OnDestroy {
|
||||
appType: [WEB_TYPE, [Validators.required]],
|
||||
});
|
||||
|
||||
this.samlConfigForm = this.fb.group({
|
||||
metadataUrl: ['', []],
|
||||
});
|
||||
|
||||
this.firstFormGroup.valueChanges.subscribe((value) => {
|
||||
if (this.firstFormGroup.valid) {
|
||||
this.oidcAppRequest.name = this.name?.value;
|
||||
this.apiAppRequest.name = this.name?.value;
|
||||
this.oidcAppRequest.setName(this.name?.value);
|
||||
this.apiAppRequest.setName(this.name?.value);
|
||||
this.samlAppRequest.setName(this.name?.value);
|
||||
|
||||
if (this.isStepperOIDC) {
|
||||
const oidcAppType = (this.appType?.value as RadioItemAppType).oidcAppType;
|
||||
if (oidcAppType !== undefined) {
|
||||
this.oidcAppRequest.appType = oidcAppType;
|
||||
this.oidcAppRequest.setAppType(oidcAppType);
|
||||
}
|
||||
|
||||
switch (this.oidcAppRequest.appType) {
|
||||
switch (this.appType?.value.oidcAppType) {
|
||||
case OIDCAppType.OIDC_APP_TYPE_NATIVE:
|
||||
this.authMethods = [PKCE_METHOD];
|
||||
|
||||
// automatically set to PKCE and skip step
|
||||
this.oidcAppRequest.responseTypesList = [OIDCResponseType.OIDC_RESPONSE_TYPE_CODE];
|
||||
this.oidcAppRequest.grantTypesList = [OIDCGrantType.OIDC_GRANT_TYPE_AUTHORIZATION_CODE];
|
||||
this.oidcAppRequest.authMethodType = OIDCAuthMethodType.OIDC_AUTH_METHOD_TYPE_NONE;
|
||||
this.oidcAppRequest.setResponseTypesList([OIDCResponseType.OIDC_RESPONSE_TYPE_CODE]);
|
||||
this.oidcAppRequest.setGrantTypesList([OIDCGrantType.OIDC_GRANT_TYPE_AUTHORIZATION_CODE]);
|
||||
this.oidcAppRequest.setAuthMethodType(OIDCAuthMethodType.OIDC_AUTH_METHOD_TYPE_NONE);
|
||||
|
||||
break;
|
||||
case OIDCAppType.OIDC_APP_TYPE_WEB:
|
||||
@ -179,19 +196,28 @@ export class AppCreateComponent implements OnInit, OnDestroy {
|
||||
this.secondFormGroup = this.fb.group({
|
||||
authMethod: [this.authMethods[0].key, [Validators.required]],
|
||||
});
|
||||
|
||||
this.secondFormGroup.valueChanges.subscribe((form) => {
|
||||
const partialConfig = getPartialConfigFromAuthMethod(form.authMethod);
|
||||
|
||||
if (this.isStepperOIDC && partialConfig && partialConfig.oidc) {
|
||||
this.oidcAppRequest.responseTypesList = partialConfig.oidc?.responseTypesList ?? [];
|
||||
this.oidcAppRequest.setResponseTypesList(partialConfig.oidc?.responseTypesList ?? []);
|
||||
|
||||
this.oidcAppRequest.grantTypesList = partialConfig.oidc?.grantTypesList ?? [];
|
||||
this.oidcAppRequest.setGrantTypesList(partialConfig.oidc?.grantTypesList ?? []);
|
||||
|
||||
this.oidcAppRequest.authMethodType =
|
||||
partialConfig.oidc?.authMethodType ?? OIDCAuthMethodType.OIDC_AUTH_METHOD_TYPE_NONE;
|
||||
this.oidcAppRequest.setAuthMethodType(
|
||||
partialConfig.oidc?.authMethodType ?? OIDCAuthMethodType.OIDC_AUTH_METHOD_TYPE_NONE,
|
||||
);
|
||||
} else if (this.isStepperAPI && partialConfig && partialConfig.api) {
|
||||
this.apiAppRequest.authMethodType =
|
||||
partialConfig.api?.authMethodType ?? APIAuthMethodType.API_AUTH_METHOD_TYPE_BASIC;
|
||||
this.apiAppRequest.setAuthMethodType(
|
||||
partialConfig.api?.authMethodType ?? APIAuthMethodType.API_AUTH_METHOD_TYPE_BASIC,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
this.samlConfigForm.valueChanges.subscribe((form) => {
|
||||
if (form.metadataUrl && form.metadataUrl.length > 0) {
|
||||
this.samlAppRequest.setMetadataUrl(form.metadataUrl);
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -225,18 +251,23 @@ export class AppCreateComponent implements OnInit, OnDestroy {
|
||||
|
||||
public initForm(): void {
|
||||
this.form.valueChanges.pipe(takeUntil(this.destroyed$), debounceTime(150)).subscribe(() => {
|
||||
this.oidcAppRequest.name = this.formname?.value;
|
||||
this.apiAppRequest.name = this.formname?.value;
|
||||
this.oidcAppRequest.setName(this.formname?.value);
|
||||
this.apiAppRequest.setName(this.formname?.value);
|
||||
this.samlAppRequest.setName(this.formname?.value);
|
||||
|
||||
this.oidcAppRequest.responseTypesList = this.formresponseTypesList?.value;
|
||||
this.oidcAppRequest.grantTypesList = this.formgrantTypesList?.value;
|
||||
this.oidcAppRequest.setResponseTypesList(this.formresponseTypesList?.value);
|
||||
this.oidcAppRequest.setGrantTypesList(this.grantTypesList?.value);
|
||||
|
||||
this.oidcAppRequest.authMethodType = this.formauthMethodType?.value;
|
||||
this.apiAppRequest.authMethodType = this.formauthMethodType?.value;
|
||||
this.oidcAppRequest.setAuthMethodType(this.authMethodType?.value);
|
||||
this.apiAppRequest.setAuthMethodType(this.authMethodType?.value);
|
||||
|
||||
if (this.formMetadataUrl?.value) {
|
||||
this.samlAppRequest.setMetadataUrl(this.formMetadataUrl?.value);
|
||||
}
|
||||
|
||||
const oidcAppType = (this.formappType?.value as RadioItemAppType).oidcAppType;
|
||||
if (oidcAppType !== undefined) {
|
||||
this.oidcAppRequest.appType = oidcAppType;
|
||||
this.oidcAppRequest.setAppType(oidcAppType);
|
||||
}
|
||||
});
|
||||
|
||||
@ -282,17 +313,41 @@ export class AppCreateComponent implements OnInit, OnDestroy {
|
||||
|
||||
private async getData({ projectid }: Params): Promise<void> {
|
||||
this.projectId = projectid;
|
||||
this.oidcAppRequest.projectId = projectid;
|
||||
this.apiAppRequest.projectId = projectid;
|
||||
this.oidcAppRequest.setProjectId(projectid);
|
||||
this.apiAppRequest.setProjectId(projectid);
|
||||
this.samlAppRequest.setProjectId(projectid);
|
||||
}
|
||||
|
||||
public close(): void {
|
||||
this._location.back();
|
||||
}
|
||||
|
||||
public onDropXML(filelist: FileList): void {
|
||||
const file = filelist.item(0);
|
||||
this.metadataUrl?.setValue('');
|
||||
if (file) {
|
||||
if (file.size > MAX_ALLOWED_SIZE) {
|
||||
this.toast.showInfo('POLICY.PRIVATELABELING.MAXSIZEEXCEEDED', true);
|
||||
} else {
|
||||
const reader = new FileReader();
|
||||
reader.onload = ((aXML) => {
|
||||
return (e) => {
|
||||
const xmlBase64 = e.target?.result;
|
||||
if (xmlBase64 && typeof xmlBase64 === 'string') {
|
||||
const cropped = xmlBase64.replace('data:text/xml;base64,', '');
|
||||
this.samlAppRequest.setMetadataXml(cropped);
|
||||
}
|
||||
};
|
||||
})(file);
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public createApp(): void {
|
||||
const appOIDCCheck = this.devmode ? this.isDevOIDC : this.isStepperOIDC;
|
||||
const appAPICheck = this.devmode ? this.isDevAPI : this.isStepperAPI;
|
||||
const appSAMLCheck = this.devmode ? this.isDevSAML : this.isStepperSAML;
|
||||
|
||||
if (appOIDCCheck) {
|
||||
this.requestRedirectValuesSubject$.next();
|
||||
@ -331,6 +386,19 @@ export class AppCreateComponent implements OnInit, OnDestroy {
|
||||
this.loading = false;
|
||||
this.toast.showError(error);
|
||||
});
|
||||
} else if (appSAMLCheck) {
|
||||
this.loading = true;
|
||||
this.toast.showInfo('APP.TOAST.CREATED', true);
|
||||
this.mgmtService
|
||||
.addSAMLApp(this.samlAppRequest)
|
||||
.then((resp) => {
|
||||
this.loading = false;
|
||||
this.router.navigate(['projects', this.projectId, 'apps', resp.appId]);
|
||||
})
|
||||
.catch((error) => {
|
||||
this.loading = false;
|
||||
this.toast.showError(error);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -381,19 +449,27 @@ export class AppCreateComponent implements OnInit, OnDestroy {
|
||||
get formname(): AbstractControl | null {
|
||||
return this.form.get('name');
|
||||
}
|
||||
|
||||
get formresponseTypesList(): AbstractControl | null {
|
||||
return this.form.get('responseTypesList');
|
||||
}
|
||||
get formgrantTypesList(): AbstractControl | null {
|
||||
|
||||
get grantTypesList(): AbstractControl | null {
|
||||
return this.form.get('grantTypesList');
|
||||
}
|
||||
|
||||
get formappType(): AbstractControl | null {
|
||||
return this.form.get('appType');
|
||||
}
|
||||
|
||||
get formMetadataUrl(): AbstractControl | null {
|
||||
return this.form.get('metadataUrl');
|
||||
}
|
||||
// get formapplicationType(): AbstractControl | null {
|
||||
// return this.form.get('applicationType');
|
||||
// }
|
||||
get formauthMethodType(): AbstractControl | null {
|
||||
|
||||
get authMethodType(): AbstractControl | null {
|
||||
return this.form.get('authMethodType');
|
||||
}
|
||||
|
||||
@ -409,7 +485,35 @@ export class AppCreateComponent implements OnInit, OnDestroy {
|
||||
return (this.formappType?.value as RadioItemAppType).createType === AppCreateType.API;
|
||||
}
|
||||
|
||||
get isDevSAML(): boolean {
|
||||
return (this.formappType?.value as RadioItemAppType).createType === AppCreateType.SAML;
|
||||
}
|
||||
|
||||
get isStepperAPI(): boolean {
|
||||
return (this.appType?.value as RadioItemAppType).createType === AppCreateType.API;
|
||||
}
|
||||
|
||||
get isStepperSAML(): boolean {
|
||||
return (this.appType?.value as RadioItemAppType).createType === AppCreateType.SAML;
|
||||
}
|
||||
|
||||
get decodedBase64(): string {
|
||||
const samlReq = this.samlAppRequest.toObject();
|
||||
if (samlReq && samlReq.metadataXml && typeof samlReq.metadataXml === 'string') {
|
||||
return Buffer.from(samlReq.metadataXml, 'base64').toString('ascii');
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
set decodedBase64(xmlString) {
|
||||
if (this.samlAppRequest) {
|
||||
const base64 = Buffer.from(xmlString, 'ascii').toString('base64');
|
||||
this.samlAppRequest.setMetadataXml(base64);
|
||||
}
|
||||
}
|
||||
|
||||
public get metadataUrl(): AbstractControl | null {
|
||||
return this.samlConfigForm.get('metadataUrl');
|
||||
}
|
||||
}
|
||||
|
@ -2,7 +2,7 @@
|
||||
title="{{ app?.name }}"
|
||||
[hasActions]="isZitadel === false && (['project.app.write:' + projectId, 'project.app.write'] | hasRole | async)"
|
||||
docLink="https://docs.zitadel.com/docs/guides/basics/projects"
|
||||
[sub]="app?.oidcConfig ? ('APP.OIDC.APPTYPE.' + app?.oidcConfig?.appType | translate) : 'API'"
|
||||
[sub]="app?.oidcConfig ? ('APP.OIDC.APPTYPE.' + app?.oidcConfig?.appType | translate) : app?.apiConfig ? 'API' : 'SAML'"
|
||||
[isActive]="app?.state === AppState.APP_STATE_ACTIVE"
|
||||
[isInactive]="app?.state === AppState.APP_STATE_INACTIVE"
|
||||
stateTooltip="{{ 'APP.PAGES.DETAIL.STATE.' + app?.state | translate }}"
|
||||
@ -156,6 +156,60 @@
|
||||
</div>
|
||||
</form>
|
||||
</cnsl-card>
|
||||
|
||||
<cnsl-card *ngIf="samlForm && app?.samlConfig">
|
||||
<form [formGroup]="samlForm">
|
||||
<cnsl-form-field class="formfield">
|
||||
<cnsl-label>{{ 'APP.SAML.URL' | translate }}</cnsl-label>
|
||||
<input cnslInput formControlName="metadataUrl" placeholder="https://" />
|
||||
</cnsl-form-field>
|
||||
</form>
|
||||
|
||||
<div class="cnsl-saml-config-line">
|
||||
<span class="cnsl-app-or cnsl-secondary-text">{{ 'APP.SAML.OR' | translate }}</span>
|
||||
|
||||
<input
|
||||
#xmlFileInput
|
||||
style="display: none"
|
||||
class="file-input"
|
||||
type="file"
|
||||
accept="text/xml,application/xml"
|
||||
(change)="onDropXML($any($event.target).files)"
|
||||
/>
|
||||
<button mat-stroked-button (click)="$event.preventDefault(); xmlFileInput.click()">
|
||||
{{ 'APP.SAML.XML' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="saml-xml"
|
||||
[ngClass]="{ disabled: !!metadataUrl?.value }"
|
||||
*ngIf="decodedBase64 && app && app.samlConfig && !app.samlConfig.metadataUrl"
|
||||
>
|
||||
<ngx-codemirror
|
||||
[(ngModel)]="decodedBase64"
|
||||
[disabled]="!!metadataUrl?.value"
|
||||
[options]="{
|
||||
lineNumbers: true,
|
||||
theme: 'material',
|
||||
mode: 'application/xml'
|
||||
}"
|
||||
></ngx-codemirror>
|
||||
</div>
|
||||
|
||||
<div class="btn-container">
|
||||
<button
|
||||
class="submit-button"
|
||||
type="submit"
|
||||
color="primary"
|
||||
(click)="saveSAMLApp()"
|
||||
[disabled]="samlForm.invalid || !canWrite"
|
||||
mat-raised-button
|
||||
>
|
||||
{{ 'ACTIONS.SAVE' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
</cnsl-card>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="currentSetting === 'token'">
|
||||
|
@ -158,6 +158,24 @@
|
||||
margin-left: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.cnsl-saml-config-line {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.cnsl-app-or {
|
||||
margin-right: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.saml-xml {
|
||||
margin-top: 2rem;
|
||||
|
||||
&.disabled {
|
||||
opacity: 0.5;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -7,6 +7,7 @@ import { MatCheckboxChange } from '@angular/material/checkbox';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { Buffer } from 'buffer';
|
||||
import { Duration } from 'google-protobuf/google/protobuf/duration_pb';
|
||||
import { Subject, Subscription } from 'rxjs';
|
||||
import { take } from 'rxjs/operators';
|
||||
@ -27,11 +28,13 @@ import {
|
||||
OIDCGrantType,
|
||||
OIDCResponseType,
|
||||
OIDCTokenType,
|
||||
SAMLConfig,
|
||||
} from 'src/app/proto/generated/zitadel/app_pb';
|
||||
import {
|
||||
GetOIDCInformationResponse,
|
||||
UpdateAPIAppConfigRequest,
|
||||
UpdateOIDCAppConfigRequest,
|
||||
UpdateSAMLAppConfigRequest,
|
||||
} from 'src/app/proto/generated/zitadel/management_pb';
|
||||
import { Breadcrumb, BreadcrumbService, BreadcrumbType } from 'src/app/services/breadcrumb.service';
|
||||
import { GrpcAuthService } from 'src/app/services/grpc-auth.service';
|
||||
@ -52,6 +55,8 @@ import {
|
||||
} from '../authmethods';
|
||||
import { AuthMethodDialogComponent } from './auth-method-dialog/auth-method-dialog.component';
|
||||
|
||||
const MAX_ALLOWED_SIZE = 1 * 1024 * 1024;
|
||||
|
||||
@Component({
|
||||
selector: 'cnsl-app-detail',
|
||||
templateUrl: './app-detail.component.html',
|
||||
@ -104,6 +109,7 @@ export class AppDetailComponent implements OnInit, OnDestroy {
|
||||
public oidcForm!: UntypedFormGroup;
|
||||
public oidcTokenForm!: UntypedFormGroup;
|
||||
public apiForm!: UntypedFormGroup;
|
||||
public samlForm!: UntypedFormGroup;
|
||||
|
||||
public redirectUrisList: string[] = [];
|
||||
public postLogoutRedirectUrisList: string[] = [];
|
||||
@ -162,6 +168,11 @@ export class AppDetailComponent implements OnInit, OnDestroy {
|
||||
authMethodType: [{ value: '', disabled: true }],
|
||||
});
|
||||
|
||||
this.samlForm = this.fb.group({
|
||||
metadataUrl: [{ value: '', disabled: true }],
|
||||
metadataXml: [{ value: '', disabled: true }],
|
||||
});
|
||||
|
||||
this.http.get('./assets/environment.json').subscribe((env: any) => {
|
||||
this.environmentMap = {
|
||||
issuer: env.issuer,
|
||||
@ -290,12 +301,15 @@ export class AppDetailComponent implements OnInit, OnDestroy {
|
||||
} else {
|
||||
this.authMethods = this.authMethods.filter((element) => element !== CUSTOM_METHOD);
|
||||
}
|
||||
} else if (this.app.samlConfig) {
|
||||
this.settingsList = [{ id: 'configuration', i18nKey: 'APP.CONFIGURATION' }];
|
||||
}
|
||||
|
||||
if (allowed) {
|
||||
this.oidcForm.enable();
|
||||
this.oidcTokenForm.enable();
|
||||
this.apiForm.enable();
|
||||
this.samlForm.enable();
|
||||
}
|
||||
|
||||
if (this.app.oidcConfig?.redirectUrisList) {
|
||||
@ -370,6 +384,30 @@ export class AppDetailComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
}
|
||||
|
||||
public onDropXML(filelist: FileList): void {
|
||||
const file = filelist.item(0);
|
||||
if (file) {
|
||||
if (file.size > MAX_ALLOWED_SIZE) {
|
||||
this.toast.showInfo('POLICY.PRIVATELABELING.MAXSIZEEXCEEDED', true);
|
||||
} else {
|
||||
this.metadataUrl?.setValue('');
|
||||
const reader = new FileReader();
|
||||
reader.onload = ((aXML) => {
|
||||
return (e) => {
|
||||
const xmlBase64 = e.target?.result;
|
||||
if (xmlBase64 && typeof xmlBase64 === 'string' && this.app?.samlConfig) {
|
||||
const samlConfig = new SAMLConfig();
|
||||
const cropped = xmlBase64.replace('data:text/xml;base64,', '');
|
||||
samlConfig.setMetadataXml(cropped);
|
||||
this.app.samlConfig.metadataXml = cropped;
|
||||
}
|
||||
};
|
||||
})(file);
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public authMethodFromPartialConfig(config: { oidc?: OIDCConfig.AsObject; api?: APIConfig.AsObject }): string {
|
||||
const key = getAuthMethodFromPartialConfig(config);
|
||||
return key;
|
||||
@ -581,6 +619,28 @@ export class AppDetailComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
}
|
||||
|
||||
public saveSAMLApp(): void {
|
||||
if (this.samlForm.valid && this.app?.samlConfig) {
|
||||
const req = new UpdateSAMLAppConfigRequest();
|
||||
req.setProjectId(this.projectId);
|
||||
req.setAppId(this.app.id);
|
||||
|
||||
if (this.app.samlConfig) {
|
||||
req.setMetadataUrl(this.app.samlConfig?.metadataUrl);
|
||||
req.setMetadataXml(this.app.samlConfig?.metadataXml);
|
||||
}
|
||||
|
||||
this.mgmtService
|
||||
.updateSAMLAppConfig(req)
|
||||
.then(() => {
|
||||
this.toast.showInfo('APP.TOAST.APIUPDATED', true);
|
||||
})
|
||||
.catch((error) => {
|
||||
this.toast.showError(error);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public regenerateOIDCClientSecret(): void {
|
||||
if (this.app) {
|
||||
this.mgmtService
|
||||
@ -693,4 +753,31 @@ export class AppDetailComponent implements OnInit, OnDestroy {
|
||||
public get clockSkewSeconds(): AbstractControl | null {
|
||||
return this.oidcTokenForm.get('clockSkewSeconds');
|
||||
}
|
||||
|
||||
public get metadataUrl(): AbstractControl | null {
|
||||
return this.samlForm.get('metadataUrl');
|
||||
}
|
||||
|
||||
get decodedBase64(): string {
|
||||
if (
|
||||
this.app &&
|
||||
this.app.samlConfig &&
|
||||
this.app.samlConfig.metadataXml &&
|
||||
typeof this.app.samlConfig.metadataXml === 'string'
|
||||
) {
|
||||
return Buffer.from(this.app?.samlConfig.metadataXml, 'base64').toString('ascii');
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
set decodedBase64(xmlString: string) {
|
||||
if (this.app && this.app.samlConfig && this.app.samlConfig.metadataXml) {
|
||||
const base64 = Buffer.from(xmlString, 'ascii').toString('base64');
|
||||
|
||||
if (this.app.samlConfig) {
|
||||
this.app.samlConfig.metadataXml = base64;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -16,6 +16,7 @@ import { MatSlideToggleModule } from '@angular/material/slide-toggle';
|
||||
import { MatSliderModule } from '@angular/material/slider';
|
||||
import { MatStepperModule } from '@angular/material/stepper';
|
||||
import { MatTooltipModule } from '@angular/material/tooltip';
|
||||
import { CodemirrorModule } from '@ctrl/ngx-codemirror';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { CopyToClipboardModule } from 'src/app/directives/copy-to-clipboard/copy-to-clipboard.module';
|
||||
import { HasRoleModule } from 'src/app/directives/has-role/has-role.module';
|
||||
@ -89,6 +90,7 @@ import { RedirectUrisComponent } from './redirect-uris/redirect-uris.component';
|
||||
InputModule,
|
||||
MetaLayoutModule,
|
||||
MatSliderModule,
|
||||
CodemirrorModule,
|
||||
ChangesModule,
|
||||
InfoSectionModule,
|
||||
],
|
||||
|
@ -10,6 +10,7 @@ import { OIDCAppType } from 'src/app/proto/generated/zitadel/app_pb';
|
||||
export enum AppCreateType {
|
||||
API = 'API',
|
||||
OIDC = 'OIDC',
|
||||
SAML = 'SAML',
|
||||
}
|
||||
|
||||
export interface RadioItemAppType {
|
||||
@ -20,6 +21,7 @@ export interface RadioItemAppType {
|
||||
descI18nKey: string;
|
||||
prefix: string;
|
||||
background: string;
|
||||
protocol: 'OIDC' | 'SAML';
|
||||
}
|
||||
|
||||
export const WEB_TYPE: RadioItemAppType = {
|
||||
@ -30,6 +32,7 @@ export const WEB_TYPE: RadioItemAppType = {
|
||||
oidcAppType: OIDCAppType.OIDC_APP_TYPE_WEB,
|
||||
prefix: 'WEB',
|
||||
background: 'linear-gradient(40deg, #059669 30%, #047857)',
|
||||
protocol: 'OIDC',
|
||||
};
|
||||
|
||||
export const USER_AGENT_TYPE: RadioItemAppType = {
|
||||
@ -40,6 +43,7 @@ export const USER_AGENT_TYPE: RadioItemAppType = {
|
||||
oidcAppType: OIDCAppType.OIDC_APP_TYPE_USER_AGENT,
|
||||
prefix: 'UA',
|
||||
background: 'linear-gradient(40deg, #dc2626 30%, #db2777)',
|
||||
protocol: 'OIDC',
|
||||
};
|
||||
|
||||
export const NATIVE_TYPE: RadioItemAppType = {
|
||||
@ -50,6 +54,7 @@ export const NATIVE_TYPE: RadioItemAppType = {
|
||||
oidcAppType: OIDCAppType.OIDC_APP_TYPE_NATIVE,
|
||||
prefix: 'N',
|
||||
background: 'linear-gradient(40deg, #306ccc 30%, #4f46e5)',
|
||||
protocol: 'OIDC',
|
||||
};
|
||||
|
||||
export const API_TYPE: RadioItemAppType = {
|
||||
@ -59,4 +64,14 @@ export const API_TYPE: RadioItemAppType = {
|
||||
createType: AppCreateType.API,
|
||||
prefix: 'API',
|
||||
background: 'linear-gradient(40deg, #1f2937, #111827)',
|
||||
protocol: 'OIDC',
|
||||
};
|
||||
|
||||
export const SAML_TYPE: RadioItemAppType = {
|
||||
titleI18nKey: 'APP.SAML.SELECTION.TITLE',
|
||||
descI18nKey: 'APP.SAML.SELECTION.DESCRIPTION',
|
||||
createType: AppCreateType.SAML,
|
||||
prefix: 'SAML',
|
||||
background: 'linear-gradient(40deg,rgb(110, 56, 124), rgb(88, 37, 103))',
|
||||
protocol: 'SAML',
|
||||
};
|
||||
|
@ -17,20 +17,27 @@
|
||||
*ngFor="let app of appsSubject | async"
|
||||
matTooltip="{{ 'ACTIONS.EDIT' | translate }}"
|
||||
>
|
||||
<cnsl-app-card class="grid-card" matRipple [type]="app.oidcConfig?.appType" [isApiApp]="app.apiConfig !== undefined">
|
||||
<cnsl-app-card
|
||||
class="grid-card"
|
||||
matRipple
|
||||
[type]="app.samlConfig ? 'SAML' : app.oidcConfig?.appType"
|
||||
[isApiApp]="app.apiConfig !== undefined"
|
||||
>
|
||||
{{ app.name.charAt(0) }}
|
||||
<ng-container *ngIf="app.oidcConfig?.appType !== undefined">
|
||||
<i *ngIf="app.oidcConfig?.appType === OIDCAppType.OIDC_APP_TYPE_NATIVE" class="las la-mobile"></i>
|
||||
<i *ngIf="app.oidcConfig?.appType === OIDCAppType.OIDC_APP_TYPE_WEB" class="las la-code"></i>
|
||||
<i *ngIf="app.oidcConfig?.appType === OIDCAppType.OIDC_APP_TYPE_USER_AGENT" class="las la-code"></i>
|
||||
<i *ngIf="app.oidcConfig?.appType === OIDCAppType.OIDC_APP_TYPE_NATIVE" class="lab la-openid"></i>
|
||||
<i *ngIf="app.oidcConfig?.appType === OIDCAppType.OIDC_APP_TYPE_WEB" class="lab la-openid"></i>
|
||||
<i *ngIf="app.oidcConfig?.appType === OIDCAppType.OIDC_APP_TYPE_USER_AGENT" class="lab la-openid"></i>
|
||||
<i *ngIf="app.apiConfig" class="las la-robot"></i>
|
||||
</ng-container>
|
||||
<span *ngIf="app.samlConfig" class="samlspan">SAML</span>
|
||||
</cnsl-app-card>
|
||||
<span class="name">{{ app.name }}</span>
|
||||
<span *ngIf="app.oidcConfig?.appType !== undefined && app.oidcConfig?.appType !== null" class="type">
|
||||
{{ 'APP.OIDC.APPTYPE.' + app.oidcConfig?.appType | translate }}</span
|
||||
>
|
||||
<span *ngIf="app.apiConfig !== undefined" class="type"> API</span>
|
||||
<span *ngIf="app.samlConfig !== undefined" class="type"> SAML</span>
|
||||
</div>
|
||||
|
||||
<ng-template cnslHasRole [hasRole]="['project.app.write']">
|
||||
|
@ -49,6 +49,17 @@
|
||||
border-radius: 0.5rem;
|
||||
font-weight: 800;
|
||||
box-sizing: border-box;
|
||||
|
||||
i {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.samlspan {
|
||||
font-size: 11px;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.05em;
|
||||
line-height: 1.2rem;
|
||||
}
|
||||
}
|
||||
|
||||
.name {
|
||||
|
@ -63,6 +63,8 @@ import {
|
||||
AddProjectResponse,
|
||||
AddProjectRoleRequest,
|
||||
AddProjectRoleResponse,
|
||||
AddSAMLAppRequest,
|
||||
AddSAMLAppResponse,
|
||||
AddSecondFactorToLoginPolicyRequest,
|
||||
AddSecondFactorToLoginPolicyResponse,
|
||||
AddUserGrantRequest,
|
||||
@ -423,6 +425,8 @@ import {
|
||||
UpdateProjectResponse,
|
||||
UpdateProjectRoleRequest,
|
||||
UpdateProjectRoleResponse,
|
||||
UpdateSAMLAppConfigRequest,
|
||||
UpdateSAMLAppConfigResponse,
|
||||
UpdateUserGrantRequest,
|
||||
UpdateUserGrantResponse,
|
||||
UpdateUserNameRequest,
|
||||
@ -2268,27 +2272,18 @@ export class ManagementService {
|
||||
return this.grpcService.mgmt.reactivateProjectGrant(req, null).then((resp) => resp.toObject());
|
||||
}
|
||||
|
||||
public addOIDCApp(app: AddOIDCAppRequest.AsObject): Promise<AddOIDCAppResponse.AsObject> {
|
||||
const req: AddOIDCAppRequest = new AddOIDCAppRequest();
|
||||
req.setAuthMethodType(app.authMethodType);
|
||||
req.setName(app.name);
|
||||
req.setProjectId(app.projectId);
|
||||
req.setResponseTypesList(app.responseTypesList);
|
||||
req.setGrantTypesList(app.grantTypesList);
|
||||
req.setAppType(app.appType);
|
||||
req.setPostLogoutRedirectUrisList(app.postLogoutRedirectUrisList);
|
||||
req.setRedirectUrisList(app.redirectUrisList);
|
||||
public addOIDCApp(req: AddOIDCAppRequest): Promise<AddOIDCAppResponse.AsObject> {
|
||||
return this.grpcService.mgmt.addOIDCApp(req, null).then((resp) => resp.toObject());
|
||||
}
|
||||
|
||||
public addAPIApp(app: AddAPIAppRequest.AsObject): Promise<AddAPIAppResponse.AsObject> {
|
||||
const req: AddAPIAppRequest = new AddAPIAppRequest();
|
||||
req.setAuthMethodType(app.authMethodType);
|
||||
req.setName(app.name);
|
||||
req.setProjectId(app.projectId);
|
||||
public addAPIApp(req: AddAPIAppRequest): Promise<AddAPIAppResponse.AsObject> {
|
||||
return this.grpcService.mgmt.addAPIApp(req, null).then((resp) => resp.toObject());
|
||||
}
|
||||
|
||||
public addSAMLApp(req: AddSAMLAppRequest): Promise<AddSAMLAppResponse.AsObject> {
|
||||
return this.grpcService.mgmt.addSAMLApp(req, null).then((resp) => resp.toObject());
|
||||
}
|
||||
|
||||
public regenerateAPIClientSecret(appId: string, projectId: string): Promise<RegenerateAPIClientSecretResponse.AsObject> {
|
||||
const req = new RegenerateAPIClientSecretRequest();
|
||||
req.setAppId(appId);
|
||||
@ -2312,6 +2307,10 @@ export class ManagementService {
|
||||
return this.grpcService.mgmt.updateAPIAppConfig(req, null).then((resp) => resp.toObject());
|
||||
}
|
||||
|
||||
public updateSAMLAppConfig(req: UpdateSAMLAppConfigRequest): Promise<UpdateSAMLAppConfigResponse.AsObject> {
|
||||
return this.grpcService.mgmt.updateSAMLAppConfig(req, null).then((resp) => resp.toObject());
|
||||
}
|
||||
|
||||
public removeApp(projectId: string, appId: string): Promise<RemoveAppResponse.AsObject> {
|
||||
const req = new RemoveAppRequest();
|
||||
req.setAppId(appId);
|
||||
|
@ -1609,9 +1609,9 @@
|
||||
"TITLE": "Anwendung",
|
||||
"ID": "ID",
|
||||
"DESCRIPTION": "Hier kannst Du Deine Applikationen bearbeiten und deren Konfiguration anpassen.",
|
||||
"CREATE_OIDC": "OIDC-Anwendung",
|
||||
"CREATE_OIDC_DESC_TITLE": "Gebe die Daten der Anwendung Schritt für Schritt ein.",
|
||||
"CREATE_OIDC_DESC_SUB": "Es wird automatisch eine empfohlene Konfiguration generiert.",
|
||||
"CREATE": "Applikation erstellen",
|
||||
"CREATE_DESC_TITLE": "Gebe die Daten der Anwendung Schritt für Schritt ein.",
|
||||
"CREATE_DESC_SUB": "Es wird automatisch eine empfohlene Konfiguration generiert.",
|
||||
"STATE": "Status",
|
||||
"DATECREATED": "Erstellt",
|
||||
"DATECHANGED": "Geändert",
|
||||
@ -1664,6 +1664,10 @@
|
||||
"ADDITIONALORIGINSDESC": "Wenn sie zusätzliche Origins definieren wollen, die nicht den Redirect URIs gleichzusetzen sind, können Sie dies hier tun.",
|
||||
"ORIGINS": "Origins",
|
||||
"NOTANORIGIN": "Der Angegebene Wert ist kein Origin.",
|
||||
"PROSWITCH": "Konfigurator überspringen",
|
||||
"NAMEANDTYPESECTION": "Name und Typ",
|
||||
"TITLEFIRST": "Name der Applikation.",
|
||||
"TYPETITLE": "Art der Anwendung",
|
||||
"OIDC": {
|
||||
"INFO": {
|
||||
"ISSUER": "Issuer",
|
||||
@ -1672,10 +1676,6 @@
|
||||
"CURRENT": "Aktuelle Konfiguration",
|
||||
"TOKENSECTIONTITLE": "AuthToken Optionen",
|
||||
"REDIRECTSECTIONTITLE": "Weiterleitungseinstellungen",
|
||||
"PROSWITCH": "Konfigurator überspringen",
|
||||
"NAMEANDTYPESECTION": "Name und Typ",
|
||||
"TITLEFIRST": "Gebe zuerst einen Namen ein.",
|
||||
"TYPETITLE": "Welche Art von Anwendung möchtest Du erstellen?",
|
||||
"REDIRECTTITLE": "Wohin soll nach dem Log-in weitergeleitet werden?",
|
||||
"REDIRECTDESCRIPTIONWEB": "Die Weiterleitung muss mit https:// beginnen. http:// ist nur im Entwicklermodus zulässig.",
|
||||
"REDIRECTDESCRIPTIONNATIVE": "Die Weiterleitung muss mit einem eigenen Protokoll, http://127.0.0.1, http://[::1] oder http://localhost beginnen.",
|
||||
@ -1767,6 +1767,18 @@
|
||||
"1": "Private Key JWT"
|
||||
}
|
||||
},
|
||||
"SAML": {
|
||||
"SELECTION": {
|
||||
"TITLE": "SAML",
|
||||
"DESCRIPTION": "SAML Applikationen"
|
||||
},
|
||||
"CONFIGSECTION": "SAML Konfiguration",
|
||||
"URL": "Url des Metadata Files",
|
||||
"OR": "oder",
|
||||
"XML": "Metadata XML hochladen",
|
||||
"METADATA": "Metadata",
|
||||
"METADATAFROMFILE": "Metadata aus Datei"
|
||||
},
|
||||
"AUTHMETHODS": {
|
||||
"CODE": {
|
||||
"TITLE": "Code",
|
||||
|
@ -1609,9 +1609,9 @@
|
||||
"TITLE": "Application",
|
||||
"ID": "ID",
|
||||
"DESCRIPTION": "Here you can edit your application data and it's configuration.",
|
||||
"CREATE_OIDC": "OIDC Application",
|
||||
"CREATE_OIDC_DESC_TITLE": "Enter Your Application Details Step by Step",
|
||||
"CREATE_OIDC_DESC_SUB": "A recommended configuration will be automatically generated.",
|
||||
"CREATE": "Create application",
|
||||
"CREATE_DESC_TITLE": "Enter Your Application Details Step by Step",
|
||||
"CREATE_DESC_SUB": "A recommended configuration will be automatically generated.",
|
||||
"STATE": "Status",
|
||||
"DATECREATED": "Created",
|
||||
"DATECHANGED": "Changed",
|
||||
@ -1664,6 +1664,10 @@
|
||||
"ADDITIONALORIGINSDESC": "If you want to add additional Origins to your app which is not used as a redirect you can do that here.",
|
||||
"ORIGINS": "Origins",
|
||||
"NOTANORIGIN": "The entered value is not an origin",
|
||||
"PROSWITCH": "I'm a pro. Skip this wizard.",
|
||||
"NAMEANDTYPESECTION": "Name and Type",
|
||||
"TITLEFIRST": "Name of the application",
|
||||
"TYPETITLE": "Type of application",
|
||||
"OIDC": {
|
||||
"INFO": {
|
||||
"ISSUER": "Issuer",
|
||||
@ -1672,10 +1676,6 @@
|
||||
"CURRENT": "Current Config",
|
||||
"TOKENSECTIONTITLE": "AuthToken Options",
|
||||
"REDIRECTSECTIONTITLE": "Redirect Settings",
|
||||
"PROSWITCH": "I'm a pro. Skip this wizard.",
|
||||
"NAMEANDTYPESECTION": "Name and Type",
|
||||
"TITLEFIRST": "Insert a name first.",
|
||||
"TYPETITLE": "What type of application do you want to create?",
|
||||
"REDIRECTTITLE": "Specify the URIs where the login will redirect to.",
|
||||
"POSTREDIRECTTITLE": "This is the redirect URI after logout.",
|
||||
"REDIRECTDESCRIPTIONWEB": "Redirect URIs must begin with https://. http:// is only valid with enabled development mode.",
|
||||
@ -1767,6 +1767,18 @@
|
||||
"1": "Private Key JWT"
|
||||
}
|
||||
},
|
||||
"SAML": {
|
||||
"SELECTION": {
|
||||
"TITLE": "SAML",
|
||||
"DESCRIPTION": "SAML Applications"
|
||||
},
|
||||
"CONFIGSECTION": "SAML Configuration",
|
||||
"URL": "Url where Metadata file is located",
|
||||
"OR": "or",
|
||||
"XML": "Upload Metadata XML",
|
||||
"METADATA": "Metadata",
|
||||
"METADATAFROMFILE": "Metadata from File"
|
||||
},
|
||||
"AUTHMETHODS": {
|
||||
"CODE": {
|
||||
"TITLE": "Code",
|
||||
|
@ -1609,9 +1609,9 @@
|
||||
"TITLE": "Applicazione",
|
||||
"ID": "ID",
|
||||
"DESCRIPTION": "Qui puoi modificare i dati della tua applicazione e la sua configurazione.",
|
||||
"CREATE_OIDC": "Applicazione OIDC",
|
||||
"CREATE_OIDC_DESC_TITLE": "Inserisci i dettagli della tua applicazione passo dopo passo",
|
||||
"CREATE_OIDC_DESC_SUB": "Una configurazione raccomandata sar\u00e0 generata automaticamente.",
|
||||
"CREATE": "Crea Applicazione",
|
||||
"CREATE_DESC_TITLE": "Inserisci i dettagli della tua applicazione passo dopo passo",
|
||||
"CREATE_DESC_SUB": "Una configurazione raccomandata sar\u00e0 generata automaticamente.",
|
||||
"STATE": "Stato",
|
||||
"DATECREATED": "Creato",
|
||||
"DATECHANGED": "Cambiato",
|
||||
@ -1664,6 +1664,10 @@
|
||||
"ADDITIONALORIGINSDESC": "Se vuoi aggiungere ulteriori Origini alla tua app che non \u00e8 usata come reindirizzamento puoi farlo qui.",
|
||||
"ORIGINS": "Origini",
|
||||
"NOTANORIGIN": "Il valore inserito non \u00e8 un'origine",
|
||||
"PROSWITCH": "Sono un professionista. Salta questo passo.",
|
||||
"NAMEANDTYPESECTION": "Nome e tipo",
|
||||
"TITLEFIRST": "Nome dell' applicazione",
|
||||
"TYPETITLE": "Che tipo di applicazione vuoi creare?",
|
||||
"OIDC": {
|
||||
"INFO": {
|
||||
"ISSUER": "Issuer",
|
||||
@ -1672,10 +1676,6 @@
|
||||
"CURRENT": "Configurazione attuale",
|
||||
"TOKENSECTIONTITLE": "Opzioni AuthToken",
|
||||
"REDIRECTSECTIONTITLE": "Impostazioni di reindirizzamento",
|
||||
"PROSWITCH": "Sono un professionista. Salta questo passo.",
|
||||
"NAMEANDTYPESECTION": "Nome e tipo",
|
||||
"TITLEFIRST": "Inserisci un nome.",
|
||||
"TYPETITLE": "Che tipo di applicazione vuoi creare?",
|
||||
"REDIRECTTITLE": "Specifica gli URI a cui il login sar\u00e0 reindirizzato.",
|
||||
"POSTREDIRECTTITLE": "Questo \u00e8 l'URI di reindirizzamento dopo il logout.",
|
||||
"REDIRECTDESCRIPTIONWEB": "Gli URI di reindirizzamento devono iniziare con https://. http:// \u00e8 valido solo con la modalit\u00e0 di sviluppo abilitata (DEV Mode).",
|
||||
@ -1767,6 +1767,18 @@
|
||||
"1": "Private Key JWT"
|
||||
}
|
||||
},
|
||||
"SAML": {
|
||||
"SELECTION": {
|
||||
"TITLE": "SAML",
|
||||
"DESCRIPTION": "Applicazioni SAMML"
|
||||
},
|
||||
"CONFIGSECTION": "Configurazione SAML",
|
||||
"URL": "URL in cui si trova il file di metadati",
|
||||
"OR": "o",
|
||||
"XML": "Carica Metadata XML",
|
||||
"METADATA": "Metadata",
|
||||
"METADATAFROMFILE": "Metadati dal file"
|
||||
},
|
||||
"AUTHMETHODS": {
|
||||
"CODE": {
|
||||
"TITLE": "Code",
|
||||
|
@ -1,4 +1,5 @@
|
||||
import 'codemirror/mode/javascript/javascript';
|
||||
import 'codemirror/mode/xml/xml';
|
||||
|
||||
import { enableProdMode } from '@angular/core';
|
||||
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
|
||||
|
@ -33,6 +33,7 @@ title: zitadel/app.proto
|
||||
| name | string | - | |
|
||||
| [**oneof**](https://developers.google.com/protocol-buffers/docs/proto3#oneof) config.oidc_config | OIDCConfig | - | |
|
||||
| [**oneof**](https://developers.google.com/protocol-buffers/docs/proto3#oneof) config.api_config | APIConfig | - | |
|
||||
| [**oneof**](https://developers.google.com/protocol-buffers/docs/proto3#oneof) config.saml_config | SAMLConfig | - | |
|
||||
|
||||
|
||||
|
||||
@ -88,6 +89,18 @@ title: zitadel/app.proto
|
||||
|
||||
|
||||
|
||||
### SAMLConfig
|
||||
|
||||
|
||||
|
||||
| Field | Type | Description | Validation |
|
||||
| ----- | ---- | ----------- | ----------- |
|
||||
| [**oneof**](https://developers.google.com/protocol-buffers/docs/proto3#oneof) metadata.metadata_xml | bytes | - | |
|
||||
| [**oneof**](https://developers.google.com/protocol-buffers/docs/proto3#oneof) metadata.metadata_url | string | - | |
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## Enums
|
||||
|
@ -1254,6 +1254,19 @@ Returns a new generated secret if needed (Depending on the configuration)
|
||||
POST: /projects/{project_id}/apps/oidc
|
||||
|
||||
|
||||
### AddSAMLApp
|
||||
|
||||
> **rpc** AddSAMLApp([AddSAMLAppRequest](#addsamlapprequest))
|
||||
[AddSAMLAppResponse](#addsamlappresponse)
|
||||
|
||||
Adds a new saml service provider
|
||||
Returns a entityID
|
||||
|
||||
|
||||
|
||||
POST: /projects/{project_id}/apps/saml
|
||||
|
||||
|
||||
### AddAPIApp
|
||||
|
||||
> **rpc** AddAPIApp([AddAPIAppRequest](#addapiapprequest))
|
||||
@ -1292,6 +1305,18 @@ Changes the configuration of the oidc client
|
||||
PUT: /projects/{project_id}/apps/{app_id}/oidc_config
|
||||
|
||||
|
||||
### UpdateSAMLAppConfig
|
||||
|
||||
> **rpc** UpdateSAMLAppConfig([UpdateSAMLAppConfigRequest](#updatesamlappconfigrequest))
|
||||
[UpdateSAMLAppConfigResponse](#updatesamlappconfigresponse)
|
||||
|
||||
Changes the configuration of the saml application
|
||||
|
||||
|
||||
|
||||
PUT: /projects/{project_id}/apps/{app_id}/saml_config
|
||||
|
||||
|
||||
### UpdateAPIAppConfig
|
||||
|
||||
> **rpc** UpdateAPIAppConfig([UpdateAPIAppConfigRequest](#updateapiappconfigrequest))
|
||||
@ -2994,7 +3019,7 @@ This is an empty request
|
||||
| Field | Type | Description | Validation |
|
||||
| ----- | ---- | ----------- | ----------- |
|
||||
| primary_color | string | - | string.max_len: 50<br /> |
|
||||
| hide_login_name_suffix | bool | hides the org suffix on the login form if the scope \"urn:zitadel:iam:org:domain:primary:{domainname}\" is set. Details about this [scope in](../openidoauth/scopes) | |
|
||||
| hide_login_name_suffix | bool | hides the org suffix on the login form if the scope \"urn:zitadel:iam:org:domain:primary:{domainname}\" is set. Details about this scope in https://docs.zitadel.com/concepts#Reserved_Scopes | |
|
||||
| warn_color | string | - | string.max_len: 50<br /> |
|
||||
| background_color | string | - | string.max_len: 50<br /> |
|
||||
| font_color | string | - | string.max_len: 50<br /> |
|
||||
@ -3670,6 +3695,32 @@ This is an empty request
|
||||
|
||||
|
||||
|
||||
### AddSAMLAppRequest
|
||||
|
||||
|
||||
|
||||
| Field | Type | Description | Validation |
|
||||
| ----- | ---- | ----------- | ----------- |
|
||||
| project_id | string | - | string.min_len: 1<br /> string.max_len: 200<br /> |
|
||||
| name | string | - | string.min_len: 1<br /> string.max_len: 200<br /> |
|
||||
| [**oneof**](https://developers.google.com/protocol-buffers/docs/proto3#oneof) metadata.metadata_xml | bytes | - | bytes.max_len: 500000<br /> |
|
||||
| [**oneof**](https://developers.google.com/protocol-buffers/docs/proto3#oneof) metadata.metadata_url | string | - | string.max_len: 200<br /> |
|
||||
|
||||
|
||||
|
||||
|
||||
### AddSAMLAppResponse
|
||||
|
||||
|
||||
|
||||
| Field | Type | Description | Validation |
|
||||
| ----- | ---- | ----------- | ----------- |
|
||||
| app_id | string | - | |
|
||||
| details | zitadel.v1.ObjectDetails | - | |
|
||||
|
||||
|
||||
|
||||
|
||||
### AddSecondFactorToLoginPolicyRequest
|
||||
|
||||
|
||||
@ -8309,6 +8360,31 @@ This is an empty request
|
||||
|
||||
|
||||
|
||||
### UpdateSAMLAppConfigRequest
|
||||
|
||||
|
||||
|
||||
| Field | Type | Description | Validation |
|
||||
| ----- | ---- | ----------- | ----------- |
|
||||
| project_id | string | - | string.min_len: 1<br /> string.max_len: 200<br /> |
|
||||
| app_id | string | - | string.min_len: 1<br /> string.max_len: 200<br /> |
|
||||
| [**oneof**](https://developers.google.com/protocol-buffers/docs/proto3#oneof) metadata.metadata_xml | bytes | - | bytes.max_len: 500000<br /> |
|
||||
| [**oneof**](https://developers.google.com/protocol-buffers/docs/proto3#oneof) metadata.metadata_url | string | - | string.max_len: 200<br /> |
|
||||
|
||||
|
||||
|
||||
|
||||
### UpdateSAMLAppConfigResponse
|
||||
|
||||
|
||||
|
||||
| Field | Type | Description | Validation |
|
||||
| ----- | ---- | ----------- | ----------- |
|
||||
| details | zitadel.v1.ObjectDetails | - | |
|
||||
|
||||
|
||||
|
||||
|
||||
### UpdateUserGrantRequest
|
||||
|
||||
|
||||
|
@ -33,7 +33,7 @@ title: zitadel/policy.proto
|
||||
| details | zitadel.v1.ObjectDetails | - | |
|
||||
| primary_color | string | hex value for primary color | |
|
||||
| is_default | bool | defines if the organisation's admin changed the policy | |
|
||||
| hide_login_name_suffix | bool | hides the org suffix on the login form if the scope \"urn:zitadel:iam:org:domain:primary:{domainname}\" is set. Details about this [scope in](../openidoauth/scopes) | |
|
||||
| hide_login_name_suffix | bool | hides the org suffix on the login form if the scope \"urn:zitadel:iam:org:domain:primary:{domainname}\" is set. Details about this scope in https://docs.zitadel.com/concepts#Reserved_Scopes | |
|
||||
| warn_color | string | hex value for secondary color | |
|
||||
| background_color | string | hex value for background color | |
|
||||
| font_color | string | hex value for font color | |
|
||||
|
60
docs/docs/apis/saml/endpoints.md
Normal file
@ -0,0 +1,60 @@
|
||||
---
|
||||
title: Endpoints
|
||||
---
|
||||
|
||||
## SAML 2.0 Metadata
|
||||
|
||||
The SAML Metadata is located within the issuer domain. This would give us {your_domain}/saml/v2/metadata.
|
||||
|
||||
This metadata contains all the information defined in the spec.
|
||||
|
||||
**Link to
|
||||
spec.** [Metadata for the OASIS Security Assertion Markup Language (SAML) V2.0 – Errata Composite](https://www.oasis-open.org/committees/download.php/35391/sstc-saml-metadata-errata-2.0-wd-04-diff.pdf)
|
||||
|
||||
## Certificate Endpoint
|
||||
|
||||
{your_domain}/saml/v2/certificate
|
||||
|
||||
The certificate endpoint provides the certificate which is used to sign the responses for download, for easier use with
|
||||
different service providers which want the certificate separately instead of inside the metadata.
|
||||
|
||||
## SSO Endpoint
|
||||
|
||||
{your_domain}/saml/v2/SSO
|
||||
|
||||
The SSO endpoint is the starting point for all initial user authentications. The user agent (browser) will be redirected
|
||||
to this endpoint to authenticate the user.
|
||||
|
||||
Supported on this endpoint or currently `urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect`
|
||||
or `urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST` bindings.
|
||||
|
||||
**Link to
|
||||
spec.** [Bindings for the OASIS Security Assertion Markup Language (SAML) V2.0 – Errata Composite](https://www.oasis-open.org/committees/download.php/35387/sstc-saml-bindings-errata-2.0-wd-05-diff.pdf)
|
||||
|
||||
### Required request parameters
|
||||
|
||||
| Parameter | Description |
|
||||
|---------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| RelayState | ID to associate the exchange with the original request. |
|
||||
| SAMLRequest | The request made to the SAML IDP. (base64 encoded) |
|
||||
| SigAlg | Algorithm used to sign the request, only if binding is 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect' as signature has to be provided es separate parameter. (base64 encoded) |
|
||||
| Signature | Signature of the request as parameter with 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect' binding. (base64 encoded) |
|
||||
|
||||
### Successful Response
|
||||
|
||||
Depending on the content of the request the response comes back in the requested binding, but the content is the same.
|
||||
|
||||
| Parameter | Description |
|
||||
|---------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| RelayState | ID to associate the exchange with the original request. |
|
||||
| SAMLResponse | The response form the SAML IDP. (base64 encoded) |
|
||||
| SigAlg | Algorithm used to sign the response, only if binding is 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect' as signature has to be provided es separate parameter. (base64 encoded) |
|
||||
| Signature | Signature of the response as parameter with 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect' binding. (base64 encoded) |
|
||||
|
||||
### Error Response
|
||||
|
||||
Regardless of the error, the used http error code will be '200', which represents a successful request. Whereas the
|
||||
response will contain a StatusCode include a message which provides more information if an error occurred.
|
||||
|
||||
**Link to
|
||||
spec** [Assertions and Protocols for the OASIS Security Assertion Markup Language (SAML) V2.0 – Errata Composite](https://www.oasis-open.org/committees/download.php/35711/sstc-saml-core-errata-2.0-wd-06-diff.pdf)
|
@ -40,6 +40,7 @@ All APIs of ZITADEL are always available as gRCP, gRPC-web and REST service.
|
||||
The only exception is the [OpenID Connect & OAuth](/docs/apis/openidoauth/endpoints) and [Asset API](/docs/apis/introduction#assets) due their unique nature.
|
||||
|
||||
- [OpenID Connect & OAuth](/docs/apis/openidoauth/endpoints) - allows to request authentication and authorization of ZITADEL
|
||||
- [SAML](/docs/apis/saml/endpoints) - allows to request authentication and authorization of ZITADEL through the SAML standard
|
||||
- [Authentication API](/docs/apis/introduction#authentication) - allow a user to do operation in its own context
|
||||
- [Management API](/docs/apis/introduction#management) - allows an admin or machine to manage the ZITADEL resources on an organization level
|
||||
- [Administration API](/docs/apis/introduction#administration) - allows an admin or machine to manage the ZITADEL resources on an instance level
|
||||
|
96
docs/docs/guides/integrate/atlassian-saml.md
Normal file
@ -0,0 +1,96 @@
|
||||
---
|
||||
title: Connect with Atlassian through SAML 2.0
|
||||
---
|
||||
|
||||
This guide shows how to enable login with ZITADEL on Atlassian.
|
||||
|
||||
It covers how to:
|
||||
|
||||
- create and configure the application in your project
|
||||
- create and configure the connection in Atlassian Access
|
||||
|
||||
Prerequisites:
|
||||
|
||||
- existing ZITADEL Instance, if not present follow [this guide](../../guides/start/quickstart)
|
||||
- existing ZITADEL Organization, if not present follow [this guide](../../guides/manage/console/organizations)
|
||||
- existing ZITADEL project, if not present follow the first 3 steps [here](../../guides/manage/console/projects)
|
||||
- existing Atlassian Access setup, including verified domain
|
||||
|
||||
> We have to switch between ZITADEL and Atlassian. If the headings begin with "ZITADEL" switch to the ZITADEL
|
||||
> Console and
|
||||
> if the headings start with "Atlassian" please switch to the Atlassian Admin GUI.
|
||||
|
||||
## **Atlassian**: Create a new external identity provider
|
||||
|
||||
Please follow the instructions on [Atlassian's support page](https://support.atlassian.com/security-and-access-policies/docs/configure-saml-single-sign-on-with-an-identity-provider/) to configure a SAML identity provider for SSO.
|
||||
The following instructions give you a quick overview of the most important steps.
|
||||
|
||||
Login to Atlassian's security center and select Identity providers.
|
||||
Select the option to Set up SAML single sign-on.
|
||||

|
||||
|
||||
For Identity Provider select "Other provider" and enter a Directory Name.
|
||||

|
||||
|
||||
Follow the wizard.
|
||||
Fill in the following information:
|
||||
|
||||
- `Identity provider Entity ID`: {your_instance_domain}/saml/v2/metadata
|
||||
- `Identity provider SSO URL`: {your_instance_domain}/saml/v2/SSO
|
||||
- `Public x509 certificate`: You need to download and paste the value of the certificate from {your_instance_domain}/saml/v2/certificate
|
||||
|
||||

|
||||
|
||||
Create a new .xml file with the following minimal SAML metadata contents:
|
||||
|
||||
```xml
|
||||
<?xml version="1.0"?>
|
||||
<md:EntityDescriptor xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata" entityID="${ENTITYID}">
|
||||
<md:SPSSODescriptor protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol urn:oasis:names:tc:SAML:1.1:protocol">
|
||||
<md:AssertionConsumerService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="${ACSURL}" index="0"/>
|
||||
</md:SPSSODescriptor>
|
||||
</md:EntityDescriptor>
|
||||
```
|
||||
|
||||
Set or replace the variables with the values from the next screen as follows:
|
||||
|
||||
- `${ENTITYID}`: Copy the value from "Service provider entity URL"
|
||||
- `${ACSURL}`: Copy the value from "Service provider assertion consumer service URL"
|
||||
|
||||

|
||||
|
||||
## **ZITADEL**: Create the application
|
||||
|
||||
In your existing project:
|
||||
|
||||
Press the "+"-button to add an application
|
||||

|
||||
|
||||
Fill in a name for the application and chose the SAML type, then click "Continue".
|
||||

|
||||
|
||||
Either fill in the URL where ZITADEL can read the metadata from, or upload the metadata XML directly, then click "Continue".
|
||||

|
||||
|
||||
Check your application, if everything is correct, press "Create".
|
||||

|
||||
|
||||
## **Atlassian**: Setup authentication policies
|
||||
|
||||
Under Authentication policies, select "Edit" on the directory that you have created.
|
||||
Then check the box "Enforce single sign-on" and confirm by clicking "Update".
|
||||

|
||||
|
||||
Add members to your policy.
|
||||

|
||||
|
||||
## Verify configuration
|
||||
|
||||
Now you should be all set to verify your setup:
|
||||
|
||||
- Create an user in ZITADEL with the same email address as a member in your authentication policy.
|
||||
- In a new browser session go to https://id.atlassian.com
|
||||
- Enter the user's email address
|
||||
- You should be redirected to ZITADEL's Login screen
|
||||
- Enter the email address and password
|
||||
- Continue and you should be redirected back to Atlassian
|
@ -1,5 +1,5 @@
|
||||
---
|
||||
title: Connect with Auth0
|
||||
title: Connect with Auth0 through OIDC
|
||||
---
|
||||
|
||||
This guide shows how to enable login with ZITADEL on Auth0.
|
||||
@ -16,7 +16,7 @@ Prerequisites:
|
||||
- existing ZITADEL project, if not present follow the first 3 steps [here](../../guides/manage/console/projects)
|
||||
- existing Auth0 tenant as described [here](https://auth0.com/docs/get-started/auth0-overview/create-tenants)
|
||||
|
||||
> We have to switch between ZITADEL and a Auth0. If the headings begin with "ZITADEL" switch to the ZITADEL Console and if the headings start with Auth0 please switch to the Auth0 GUI.
|
||||
> We have to switch between ZITADEL and a Auth0. If the headings begin with "ZITADEL" switch to the ZITADEL Console and if the headings start with "Auth0" please switch to the Auth0 GUI.
|
||||
|
||||
## **Auth0**: Create a new connection
|
||||
|
78
docs/docs/guides/integrate/auth0-saml.md
Normal file
@ -0,0 +1,78 @@
|
||||
---
|
||||
title: Connect with Auth0 through SAML 2.0
|
||||
---
|
||||
|
||||
This guide shows how to enable login with ZITADEL on Auth0.
|
||||
|
||||
It covers how to:
|
||||
|
||||
- create and configure the application in your project
|
||||
- create and configure the connection in your Auth0 tenant
|
||||
|
||||
Prerequisites:
|
||||
|
||||
- existing ZITADEL Instance, if not present follow [this guide](../../guides/start/quickstart)
|
||||
- existing ZITADEL Organization, if not present follow [this guide](../../guides/manage/console/organizations)
|
||||
- existing ZITADEL project, if not present follow the first 3 steps [here](../../guides/manage/console/projects)
|
||||
- existing Auth0 tenant as described [here](https://auth0.com/docs/get-started/auth0-overview/create-tenants)
|
||||
|
||||
> We have to switch between ZITADEL and a Auth0. If the headings begin with "ZITADEL" switch to the ZITADEL Console and
|
||||
> if the headings start with "Auth0" please switch to the Auth0 GUI.
|
||||
|
||||
## **Auth0**: Create a new connection
|
||||
|
||||
In Authentication -> Enterprise
|
||||

|
||||
|
||||
1. Press the "+" button right to "SAML"
|
||||

|
||||
2. Fill out the fields as follows in the SAML Connection:
|
||||

|
||||
|
||||
This includes:
|
||||
|
||||
- a unique "Connection name"
|
||||
- the "Sign In URL"
|
||||
- the "Sign Out URL"
|
||||
- used "User ID Attribute"
|
||||
- the definition how the request should be signed
|
||||
- which binding should be used to call ZITADEL
|
||||
|
||||
All the information is filled out as an example, and to connect with any other environment you only have to change the
|
||||
used domain, for example "example.com" with "zitadel.cloud".
|
||||
Lastly, upload the certificate used to sign the reponses, provided for you under the
|
||||
URL {your_instance_domain}/saml/v2/certificate.
|
||||
|
||||
Then just press the button "Create" and the connection on Auth0 is configured.
|
||||
|
||||
## **ZITADEL**: Create the application
|
||||
|
||||
You need to upload the SAML metadata to ZITADEL for it to recognize this newly created connection.
|
||||
[Under this link](https://auth0.com/docs/authenticate/protocols/saml/saml-identity-provider-configuration-settings) are
|
||||
all necessary information to correctly fill out the metadata or download the metadata-file directly under the
|
||||
URL https://YOUR_AUTH0_DOMAIN/samlp/metadata?connection=YOUR_CONNECTION_NAME, which in this example would
|
||||
be https://example.auth0.com/samlp/metadata?connection=SAML-ZITADEL.
|
||||
|
||||
In your existing project:
|
||||
|
||||
1. Press the "+"-button to add an application
|
||||

|
||||
2. Fill in a name for the application and chose the SAML type, then click "Continue".
|
||||

|
||||
3. Either fill in the URL where ZITADEL can read the metadata from, or upload the metadata XML directly, then click "
|
||||
Continue".
|
||||

|
||||
4. Check your application, if everything is correct, press "Create".
|
||||

|
||||
|
||||
Everything on the side of ZITADEL is done if the application is correctly created.
|
||||
|
||||
## **Auth0**: Try the connection
|
||||
|
||||
To then test the connection you only have to press "Try" on the created connection in the Authentication -> Enterprise
|
||||
screen.
|
||||
|
||||

|
||||
|
||||
To further customize the requests you can also customize the SAML communication as
|
||||
described [here](https://auth0.com/docs/authenticate/protocols/saml/saml-configuration/customize-saml-assertions)
|
78
docs/docs/guides/integrate/aws-saml.md
Normal file
@ -0,0 +1,78 @@
|
||||
---
|
||||
title: Connect with AWS through SAML 2.0
|
||||
---
|
||||
|
||||
This guide shows how to enable login with ZITADEL on AWS SSO.
|
||||
|
||||
It covers how to:
|
||||
|
||||
- create and configure the application in your project
|
||||
- create and configure the connection in your AWS SSO external IDP
|
||||
|
||||
Prerequisites:
|
||||
|
||||
- existing ZITADEL Instance, if not present follow [this guide](../../guides/start/quickstart)
|
||||
- existing ZITADEL Organization, if not present follow [this guide](../../guides/manage/console/organizations)
|
||||
- existing ZITADEL project, if not present follow the first 3 steps [here](../../guides/manage/console/projects)
|
||||
- prerequisites on AWS side [here](https://docs.aws.amazon.com/singlesignon/latest/userguide/prereqs.html).
|
||||
- enabled AWS SSO [here](https://docs.aws.amazon.com/singlesignon/latest/userguide/step1.html?icmpid=docs_sso_console)
|
||||
|
||||
> We have to switch between ZITADEL and a AWS. If the headings begin with "ZITADEL" switch to the ZITADEL Console and if
|
||||
> the headings start with "AWS" please switch to the AWS GUI.
|
||||
|
||||
## **AWS**: Change to external identity provider ZITADEL
|
||||
|
||||
As you have activated SSO you still have the possibility to use AWS itself to manage the users, but you can also use a
|
||||
Microsoft AD or an external IDP.
|
||||
|
||||
Described [here](https://docs.aws.amazon.com/singlesignon/latest/userguide/manage-your-identity-source-idp.html) how you
|
||||
can connect to ZITADEL as a SAML2 IDP.
|
||||
|
||||
1. Chose the External identity provider:
|
||||

|
||||
|
||||
2. Download the metadata file, to provide ZITADEL with all the information it needs, and save the AWS SSO Sign-in URL,
|
||||
which you use to login afterwards.
|
||||
|
||||
3. Fill out the fields as follows, to provide AWS with all the information it needs:
|
||||

|
||||
|
||||
To connect to another environment, change the domains, for example if you would use ZITADEL under the domain "
|
||||
example.com" you would have the URLs "https://accounts.example.com/saml/SSO"
|
||||
and "https://accounts.exmaple.com/saml/metadata".
|
||||
|
||||
4. Download the ZITADEL-used certificate to sign the responses, so that AWS can validation the signature.
|
||||
|
||||
You can download the certificate from following
|
||||
URL: {your_instance_domain}/saml/v2/certificate
|
||||
|
||||
5. Then upload the ".crt"-file to AWS and click "next".
|
||||
|
||||
6. Lastly, accept to confirm the change and ZITADEL is used as the external identity provider for AWS SSO to provide
|
||||
connectivity to your AWS Accounts.
|
||||
|
||||
As for how the SSO users are then connected to the AWS accounts, you can find more information in the AWS documentation,
|
||||
for example [here](https://docs.aws.amazon.com/singlesignon/latest/userguide/useraccess.html).
|
||||
|
||||
## **ZITADEL**: Create the application
|
||||
|
||||
The metadata used in this part is from "Change to external identity provider ZITADEL" step 2.
|
||||
|
||||
In your existing project:
|
||||
|
||||
1. Press the "+"-button to add an application
|
||||

|
||||
2. Fill in a name for the application and chose the SAML type, then click "Continue".
|
||||

|
||||
3. Either fill in the URL where ZITADEL can read the metadata from, or upload the metadata XML directly, then click "
|
||||
Continue".
|
||||

|
||||
4. Check your application, if everything is correct, press "Create".
|
||||

|
||||
|
||||
Everything on the side of ZITADEL is done if the application is correctly created.
|
||||
|
||||
## **AWS**: Test the connection
|
||||
|
||||
The result, you can now login to you AWS account through your ZITADEL-login with the AWS SSO Sign-in URL, which you
|
||||
should have saved in "Change to external identity provider ZITADEL" step 2.
|
@ -1,5 +1,5 @@
|
||||
---
|
||||
title: Connect with AzureAD
|
||||
title: Connect with AzureAD through OIDC
|
||||
---
|
||||
|
||||
## AzureAD Tenant as Identity Provider for ZITADEL
|
67
docs/docs/guides/integrate/gitlab-saml.md
Normal file
@ -0,0 +1,67 @@
|
||||
---
|
||||
title: Connect with Gitlab through SAML 2.0
|
||||
---
|
||||
|
||||
This guide shows how to enable login with ZITADEL on Gitlab.
|
||||
|
||||
It covers how to:
|
||||
|
||||
- create and configure the application in your project
|
||||
- create and configure the connection in Gitlab SaaS
|
||||
|
||||
Prerequisites:
|
||||
|
||||
- existing ZITADEL Instance, if not present follow [this guide](../../guides/start/quickstart)
|
||||
- existing ZITADEL Organization, if not present follow [this guide](../../guides/manage/console/organizations)
|
||||
- existing ZITADEL project, if not present follow the first 3 steps [here](../../guides/manage/console/projects)
|
||||
- existing Gitlab SaaS Setup in the premium tier
|
||||
|
||||
> We have to switch between ZITADEL and Gitlab. If the headings begin with "ZITADEL" switch to the ZITADEL
|
||||
> Console and
|
||||
> if the headings start with "Gitlab" please switch to the Gitlab GUI.
|
||||
|
||||
## **Gitlab**: Create a new external identity provider
|
||||
|
||||
Please follow the instructions on [Gitlab docs](https://docs.gitlab.com/ee/user/group/saml_sso/index.html) to configure a SAML identity provider for SSO.
|
||||
The following instructions give you a quick overview of the most important steps.
|
||||
|
||||
[Open the group](https://gitlab.com/dashboard/groups) to which you want to add the SSO configuration.
|
||||
Select on the menu Settings and then SAML SSO.
|
||||
Copy `GitLab metadata URL` for the next step.
|
||||

|
||||
|
||||
## **ZITADEL**: Create the application
|
||||
|
||||
In your existing project:
|
||||
|
||||
Press the "+"-button to add an application
|
||||

|
||||
|
||||
Fill in a name for the application and chose the SAML type, then click "Continue".
|
||||

|
||||
|
||||
Enter the URL from before, then click "Continue".
|
||||

|
||||
|
||||
Check your application, if everything is correct, press "Create".
|
||||

|
||||
|
||||
## **Gitlab**: Configuration
|
||||
|
||||
Complete the configuration as follows:
|
||||
|
||||
- `Identity provider single sign-on URL`: {your_instance_domain}/saml/v2/SSO
|
||||
- `Certificate fingerprint`: You need to download the certificate from {your_instance_domain}/saml/v2/certificate and create a SHA1 fingerprint
|
||||
|
||||
Save the changes.
|
||||
|
||||

|
||||
|
||||
## **Gitlab**: Verify SAML configuration
|
||||
|
||||
Once you saved the changes, click on the button "Verify SAML configuration".
|
||||
|
||||
You should be redirected to ZITADEL.
|
||||
Login with your user.
|
||||
After that you should be redirected back to GitLab and you can inspect the Response Output.
|
||||

|
84
docs/docs/guides/integrate/pingidentity-saml.md
Normal file
@ -0,0 +1,84 @@
|
||||
---
|
||||
title: Connect with Ping Identity through SAML 2.0
|
||||
---
|
||||
|
||||
This guide shows how to enable login with ZITADEL on Auth0.
|
||||
|
||||
It covers how to:
|
||||
|
||||
- create and configure the application in your project
|
||||
- create and configure the connection in your Ping Identity tenant
|
||||
|
||||
Prerequisites:
|
||||
|
||||
- existing ZITADEL Instance, if not present follow [this guide](../../guides/start/quickstart)
|
||||
- existing ZITADEL Organization, if not present follow [this guide](../../guides/manage/console/organizations)
|
||||
- existing ZITADEL project, if not present follow the first 3 steps [here](../../guides/manage/console/projects)
|
||||
- existing Pingidentity environment [here](https://docs.pingidentity.com/bundle/pingone/page/wqe1564020490538.html)
|
||||
|
||||
> We have to switch between ZITADEL and Ping Identity. If the headings begin with "ZITADEL" switch to the ZITADEL
|
||||
> Console and
|
||||
> if the headings start with "Ping" please switch to the PingIdentity GUI.
|
||||
|
||||
## **Ping**: Create a new external identity provider
|
||||
|
||||
To add an
|
||||
additional [external identity provider](https://docs.pingidentity.com/bundle/pingone/page/jvz1567784210191.html), you
|
||||
can follow the instructions [here](https://docs.pingidentity.com/bundle/pingone/page/ovy1567784211297.html)
|
||||
|
||||
1. As described you have to create a new provider, with a unique identifier:
|
||||

|
||||
|
||||
We recommend activating signing the auth request whenever possible:
|
||||

|
||||
|
||||
2. Manually enter the necessary information:
|
||||
|
||||
- SSO Endpoint, for example https://accounts.example.com/saml/SSO
|
||||
- IDP EntityID, for example https://accounts.example.com/saml/metadata
|
||||
- Binding, which is a decision which you can take yourself, we recommend HTTP POST as it has fewer restrictions
|
||||
- Import certificate, provided from the certificate endpoint
|
||||

|
||||
|
||||
Everything you need to know about the attribute mapping you can find
|
||||
in [Ping Identity's documentation](https://docs.pingidentity.com/bundle/pingone/page/pwv1567784207915.html)
|
||||
|
||||
3. With this you have defined to connection to ZITADEL as an external IDP, next is the policy to use ZITADEL as an IDP
|
||||
to
|
||||
connect to an application. The "How to" for that can be
|
||||
found [here](https://docs.pingidentity.com/bundle/pingone/page/zqd1616600404402.html).
|
||||
|
||||
## **ZITADEL**: Create the application
|
||||
|
||||
To add the connection to ZITADEL you have to build the metadata, which should minimalistic look like this, the necessary
|
||||
information can be found on the External IDPs page under "P1Connection" and "IDP Configuration" :
|
||||
|
||||
```xml
|
||||
ENTITYID="PINGONE (SP) ENTITY ID"
|
||||
ACSURL="ACS ENDPOINT"
|
||||
<?xml version="1.0"?>
|
||||
<md:EntityDescriptor xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata" entityID="${ENTITYID}">
|
||||
<md:SPSSODescriptor
|
||||
protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol urn:oasis:names:tc:SAML:1.1:protocol">
|
||||
<md:AssertionConsumerService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="${ACSURL}"
|
||||
index="0"/>
|
||||
</md:SPSSODescriptor>
|
||||
</md:EntityDescriptor>
|
||||
```
|
||||
|
||||

|
||||

|
||||
|
||||
In your existing project:
|
||||
|
||||
1. Press the "+"-button to add an application
|
||||

|
||||
2. Fill in a name for the application and chose the SAML type, then click "Continue".
|
||||

|
||||
3. Either fill in the URL where ZITADEL can read the metadata from, or upload the metadata XML directly, then click "
|
||||
Continue".
|
||||

|
||||
4. Check your application, if everything is correct, press "Create".
|
||||

|
||||
|
||||
Everything on the side of ZITADEL is done if the application is correctly created.
|
@ -115,17 +115,43 @@ module.exports = {
|
||||
label: "Integrate",
|
||||
collapsed: false,
|
||||
items: [
|
||||
"guides/integrate/login-users",
|
||||
"guides/integrate/identity-brokering",
|
||||
{
|
||||
type: "category",
|
||||
label: "Access ZITADEL APIs",
|
||||
collapsed: false,
|
||||
items: [
|
||||
"guides/integrate/serviceusers",
|
||||
"guides/integrate/access-zitadel-apis",
|
||||
"guides/integrate/access-zitadel-system-api",
|
||||
"guides/integrate/authenticated-mongodb-charts",
|
||||
"guides/integrate/auth0",
|
||||
"guides/integrate/azuread",
|
||||
"guides/integrate/gitlab-self-hosted",
|
||||
"guides/integrate/login-users",
|
||||
"guides/integrate/serviceusers",
|
||||
"guides/integrate/export-and-import",
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "category",
|
||||
label: "OpenID Connect 1.0 Clients",
|
||||
collapsed: false,
|
||||
items: [
|
||||
"guides/integrate/oauth-recommended-flows",
|
||||
"guides/integrate/export-and-import"
|
||||
"guides/integrate/auth0-oidc",
|
||||
"guides/integrate/azuread-oidc",
|
||||
"guides/integrate/authenticated-mongodb-charts",
|
||||
"guides/integrate/gitlab-self-hosted",
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "category",
|
||||
label: "SAML 2.0 Clients",
|
||||
collapsed: false,
|
||||
items: [
|
||||
"guides/integrate/auth0-saml",
|
||||
"guides/integrate/aws-saml",
|
||||
"guides/integrate/pingidentity-saml",
|
||||
"guides/integrate/atlassian-saml",
|
||||
"guides/integrate/gitlab-saml",
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
@ -205,6 +231,14 @@ module.exports = {
|
||||
"apis/openidoauth/grant-types",
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "category",
|
||||
label: "SAML",
|
||||
collapsed: false,
|
||||
items: [
|
||||
"apis/saml/endpoints",
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "category",
|
||||
label: "Rate Limits",
|
||||
|
BIN
docs/static/img/saml/atlassian/atlassian-01.png
vendored
Normal file
After Width: | Height: | Size: 182 KiB |
BIN
docs/static/img/saml/atlassian/atlassian-02.png
vendored
Normal file
After Width: | Height: | Size: 87 KiB |
BIN
docs/static/img/saml/atlassian/atlassian-03.png
vendored
Normal file
After Width: | Height: | Size: 127 KiB |
BIN
docs/static/img/saml/atlassian/atlassian-04.png
vendored
Normal file
After Width: | Height: | Size: 76 KiB |
BIN
docs/static/img/saml/atlassian/atlassian-05.png
vendored
Normal file
After Width: | Height: | Size: 185 KiB |
BIN
docs/static/img/saml/atlassian/atlassian-06.png
vendored
Normal file
After Width: | Height: | Size: 95 KiB |
BIN
docs/static/img/saml/auth0/auth_enterprise.png
vendored
Normal file
After Width: | Height: | Size: 27 KiB |
BIN
docs/static/img/saml/auth0/auth_enterprise_try.png
vendored
Normal file
After Width: | Height: | Size: 37 KiB |
BIN
docs/static/img/saml/auth0/connection.png
vendored
Normal file
After Width: | Height: | Size: 80 KiB |
BIN
docs/static/img/saml/auth0/enterprise_connections.png
vendored
Normal file
After Width: | Height: | Size: 36 KiB |
BIN
docs/static/img/saml/aws/change_idp.png
vendored
Normal file
After Width: | Height: | Size: 83 KiB |
BIN
docs/static/img/saml/aws/configure_idp.png
vendored
Normal file
After Width: | Height: | Size: 119 KiB |
BIN
docs/static/img/saml/gitlab/gitlab-01.png
vendored
Normal file
After Width: | Height: | Size: 207 KiB |
BIN
docs/static/img/saml/gitlab/gitlab-02.png
vendored
Normal file
After Width: | Height: | Size: 207 KiB |
BIN
docs/static/img/saml/gitlab/gitlab-03.png
vendored
Normal file
After Width: | Height: | Size: 160 KiB |
BIN
docs/static/img/saml/pingidentity/conf_connection.png
vendored
Normal file
After Width: | Height: | Size: 70 KiB |
BIN
docs/static/img/saml/pingidentity/conf_idp_connection.png
vendored
Normal file
After Width: | Height: | Size: 86 KiB |
BIN
docs/static/img/saml/pingidentity/create_idp_profile.png
vendored
Normal file
After Width: | Height: | Size: 67 KiB |
BIN
docs/static/img/saml/pingidentity/idp_idp_configuration.png
vendored
Normal file
After Width: | Height: | Size: 43 KiB |
BIN
docs/static/img/saml/pingidentity/idp_p1_connection.png
vendored
Normal file
After Width: | Height: | Size: 36 KiB |
BIN
docs/static/img/saml/zitadel/application_saml.png
vendored
Normal file
After Width: | Height: | Size: 119 KiB |
BIN
docs/static/img/saml/zitadel/application_saml_create.png
vendored
Normal file
After Width: | Height: | Size: 37 KiB |
BIN
docs/static/img/saml/zitadel/application_saml_metadata.png
vendored
Normal file
After Width: | Height: | Size: 156 KiB |
BIN
docs/static/img/saml/zitadel/project.png
vendored
Normal file
After Width: | Height: | Size: 37 KiB |
@ -26,7 +26,7 @@ export function ensureSomethingExists(api: apiCallProperties, searchPath: string
|
||||
}
|
||||
})
|
||||
}).then((data) => {
|
||||
awaitDesired(30, (entity) => !!entity, data.initialSequence, api, searchPath, find)
|
||||
awaitDesired(90, (entity) => !!entity, data.initialSequence, api, searchPath, find)
|
||||
return cy.wrap<number>(data.id)
|
||||
})
|
||||
}
|
||||
@ -49,7 +49,7 @@ export function ensureSomethingDoesntExist(api: apiCallProperties, searchPath: s
|
||||
return sRes.sequence
|
||||
})
|
||||
}).then((initialSequence) => {
|
||||
awaitDesired(30, (entity) => !entity , initialSequence, api, searchPath, find)
|
||||
awaitDesired(90, (entity) => !entity , initialSequence, api, searchPath, find)
|
||||
return null
|
||||
})
|
||||
}
|
||||
@ -77,7 +77,10 @@ function searchSomething(api: apiCallProperties, searchPath: string, find: (enti
|
||||
|
||||
function awaitDesired(trials: number, expectEntity: (entity: any) => boolean, initialSequence: number, api: apiCallProperties, searchPath: string, find: (entity: any) => boolean) {
|
||||
searchSomething(api, searchPath, find).then(resp => {
|
||||
if (!expectEntity(resp.entity) || resp.sequence <= initialSequence) {
|
||||
const foundExpectedEntity = expectEntity(resp.entity)
|
||||
const foundExpectedSequence = resp.sequence > initialSequence
|
||||
|
||||
if (!foundExpectedEntity || !foundExpectedSequence) {
|
||||
expect(trials, `trying ${trials} more times`).to.be.greaterThan(0);
|
||||
cy.wait(1000)
|
||||
awaitDesired(trials - 1, expectEntity, initialSequence, api, searchPath, find)
|
||||
|
9
go.mod
@ -48,11 +48,12 @@ require (
|
||||
github.com/sony/sonyflake v1.0.0
|
||||
github.com/spf13/cobra v1.3.0
|
||||
github.com/spf13/viper v1.10.1
|
||||
github.com/stretchr/testify v1.7.1
|
||||
github.com/stretchr/testify v1.8.0
|
||||
github.com/superseriousbusiness/exifremove v0.0.0-20210330092427-6acd27eac203
|
||||
github.com/ttacon/libphonenumber v1.2.1
|
||||
github.com/zitadel/logging v0.3.4
|
||||
github.com/zitadel/oidc/v2 v2.0.0-dynamic-issuer.5
|
||||
github.com/zitadel/saml v0.0.6
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.27.0
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.27.0
|
||||
go.opentelemetry.io/otel v1.2.0
|
||||
@ -82,6 +83,8 @@ require (
|
||||
cloud.google.com/go/trace v1.0.0 // indirect
|
||||
github.com/Masterminds/goutils v1.1.1 // indirect
|
||||
github.com/Masterminds/semver v1.5.0 // indirect
|
||||
github.com/amdonov/xmlsig v0.1.0 // indirect
|
||||
github.com/beevik/etree v1.1.0 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/cenkalti/backoff/v4 v4.1.2 // indirect
|
||||
github.com/census-instrumentation/opencensus-proto v0.3.0 // indirect
|
||||
@ -135,6 +138,7 @@ require (
|
||||
github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b // indirect
|
||||
github.com/jarcoal/jpath v0.0.0-20140328210829-f76b8b2dbf52
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
github.com/jonboulle/clockwork v0.2.2 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/kevinburke/go-types v0.0.0-20210723172823-2deba1f80ba7 // indirect
|
||||
github.com/kevinburke/rest v0.0.0-20210506044642-5611499aa33c // indirect
|
||||
@ -161,6 +165,7 @@ require (
|
||||
github.com/prometheus/common v0.26.0 // indirect
|
||||
github.com/prometheus/procfs v0.6.0 // indirect
|
||||
github.com/rs/xid v1.2.1 // indirect
|
||||
github.com/russellhaering/goxmldsig v1.2.0 // indirect
|
||||
github.com/satori/go.uuid v1.2.0 // indirect
|
||||
github.com/sirupsen/logrus v1.9.0 // indirect
|
||||
github.com/spf13/afero v1.8.1 // indirect
|
||||
@ -184,7 +189,7 @@ require (
|
||||
google.golang.org/appengine v1.6.7 // indirect
|
||||
gopkg.in/ini.v1 v1.66.4 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
nhooyr.io/websocket v1.8.7 // indirect
|
||||
)
|
||||
|
||||
|
28
go.sum
@ -95,6 +95,8 @@ github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk5
|
||||
github.com/allegro/bigcache v1.2.1-0.20190218064605-e24eb225f156/go.mod h1:Cb/ax3seSYIx7SuZdm2G2xzfwmv3TPSk2ucNfQESPXM=
|
||||
github.com/allegro/bigcache v1.2.1 h1:hg1sY1raCwic3Vnsvje6TT7/pnZba83LeFck5NrFKSc=
|
||||
github.com/allegro/bigcache v1.2.1/go.mod h1:Cb/ax3seSYIx7SuZdm2G2xzfwmv3TPSk2ucNfQESPXM=
|
||||
github.com/amdonov/xmlsig v0.1.0 h1:i0iQ3neKLmUhcfIRgiiR3eRPKgXZj+n5lAfqnfKoeXI=
|
||||
github.com/amdonov/xmlsig v0.1.0/go.mod h1:jTR/jO0E8fSl/cLvMesP+RjxyV4Ux4WL1Ip64ZnQpA0=
|
||||
github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=
|
||||
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
|
||||
github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
|
||||
@ -108,6 +110,8 @@ github.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a/go.mod h1:DAHtR1m6l
|
||||
github.com/aws/aws-lambda-go v1.13.3/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQwij/eHl5CU=
|
||||
github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
|
||||
github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g=
|
||||
github.com/beevik/etree v1.1.0 h1:T0xke/WvNtMoCqgzPhkX2r4rjY3GDZFi+FjpRZY2Jbs=
|
||||
github.com/beevik/etree v1.1.0/go.mod h1:r8Aw8JqVegEf0w2fDnATrX9VpkMcyFeM0FhwO62wh+A=
|
||||
github.com/benbjohnson/clock v1.2.0 h1:9Re3G2TWxkE06LdMWMpcY6KV81GLXMGiYpPYUPkFAws=
|
||||
github.com/benbjohnson/clock v1.2.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
|
||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||
@ -560,6 +564,8 @@ github.com/jinzhu/now v1.1.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/
|
||||
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
|
||||
github.com/jmoiron/sqlx v1.3.1/go.mod h1:2BljVx/86SuTyjE+aPYlHCTNvZrnJXghYGpNiXLBMCQ=
|
||||
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
|
||||
github.com/jonboulle/clockwork v0.2.2 h1:UOGuzwb1PwsrDAObMuhUnj0p5ULPj8V/xJ7Kx9qUBdQ=
|
||||
github.com/jonboulle/clockwork v0.2.2/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8=
|
||||
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
|
||||
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||
github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
@ -600,8 +606,9 @@ github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
|
||||
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
|
||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
|
||||
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
@ -735,6 +742,7 @@ github.com/pelletier/go-toml v1.9.4/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCko
|
||||
github.com/performancecopilot/speed v3.0.0+incompatible/go.mod h1:/CLtqpZ5gBg1M9iaPbIdPPGyKcA8hKdoy6hAWba7Yac=
|
||||
github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc=
|
||||
github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
|
||||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
@ -785,6 +793,9 @@ github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqn
|
||||
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
|
||||
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
|
||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
|
||||
github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
|
||||
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
|
||||
github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU=
|
||||
github.com/rs/cors v1.8.0 h1:P2KMzcFwrPoSjkF1WLRPsp3UMLyql8L4v9hQpVeK5so=
|
||||
github.com/rs/cors v1.8.0/go.mod h1:EBwu+T5AvHOcXwvZIkQFjUN6s8Czyqw12GL/Y0tUyRM=
|
||||
@ -792,6 +803,8 @@ github.com/rs/xid v1.2.1 h1:mhH9Nq+C1fY2l1XIpgxIiUOfNpRBYH1kKcr+qfKgjRc=
|
||||
github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
|
||||
github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU=
|
||||
github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc=
|
||||
github.com/russellhaering/goxmldsig v1.2.0 h1:Y6GTTc9Un5hCxSzVz4UIWQ/zuVwDvzJk80guqzwx6Vg=
|
||||
github.com/russellhaering/goxmldsig v1.2.0/go.mod h1:gM4MDENBQf7M+V824SGfyIUVFWydB7n0KkEubVJl+Tw=
|
||||
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
|
||||
@ -845,14 +858,16 @@ github.com/streadway/handy v0.0.0-20190108123426-d5acb3125c2a/go.mod h1:qNTQ5P5J
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s=
|
||||
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
|
||||
github.com/superseriousbusiness/exifremove v0.0.0-20210330092427-6acd27eac203 h1:1SWXcTphBQjYGWRRxLFIAR1LVtQEj4eR7xPtyeOVM/c=
|
||||
@ -886,6 +901,12 @@ github.com/zitadel/logging v0.3.4 h1:9hZsTjMMTE3X2LUi0xcF9Q9EdLo+FAezeu52ireBbHM
|
||||
github.com/zitadel/logging v0.3.4/go.mod h1:aPpLQhE+v6ocNK0TWrBrd363hZ95KcI17Q1ixAQwZF0=
|
||||
github.com/zitadel/oidc/v2 v2.0.0-dynamic-issuer.5 h1:dP+6SheVtpF4T/oql6mJoqou8jlW3J/9NCTYnEpKgpM=
|
||||
github.com/zitadel/oidc/v2 v2.0.0-dynamic-issuer.5/go.mod h1:uoJw5Xc6HXfnQbNZiLbld9dED0/8UMu0M4gOipTRZBA=
|
||||
github.com/zitadel/saml v0.0.4 h1:xz97KyKD3mrQcIEKi0aUPyNX834xm3p1ToO9HK/vVeY=
|
||||
github.com/zitadel/saml v0.0.4/go.mod h1:DIy/ln32rNYv/bIBA8uOB6Y2JmxjZldDYBeMNn7YyeQ=
|
||||
github.com/zitadel/saml v0.0.5 h1:ufLE0MeWe2SLGGkSWbY3J20xxYtAL57IddeDsyMqCuM=
|
||||
github.com/zitadel/saml v0.0.5/go.mod h1:DIy/ln32rNYv/bIBA8uOB6Y2JmxjZldDYBeMNn7YyeQ=
|
||||
github.com/zitadel/saml v0.0.6 h1:avcOanSNd0x4jkn/+MsXIGQpJOvwqkk+slGJVIhqk84=
|
||||
github.com/zitadel/saml v0.0.6/go.mod h1:DIy/ln32rNYv/bIBA8uOB6Y2JmxjZldDYBeMNn7YyeQ=
|
||||
go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
|
||||
go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg=
|
||||
go.etcd.io/etcd/api/v3 v3.5.1/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs=
|
||||
@ -1479,8 +1500,9 @@ gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gorm.io/driver/postgres v1.0.8/go.mod h1:4eOzrI1MUfm6ObJU/UcmbXyiHSs8jSwH95G5P5dxcAg=
|
||||
gorm.io/gorm v1.20.12/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw=
|
||||
gorm.io/gorm v1.21.4/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw=
|
||||
|
@ -67,6 +67,16 @@ func (s *Server) AddOIDCApp(ctx context.Context, req *mgmt_pb.AddOIDCAppRequest)
|
||||
ComplianceProblems: project_grpc.ComplianceProblemsToLocalizedMessages(app.Compliance.Problems),
|
||||
}, nil
|
||||
}
|
||||
func (s *Server) AddSAMLApp(ctx context.Context, req *mgmt_pb.AddSAMLAppRequest) (*mgmt_pb.AddSAMLAppResponse, error) {
|
||||
app, err := s.command.AddSAMLApplication(ctx, AddSAMLAppRequestToDomain(req), authz.GetCtxData(ctx).OrgID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &mgmt_pb.AddSAMLAppResponse{
|
||||
AppId: app.AppID,
|
||||
Details: object_grpc.AddToDetailsPb(app.Sequence, app.ChangeDate, app.ResourceOwner),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Server) AddAPIApp(ctx context.Context, req *mgmt_pb.AddAPIAppRequest) (*mgmt_pb.AddAPIAppResponse, error) {
|
||||
appSecretGenerator, err := s.query.InitHashGenerator(ctx, domain.SecretGeneratorTypeAppSecret, s.passwordHashAlg)
|
||||
@ -109,6 +119,20 @@ func (s *Server) UpdateOIDCAppConfig(ctx context.Context, req *mgmt_pb.UpdateOID
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Server) UpdateSAMLAppConfig(ctx context.Context, req *mgmt_pb.UpdateSAMLAppConfigRequest) (*mgmt_pb.UpdateSAMLAppConfigResponse, error) {
|
||||
config, err := s.command.ChangeSAMLApplication(ctx, UpdateSAMLAppConfigRequestToDomain(req), authz.GetCtxData(ctx).OrgID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &mgmt_pb.UpdateSAMLAppConfigResponse{
|
||||
Details: object_grpc.ChangeToDetailsPb(
|
||||
config.Sequence,
|
||||
config.ChangeDate,
|
||||
config.ResourceOwner,
|
||||
),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Server) UpdateAPIAppConfig(ctx context.Context, req *mgmt_pb.UpdateAPIAppConfigRequest) (*mgmt_pb.UpdateAPIAppConfigResponse, error) {
|
||||
config, err := s.command.ChangeAPIApplication(ctx, UpdateAPIAppConfigRequestToDomain(req), authz.GetCtxData(ctx).OrgID)
|
||||
if err != nil {
|
||||
|
@ -59,6 +59,17 @@ func AddOIDCAppRequestToDomain(req *mgmt_pb.AddOIDCAppRequest) *domain.OIDCApp {
|
||||
}
|
||||
}
|
||||
|
||||
func AddSAMLAppRequestToDomain(req *mgmt_pb.AddSAMLAppRequest) *domain.SAMLApp {
|
||||
return &domain.SAMLApp{
|
||||
ObjectRoot: models.ObjectRoot{
|
||||
AggregateID: req.ProjectId,
|
||||
},
|
||||
AppName: req.Name,
|
||||
Metadata: req.GetMetadataXml(),
|
||||
MetadataURL: req.GetMetadataUrl(),
|
||||
}
|
||||
}
|
||||
|
||||
func AddAPIAppRequestToDomain(app *mgmt_pb.AddAPIAppRequest) *domain.APIApp {
|
||||
return &domain.APIApp{
|
||||
ObjectRoot: models.ObjectRoot{
|
||||
@ -98,6 +109,17 @@ func UpdateOIDCAppConfigRequestToDomain(app *mgmt_pb.UpdateOIDCAppConfigRequest)
|
||||
}
|
||||
}
|
||||
|
||||
func UpdateSAMLAppConfigRequestToDomain(app *mgmt_pb.UpdateSAMLAppConfigRequest) *domain.SAMLApp {
|
||||
return &domain.SAMLApp{
|
||||
ObjectRoot: models.ObjectRoot{
|
||||
AggregateID: app.ProjectId,
|
||||
},
|
||||
AppID: app.AppId,
|
||||
Metadata: app.GetMetadataXml(),
|
||||
MetadataURL: app.GetMetadataUrl(),
|
||||
}
|
||||
}
|
||||
|
||||
func UpdateAPIAppConfigRequestToDomain(app *mgmt_pb.UpdateAPIAppConfigRequest) *domain.APIApp {
|
||||
return &domain.APIApp{
|
||||
ObjectRoot: models.ObjectRoot{
|
||||
|
@ -33,6 +33,9 @@ func AppConfigToPb(app *query.App) app_pb.AppConfig {
|
||||
if app.OIDCConfig != nil {
|
||||
return AppOIDCConfigToPb(app.OIDCConfig)
|
||||
}
|
||||
if app.SAMLConfig != nil {
|
||||
return AppSAMLConfigToPb(app.SAMLConfig)
|
||||
}
|
||||
return AppAPIConfigToPb(app.APIConfig)
|
||||
}
|
||||
|
||||
@ -61,6 +64,14 @@ func AppOIDCConfigToPb(app *query.OIDCApp) *app_pb.App_OidcConfig {
|
||||
}
|
||||
}
|
||||
|
||||
func AppSAMLConfigToPb(app *query.SAMLApp) app_pb.AppConfig {
|
||||
return &app_pb.App_SamlConfig{
|
||||
SamlConfig: &app_pb.SAMLConfig{
|
||||
Metadata: &app_pb.SAMLConfig_MetadataXml{MetadataXml: app.Metadata},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func AppAPIConfigToPb(app *query.APIApp) app_pb.AppConfig {
|
||||
return &app_pb.App_ApiConfig{
|
||||
ApiConfig: &app_pb.APIConfig{
|
||||
|
99
internal/api/saml/auth_request_converter.go
Normal file
@ -0,0 +1,99 @@
|
||||
package saml
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/zitadel/saml/pkg/provider/models"
|
||||
"github.com/zitadel/saml/pkg/provider/xml/samlp"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/authz"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/errors"
|
||||
)
|
||||
|
||||
var _ models.AuthRequestInt = &AuthRequest{}
|
||||
|
||||
type AuthRequest struct {
|
||||
*domain.AuthRequest
|
||||
}
|
||||
|
||||
func (a *AuthRequest) GetApplicationID() string {
|
||||
return a.ApplicationID
|
||||
}
|
||||
|
||||
func (a *AuthRequest) GetID() string {
|
||||
return a.ID
|
||||
}
|
||||
func (a *AuthRequest) GetRelayState() string {
|
||||
return a.TransferState
|
||||
}
|
||||
func (a *AuthRequest) GetAccessConsumerServiceURL() string {
|
||||
return a.CallbackURI
|
||||
}
|
||||
|
||||
func (a *AuthRequest) GetNameID() string {
|
||||
return a.UserName
|
||||
}
|
||||
|
||||
func (a *AuthRequest) saml() *domain.AuthRequestSAML {
|
||||
return a.Request.(*domain.AuthRequestSAML)
|
||||
}
|
||||
func (a *AuthRequest) GetAuthRequestID() string {
|
||||
return a.saml().ID
|
||||
}
|
||||
func (a *AuthRequest) GetBindingType() string {
|
||||
return a.saml().BindingType
|
||||
}
|
||||
func (a *AuthRequest) GetIssuer() string {
|
||||
return a.saml().Issuer
|
||||
}
|
||||
func (a *AuthRequest) GetIssuerName() string {
|
||||
return a.saml().IssuerName
|
||||
}
|
||||
func (a *AuthRequest) GetDestination() string {
|
||||
return a.saml().Destination
|
||||
}
|
||||
func (a *AuthRequest) GetCode() string {
|
||||
return a.saml().Code
|
||||
}
|
||||
func (a *AuthRequest) GetUserID() string {
|
||||
return a.UserID
|
||||
}
|
||||
func (a *AuthRequest) GetUserName() string {
|
||||
return a.UserName
|
||||
}
|
||||
func (a *AuthRequest) Done() bool {
|
||||
for _, step := range a.PossibleSteps {
|
||||
if step.Type() == domain.NextStepRedirectToCallback {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func AuthRequestFromBusiness(authReq *domain.AuthRequest) (_ models.AuthRequestInt, err error) {
|
||||
if _, ok := authReq.Request.(*domain.AuthRequestSAML); !ok {
|
||||
return nil, errors.ThrowInvalidArgument(nil, "SAML-Hbz7A", "auth request is not of type saml")
|
||||
}
|
||||
return &AuthRequest{authReq}, nil
|
||||
}
|
||||
|
||||
func CreateAuthRequestToBusiness(ctx context.Context, authReq *samlp.AuthnRequestType, acsUrl, protocolBinding, applicationID, relayState, userAgentID string) *domain.AuthRequest {
|
||||
return &domain.AuthRequest{
|
||||
CreationDate: time.Now(),
|
||||
AgentID: userAgentID,
|
||||
ApplicationID: applicationID,
|
||||
CallbackURI: acsUrl,
|
||||
TransferState: relayState,
|
||||
InstanceID: authz.GetInstance(ctx).InstanceID(),
|
||||
Request: &domain.AuthRequestSAML{
|
||||
ID: authReq.Id,
|
||||
BindingType: protocolBinding,
|
||||
Code: "",
|
||||
Issuer: authReq.Issuer.Text,
|
||||
IssuerName: authReq.Issuer.SPProvidedID,
|
||||
Destination: authReq.Destination,
|
||||
},
|
||||
}
|
||||
}
|
202
internal/api/saml/certificate.go
Normal file
@ -0,0 +1,202 @@
|
||||
package saml
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/zitadel/logging"
|
||||
"github.com/zitadel/saml/pkg/provider/key"
|
||||
"gopkg.in/square/go-jose.v2"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/authz"
|
||||
"github.com/zitadel/zitadel/internal/crypto"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/errors"
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/query"
|
||||
"github.com/zitadel/zitadel/internal/repository/keypair"
|
||||
)
|
||||
|
||||
const (
|
||||
locksTable = "projections.locks"
|
||||
signingKey = "signing_key"
|
||||
samlUser = "SAML"
|
||||
|
||||
retryBackoff = 500 * time.Millisecond
|
||||
retryCount = 3
|
||||
lockDuration = retryCount * retryBackoff * 5
|
||||
gracefulPeriod = 10 * time.Minute
|
||||
)
|
||||
|
||||
type CertificateAndKey struct {
|
||||
algorithm jose.SignatureAlgorithm
|
||||
id string
|
||||
key interface{}
|
||||
certificate interface{}
|
||||
}
|
||||
|
||||
func (c *CertificateAndKey) SignatureAlgorithm() jose.SignatureAlgorithm {
|
||||
return c.algorithm
|
||||
}
|
||||
|
||||
func (c *CertificateAndKey) Key() interface{} {
|
||||
return c.key
|
||||
}
|
||||
|
||||
func (c *CertificateAndKey) Certificate() interface{} {
|
||||
return c.certificate
|
||||
}
|
||||
|
||||
func (c *CertificateAndKey) ID() string {
|
||||
return c.id
|
||||
}
|
||||
|
||||
func (p *Storage) GetCertificateAndKey(ctx context.Context, usage domain.KeyUsage) (certAndKey *key.CertificateAndKey, err error) {
|
||||
err = retry(func() error {
|
||||
certAndKey, err = p.getCertificateAndKey(ctx, usage)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if certAndKey == nil {
|
||||
return errors.ThrowInternal(err, "SAML-8u01nks", "no certificate found")
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return certAndKey, err
|
||||
}
|
||||
|
||||
func (p *Storage) getCertificateAndKey(ctx context.Context, usage domain.KeyUsage) (*key.CertificateAndKey, error) {
|
||||
certs, err := p.query.ActiveCertificates(ctx, time.Now().Add(gracefulPeriod), usage)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(certs.Certificates) > 0 {
|
||||
return p.certificateToCertificateAndKey(selectCertificate(certs.Certificates))
|
||||
}
|
||||
|
||||
var sequence uint64
|
||||
if certs.LatestSequence != nil {
|
||||
sequence = certs.LatestSequence.Sequence
|
||||
}
|
||||
|
||||
return nil, p.refreshCertificate(ctx, usage, sequence)
|
||||
}
|
||||
|
||||
func (p *Storage) refreshCertificate(
|
||||
ctx context.Context,
|
||||
usage domain.KeyUsage,
|
||||
sequence uint64,
|
||||
) error {
|
||||
ok, err := p.ensureIsLatestCertificate(ctx, sequence)
|
||||
if err != nil {
|
||||
logging.WithError(err).Error("could not ensure latest key")
|
||||
return err
|
||||
}
|
||||
if !ok {
|
||||
logging.Warn("view not up to date, retrying later")
|
||||
return err
|
||||
}
|
||||
err = p.lockAndGenerateCertificateAndKey(ctx, usage, sequence)
|
||||
logging.OnError(err).Warn("could not create signing key")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Storage) ensureIsLatestCertificate(ctx context.Context, sequence uint64) (bool, error) {
|
||||
maxSequence, err := p.getMaxKeySequence(ctx)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("error retrieving new events: %w", err)
|
||||
}
|
||||
return sequence == maxSequence, nil
|
||||
}
|
||||
|
||||
func (p *Storage) lockAndGenerateCertificateAndKey(ctx context.Context, usage domain.KeyUsage, sequence uint64) error {
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
defer cancel()
|
||||
ctx = setSAMLCtx(ctx)
|
||||
|
||||
errs := p.locker.Lock(ctx, lockDuration, authz.GetInstance(ctx).InstanceID())
|
||||
err, ok := <-errs
|
||||
if err != nil || !ok {
|
||||
if errors.IsErrorAlreadyExists(err) {
|
||||
return nil
|
||||
}
|
||||
logging.OnError(err).Warn("initial lock failed")
|
||||
return err
|
||||
}
|
||||
|
||||
switch usage {
|
||||
case domain.KeyUsageSAMLMetadataSigning, domain.KeyUsageSAMLResponseSinging:
|
||||
certAndKey, err := p.GetCertificateAndKey(ctx, domain.KeyUsageSAMLCA)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error while reading ca certificate: %w", err)
|
||||
}
|
||||
if certAndKey.Key == nil || certAndKey.Certificate == nil {
|
||||
return fmt.Errorf("has no ca certificate")
|
||||
}
|
||||
|
||||
switch usage {
|
||||
case domain.KeyUsageSAMLMetadataSigning:
|
||||
return p.command.GenerateSAMLMetadataCertificate(setSAMLCtx(ctx), p.certificateAlgorithm, certAndKey.Key, certAndKey.Certificate)
|
||||
case domain.KeyUsageSAMLResponseSinging:
|
||||
return p.command.GenerateSAMLResponseCertificate(setSAMLCtx(ctx), p.certificateAlgorithm, certAndKey.Key, certAndKey.Certificate)
|
||||
default:
|
||||
return fmt.Errorf("unknown usage")
|
||||
}
|
||||
case domain.KeyUsageSAMLCA:
|
||||
return p.command.GenerateSAMLCACertificate(setSAMLCtx(ctx), p.certificateAlgorithm)
|
||||
default:
|
||||
return fmt.Errorf("unknown certificate usage")
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Storage) getMaxKeySequence(ctx context.Context) (uint64, error) {
|
||||
return p.eventstore.LatestSequence(ctx,
|
||||
eventstore.NewSearchQueryBuilder(eventstore.ColumnsMaxSequence).
|
||||
ResourceOwner(authz.GetInstance(ctx).InstanceID()).
|
||||
AddQuery().
|
||||
AggregateTypes(keypair.AggregateType).
|
||||
Builder(),
|
||||
)
|
||||
}
|
||||
|
||||
func (p *Storage) certificateToCertificateAndKey(certificate query.Certificate) (_ *key.CertificateAndKey, err error) {
|
||||
keyData, err := crypto.Decrypt(certificate.Key(), p.encAlg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
privateKey, err := crypto.BytesToPrivateKey(keyData)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cert, err := crypto.BytesToCertificate(certificate.Certificate())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &key.CertificateAndKey{
|
||||
Key: privateKey,
|
||||
Certificate: cert,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func selectCertificate(certs []query.Certificate) query.Certificate {
|
||||
return certs[len(certs)-1]
|
||||
}
|
||||
|
||||
func setSAMLCtx(ctx context.Context) context.Context {
|
||||
return authz.SetCtxData(ctx, authz.CtxData{UserID: samlUser, OrgID: authz.GetInstance(ctx).InstanceID()})
|
||||
}
|
||||
|
||||
func retry(retryable func() error) (err error) {
|
||||
for i := 0; i < retryCount; i++ {
|
||||
err = retryable()
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
time.Sleep(retryBackoff)
|
||||
}
|
||||
return err
|
||||
}
|
102
internal/api/saml/provider.go
Normal file
@ -0,0 +1,102 @@
|
||||
package saml
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/zitadel/saml/pkg/provider"
|
||||
|
||||
http_utils "github.com/zitadel/zitadel/internal/api/http"
|
||||
"github.com/zitadel/zitadel/internal/api/http/middleware"
|
||||
"github.com/zitadel/zitadel/internal/api/ui/login"
|
||||
"github.com/zitadel/zitadel/internal/auth/repository"
|
||||
"github.com/zitadel/zitadel/internal/command"
|
||||
"github.com/zitadel/zitadel/internal/crypto"
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/eventstore/handler/crdb"
|
||||
"github.com/zitadel/zitadel/internal/query"
|
||||
"github.com/zitadel/zitadel/internal/telemetry/metrics"
|
||||
)
|
||||
|
||||
const (
|
||||
HandlerPrefix = "/saml/v2"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
ProviderConfig *provider.Config
|
||||
}
|
||||
|
||||
func NewProvider(
|
||||
ctx context.Context,
|
||||
conf Config,
|
||||
externalSecure bool,
|
||||
command *command.Commands,
|
||||
query *query.Queries,
|
||||
repo repository.Repository,
|
||||
encAlg crypto.EncryptionAlgorithm,
|
||||
certEncAlg crypto.EncryptionAlgorithm,
|
||||
es *eventstore.Eventstore,
|
||||
projections *sql.DB,
|
||||
instanceHandler,
|
||||
userAgentCookie func(http.Handler) http.Handler,
|
||||
) (*provider.Provider, error) {
|
||||
metricTypes := []metrics.MetricType{metrics.MetricTypeRequestCount, metrics.MetricTypeStatusCode, metrics.MetricTypeTotalCount}
|
||||
|
||||
provStorage, err := newStorage(
|
||||
command,
|
||||
query,
|
||||
repo,
|
||||
encAlg,
|
||||
certEncAlg,
|
||||
es,
|
||||
projections,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
options := []provider.Option{
|
||||
provider.WithHttpInterceptors(
|
||||
middleware.MetricsHandler(metricTypes),
|
||||
middleware.TelemetryHandler(),
|
||||
middleware.NoCacheInterceptor().Handler,
|
||||
instanceHandler,
|
||||
userAgentCookie,
|
||||
http_utils.CopyHeadersToContext,
|
||||
),
|
||||
}
|
||||
if !externalSecure {
|
||||
options = append(options, provider.WithAllowInsecure())
|
||||
}
|
||||
|
||||
return provider.NewProvider(
|
||||
ctx,
|
||||
provStorage,
|
||||
HandlerPrefix,
|
||||
conf.ProviderConfig,
|
||||
options...,
|
||||
)
|
||||
}
|
||||
|
||||
func newStorage(
|
||||
command *command.Commands,
|
||||
query *query.Queries,
|
||||
repo repository.Repository,
|
||||
encAlg crypto.EncryptionAlgorithm,
|
||||
certEncAlg crypto.EncryptionAlgorithm,
|
||||
es *eventstore.Eventstore,
|
||||
projections *sql.DB,
|
||||
) (*Storage, error) {
|
||||
return &Storage{
|
||||
encAlg: encAlg,
|
||||
certEncAlg: certEncAlg,
|
||||
locker: crdb.NewLocker(projections, locksTable, signingKey),
|
||||
eventstore: es,
|
||||
repo: repo,
|
||||
command: command,
|
||||
query: query,
|
||||
defaultLoginURL: fmt.Sprintf("%s%s?%s=", login.HandlerPrefix, login.EndpointLogin, login.QueryAuthRequestID),
|
||||
}, nil
|
||||
}
|
187
internal/api/saml/storage.go
Normal file
@ -0,0 +1,187 @@
|
||||
package saml
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/zitadel/saml/pkg/provider"
|
||||
"github.com/zitadel/saml/pkg/provider/key"
|
||||
"github.com/zitadel/saml/pkg/provider/models"
|
||||
"github.com/zitadel/saml/pkg/provider/serviceprovider"
|
||||
"github.com/zitadel/saml/pkg/provider/xml/samlp"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/http/middleware"
|
||||
"github.com/zitadel/zitadel/internal/auth/repository"
|
||||
"github.com/zitadel/zitadel/internal/command"
|
||||
"github.com/zitadel/zitadel/internal/crypto"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/errors"
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/eventstore/handler/crdb"
|
||||
"github.com/zitadel/zitadel/internal/query"
|
||||
"github.com/zitadel/zitadel/internal/telemetry/tracing"
|
||||
)
|
||||
|
||||
var _ provider.EntityStorage = &Storage{}
|
||||
var _ provider.IdentityProviderStorage = &Storage{}
|
||||
var _ provider.AuthStorage = &Storage{}
|
||||
var _ provider.UserStorage = &Storage{}
|
||||
|
||||
type Storage struct {
|
||||
certChan <-chan interface{}
|
||||
defaultCertificateLifetime time.Duration
|
||||
|
||||
currentCACertificate query.Certificate
|
||||
currentMetadataCertificate query.Certificate
|
||||
currentResponseCertificate query.Certificate
|
||||
|
||||
locker crdb.Locker
|
||||
certificateAlgorithm string
|
||||
encAlg crypto.EncryptionAlgorithm
|
||||
certEncAlg crypto.EncryptionAlgorithm
|
||||
|
||||
eventstore *eventstore.Eventstore
|
||||
repo repository.Repository
|
||||
command *command.Commands
|
||||
query *query.Queries
|
||||
|
||||
defaultLoginURL string
|
||||
}
|
||||
|
||||
func (p *Storage) GetEntityByID(ctx context.Context, entityID string) (*serviceprovider.ServiceProvider, error) {
|
||||
app, err := p.query.AppBySAMLEntityID(ctx, entityID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return serviceprovider.NewServiceProvider(
|
||||
app.ID,
|
||||
&serviceprovider.Config{
|
||||
Metadata: app.SAMLConfig.Metadata,
|
||||
},
|
||||
p.defaultLoginURL,
|
||||
)
|
||||
}
|
||||
|
||||
func (p *Storage) GetEntityIDByAppID(ctx context.Context, appID string) (string, error) {
|
||||
app, err := p.query.AppByID(ctx, appID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return app.SAMLConfig.EntityID, nil
|
||||
}
|
||||
|
||||
func (p *Storage) Health(context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Storage) GetCA(ctx context.Context) (*key.CertificateAndKey, error) {
|
||||
return p.GetCertificateAndKey(ctx, domain.KeyUsageSAMLCA)
|
||||
}
|
||||
|
||||
func (p *Storage) GetMetadataSigningKey(ctx context.Context) (*key.CertificateAndKey, error) {
|
||||
return p.GetCertificateAndKey(ctx, domain.KeyUsageSAMLMetadataSigning)
|
||||
}
|
||||
|
||||
func (p *Storage) GetResponseSigningKey(ctx context.Context) (*key.CertificateAndKey, error) {
|
||||
return p.GetCertificateAndKey(ctx, domain.KeyUsageSAMLResponseSinging)
|
||||
}
|
||||
|
||||
func (p *Storage) CreateAuthRequest(ctx context.Context, req *samlp.AuthnRequestType, acsUrl, protocolBinding, relayState, applicationID string) (_ models.AuthRequestInt, err error) {
|
||||
ctx, span := tracing.NewSpan(ctx)
|
||||
defer func() { span.EndWithError(err) }()
|
||||
userAgentID, ok := middleware.UserAgentIDFromCtx(ctx)
|
||||
if !ok {
|
||||
return nil, errors.ThrowPreconditionFailed(nil, "SAML-sd436", "no user agent id")
|
||||
}
|
||||
|
||||
authRequest := CreateAuthRequestToBusiness(ctx, req, acsUrl, protocolBinding, applicationID, relayState, userAgentID)
|
||||
|
||||
resp, err := p.repo.CreateAuthRequest(ctx, authRequest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return AuthRequestFromBusiness(resp)
|
||||
}
|
||||
|
||||
func (p *Storage) AuthRequestByID(ctx context.Context, id string) (_ models.AuthRequestInt, err error) {
|
||||
ctx, span := tracing.NewSpan(ctx)
|
||||
defer func() { span.EndWithError(err) }()
|
||||
userAgentID, ok := middleware.UserAgentIDFromCtx(ctx)
|
||||
if !ok {
|
||||
return nil, errors.ThrowPreconditionFailed(nil, "SAML-D3g21", "no user agent id")
|
||||
}
|
||||
resp, err := p.repo.AuthRequestByIDCheckLoggedIn(ctx, id, userAgentID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return AuthRequestFromBusiness(resp)
|
||||
}
|
||||
|
||||
func (p *Storage) SetUserinfoWithUserID(ctx context.Context, userinfo models.AttributeSetter, userID string, attributes []int) (err error) {
|
||||
ctx, span := tracing.NewSpan(ctx)
|
||||
defer func() { span.EndWithError(err) }()
|
||||
user, err := p.query.GetUserByID(ctx, true, userID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
setUserinfo(user, userinfo, attributes)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Storage) SetUserinfoWithLoginName(ctx context.Context, userinfo models.AttributeSetter, loginName string, attributes []int) (err error) {
|
||||
ctx, span := tracing.NewSpan(ctx)
|
||||
defer func() { span.EndWithError(err) }()
|
||||
|
||||
loginNameSQ, err := query.NewUserLoginNamesSearchQuery(loginName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
user, err := p.query.GetUser(ctx, true, loginNameSQ)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
setUserinfo(user, userinfo, attributes)
|
||||
return nil
|
||||
}
|
||||
|
||||
func setUserinfo(user *query.User, userinfo models.AttributeSetter, attributes []int) {
|
||||
if len(attributes) == 0 {
|
||||
userinfo.SetUsername(user.PreferredLoginName)
|
||||
userinfo.SetUserID(user.ID)
|
||||
if user.Human == nil {
|
||||
return
|
||||
}
|
||||
userinfo.SetEmail(user.Human.Email)
|
||||
userinfo.SetSurname(user.Human.LastName)
|
||||
userinfo.SetGivenName(user.Human.FirstName)
|
||||
userinfo.SetFullName(user.Human.DisplayName)
|
||||
return
|
||||
}
|
||||
for _, attribute := range attributes {
|
||||
switch attribute {
|
||||
case provider.AttributeEmail:
|
||||
if user.Human != nil {
|
||||
userinfo.SetEmail(user.Human.Email)
|
||||
}
|
||||
case provider.AttributeSurname:
|
||||
if user.Human != nil {
|
||||
userinfo.SetSurname(user.Human.LastName)
|
||||
}
|
||||
case provider.AttributeFullName:
|
||||
if user.Human != nil {
|
||||
userinfo.SetFullName(user.Human.DisplayName)
|
||||
}
|
||||
case provider.AttributeGivenName:
|
||||
if user.Human != nil {
|
||||
userinfo.SetGivenName(user.Human.FirstName)
|
||||
}
|
||||
case provider.AttributeUsername:
|
||||
userinfo.SetUsername(user.PreferredLoginName)
|
||||
case provider.AttributeUserID:
|
||||
userinfo.SetUserID(user.ID)
|
||||
}
|
||||
}
|
||||
}
|
@ -37,6 +37,7 @@ type Login struct {
|
||||
externalSecure bool
|
||||
consolePath string
|
||||
oidcAuthCallbackURL func(context.Context, string) string
|
||||
samlAuthCallbackURL func(context.Context, string) string
|
||||
idpConfigAlg crypto.EncryptionAlgorithm
|
||||
userCodeAlg crypto.EncryptionAlgorithm
|
||||
}
|
||||
@ -61,10 +62,12 @@ func CreateLogin(config Config,
|
||||
staticStorage static.Storage,
|
||||
consolePath string,
|
||||
oidcAuthCallbackURL func(context.Context, string) string,
|
||||
samlAuthCallbackURL func(context.Context, string) string,
|
||||
externalSecure bool,
|
||||
userAgentCookie,
|
||||
issuerInterceptor,
|
||||
instanceHandler,
|
||||
oidcInstanceHandler,
|
||||
samlInstanceHandler mux.MiddlewareFunc,
|
||||
assetCache mux.MiddlewareFunc,
|
||||
userCodeAlg crypto.EncryptionAlgorithm,
|
||||
idpConfigAlg crypto.EncryptionAlgorithm,
|
||||
@ -73,6 +76,7 @@ func CreateLogin(config Config,
|
||||
|
||||
login := &Login{
|
||||
oidcAuthCallbackURL: oidcAuthCallbackURL,
|
||||
samlAuthCallbackURL: samlAuthCallbackURL,
|
||||
externalSecure: externalSecure,
|
||||
consolePath: consolePath,
|
||||
command: command,
|
||||
@ -91,7 +95,7 @@ func CreateLogin(config Config,
|
||||
cacheInterceptor := createCacheInterceptor(config.Cache.MaxAge, config.Cache.SharedMaxAge, assetCache)
|
||||
security := middleware.SecurityHeaders(csp(), login.cspErrorHandler)
|
||||
|
||||
login.router = CreateRouter(login, statikFS, middleware.TelemetryHandler(IgnoreInstanceEndpoints...), instanceHandler, csrfInterceptor, cacheInterceptor, security, userAgentCookie, issuerInterceptor)
|
||||
login.router = CreateRouter(login, statikFS, middleware.TelemetryHandler(IgnoreInstanceEndpoints...), oidcInstanceHandler, samlInstanceHandler, csrfInterceptor, cacheInterceptor, security, userAgentCookie, issuerInterceptor)
|
||||
login.renderer = CreateRenderer(HandlerPrefix, statikFS, staticStorage, config.LanguageCookieName)
|
||||
login.parser = form.NewParser()
|
||||
return login, nil
|
||||
|
@ -4,6 +4,7 @@ import (
|
||||
"net/http"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
caos_errs "github.com/zitadel/zitadel/internal/errors"
|
||||
)
|
||||
|
||||
const (
|
||||
@ -43,11 +44,26 @@ func (l *Login) renderSuccessAndCallback(w http.ResponseWriter, r *http.Request,
|
||||
userData: l.getUserData(r, authReq, "Login Successful", errID, errMessage),
|
||||
}
|
||||
if authReq != nil {
|
||||
data.RedirectURI = l.oidcAuthCallbackURL(r.Context(), "") //the id will be set via the html (maybe change this with the login refactoring)
|
||||
//the id will be set via the html (maybe change this with the login refactoring)
|
||||
if _, ok := authReq.Request.(*domain.AuthRequestOIDC); ok {
|
||||
data.RedirectURI = l.oidcAuthCallbackURL(r.Context(), "")
|
||||
} else if _, ok := authReq.Request.(*domain.AuthRequestSAML); ok {
|
||||
data.RedirectURI = l.samlAuthCallbackURL(r.Context(), "")
|
||||
}
|
||||
}
|
||||
l.renderer.RenderTemplate(w, r, l.getTranslator(r.Context(), authReq), l.renderer.Templates[tmplLoginSuccess], data, nil)
|
||||
}
|
||||
|
||||
func (l *Login) redirectToCallback(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest) {
|
||||
http.Redirect(w, r, l.oidcAuthCallbackURL(r.Context(), authReq.ID), http.StatusFound)
|
||||
var callback string
|
||||
switch authReq.Request.(type) {
|
||||
case *domain.AuthRequestOIDC:
|
||||
callback = l.oidcAuthCallbackURL(r.Context(), authReq.ID)
|
||||
case *domain.AuthRequestSAML:
|
||||
callback = l.samlAuthCallbackURL(r.Context(), authReq.ID)
|
||||
default:
|
||||
l.renderInternalError(w, r, authReq, caos_errs.ThrowInternal(nil, "LOGIN-rhjQF", "Errors.AuthRequest.RequestTypeNotSupported"))
|
||||
return
|
||||
}
|
||||
http.Redirect(w, r, callback, http.StatusFound)
|
||||
}
|
||||
|
@ -97,12 +97,12 @@ type orgViewProvider interface {
|
||||
}
|
||||
|
||||
type userGrantProvider interface {
|
||||
ProjectByOIDCClientID(context.Context, string) (*query.Project, error)
|
||||
ProjectByClientID(context.Context, string) (*query.Project, error)
|
||||
UserGrantsByProjectAndUserID(context.Context, string, string) ([]*query.UserGrant, error)
|
||||
}
|
||||
|
||||
type projectProvider interface {
|
||||
ProjectByOIDCClientID(context.Context, string) (*query.Project, error)
|
||||
ProjectByClientID(context.Context, string) (*query.Project, error)
|
||||
OrgProjectMappingByIDs(orgID, projectID, instanceID string) (*project_view_model.OrgProjectMapping, error)
|
||||
}
|
||||
|
||||
@ -122,7 +122,7 @@ func (repo *AuthRequestRepo) CreateAuthRequest(ctx context.Context, request *dom
|
||||
return nil, err
|
||||
}
|
||||
request.ID = reqID
|
||||
project, err := repo.ProjectProvider.ProjectByOIDCClientID(ctx, request.ApplicationID)
|
||||
project, err := repo.ProjectProvider.ProjectByClientID(ctx, request.ApplicationID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -1048,6 +1048,9 @@ func (repo *AuthRequestRepo) getLoginTexts(ctx context.Context, aggregateID stri
|
||||
}
|
||||
|
||||
func (repo *AuthRequestRepo) hasSucceededPage(ctx context.Context, request *domain.AuthRequest, provider applicationProvider) (bool, error) {
|
||||
if _, ok := request.Request.(*domain.AuthRequestOIDC); !ok {
|
||||
return false, nil
|
||||
}
|
||||
app, err := provider.AppByOIDCClientID(ctx, request.ApplicationID)
|
||||
if err != nil {
|
||||
return false, err
|
||||
@ -1257,8 +1260,8 @@ func linkingIDPConfigExistingInAllowedIDPs(linkingUsers []*domain.ExternalUser,
|
||||
func userGrantRequired(ctx context.Context, request *domain.AuthRequest, user *user_model.UserView, userGrantProvider userGrantProvider) (_ bool, err error) {
|
||||
var project *query.Project
|
||||
switch request.Request.Type() {
|
||||
case domain.AuthRequestTypeOIDC:
|
||||
project, err = userGrantProvider.ProjectByOIDCClientID(ctx, request.ApplicationID)
|
||||
case domain.AuthRequestTypeOIDC, domain.AuthRequestTypeSAML:
|
||||
project, err = userGrantProvider.ProjectByClientID(ctx, request.ApplicationID)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
@ -1278,8 +1281,8 @@ func userGrantRequired(ctx context.Context, request *domain.AuthRequest, user *u
|
||||
func projectRequired(ctx context.Context, request *domain.AuthRequest, projectProvider projectProvider) (_ bool, err error) {
|
||||
var project *query.Project
|
||||
switch request.Request.Type() {
|
||||
case domain.AuthRequestTypeOIDC:
|
||||
project, err = projectProvider.ProjectByOIDCClientID(ctx, request.ApplicationID)
|
||||
case domain.AuthRequestTypeOIDC, domain.AuthRequestTypeSAML:
|
||||
project, err = projectProvider.ProjectByClientID(ctx, request.ApplicationID)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
@ -22,6 +22,10 @@ import (
|
||||
user_view_model "github.com/zitadel/zitadel/internal/user/repository/view/model"
|
||||
)
|
||||
|
||||
var (
|
||||
testNow = time.Now()
|
||||
)
|
||||
|
||||
type mockViewNoUserSession struct{}
|
||||
|
||||
func (m *mockViewNoUserSession) UserSessionByIDs(string, string, string) (*user_view_model.UserSessionView, error) {
|
||||
@ -191,7 +195,7 @@ type mockUserGrants struct {
|
||||
userGrants int
|
||||
}
|
||||
|
||||
func (m *mockUserGrants) ProjectByOIDCClientID(ctx context.Context, s string) (*query.Project, error) {
|
||||
func (m *mockUserGrants) ProjectByClientID(ctx context.Context, s string) (*query.Project, error) {
|
||||
return &query.Project{ProjectRoleCheck: m.roleCheck}, nil
|
||||
}
|
||||
|
||||
@ -208,7 +212,7 @@ type mockProject struct {
|
||||
projectCheck bool
|
||||
}
|
||||
|
||||
func (m *mockProject) ProjectByOIDCClientID(ctx context.Context, s string) (*query.Project, error) {
|
||||
func (m *mockProject) ProjectByClientID(ctx context.Context, s string) (*query.Project, error) {
|
||||
return &query.Project{HasProjectCheck: m.projectCheck}, nil
|
||||
}
|
||||
|
||||
@ -615,8 +619,8 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) {
|
||||
"passwordless verified, email not verified, email verification step",
|
||||
fields{
|
||||
userSessionViewProvider: &mockViewUserSession{
|
||||
PasswordlessVerification: time.Now().Add(-5 * time.Minute),
|
||||
MultiFactorVerification: time.Now().Add(-5 * time.Minute),
|
||||
PasswordlessVerification: testNow.Add(-5 * time.Minute),
|
||||
MultiFactorVerification: testNow.Add(-5 * time.Minute),
|
||||
},
|
||||
userViewProvider: &mockViewUser{
|
||||
PasswordSet: true,
|
||||
@ -667,7 +671,7 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) {
|
||||
"external user (no external verification), external login step",
|
||||
fields{
|
||||
userSessionViewProvider: &mockViewUserSession{
|
||||
SecondFactorVerification: time.Now().UTC().Add(-5 * time.Minute),
|
||||
SecondFactorVerification: testNow.Add(-5 * time.Minute),
|
||||
},
|
||||
userViewProvider: &mockViewUser{
|
||||
IsEmailVerified: true,
|
||||
@ -699,8 +703,8 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) {
|
||||
"external user (external verification set), callback",
|
||||
fields{
|
||||
userSessionViewProvider: &mockViewUserSession{
|
||||
ExternalLoginVerification: time.Now().UTC().Add(-5 * time.Minute),
|
||||
SecondFactorVerification: time.Now().UTC().Add(-5 * time.Minute),
|
||||
ExternalLoginVerification: testNow.Add(-5 * time.Minute),
|
||||
SecondFactorVerification: testNow.Add(-5 * time.Minute),
|
||||
},
|
||||
userViewProvider: &mockViewUser{
|
||||
IsEmailVerified: true,
|
||||
@ -759,8 +763,8 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) {
|
||||
"external user (no password check needed), callback",
|
||||
fields{
|
||||
userSessionViewProvider: &mockViewUserSession{
|
||||
SecondFactorVerification: time.Now().UTC().Add(-5 * time.Minute),
|
||||
ExternalLoginVerification: time.Now().UTC().Add(-5 * time.Minute),
|
||||
SecondFactorVerification: testNow.Add(-5 * time.Minute),
|
||||
ExternalLoginVerification: testNow.Add(-5 * time.Minute),
|
||||
},
|
||||
userViewProvider: &mockViewUser{
|
||||
PasswordSet: true,
|
||||
@ -795,7 +799,7 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) {
|
||||
"password verified, passwordless set up, mfa not verified, mfa check step",
|
||||
fields{
|
||||
userSessionViewProvider: &mockViewUserSession{
|
||||
PasswordVerification: time.Now().UTC().Add(-5 * time.Minute),
|
||||
PasswordVerification: testNow.Add(-5 * time.Minute),
|
||||
},
|
||||
userViewProvider: &mockViewUser{
|
||||
PasswordSet: true,
|
||||
@ -829,7 +833,7 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) {
|
||||
"mfa not verified, mfa check step",
|
||||
fields{
|
||||
userSessionViewProvider: &mockViewUserSession{
|
||||
PasswordVerification: time.Now().UTC().Add(-5 * time.Minute),
|
||||
PasswordVerification: testNow.Add(-5 * time.Minute),
|
||||
},
|
||||
userViewProvider: &mockViewUser{
|
||||
PasswordSet: true,
|
||||
@ -862,8 +866,8 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) {
|
||||
"external user, mfa not verified, mfa check step",
|
||||
fields{
|
||||
userSessionViewProvider: &mockViewUserSession{
|
||||
PasswordVerification: time.Now().UTC().Add(-5 * time.Minute),
|
||||
ExternalLoginVerification: time.Now().UTC().Add(-5 * time.Minute),
|
||||
PasswordVerification: testNow.Add(-5 * time.Minute),
|
||||
ExternalLoginVerification: testNow.Add(-5 * time.Minute),
|
||||
},
|
||||
userViewProvider: &mockViewUser{
|
||||
PasswordSet: true,
|
||||
@ -898,8 +902,8 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) {
|
||||
"password change required and email verified, password change step",
|
||||
fields{
|
||||
userSessionViewProvider: &mockViewUserSession{
|
||||
PasswordVerification: time.Now().UTC().Add(-5 * time.Minute),
|
||||
SecondFactorVerification: time.Now().UTC().Add(-5 * time.Minute),
|
||||
PasswordVerification: testNow.Add(-5 * time.Minute),
|
||||
SecondFactorVerification: testNow.Add(-5 * time.Minute),
|
||||
},
|
||||
userViewProvider: &mockViewUser{
|
||||
PasswordSet: true,
|
||||
@ -931,8 +935,8 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) {
|
||||
"email not verified and no password change required, mail verification step",
|
||||
fields{
|
||||
userSessionViewProvider: &mockViewUserSession{
|
||||
PasswordVerification: time.Now().UTC().Add(-5 * time.Minute),
|
||||
SecondFactorVerification: time.Now().UTC().Add(-5 * time.Minute),
|
||||
PasswordVerification: testNow.Add(-5 * time.Minute),
|
||||
SecondFactorVerification: testNow.Add(-5 * time.Minute),
|
||||
},
|
||||
userViewProvider: &mockViewUser{
|
||||
PasswordSet: true,
|
||||
@ -961,8 +965,8 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) {
|
||||
"email not verified and password change required, mail verification step",
|
||||
fields{
|
||||
userSessionViewProvider: &mockViewUserSession{
|
||||
PasswordVerification: time.Now().UTC().Add(-5 * time.Minute),
|
||||
SecondFactorVerification: time.Now().UTC().Add(-5 * time.Minute),
|
||||
PasswordVerification: testNow.Add(-5 * time.Minute),
|
||||
SecondFactorVerification: testNow.Add(-5 * time.Minute),
|
||||
},
|
||||
userViewProvider: &mockViewUser{
|
||||
PasswordSet: true,
|
||||
@ -992,8 +996,8 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) {
|
||||
"email verified and no password change required, redirect to callback step",
|
||||
fields{
|
||||
userSessionViewProvider: &mockViewUserSession{
|
||||
PasswordVerification: time.Now().UTC().Add(-5 * time.Minute),
|
||||
SecondFactorVerification: time.Now().UTC().Add(-5 * time.Minute),
|
||||
PasswordVerification: testNow.Add(-5 * time.Minute),
|
||||
SecondFactorVerification: testNow.Add(-5 * time.Minute),
|
||||
},
|
||||
userViewProvider: &mockViewUser{
|
||||
PasswordSet: true,
|
||||
@ -1027,8 +1031,8 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) {
|
||||
"prompt none, checkLoggedIn true and authenticated, redirect to callback step",
|
||||
fields{
|
||||
userSessionViewProvider: &mockViewUserSession{
|
||||
PasswordVerification: time.Now().UTC().Add(-5 * time.Minute),
|
||||
SecondFactorVerification: time.Now().UTC().Add(-5 * time.Minute),
|
||||
PasswordVerification: testNow.Add(-5 * time.Minute),
|
||||
SecondFactorVerification: testNow.Add(-5 * time.Minute),
|
||||
},
|
||||
userViewProvider: &mockViewUser{
|
||||
PasswordSet: true,
|
||||
@ -1063,8 +1067,8 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) {
|
||||
"prompt none, checkLoggedIn true, authenticated and native, login succeeded step",
|
||||
fields{
|
||||
userSessionViewProvider: &mockViewUserSession{
|
||||
PasswordVerification: time.Now().UTC().Add(-5 * time.Minute),
|
||||
SecondFactorVerification: time.Now().UTC().Add(-5 * time.Minute),
|
||||
PasswordVerification: testNow.Add(-5 * time.Minute),
|
||||
SecondFactorVerification: testNow.Add(-5 * time.Minute),
|
||||
},
|
||||
userViewProvider: &mockViewUser{
|
||||
PasswordSet: true,
|
||||
@ -1099,8 +1103,8 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) {
|
||||
"prompt none, checkLoggedIn true, authenticated and required user grants missing, grant required step",
|
||||
fields{
|
||||
userSessionViewProvider: &mockViewUserSession{
|
||||
PasswordVerification: time.Now().UTC().Add(-5 * time.Minute),
|
||||
SecondFactorVerification: time.Now().UTC().Add(-5 * time.Minute),
|
||||
PasswordVerification: testNow.Add(-5 * time.Minute),
|
||||
SecondFactorVerification: testNow.Add(-5 * time.Minute),
|
||||
},
|
||||
userViewProvider: &mockViewUser{
|
||||
PasswordSet: true,
|
||||
@ -1137,8 +1141,8 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) {
|
||||
"prompt none, checkLoggedIn true, authenticated and required user grants exist, redirect to callback step",
|
||||
fields{
|
||||
userSessionViewProvider: &mockViewUserSession{
|
||||
PasswordVerification: time.Now().UTC().Add(-5 * time.Minute),
|
||||
SecondFactorVerification: time.Now().UTC().Add(-5 * time.Minute),
|
||||
PasswordVerification: testNow.Add(-5 * time.Minute),
|
||||
SecondFactorVerification: testNow.Add(-5 * time.Minute),
|
||||
},
|
||||
userViewProvider: &mockViewUser{
|
||||
PasswordSet: true,
|
||||
@ -1176,8 +1180,8 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) {
|
||||
"prompt none, checkLoggedIn true, authenticated and required project missing, project required step",
|
||||
fields{
|
||||
userSessionViewProvider: &mockViewUserSession{
|
||||
PasswordVerification: time.Now().UTC().Add(-5 * time.Minute),
|
||||
SecondFactorVerification: time.Now().UTC().Add(-5 * time.Minute),
|
||||
PasswordVerification: testNow.Add(-5 * time.Minute),
|
||||
SecondFactorVerification: testNow.Add(-5 * time.Minute),
|
||||
},
|
||||
userViewProvider: &mockViewUser{
|
||||
PasswordSet: true,
|
||||
@ -1214,8 +1218,8 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) {
|
||||
"prompt none, checkLoggedIn true, authenticated and required project exist, redirect to callback step",
|
||||
fields{
|
||||
userSessionViewProvider: &mockViewUserSession{
|
||||
PasswordVerification: time.Now().UTC().Add(-5 * time.Minute),
|
||||
SecondFactorVerification: time.Now().UTC().Add(-5 * time.Minute),
|
||||
PasswordVerification: testNow.Add(-5 * time.Minute),
|
||||
SecondFactorVerification: testNow.Add(-5 * time.Minute),
|
||||
},
|
||||
userViewProvider: &mockViewUser{
|
||||
PasswordSet: true,
|
||||
@ -1253,7 +1257,7 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) {
|
||||
"linking users, password step",
|
||||
fields{
|
||||
userSessionViewProvider: &mockViewUserSession{
|
||||
SecondFactorVerification: time.Now().UTC().Add(-5 * time.Minute),
|
||||
SecondFactorVerification: testNow.Add(-5 * time.Minute),
|
||||
},
|
||||
userViewProvider: &mockViewUser{
|
||||
PasswordSet: true,
|
||||
@ -1287,8 +1291,8 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) {
|
||||
"linking users, linking step",
|
||||
fields{
|
||||
userSessionViewProvider: &mockViewUserSession{
|
||||
PasswordVerification: time.Now().UTC().Add(-5 * time.Minute),
|
||||
SecondFactorVerification: time.Now().UTC().Add(-5 * time.Minute),
|
||||
PasswordVerification: testNow.Add(-5 * time.Minute),
|
||||
SecondFactorVerification: testNow.Add(-5 * time.Minute),
|
||||
},
|
||||
userViewProvider: &mockViewUser{
|
||||
PasswordSet: true,
|
||||
@ -1463,7 +1467,7 @@ func TestAuthRequestRepo_mfaChecked(t *testing.T) {
|
||||
user: &user_model.UserView{
|
||||
HumanView: &user_model.HumanView{
|
||||
MFAMaxSetUp: domain.MFALevelNotSetUp,
|
||||
MFAInitSkipped: time.Now().UTC(),
|
||||
MFAInitSkipped: testNow,
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -1486,7 +1490,7 @@ func TestAuthRequestRepo_mfaChecked(t *testing.T) {
|
||||
OTPState: user_model.MFAStateReady,
|
||||
},
|
||||
},
|
||||
userSession: &user_model.UserSessionView{SecondFactorVerification: time.Now().UTC().Add(-5 * time.Hour)},
|
||||
userSession: &user_model.UserSessionView{SecondFactorVerification: testNow.Add(-5 * time.Hour)},
|
||||
},
|
||||
nil,
|
||||
true,
|
||||
@ -1569,7 +1573,7 @@ func TestAuthRequestRepo_mfaSkippedOrSetUp(t *testing.T) {
|
||||
user: &user_model.UserView{
|
||||
HumanView: &user_model.HumanView{
|
||||
MFAMaxSetUp: -1,
|
||||
MFAInitSkipped: time.Now().UTC().Add(-10 * time.Hour),
|
||||
MFAInitSkipped: testNow.Add(-10 * time.Hour),
|
||||
},
|
||||
},
|
||||
request: &domain.AuthRequest{
|
||||
@ -1587,7 +1591,7 @@ func TestAuthRequestRepo_mfaSkippedOrSetUp(t *testing.T) {
|
||||
user: &user_model.UserView{
|
||||
HumanView: &user_model.HumanView{
|
||||
MFAMaxSetUp: -1,
|
||||
MFAInitSkipped: time.Now().UTC().Add(-40 * 24 * time.Hour),
|
||||
MFAInitSkipped: testNow.Add(-40 * 24 * time.Hour),
|
||||
},
|
||||
},
|
||||
request: &domain.AuthRequest{
|
||||
@ -1645,13 +1649,13 @@ func Test_userSessionByIDs(t *testing.T) {
|
||||
"error user events, old view model state",
|
||||
args{
|
||||
userProvider: &mockViewUserSession{
|
||||
PasswordVerification: time.Now().UTC().Round(1 * time.Second),
|
||||
PasswordVerification: testNow,
|
||||
},
|
||||
user: &user_model.UserView{ID: "id", HumanView: &user_model.HumanView{FirstName: "FirstName"}},
|
||||
eventProvider: &mockEventErrUser{},
|
||||
},
|
||||
&user_model.UserSessionView{
|
||||
PasswordVerification: time.Now().UTC().Round(1 * time.Second),
|
||||
PasswordVerification: testNow,
|
||||
SecondFactorVerification: time.Time{},
|
||||
MultiFactorVerification: time.Time{},
|
||||
},
|
||||
@ -1661,7 +1665,7 @@ func Test_userSessionByIDs(t *testing.T) {
|
||||
"new user events but error, old view model state",
|
||||
args{
|
||||
userProvider: &mockViewUserSession{
|
||||
PasswordVerification: time.Now().UTC().Round(1 * time.Second),
|
||||
PasswordVerification: testNow,
|
||||
},
|
||||
agentID: "agentID",
|
||||
user: &user_model.UserView{ID: "id", HumanView: &user_model.HumanView{FirstName: "FirstName"}},
|
||||
@ -1669,12 +1673,12 @@ func Test_userSessionByIDs(t *testing.T) {
|
||||
&es_models.Event{
|
||||
AggregateType: user_repo.AggregateType,
|
||||
Type: es_models.EventType(user_repo.UserV1MFAOTPCheckSucceededType),
|
||||
CreationDate: time.Now().UTC().Round(1 * time.Second),
|
||||
CreationDate: testNow,
|
||||
},
|
||||
},
|
||||
},
|
||||
&user_model.UserSessionView{
|
||||
PasswordVerification: time.Now().UTC().Round(1 * time.Second),
|
||||
PasswordVerification: testNow,
|
||||
SecondFactorVerification: time.Time{},
|
||||
MultiFactorVerification: time.Time{},
|
||||
},
|
||||
@ -1684,7 +1688,7 @@ func Test_userSessionByIDs(t *testing.T) {
|
||||
"new user events but other agentID, old view model state",
|
||||
args{
|
||||
userProvider: &mockViewUserSession{
|
||||
PasswordVerification: time.Now().UTC().Round(1 * time.Second),
|
||||
PasswordVerification: testNow,
|
||||
},
|
||||
agentID: "agentID",
|
||||
user: &user_model.UserView{ID: "id"},
|
||||
@ -1692,7 +1696,7 @@ func Test_userSessionByIDs(t *testing.T) {
|
||||
&es_models.Event{
|
||||
AggregateType: user_repo.AggregateType,
|
||||
Type: es_models.EventType(user_repo.UserV1MFAOTPCheckSucceededType),
|
||||
CreationDate: time.Now().UTC().Round(1 * time.Second),
|
||||
CreationDate: testNow,
|
||||
Data: func() []byte {
|
||||
data, _ := json.Marshal(&user_es_model.AuthRequest{UserAgentID: "otherID"})
|
||||
return data
|
||||
@ -1701,7 +1705,7 @@ func Test_userSessionByIDs(t *testing.T) {
|
||||
},
|
||||
},
|
||||
&user_model.UserSessionView{
|
||||
PasswordVerification: time.Now().UTC().Round(1 * time.Second),
|
||||
PasswordVerification: testNow,
|
||||
SecondFactorVerification: time.Time{},
|
||||
MultiFactorVerification: time.Time{},
|
||||
},
|
||||
@ -1711,7 +1715,7 @@ func Test_userSessionByIDs(t *testing.T) {
|
||||
"new user events, new view model state",
|
||||
args{
|
||||
userProvider: &mockViewUserSession{
|
||||
PasswordVerification: time.Now().UTC().Round(1 * time.Second),
|
||||
PasswordVerification: testNow,
|
||||
},
|
||||
agentID: "agentID",
|
||||
user: &user_model.UserView{ID: "id", HumanView: &user_model.HumanView{FirstName: "FirstName"}},
|
||||
@ -1719,7 +1723,7 @@ func Test_userSessionByIDs(t *testing.T) {
|
||||
&es_models.Event{
|
||||
AggregateType: user_repo.AggregateType,
|
||||
Type: es_models.EventType(user_repo.UserV1MFAOTPCheckSucceededType),
|
||||
CreationDate: time.Now().UTC().Round(1 * time.Second),
|
||||
CreationDate: testNow,
|
||||
Data: func() []byte {
|
||||
data, _ := json.Marshal(&user_es_model.AuthRequest{UserAgentID: "agentID"})
|
||||
return data
|
||||
@ -1728,9 +1732,9 @@ func Test_userSessionByIDs(t *testing.T) {
|
||||
},
|
||||
},
|
||||
&user_model.UserSessionView{
|
||||
PasswordVerification: time.Now().UTC().Round(1 * time.Second),
|
||||
SecondFactorVerification: time.Now().UTC().Round(1 * time.Second),
|
||||
ChangeDate: time.Now().UTC().Round(1 * time.Second),
|
||||
PasswordVerification: testNow,
|
||||
SecondFactorVerification: testNow,
|
||||
ChangeDate: testNow,
|
||||
},
|
||||
nil,
|
||||
},
|
||||
@ -1738,7 +1742,7 @@ func Test_userSessionByIDs(t *testing.T) {
|
||||
"new user events (user deleted), precondition failed error",
|
||||
args{
|
||||
userProvider: &mockViewUserSession{
|
||||
PasswordVerification: time.Now().UTC().Round(1 * time.Second),
|
||||
PasswordVerification: testNow,
|
||||
},
|
||||
agentID: "agentID",
|
||||
user: &user_model.UserView{ID: "id"},
|
||||
@ -1816,7 +1820,7 @@ func Test_userByID(t *testing.T) {
|
||||
&es_models.Event{
|
||||
AggregateType: user_repo.AggregateType,
|
||||
Type: es_models.EventType(user_repo.UserV1PasswordChangedType),
|
||||
CreationDate: time.Now().UTC().Round(1 * time.Second),
|
||||
CreationDate: testNow,
|
||||
Data: nil,
|
||||
},
|
||||
},
|
||||
@ -1842,7 +1846,7 @@ func Test_userByID(t *testing.T) {
|
||||
&es_models.Event{
|
||||
AggregateType: user_repo.AggregateType,
|
||||
Type: es_models.EventType(user_repo.UserV1PasswordChangedType),
|
||||
CreationDate: time.Now().UTC().Round(1 * time.Second),
|
||||
CreationDate: testNow,
|
||||
Data: func() []byte {
|
||||
data, _ := json.Marshal(user_es_model.Password{ChangeRequired: false, Secret: &crypto.CryptoValue{}})
|
||||
return data
|
||||
@ -1851,13 +1855,13 @@ func Test_userByID(t *testing.T) {
|
||||
},
|
||||
},
|
||||
&user_model.UserView{
|
||||
ChangeDate: time.Now().UTC().Round(1 * time.Second),
|
||||
ChangeDate: testNow,
|
||||
State: user_model.UserStateActive,
|
||||
UserName: "UserName",
|
||||
HumanView: &user_model.HumanView{
|
||||
PasswordSet: true,
|
||||
PasswordChangeRequired: false,
|
||||
PasswordChanged: time.Now().UTC().Round(1 * time.Second),
|
||||
PasswordChanged: testNow,
|
||||
FirstName: "FirstName",
|
||||
},
|
||||
},
|
||||
|
@ -1,10 +1,11 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/authz"
|
||||
"github.com/zitadel/zitadel/internal/api/http"
|
||||
api_http "github.com/zitadel/zitadel/internal/api/http"
|
||||
sd "github.com/zitadel/zitadel/internal/config/systemdefaults"
|
||||
"github.com/zitadel/zitadel/internal/crypto"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
@ -23,6 +24,8 @@ import (
|
||||
)
|
||||
|
||||
type Commands struct {
|
||||
httpClient *http.Client
|
||||
|
||||
eventstore *eventstore.Eventstore
|
||||
static static.Storage
|
||||
idGenerator id.Generator
|
||||
@ -40,14 +43,17 @@ type Commands struct {
|
||||
applicationKeySize int
|
||||
domainVerificationAlg crypto.EncryptionAlgorithm
|
||||
domainVerificationGenerator crypto.Generator
|
||||
domainVerificationValidator func(domain, token, verifier string, checkType http.CheckType) error
|
||||
domainVerificationValidator func(domain, token, verifier string, checkType api_http.CheckType) error
|
||||
|
||||
multifactors domain.MultifactorConfigs
|
||||
webauthnConfig *webauthn_helper.Config
|
||||
keySize int
|
||||
keyAlgorithm crypto.EncryptionAlgorithm
|
||||
certificateAlgorithm crypto.EncryptionAlgorithm
|
||||
certKeySize int
|
||||
privateKeyLifetime time.Duration
|
||||
publicKeyLifetime time.Duration
|
||||
certificateLifetime time.Duration
|
||||
}
|
||||
|
||||
func StartCommands(es *eventstore.Eventstore,
|
||||
@ -64,7 +70,9 @@ func StartCommands(es *eventstore.Eventstore,
|
||||
smsEncryption,
|
||||
userEncryption,
|
||||
domainVerificationEncryption,
|
||||
oidcEncryption crypto.EncryptionAlgorithm,
|
||||
oidcEncryption,
|
||||
samlEncryption crypto.EncryptionAlgorithm,
|
||||
httpClient *http.Client,
|
||||
) (repo *Commands, err error) {
|
||||
if externalDomain == "" {
|
||||
return nil, errors.ThrowInvalidArgument(nil, "COMMAND-Df21s", "no external domain specified")
|
||||
@ -78,15 +86,19 @@ func StartCommands(es *eventstore.Eventstore,
|
||||
externalSecure: externalSecure,
|
||||
externalPort: externalPort,
|
||||
keySize: defaults.KeyConfig.Size,
|
||||
certKeySize: defaults.KeyConfig.CertificateSize,
|
||||
privateKeyLifetime: defaults.KeyConfig.PrivateKeyLifetime,
|
||||
publicKeyLifetime: defaults.KeyConfig.PublicKeyLifetime,
|
||||
certificateLifetime: defaults.KeyConfig.CertificateLifetime,
|
||||
idpConfigEncryption: idpConfigEncryption,
|
||||
smtpEncryption: smtpEncryption,
|
||||
smsEncryption: smsEncryption,
|
||||
userEncryption: userEncryption,
|
||||
domainVerificationAlg: domainVerificationEncryption,
|
||||
keyAlgorithm: oidcEncryption,
|
||||
certificateAlgorithm: samlEncryption,
|
||||
webauthnConfig: webAuthN,
|
||||
httpClient: httpClient,
|
||||
}
|
||||
|
||||
instance_repo.RegisterEventMappers(repo.eventstore)
|
||||
@ -109,7 +121,7 @@ func StartCommands(es *eventstore.Eventstore,
|
||||
}
|
||||
|
||||
repo.domainVerificationGenerator = crypto.NewEncryptionGenerator(defaults.DomainVerification.VerificationGenerator, repo.domainVerificationAlg)
|
||||
repo.domainVerificationValidator = http.ValidateDomain
|
||||
repo.domainVerificationValidator = api_http.ValidateDomain
|
||||
return repo, nil
|
||||
}
|
||||
|
||||
|
@ -2,6 +2,10 @@ package command
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"math/big"
|
||||
"time"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/authz"
|
||||
@ -34,3 +38,138 @@ func (c *Commands) GenerateSigningKeyPair(ctx context.Context, algorithm string)
|
||||
privateKeyExp, publicKeyExp))
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *Commands) GenerateSAMLCACertificate(ctx context.Context, algorithm string) error {
|
||||
now := time.Now().UTC()
|
||||
after := now.Add(c.certificateLifetime)
|
||||
randInt, err := rand.Int(rand.Reader, big.NewInt(1000))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
privateCrypto, publicCrypto, certificateCrypto, err := crypto.GenerateEncryptedKeyPairWithCACertificate(c.certKeySize, c.keyAlgorithm, c.certificateAlgorithm, &crypto.CertificateInformations{
|
||||
SerialNumber: randInt,
|
||||
Organisation: []string{"ZITADEL"},
|
||||
CommonName: "ZITADEL SAML CA",
|
||||
NotBefore: now,
|
||||
NotAfter: after,
|
||||
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment | x509.KeyUsageCertSign,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
keyID, err := c.idGenerator.Next()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
keyPairWriteModel := NewKeyPairWriteModel(keyID, authz.GetInstance(ctx).InstanceID())
|
||||
keyAgg := KeyPairAggregateFromWriteModel(&keyPairWriteModel.WriteModel)
|
||||
_, err = c.eventstore.Push(ctx,
|
||||
keypair.NewAddedEvent(
|
||||
ctx,
|
||||
keyAgg,
|
||||
domain.KeyUsageSAMLCA,
|
||||
algorithm,
|
||||
privateCrypto, publicCrypto,
|
||||
after, after,
|
||||
),
|
||||
keypair.NewAddedCertificateEvent(
|
||||
ctx,
|
||||
keyAgg,
|
||||
certificateCrypto,
|
||||
after,
|
||||
),
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *Commands) GenerateSAMLResponseCertificate(ctx context.Context, algorithm string, caPrivateKey *rsa.PrivateKey, caCertificate []byte) error {
|
||||
now := time.Now().UTC()
|
||||
after := now.Add(c.certificateLifetime)
|
||||
randInt, err := rand.Int(rand.Reader, big.NewInt(1000))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
privateCrypto, publicCrypto, certificateCrypto, err := crypto.GenerateEncryptedKeyPairWithCertificate(c.certKeySize, c.keyAlgorithm, c.certificateAlgorithm, caPrivateKey, caCertificate, &crypto.CertificateInformations{
|
||||
SerialNumber: randInt,
|
||||
Organisation: []string{"ZITADEL"},
|
||||
CommonName: "ZITADEL SAML response",
|
||||
NotBefore: now,
|
||||
NotAfter: after,
|
||||
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
|
||||
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
keyID, err := c.idGenerator.Next()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
keyPairWriteModel := NewKeyPairWriteModel(keyID, authz.GetInstance(ctx).InstanceID())
|
||||
keyAgg := KeyPairAggregateFromWriteModel(&keyPairWriteModel.WriteModel)
|
||||
_, err = c.eventstore.Push(ctx,
|
||||
keypair.NewAddedEvent(
|
||||
ctx,
|
||||
keyAgg,
|
||||
domain.KeyUsageSAMLResponseSinging,
|
||||
algorithm,
|
||||
privateCrypto, publicCrypto,
|
||||
after, after,
|
||||
),
|
||||
keypair.NewAddedCertificateEvent(
|
||||
ctx,
|
||||
keyAgg,
|
||||
certificateCrypto,
|
||||
after,
|
||||
),
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *Commands) GenerateSAMLMetadataCertificate(ctx context.Context, algorithm string, caPrivateKey *rsa.PrivateKey, caCertificate []byte) error {
|
||||
now := time.Now().UTC()
|
||||
after := now.Add(c.certificateLifetime)
|
||||
randInt, err := rand.Int(rand.Reader, big.NewInt(1000))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
privateCrypto, publicCrypto, certificateCrypto, err := crypto.GenerateEncryptedKeyPairWithCertificate(c.certKeySize, c.keyAlgorithm, c.certificateAlgorithm, caPrivateKey, caCertificate, &crypto.CertificateInformations{
|
||||
SerialNumber: randInt,
|
||||
Organisation: []string{"ZITADEL"},
|
||||
CommonName: "ZITADEL SAML metadata",
|
||||
NotBefore: now,
|
||||
NotAfter: after,
|
||||
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
|
||||
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
keyID, err := c.idGenerator.Next()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
keyPairWriteModel := NewKeyPairWriteModel(keyID, authz.GetInstance(ctx).InstanceID())
|
||||
keyAgg := KeyPairAggregateFromWriteModel(&keyPairWriteModel.WriteModel)
|
||||
_, err = c.eventstore.Push(ctx,
|
||||
keypair.NewAddedEvent(
|
||||
ctx,
|
||||
keyAgg,
|
||||
domain.KeyUsageSAMLMetadataSigning,
|
||||
algorithm,
|
||||
privateCrypto, publicCrypto,
|
||||
after, after),
|
||||
keypair.NewAddedCertificateEvent(
|
||||
ctx,
|
||||
keyAgg,
|
||||
certificateCrypto,
|
||||
after,
|
||||
),
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
@ -13,6 +13,7 @@ type KeyPairWriteModel struct {
|
||||
Algorithm string
|
||||
PrivateKey *domain.Key
|
||||
PublicKey *domain.Key
|
||||
Certificate *domain.Key
|
||||
}
|
||||
|
||||
func NewKeyPairWriteModel(aggregateID, resourceOwner string) *KeyPairWriteModel {
|
||||
@ -42,6 +43,11 @@ func (wm *KeyPairWriteModel) Reduce() error {
|
||||
Key: e.PublicKey.Key,
|
||||
Expiry: e.PublicKey.Expiry,
|
||||
}
|
||||
case *keypair.AddedCertificateEvent:
|
||||
wm.Certificate = &domain.Key{
|
||||
Key: e.Certificate.Key,
|
||||
Expiry: e.Certificate.Expiry,
|
||||
}
|
||||
}
|
||||
}
|
||||
return wm.WriteModel.Reduce()
|
||||
@ -53,11 +59,10 @@ func (wm *KeyPairWriteModel) Query() *eventstore.SearchQueryBuilder {
|
||||
AddQuery().
|
||||
AggregateTypes(keypair.AggregateType).
|
||||
AggregateIDs(wm.AggregateID).
|
||||
EventTypes(keypair.AddedEventType).
|
||||
EventTypes(keypair.AddedEventType, keypair.AddedCertificateEventType).
|
||||
Builder()
|
||||
}
|
||||
|
||||
func KeyPairAggregateFromWriteModel(wm *eventstore.WriteModel) *eventstore.Aggregate {
|
||||
return eventstore.AggregateFromWriteModel(wm, keypair.AggregateType, keypair.AggregateVersion)
|
||||
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/zitadel/logging"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/command/preparation"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/errors"
|
||||
@ -276,9 +277,20 @@ func (c *Commands) RemoveProject(ctx context.Context, projectID, resourceOwner s
|
||||
if existingProject.State == domain.ProjectStateUnspecified || existingProject.State == domain.ProjectStateRemoved {
|
||||
return nil, caos_errs.ThrowNotFound(nil, "COMMAND-3M9sd", "Errors.Project.NotFound")
|
||||
}
|
||||
|
||||
samlEntityIDsAgg, err := c.getSAMLEntityIdsWriteModelByProjectID(ctx, projectID, resourceOwner)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
uniqueConstraints := make([]*eventstore.EventUniqueConstraint, len(samlEntityIDsAgg.EntityIDs))
|
||||
for i, entityID := range samlEntityIDsAgg.EntityIDs {
|
||||
uniqueConstraints[i] = project.NewRemoveSAMLConfigEntityIDUniqueConstraint(entityID.EntityID)
|
||||
}
|
||||
|
||||
projectAgg := ProjectAggregateFromWriteModel(&existingProject.WriteModel)
|
||||
events := []eventstore.Command{
|
||||
project.NewProjectRemovedEvent(ctx, projectAgg, existingProject.Name),
|
||||
project.NewProjectRemovedEvent(ctx, projectAgg, existingProject.Name, uniqueConstraints),
|
||||
}
|
||||
|
||||
for _, grantID := range cascadingUserGrantIDs {
|
||||
@ -309,3 +321,12 @@ func (c *Commands) getProjectWriteModelByID(ctx context.Context, projectID, reso
|
||||
}
|
||||
return projectWriteModel, nil
|
||||
}
|
||||
|
||||
func (c *Commands) getSAMLEntityIdsWriteModelByProjectID(ctx context.Context, projectID, resourceOwner string) (*SAMLEntityIDsWriteModel, error) {
|
||||
samlEntityIDsAgg := NewSAMLEntityIDsWriteModel(projectID, resourceOwner)
|
||||
err := c.eventstore.FilterToQueryReducer(ctx, samlEntityIDsAgg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return samlEntityIDsAgg, nil
|
||||
}
|
||||
|
@ -118,7 +118,13 @@ func (c *Commands) RemoveApplication(ctx context.Context, projectID, appID, reso
|
||||
}
|
||||
projectAgg := ProjectAggregateFromWriteModel(&existingApp.WriteModel)
|
||||
|
||||
pushedEvents, err := c.eventstore.Push(ctx, project.NewApplicationRemovedEvent(ctx, projectAgg, appID, existingApp.Name))
|
||||
entityID := ""
|
||||
samlWriteModel, err := c.getSAMLAppWriteModel(ctx, projectID, appID, resourceOwner)
|
||||
if err == nil && samlWriteModel.State != domain.AppStateUnspecified && samlWriteModel.State != domain.AppStateRemoved && samlWriteModel.saml {
|
||||
entityID = samlWriteModel.EntityID
|
||||
}
|
||||
|
||||
pushedEvents, err := c.eventstore.Push(ctx, project.NewApplicationRemovedEvent(ctx, projectAgg, appID, existingApp.Name, entityID))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
146
internal/command/project_application_saml.go
Normal file
@ -0,0 +1,146 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/zitadel/saml/pkg/provider/xml"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
caos_errs "github.com/zitadel/zitadel/internal/errors"
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/repository/project"
|
||||
)
|
||||
|
||||
func (c *Commands) AddSAMLApplication(ctx context.Context, application *domain.SAMLApp, resourceOwner string) (_ *domain.SAMLApp, err error) {
|
||||
if application == nil || application.AggregateID == "" {
|
||||
return nil, caos_errs.ThrowInvalidArgument(nil, "PROJECT-35Fn0", "Errors.Project.App.Invalid")
|
||||
}
|
||||
|
||||
_, err = c.getProjectByID(ctx, application.AggregateID, resourceOwner)
|
||||
if err != nil {
|
||||
return nil, caos_errs.ThrowPreconditionFailed(err, "PROJECT-3p9ss", "Errors.Project.NotFound")
|
||||
}
|
||||
|
||||
addedApplication := NewSAMLApplicationWriteModel(application.AggregateID, resourceOwner)
|
||||
projectAgg := ProjectAggregateFromWriteModel(&addedApplication.WriteModel)
|
||||
events, err := c.addSAMLApplication(ctx, projectAgg, application)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
addedApplication.AppID = application.AppID
|
||||
pushedEvents, err := c.eventstore.Push(ctx, events...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = AppendAndReduce(addedApplication, pushedEvents...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result := samlWriteModelToSAMLConfig(addedApplication)
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (c *Commands) addSAMLApplication(ctx context.Context, projectAgg *eventstore.Aggregate, samlApp *domain.SAMLApp) (events []eventstore.Command, err error) {
|
||||
|
||||
if samlApp.AppName == "" || !samlApp.IsValid() {
|
||||
return nil, caos_errs.ThrowInvalidArgument(nil, "PROJECT-1n9df", "Errors.Project.App.Invalid")
|
||||
}
|
||||
|
||||
if samlApp.Metadata == nil && samlApp.MetadataURL == "" {
|
||||
return nil, caos_errs.ThrowInvalidArgument(nil, "SAML-podix9", "Errors.Project.App.SAMLMetadataMissing")
|
||||
}
|
||||
|
||||
if samlApp.MetadataURL != "" {
|
||||
data, err := xml.ReadMetadataFromURL(c.httpClient, samlApp.MetadataURL)
|
||||
if err != nil {
|
||||
return nil, caos_errs.ThrowInvalidArgument(err, "SAML-wmqlo1", "Errors.Project.App.SAMLMetadataMissing")
|
||||
}
|
||||
samlApp.Metadata = data
|
||||
}
|
||||
|
||||
entity, err := xml.ParseMetadataXmlIntoStruct(samlApp.Metadata)
|
||||
if err != nil {
|
||||
return nil, caos_errs.ThrowInvalidArgument(err, "SAML-bquso", "Errors.Project.App.SAMLMetadataFormat")
|
||||
}
|
||||
|
||||
samlApp.AppID, err = c.idGenerator.Next()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return []eventstore.Command{
|
||||
project.NewApplicationAddedEvent(ctx, projectAgg, samlApp.AppID, samlApp.AppName),
|
||||
project.NewSAMLConfigAddedEvent(ctx,
|
||||
projectAgg,
|
||||
samlApp.AppID,
|
||||
string(entity.EntityID),
|
||||
samlApp.Metadata,
|
||||
samlApp.MetadataURL,
|
||||
),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *Commands) ChangeSAMLApplication(ctx context.Context, samlApp *domain.SAMLApp, resourceOwner string) (*domain.SAMLApp, error) {
|
||||
if !samlApp.IsValid() || samlApp.AppID == "" || samlApp.AggregateID == "" {
|
||||
return nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-5n9fs", "Errors.Project.App.SAMLConfigInvalid")
|
||||
}
|
||||
|
||||
existingSAML, err := c.getSAMLAppWriteModel(ctx, samlApp.AggregateID, samlApp.AppID, resourceOwner)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if existingSAML.State == domain.AppStateUnspecified || existingSAML.State == domain.AppStateRemoved {
|
||||
return nil, caos_errs.ThrowNotFound(nil, "COMMAND-2n8uU", "Errors.Project.App.NotExisting")
|
||||
}
|
||||
if !existingSAML.IsSAML() {
|
||||
return nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-GBr35", "Errors.Project.App.IsNotSAML")
|
||||
}
|
||||
projectAgg := ProjectAggregateFromWriteModel(&existingSAML.WriteModel)
|
||||
|
||||
if samlApp.MetadataURL != "" {
|
||||
data, err := xml.ReadMetadataFromURL(c.httpClient, samlApp.MetadataURL)
|
||||
if err != nil {
|
||||
return nil, caos_errs.ThrowInvalidArgument(err, "SAML-J3kg3", "Errors.Project.App.SAMLMetadataMissing")
|
||||
}
|
||||
samlApp.Metadata = data
|
||||
}
|
||||
|
||||
entity, err := xml.ParseMetadataXmlIntoStruct(samlApp.Metadata)
|
||||
if err != nil {
|
||||
return nil, caos_errs.ThrowInvalidArgument(err, "SAML-3fk2b", "Errors.Project.App.SAMLMetadataFormat")
|
||||
}
|
||||
|
||||
changedEvent, hasChanged, err := existingSAML.NewChangedEvent(
|
||||
ctx,
|
||||
projectAgg,
|
||||
samlApp.AppID,
|
||||
string(entity.EntityID),
|
||||
samlApp.Metadata,
|
||||
samlApp.MetadataURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !hasChanged {
|
||||
return nil, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-1m88i", "Errors.NoChangesFound")
|
||||
}
|
||||
|
||||
pushedEvents, err := c.eventstore.Push(ctx, changedEvent)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = AppendAndReduce(existingSAML, pushedEvents...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return samlWriteModelToSAMLConfig(existingSAML), nil
|
||||
}
|
||||
|
||||
func (c *Commands) getSAMLAppWriteModel(ctx context.Context, projectID, appID, resourceOwner string) (*SAMLApplicationWriteModel, error) {
|
||||
appWriteModel := NewSAMLApplicationWriteModelWithAppID(projectID, appID, resourceOwner)
|
||||
err := c.eventstore.FilterToQueryReducer(ctx, appWriteModel)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return appWriteModel, nil
|
||||
}
|
268
internal/command/project_application_saml_model.go
Normal file
@ -0,0 +1,268 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"context"
|
||||
"reflect"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/repository/project"
|
||||
)
|
||||
|
||||
type SAMLApplicationWriteModel struct {
|
||||
eventstore.WriteModel
|
||||
|
||||
AppID string
|
||||
AppName string
|
||||
EntityID string
|
||||
Metadata []byte
|
||||
MetadataURL string
|
||||
|
||||
State domain.AppState
|
||||
saml bool
|
||||
}
|
||||
|
||||
func NewSAMLApplicationWriteModelWithAppID(projectID, appID, resourceOwner string) *SAMLApplicationWriteModel {
|
||||
return &SAMLApplicationWriteModel{
|
||||
WriteModel: eventstore.WriteModel{
|
||||
AggregateID: projectID,
|
||||
ResourceOwner: resourceOwner,
|
||||
},
|
||||
AppID: appID,
|
||||
}
|
||||
}
|
||||
|
||||
func NewSAMLApplicationWriteModel(projectID, resourceOwner string) *SAMLApplicationWriteModel {
|
||||
return &SAMLApplicationWriteModel{
|
||||
WriteModel: eventstore.WriteModel{
|
||||
AggregateID: projectID,
|
||||
ResourceOwner: resourceOwner,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (wm *SAMLApplicationWriteModel) AppendEvents(events ...eventstore.Event) {
|
||||
for _, event := range events {
|
||||
switch e := event.(type) {
|
||||
case *project.ApplicationAddedEvent:
|
||||
if e.AppID != wm.AppID {
|
||||
continue
|
||||
}
|
||||
wm.WriteModel.AppendEvents(e)
|
||||
case *project.ApplicationChangedEvent:
|
||||
if e.AppID != wm.AppID {
|
||||
continue
|
||||
}
|
||||
wm.WriteModel.AppendEvents(e)
|
||||
case *project.ApplicationDeactivatedEvent:
|
||||
if e.AppID != wm.AppID {
|
||||
continue
|
||||
}
|
||||
wm.WriteModel.AppendEvents(e)
|
||||
case *project.ApplicationReactivatedEvent:
|
||||
if e.AppID != wm.AppID {
|
||||
continue
|
||||
}
|
||||
wm.WriteModel.AppendEvents(e)
|
||||
case *project.ApplicationRemovedEvent:
|
||||
if e.AppID != wm.AppID {
|
||||
continue
|
||||
}
|
||||
wm.WriteModel.AppendEvents(e)
|
||||
case *project.SAMLConfigAddedEvent:
|
||||
if e.AppID != wm.AppID {
|
||||
continue
|
||||
}
|
||||
wm.WriteModel.AppendEvents(e)
|
||||
case *project.SAMLConfigChangedEvent:
|
||||
if e.AppID != wm.AppID {
|
||||
continue
|
||||
}
|
||||
wm.WriteModel.AppendEvents(e)
|
||||
case *project.ProjectRemovedEvent:
|
||||
wm.WriteModel.AppendEvents(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (wm *SAMLApplicationWriteModel) Reduce() error {
|
||||
for _, event := range wm.Events {
|
||||
switch e := event.(type) {
|
||||
case *project.ApplicationAddedEvent:
|
||||
wm.AppName = e.Name
|
||||
wm.State = domain.AppStateActive
|
||||
case *project.ApplicationChangedEvent:
|
||||
wm.AppName = e.Name
|
||||
case *project.ApplicationDeactivatedEvent:
|
||||
if wm.State == domain.AppStateRemoved {
|
||||
continue
|
||||
}
|
||||
wm.State = domain.AppStateInactive
|
||||
case *project.ApplicationReactivatedEvent:
|
||||
if wm.State == domain.AppStateRemoved {
|
||||
continue
|
||||
}
|
||||
wm.State = domain.AppStateActive
|
||||
case *project.ApplicationRemovedEvent:
|
||||
wm.State = domain.AppStateRemoved
|
||||
case *project.SAMLConfigAddedEvent:
|
||||
wm.appendAddSAMLEvent(e)
|
||||
case *project.SAMLConfigChangedEvent:
|
||||
wm.appendChangeSAMLEvent(e)
|
||||
case *project.ProjectRemovedEvent:
|
||||
wm.State = domain.AppStateRemoved
|
||||
}
|
||||
}
|
||||
return wm.WriteModel.Reduce()
|
||||
}
|
||||
|
||||
func (wm *SAMLApplicationWriteModel) appendAddSAMLEvent(e *project.SAMLConfigAddedEvent) {
|
||||
wm.saml = true
|
||||
wm.Metadata = e.Metadata
|
||||
wm.MetadataURL = e.MetadataURL
|
||||
wm.EntityID = e.EntityID
|
||||
}
|
||||
|
||||
func (wm *SAMLApplicationWriteModel) appendChangeSAMLEvent(e *project.SAMLConfigChangedEvent) {
|
||||
wm.saml = true
|
||||
if e.Metadata != nil {
|
||||
wm.Metadata = e.Metadata
|
||||
}
|
||||
if e.MetadataURL != nil {
|
||||
wm.MetadataURL = *e.MetadataURL
|
||||
}
|
||||
if e.EntityID != "" {
|
||||
wm.EntityID = e.EntityID
|
||||
}
|
||||
}
|
||||
|
||||
func (wm *SAMLApplicationWriteModel) Query() *eventstore.SearchQueryBuilder {
|
||||
return eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent).
|
||||
ResourceOwner(wm.ResourceOwner).
|
||||
AddQuery().
|
||||
AggregateTypes(project.AggregateType).
|
||||
AggregateIDs(wm.AggregateID).
|
||||
EventTypes(
|
||||
project.ApplicationAddedType,
|
||||
project.ApplicationChangedType,
|
||||
project.ApplicationDeactivatedType,
|
||||
project.ApplicationReactivatedType,
|
||||
project.ApplicationRemovedType,
|
||||
project.SAMLConfigAddedType,
|
||||
project.SAMLConfigChangedType,
|
||||
project.ProjectRemovedType).
|
||||
Builder()
|
||||
}
|
||||
|
||||
func (wm *SAMLApplicationWriteModel) NewChangedEvent(
|
||||
ctx context.Context,
|
||||
aggregate *eventstore.Aggregate,
|
||||
appID string,
|
||||
entityID string,
|
||||
metadata []byte,
|
||||
metadataURL string,
|
||||
) (*project.SAMLConfigChangedEvent, bool, error) {
|
||||
changes := make([]project.SAMLConfigChanges, 0)
|
||||
var err error
|
||||
if !reflect.DeepEqual(wm.Metadata, metadata) {
|
||||
changes = append(changes, project.ChangeMetadata(metadata))
|
||||
}
|
||||
if wm.MetadataURL != metadataURL {
|
||||
changes = append(changes, project.ChangeMetadataURL(metadataURL))
|
||||
}
|
||||
if wm.EntityID != entityID {
|
||||
changes = append(changes, project.ChangeEntityID(entityID))
|
||||
}
|
||||
|
||||
if len(changes) == 0 {
|
||||
return nil, false, nil
|
||||
}
|
||||
changeEvent, err := project.NewSAMLConfigChangedEvent(ctx, aggregate, appID, wm.EntityID, changes)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
return changeEvent, true, nil
|
||||
}
|
||||
|
||||
func (wm *SAMLApplicationWriteModel) IsSAML() bool {
|
||||
return wm.saml
|
||||
}
|
||||
|
||||
type AppIDToEntityID struct {
|
||||
AppID string
|
||||
EntityID string
|
||||
}
|
||||
|
||||
type SAMLEntityIDsWriteModel struct {
|
||||
eventstore.WriteModel
|
||||
|
||||
EntityIDs []*AppIDToEntityID
|
||||
}
|
||||
|
||||
func NewSAMLEntityIDsWriteModel(projectID, resourceOwner string) *SAMLEntityIDsWriteModel {
|
||||
return &SAMLEntityIDsWriteModel{
|
||||
WriteModel: eventstore.WriteModel{
|
||||
AggregateID: projectID,
|
||||
ResourceOwner: resourceOwner,
|
||||
},
|
||||
EntityIDs: []*AppIDToEntityID{},
|
||||
}
|
||||
}
|
||||
|
||||
func (wm *SAMLEntityIDsWriteModel) Query() *eventstore.SearchQueryBuilder {
|
||||
return eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent).
|
||||
ResourceOwner(wm.ResourceOwner).
|
||||
AddQuery().
|
||||
AggregateTypes(project.AggregateType).
|
||||
AggregateIDs(wm.AggregateID).
|
||||
EventTypes(
|
||||
project.ApplicationRemovedType,
|
||||
project.SAMLConfigAddedType,
|
||||
project.SAMLConfigChangedType).
|
||||
Builder()
|
||||
}
|
||||
|
||||
func (wm *SAMLEntityIDsWriteModel) AppendEvents(events ...eventstore.Event) {
|
||||
for _, event := range events {
|
||||
switch e := event.(type) {
|
||||
case *project.ApplicationRemovedEvent:
|
||||
wm.WriteModel.AppendEvents(e)
|
||||
case *project.SAMLConfigAddedEvent:
|
||||
wm.WriteModel.AppendEvents(e)
|
||||
case *project.SAMLConfigChangedEvent:
|
||||
if e.EntityID != "" {
|
||||
wm.WriteModel.AppendEvents(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (wm *SAMLEntityIDsWriteModel) Reduce() error {
|
||||
for _, event := range wm.Events {
|
||||
switch e := event.(type) {
|
||||
case *project.ApplicationRemovedEvent:
|
||||
removeAppIDFromEntityIDs(wm.EntityIDs, e.AppID)
|
||||
case *project.SAMLConfigAddedEvent:
|
||||
wm.EntityIDs = append(wm.EntityIDs, &AppIDToEntityID{AppID: e.AppID, EntityID: e.EntityID})
|
||||
case *project.SAMLConfigChangedEvent:
|
||||
for i := range wm.EntityIDs {
|
||||
item := wm.EntityIDs[i]
|
||||
if e.AppID == item.AppID && e.EntityID != "" {
|
||||
item.EntityID = e.EntityID
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return wm.WriteModel.Reduce()
|
||||
}
|
||||
|
||||
func removeAppIDFromEntityIDs(items []*AppIDToEntityID, appID string) []*AppIDToEntityID {
|
||||
for i := len(items) - 1; i >= 0; i-- {
|
||||
if items[i].AppID == appID {
|
||||
items[i] = items[len(items)-1]
|
||||
items[len(items)-1] = nil
|
||||
items = items[:len(items)-1]
|
||||
}
|
||||
}
|
||||
return items
|
||||
}
|
776
internal/command/project_application_saml_test.go
Normal file
@ -0,0 +1,776 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/errors"
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/eventstore/repository"
|
||||
"github.com/zitadel/zitadel/internal/eventstore/v1/models"
|
||||
"github.com/zitadel/zitadel/internal/id"
|
||||
id_mock "github.com/zitadel/zitadel/internal/id/mock"
|
||||
"github.com/zitadel/zitadel/internal/repository/project"
|
||||
)
|
||||
|
||||
var testMetadata = []byte(`<?xml version="1.0"?>
|
||||
<md:EntityDescriptor xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata"
|
||||
validUntil="2022-08-26T14:08:16Z"
|
||||
cacheDuration="PT604800S"
|
||||
entityID="https://test.com/saml/metadata">
|
||||
<md:SPSSODescriptor AuthnRequestsSigned="false" WantAssertionsSigned="false" protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
|
||||
<md:NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified</md:NameIDFormat>
|
||||
<md:AssertionConsumerService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
|
||||
Location="https://test.com/saml/acs"
|
||||
index="1" />
|
||||
|
||||
</md:SPSSODescriptor>
|
||||
</md:EntityDescriptor>
|
||||
`)
|
||||
var testMetadataChangedEntityID = []byte(`<?xml version="1.0"?>
|
||||
<md:EntityDescriptor xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata"
|
||||
validUntil="2022-08-26T14:08:16Z"
|
||||
cacheDuration="PT604800S"
|
||||
entityID="https://test2.com/saml/metadata">
|
||||
<md:SPSSODescriptor AuthnRequestsSigned="false" WantAssertionsSigned="false" protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
|
||||
<md:NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified</md:NameIDFormat>
|
||||
<md:AssertionConsumerService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
|
||||
Location="https://test.com/saml/acs"
|
||||
index="1" />
|
||||
|
||||
</md:SPSSODescriptor>
|
||||
</md:EntityDescriptor>
|
||||
`)
|
||||
|
||||
func TestCommandSide_AddSAMLApplication(t *testing.T) {
|
||||
type fields struct {
|
||||
eventstore *eventstore.Eventstore
|
||||
idGenerator id.Generator
|
||||
httpClient *http.Client
|
||||
}
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
samlApp *domain.SAMLApp
|
||||
resourceOwner string
|
||||
}
|
||||
type res struct {
|
||||
want *domain.SAMLApp
|
||||
err func(error) bool
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
res res
|
||||
}{
|
||||
{
|
||||
name: "no aggregate id, invalid argument error",
|
||||
fields: fields{
|
||||
eventstore: eventstoreExpect(
|
||||
t,
|
||||
),
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
samlApp: &domain.SAMLApp{},
|
||||
resourceOwner: "org1",
|
||||
},
|
||||
res: res{
|
||||
err: errors.IsErrorInvalidArgument,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "project not existing, not found error",
|
||||
fields: fields{
|
||||
eventstore: eventstoreExpect(
|
||||
t,
|
||||
expectFilter(),
|
||||
),
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
samlApp: &domain.SAMLApp{
|
||||
ObjectRoot: models.ObjectRoot{
|
||||
AggregateID: "project1",
|
||||
},
|
||||
AppID: "app1",
|
||||
AppName: "app",
|
||||
},
|
||||
resourceOwner: "org1",
|
||||
},
|
||||
res: res{
|
||||
err: errors.IsPreconditionFailed,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "invalid app, invalid argument error",
|
||||
fields: fields{
|
||||
eventstore: eventstoreExpect(
|
||||
t,
|
||||
expectFilter(
|
||||
eventFromEventPusher(
|
||||
project.NewProjectAddedEvent(context.Background(),
|
||||
&project.NewAggregate("project1", "org1").Aggregate,
|
||||
"project", true, true, true,
|
||||
domain.PrivateLabelingSettingUnspecified),
|
||||
),
|
||||
),
|
||||
),
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
samlApp: &domain.SAMLApp{
|
||||
ObjectRoot: models.ObjectRoot{
|
||||
AggregateID: "project1",
|
||||
},
|
||||
AppID: "app1",
|
||||
AppName: "",
|
||||
},
|
||||
resourceOwner: "org1",
|
||||
},
|
||||
res: res{
|
||||
err: errors.IsErrorInvalidArgument,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "create saml app, metadata not parsable",
|
||||
fields: fields{
|
||||
eventstore: eventstoreExpect(
|
||||
t,
|
||||
expectFilter(
|
||||
eventFromEventPusher(
|
||||
project.NewProjectAddedEvent(context.Background(),
|
||||
&project.NewAggregate("project1", "org1").Aggregate,
|
||||
"project", true, true, true,
|
||||
domain.PrivateLabelingSettingUnspecified),
|
||||
),
|
||||
),
|
||||
),
|
||||
idGenerator: id_mock.NewIDGeneratorExpectIDs(t),
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
samlApp: &domain.SAMLApp{
|
||||
ObjectRoot: models.ObjectRoot{
|
||||
AggregateID: "project1",
|
||||
},
|
||||
AppName: "app",
|
||||
EntityID: "https://test.com/saml/metadata",
|
||||
Metadata: []byte("test metadata"),
|
||||
MetadataURL: "",
|
||||
},
|
||||
resourceOwner: "org1",
|
||||
},
|
||||
res: res{
|
||||
err: errors.IsErrorInvalidArgument,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "create saml app, ok",
|
||||
fields: fields{
|
||||
eventstore: eventstoreExpect(
|
||||
t,
|
||||
expectFilter(
|
||||
eventFromEventPusher(
|
||||
project.NewProjectAddedEvent(context.Background(),
|
||||
&project.NewAggregate("project1", "org1").Aggregate,
|
||||
"project", true, true, true,
|
||||
domain.PrivateLabelingSettingUnspecified),
|
||||
),
|
||||
),
|
||||
expectPush(
|
||||
[]*repository.Event{
|
||||
eventFromEventPusher(
|
||||
project.NewApplicationAddedEvent(context.Background(),
|
||||
&project.NewAggregate("project1", "org1").Aggregate,
|
||||
"app1",
|
||||
"app",
|
||||
),
|
||||
),
|
||||
eventFromEventPusher(
|
||||
project.NewSAMLConfigAddedEvent(context.Background(),
|
||||
&project.NewAggregate("project1", "org1").Aggregate,
|
||||
"app1",
|
||||
"https://test.com/saml/metadata",
|
||||
testMetadata,
|
||||
"",
|
||||
),
|
||||
),
|
||||
},
|
||||
uniqueConstraintsFromEventConstraint(project.NewAddApplicationUniqueConstraint("app", "project1")),
|
||||
uniqueConstraintsFromEventConstraint(project.NewAddSAMLConfigEntityIDUniqueConstraint("https://test.com/saml/metadata")),
|
||||
),
|
||||
),
|
||||
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "app1"),
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
samlApp: &domain.SAMLApp{
|
||||
ObjectRoot: models.ObjectRoot{
|
||||
AggregateID: "project1",
|
||||
},
|
||||
AppName: "app",
|
||||
EntityID: "https://test.com/saml/metadata",
|
||||
Metadata: testMetadata,
|
||||
MetadataURL: "",
|
||||
},
|
||||
resourceOwner: "org1",
|
||||
},
|
||||
res: res{
|
||||
want: &domain.SAMLApp{
|
||||
ObjectRoot: models.ObjectRoot{
|
||||
AggregateID: "project1",
|
||||
ResourceOwner: "org1",
|
||||
},
|
||||
AppID: "app1",
|
||||
AppName: "app",
|
||||
EntityID: "https://test.com/saml/metadata",
|
||||
Metadata: testMetadata,
|
||||
MetadataURL: "",
|
||||
State: domain.AppStateActive,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "create saml app metadataURL, ok",
|
||||
fields: fields{
|
||||
eventstore: eventstoreExpect(
|
||||
t,
|
||||
expectFilter(
|
||||
eventFromEventPusher(
|
||||
project.NewProjectAddedEvent(context.Background(),
|
||||
&project.NewAggregate("project1", "org1").Aggregate,
|
||||
"project", true, true, true,
|
||||
domain.PrivateLabelingSettingUnspecified),
|
||||
),
|
||||
),
|
||||
expectPush(
|
||||
[]*repository.Event{
|
||||
eventFromEventPusher(
|
||||
project.NewApplicationAddedEvent(context.Background(),
|
||||
&project.NewAggregate("project1", "org1").Aggregate,
|
||||
"app1",
|
||||
"app",
|
||||
),
|
||||
),
|
||||
eventFromEventPusher(
|
||||
project.NewSAMLConfigAddedEvent(context.Background(),
|
||||
&project.NewAggregate("project1", "org1").Aggregate,
|
||||
"app1",
|
||||
"https://test.com/saml/metadata",
|
||||
testMetadata,
|
||||
"http://localhost:8080/saml/metadata",
|
||||
),
|
||||
),
|
||||
},
|
||||
uniqueConstraintsFromEventConstraint(project.NewAddApplicationUniqueConstraint("app", "project1")),
|
||||
uniqueConstraintsFromEventConstraint(project.NewAddSAMLConfigEntityIDUniqueConstraint("https://test.com/saml/metadata")),
|
||||
),
|
||||
),
|
||||
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "app1"),
|
||||
httpClient: newTestClient(200, testMetadata),
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
samlApp: &domain.SAMLApp{
|
||||
ObjectRoot: models.ObjectRoot{
|
||||
AggregateID: "project1",
|
||||
},
|
||||
AppName: "app",
|
||||
EntityID: "https://test.com/saml/metadata",
|
||||
Metadata: nil,
|
||||
MetadataURL: "http://localhost:8080/saml/metadata",
|
||||
},
|
||||
resourceOwner: "org1",
|
||||
},
|
||||
res: res{
|
||||
want: &domain.SAMLApp{
|
||||
ObjectRoot: models.ObjectRoot{
|
||||
AggregateID: "project1",
|
||||
ResourceOwner: "org1",
|
||||
},
|
||||
AppID: "app1",
|
||||
AppName: "app",
|
||||
EntityID: "https://test.com/saml/metadata",
|
||||
Metadata: testMetadata,
|
||||
MetadataURL: "http://localhost:8080/saml/metadata",
|
||||
State: domain.AppStateActive,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "create saml app metadataURL, http error",
|
||||
fields: fields{
|
||||
eventstore: eventstoreExpect(
|
||||
t,
|
||||
expectFilter(
|
||||
eventFromEventPusher(
|
||||
project.NewProjectAddedEvent(context.Background(),
|
||||
&project.NewAggregate("project1", "org1").Aggregate,
|
||||
"project", true, true, true,
|
||||
domain.PrivateLabelingSettingUnspecified),
|
||||
),
|
||||
),
|
||||
),
|
||||
idGenerator: id_mock.NewIDGeneratorExpectIDs(t),
|
||||
httpClient: newTestClient(http.StatusNotFound, nil),
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
samlApp: &domain.SAMLApp{
|
||||
ObjectRoot: models.ObjectRoot{
|
||||
AggregateID: "project1",
|
||||
},
|
||||
AppName: "app",
|
||||
EntityID: "https://test.com/saml/metadata",
|
||||
Metadata: nil,
|
||||
MetadataURL: "http://localhost:8080/saml/metadata",
|
||||
},
|
||||
resourceOwner: "org1",
|
||||
},
|
||||
res: res{
|
||||
err: errors.IsErrorInvalidArgument,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
r := &Commands{
|
||||
eventstore: tt.fields.eventstore,
|
||||
idGenerator: tt.fields.idGenerator,
|
||||
httpClient: tt.fields.httpClient,
|
||||
}
|
||||
|
||||
got, err := r.AddSAMLApplication(tt.args.ctx, tt.args.samlApp, tt.args.resourceOwner)
|
||||
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 TestCommandSide_ChangeSAMLApplication(t *testing.T) {
|
||||
type fields struct {
|
||||
eventstore *eventstore.Eventstore
|
||||
httpClient *http.Client
|
||||
}
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
samlApp *domain.SAMLApp
|
||||
resourceOwner string
|
||||
}
|
||||
type res struct {
|
||||
want *domain.SAMLApp
|
||||
err func(error) bool
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
res res
|
||||
}{
|
||||
{
|
||||
name: "invalid app, invalid argument error",
|
||||
fields: fields{
|
||||
eventstore: eventstoreExpect(
|
||||
t,
|
||||
),
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
samlApp: &domain.SAMLApp{
|
||||
ObjectRoot: models.ObjectRoot{
|
||||
AggregateID: "project1",
|
||||
},
|
||||
AppID: "app1",
|
||||
},
|
||||
resourceOwner: "org1",
|
||||
},
|
||||
res: res{
|
||||
err: errors.IsErrorInvalidArgument,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "missing appid, invalid argument error",
|
||||
fields: fields{
|
||||
eventstore: eventstoreExpect(
|
||||
t,
|
||||
),
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
samlApp: &domain.SAMLApp{
|
||||
ObjectRoot: models.ObjectRoot{
|
||||
AggregateID: "project1",
|
||||
},
|
||||
AppID: "",
|
||||
Metadata: []byte("just not empty"),
|
||||
},
|
||||
resourceOwner: "org1",
|
||||
},
|
||||
res: res{
|
||||
err: errors.IsErrorInvalidArgument,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "missing aggregateid, invalid argument error",
|
||||
fields: fields{
|
||||
eventstore: eventstoreExpect(
|
||||
t,
|
||||
),
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
samlApp: &domain.SAMLApp{
|
||||
ObjectRoot: models.ObjectRoot{
|
||||
AggregateID: "",
|
||||
},
|
||||
AppID: "appid",
|
||||
Metadata: []byte("just not empty"),
|
||||
},
|
||||
resourceOwner: "org1",
|
||||
},
|
||||
res: res{
|
||||
err: errors.IsErrorInvalidArgument,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "app not existing, not found error",
|
||||
fields: fields{
|
||||
eventstore: eventstoreExpect(
|
||||
t,
|
||||
expectFilter(),
|
||||
),
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
samlApp: &domain.SAMLApp{
|
||||
ObjectRoot: models.ObjectRoot{
|
||||
AggregateID: "project1",
|
||||
},
|
||||
AppID: "app1",
|
||||
Metadata: []byte("just not empty"),
|
||||
},
|
||||
resourceOwner: "org1",
|
||||
},
|
||||
res: res{
|
||||
err: errors.IsNotFound,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "no changes, precondition error, metadataURL",
|
||||
fields: fields{
|
||||
eventstore: eventstoreExpect(
|
||||
t,
|
||||
expectFilter(
|
||||
eventFromEventPusher(
|
||||
project.NewApplicationAddedEvent(context.Background(),
|
||||
&project.NewAggregate("project1", "org1").Aggregate,
|
||||
"app1",
|
||||
"app",
|
||||
),
|
||||
),
|
||||
eventFromEventPusher(
|
||||
project.NewSAMLConfigAddedEvent(context.Background(),
|
||||
&project.NewAggregate("project1", "org1").Aggregate,
|
||||
"app1",
|
||||
"https://test.com/saml/metadata",
|
||||
testMetadata,
|
||||
"http://localhost:8080/saml/metadata",
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
httpClient: newTestClient(http.StatusOK, testMetadata),
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
samlApp: &domain.SAMLApp{
|
||||
ObjectRoot: models.ObjectRoot{
|
||||
AggregateID: "project1",
|
||||
ResourceOwner: "org1",
|
||||
},
|
||||
AppName: "app",
|
||||
AppID: "app1",
|
||||
EntityID: "https://test.com/saml/metadata",
|
||||
Metadata: nil,
|
||||
MetadataURL: "http://localhost:8080/saml/metadata",
|
||||
},
|
||||
resourceOwner: "org1",
|
||||
},
|
||||
res: res{
|
||||
err: errors.IsPreconditionFailed,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "no changes, precondition error, metadata",
|
||||
fields: fields{
|
||||
eventstore: eventstoreExpect(
|
||||
t,
|
||||
expectFilter(
|
||||
eventFromEventPusher(
|
||||
project.NewApplicationAddedEvent(context.Background(),
|
||||
&project.NewAggregate("project1", "org1").Aggregate,
|
||||
"app1",
|
||||
"app",
|
||||
),
|
||||
),
|
||||
eventFromEventPusher(
|
||||
project.NewSAMLConfigAddedEvent(context.Background(),
|
||||
&project.NewAggregate("project1", "org1").Aggregate,
|
||||
"app1",
|
||||
"https://test.com/saml/metadata",
|
||||
testMetadata,
|
||||
"",
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
httpClient: nil,
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
samlApp: &domain.SAMLApp{
|
||||
ObjectRoot: models.ObjectRoot{
|
||||
AggregateID: "project1",
|
||||
ResourceOwner: "org1",
|
||||
},
|
||||
AppName: "app",
|
||||
AppID: "app1",
|
||||
EntityID: "https://test.com/saml/metadata",
|
||||
Metadata: testMetadata,
|
||||
MetadataURL: "",
|
||||
},
|
||||
resourceOwner: "org1",
|
||||
},
|
||||
res: res{
|
||||
err: errors.IsPreconditionFailed,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "change saml app, ok, metadataURL",
|
||||
fields: fields{
|
||||
eventstore: eventstoreExpect(
|
||||
t,
|
||||
expectFilter(
|
||||
eventFromEventPusher(
|
||||
project.NewApplicationAddedEvent(context.Background(),
|
||||
&project.NewAggregate("project1", "org1").Aggregate,
|
||||
"app1",
|
||||
"app",
|
||||
),
|
||||
),
|
||||
eventFromEventPusher(
|
||||
project.NewSAMLConfigAddedEvent(context.Background(),
|
||||
&project.NewAggregate("project1", "org1").Aggregate,
|
||||
"app1",
|
||||
"https://test.com/saml/metadata",
|
||||
testMetadata,
|
||||
"http://localhost:8080/saml/metadata",
|
||||
),
|
||||
),
|
||||
),
|
||||
expectPush(
|
||||
[]*repository.Event{
|
||||
eventFromEventPusher(
|
||||
newSAMLAppChangedEventMetadataURL(context.Background(),
|
||||
"app1",
|
||||
"project1",
|
||||
"org1",
|
||||
"https://test.com/saml/metadata",
|
||||
"https://test2.com/saml/metadata",
|
||||
testMetadataChangedEntityID,
|
||||
),
|
||||
),
|
||||
},
|
||||
uniqueConstraintsFromEventConstraint(project.NewRemoveSAMLConfigEntityIDUniqueConstraint("https://test.com/saml/metadata")),
|
||||
uniqueConstraintsFromEventConstraint(project.NewAddSAMLConfigEntityIDUniqueConstraint("https://test2.com/saml/metadata")),
|
||||
),
|
||||
),
|
||||
httpClient: newTestClient(http.StatusOK, testMetadataChangedEntityID),
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
samlApp: &domain.SAMLApp{
|
||||
ObjectRoot: models.ObjectRoot{
|
||||
AggregateID: "project1",
|
||||
ResourceOwner: "org1",
|
||||
},
|
||||
AppID: "app1",
|
||||
AppName: "app",
|
||||
EntityID: "https://test2.com/saml/metadata",
|
||||
Metadata: nil,
|
||||
MetadataURL: "http://localhost:8080/saml/metadata",
|
||||
},
|
||||
resourceOwner: "org1",
|
||||
},
|
||||
res: res{
|
||||
want: &domain.SAMLApp{
|
||||
ObjectRoot: models.ObjectRoot{
|
||||
AggregateID: "project1",
|
||||
ResourceOwner: "org1",
|
||||
},
|
||||
AppID: "app1",
|
||||
AppName: "app",
|
||||
EntityID: "https://test2.com/saml/metadata",
|
||||
Metadata: testMetadataChangedEntityID,
|
||||
MetadataURL: "http://localhost:8080/saml/metadata",
|
||||
State: domain.AppStateActive,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "change saml app, ok, metadata",
|
||||
fields: fields{
|
||||
eventstore: eventstoreExpect(
|
||||
t,
|
||||
expectFilter(
|
||||
eventFromEventPusher(
|
||||
project.NewApplicationAddedEvent(context.Background(),
|
||||
&project.NewAggregate("project1", "org1").Aggregate,
|
||||
"app1",
|
||||
"app",
|
||||
),
|
||||
),
|
||||
eventFromEventPusher(
|
||||
project.NewSAMLConfigAddedEvent(context.Background(),
|
||||
&project.NewAggregate("project1", "org1").Aggregate,
|
||||
"app1",
|
||||
"https://test.com/saml/metadata",
|
||||
testMetadata,
|
||||
"",
|
||||
),
|
||||
),
|
||||
),
|
||||
expectPush(
|
||||
[]*repository.Event{
|
||||
eventFromEventPusher(
|
||||
newSAMLAppChangedEventMetadata(context.Background(),
|
||||
"app1",
|
||||
"project1",
|
||||
"org1",
|
||||
"https://test.com/saml/metadata",
|
||||
"https://test2.com/saml/metadata",
|
||||
testMetadataChangedEntityID,
|
||||
),
|
||||
),
|
||||
},
|
||||
uniqueConstraintsFromEventConstraint(project.NewRemoveSAMLConfigEntityIDUniqueConstraint("https://test.com/saml/metadata")),
|
||||
uniqueConstraintsFromEventConstraint(project.NewAddSAMLConfigEntityIDUniqueConstraint("https://test2.com/saml/metadata")),
|
||||
),
|
||||
),
|
||||
httpClient: nil,
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
samlApp: &domain.SAMLApp{
|
||||
ObjectRoot: models.ObjectRoot{
|
||||
AggregateID: "project1",
|
||||
ResourceOwner: "org1",
|
||||
},
|
||||
AppID: "app1",
|
||||
AppName: "app",
|
||||
EntityID: "https://test2.com/saml/metadata",
|
||||
Metadata: testMetadataChangedEntityID,
|
||||
MetadataURL: "",
|
||||
},
|
||||
resourceOwner: "org1",
|
||||
},
|
||||
res: res{
|
||||
want: &domain.SAMLApp{
|
||||
ObjectRoot: models.ObjectRoot{
|
||||
AggregateID: "project1",
|
||||
ResourceOwner: "org1",
|
||||
},
|
||||
AppID: "app1",
|
||||
AppName: "app",
|
||||
EntityID: "https://test2.com/saml/metadata",
|
||||
Metadata: testMetadataChangedEntityID,
|
||||
MetadataURL: "",
|
||||
State: domain.AppStateActive,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
r := &Commands{
|
||||
eventstore: tt.fields.eventstore,
|
||||
httpClient: tt.fields.httpClient,
|
||||
}
|
||||
got, err := r.ChangeSAMLApplication(tt.args.ctx, tt.args.samlApp, tt.args.resourceOwner)
|
||||
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 newSAMLAppChangedEventMetadata(ctx context.Context, appID, projectID, resourceOwner, oldEntityID, entityID string, metadata []byte) *project.SAMLConfigChangedEvent {
|
||||
changes := []project.SAMLConfigChanges{
|
||||
project.ChangeEntityID(entityID),
|
||||
project.ChangeMetadata(metadata),
|
||||
}
|
||||
event, _ := project.NewSAMLConfigChangedEvent(ctx,
|
||||
&project.NewAggregate(projectID, resourceOwner).Aggregate,
|
||||
appID,
|
||||
oldEntityID,
|
||||
changes,
|
||||
)
|
||||
return event
|
||||
}
|
||||
|
||||
func newSAMLAppChangedEventMetadataURL(ctx context.Context, appID, projectID, resourceOwner, oldEntityID, entityID string, metadata []byte) *project.SAMLConfigChangedEvent {
|
||||
changes := []project.SAMLConfigChanges{
|
||||
project.ChangeEntityID(entityID),
|
||||
project.ChangeMetadata(metadata),
|
||||
}
|
||||
event, _ := project.NewSAMLConfigChangedEvent(ctx,
|
||||
&project.NewAggregate(projectID, resourceOwner).Aggregate,
|
||||
appID,
|
||||
oldEntityID,
|
||||
changes,
|
||||
)
|
||||
return event
|
||||
}
|
||||
|
||||
type roundTripperFunc func(*http.Request) *http.Response
|
||||
|
||||
// RoundTrip implements the http.RoundTripper interface.
|
||||
func (fn roundTripperFunc) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
return fn(req), nil
|
||||
}
|
||||
|
||||
// NewTestClient returns *http.Client with Transport replaced to avoid making real calls
|
||||
func newTestClient(httpStatus int, metadata []byte) *http.Client {
|
||||
fn := roundTripperFunc(func(req *http.Request) *http.Response {
|
||||
return &http.Response{
|
||||
StatusCode: httpStatus,
|
||||
Body: ioutil.NopCloser(bytes.NewBuffer(metadata)),
|
||||
Header: make(http.Header), //must be non-nil value
|
||||
}
|
||||
})
|
||||
return &http.Client{
|
||||
Transport: fn,
|
||||
}
|
||||
}
|
@ -5,6 +5,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
caos_errs "github.com/zitadel/zitadel/internal/errors"
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
@ -580,6 +581,58 @@ func TestCommandSide_RemoveApplication(t *testing.T) {
|
||||
err: caos_errs.IsNotFound,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "app remove, entityID, ok",
|
||||
fields: fields{
|
||||
eventstore: eventstoreExpect(
|
||||
t,
|
||||
expectFilter(
|
||||
eventFromEventPusher(project.NewApplicationAddedEvent(context.Background(),
|
||||
&project.NewAggregate("project1", "org1").Aggregate,
|
||||
"app1",
|
||||
"app",
|
||||
)),
|
||||
),
|
||||
expectFilter(
|
||||
eventFromEventPusher(project.NewApplicationAddedEvent(context.Background(),
|
||||
&project.NewAggregate("project1", "org1").Aggregate,
|
||||
"app1",
|
||||
"app",
|
||||
)),
|
||||
eventFromEventPusher(project.NewSAMLConfigAddedEvent(context.Background(),
|
||||
&project.NewAggregate("project1", "org1").Aggregate,
|
||||
"app1",
|
||||
"https://test.com/saml/metadata",
|
||||
[]byte("<?xml version=\"1.0\"?>\n<md:EntityDescriptor xmlns:md=\"urn:oasis:names:tc:SAML:2.0:metadata\"\n validUntil=\"2022-08-26T14:08:16Z\"\n cacheDuration=\"PT604800S\"\n entityID=\"https://test.com/saml/metadata\">\n <md:SPSSODescriptor AuthnRequestsSigned=\"false\" WantAssertionsSigned=\"false\" protocolSupportEnumeration=\"urn:oasis:names:tc:SAML:2.0:protocol\">\n <md:NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified</md:NameIDFormat>\n <md:AssertionConsumerService Binding=\"urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST\"\n Location=\"https://test.com/saml/acs\"\n index=\"1\" />\n \n </md:SPSSODescriptor>\n</md:EntityDescriptor>"),
|
||||
"",
|
||||
)),
|
||||
),
|
||||
expectPush(
|
||||
[]*repository.Event{
|
||||
eventFromEventPusher(project.NewApplicationRemovedEvent(context.Background(),
|
||||
&project.NewAggregate("project1", "org1").Aggregate,
|
||||
"app1",
|
||||
"app",
|
||||
"https://test.com/saml/metadata",
|
||||
)),
|
||||
}, /**/
|
||||
uniqueConstraintsFromEventConstraint(project.NewRemoveApplicationUniqueConstraint("app", "project1")),
|
||||
uniqueConstraintsFromEventConstraint(project.NewRemoveSAMLConfigEntityIDUniqueConstraint("https://test.com/saml/metadata")),
|
||||
),
|
||||
),
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
projectID: "project1",
|
||||
appID: "app1",
|
||||
resourceOwner: "org1",
|
||||
},
|
||||
res: res{
|
||||
want: &domain.ObjectDetails{
|
||||
ResourceOwner: "org1",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "app remove, ok",
|
||||
fields: fields{
|
||||
@ -592,12 +645,15 @@ func TestCommandSide_RemoveApplication(t *testing.T) {
|
||||
"app",
|
||||
)),
|
||||
),
|
||||
// app is not saml, or no saml config available
|
||||
expectFilter(),
|
||||
expectPush(
|
||||
[]*repository.Event{
|
||||
eventFromEventPusher(project.NewApplicationRemovedEvent(context.Background(),
|
||||
&project.NewAggregate("project1", "org1").Aggregate,
|
||||
"app1",
|
||||
"app",
|
||||
"",
|
||||
)),
|
||||
},
|
||||
uniqueConstraintsFromEventConstraint(project.NewRemoveApplicationUniqueConstraint("app", "project1")),
|
||||
|
@ -57,6 +57,18 @@ func oidcWriteModelToOIDCConfig(writeModel *OIDCApplicationWriteModel) *domain.O
|
||||
}
|
||||
}
|
||||
|
||||
func samlWriteModelToSAMLConfig(writeModel *SAMLApplicationWriteModel) *domain.SAMLApp {
|
||||
return &domain.SAMLApp{
|
||||
ObjectRoot: writeModelToObjectRoot(writeModel.WriteModel),
|
||||
AppID: writeModel.AppID,
|
||||
AppName: writeModel.AppName,
|
||||
State: writeModel.State,
|
||||
Metadata: writeModel.Metadata,
|
||||
MetadataURL: writeModel.MetadataURL,
|
||||
EntityID: writeModel.EntityID,
|
||||
}
|
||||
}
|
||||
|
||||
func apiWriteModelToAPIConfig(writeModel *APIApplicationWriteModel) *domain.APIApp {
|
||||
return &domain.APIApp{
|
||||
ObjectRoot: writeModelToObjectRoot(writeModel.WriteModel),
|
||||
|
@ -5,6 +5,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
caos_errs "github.com/zitadel/zitadel/internal/errors"
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
@ -50,6 +51,7 @@ func TestCommandSide_AddProjectRole(t *testing.T) {
|
||||
project.NewProjectRemovedEvent(context.Background(),
|
||||
&project.NewAggregate("project1", "org1").Aggregate,
|
||||
"projectname1",
|
||||
nil,
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -253,6 +255,7 @@ func TestCommandSide_BulkAddProjectRole(t *testing.T) {
|
||||
project.NewProjectRemovedEvent(context.Background(),
|
||||
&project.NewAggregate("project1", "org1").Aggregate,
|
||||
"projectname1",
|
||||
nil,
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -503,6 +506,7 @@ func TestCommandSide_ChangeProjectRole(t *testing.T) {
|
||||
project.NewProjectRemovedEvent(context.Background(),
|
||||
&project.NewAggregate("project1", "org1").Aggregate,
|
||||
"projectname1",
|
||||
nil,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -269,7 +269,8 @@ func TestCommandSide_ChangeProject(t *testing.T) {
|
||||
eventFromEventPusher(
|
||||
project.NewProjectRemovedEvent(context.Background(),
|
||||
&project.NewAggregate("project1", "org1").Aggregate,
|
||||
"project"),
|
||||
"project",
|
||||
nil),
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -542,7 +543,8 @@ func TestCommandSide_DeactivateProject(t *testing.T) {
|
||||
eventFromEventPusher(
|
||||
project.NewProjectRemovedEvent(context.Background(),
|
||||
&project.NewAggregate("project1", "org1").Aggregate,
|
||||
"project"),
|
||||
"project",
|
||||
nil),
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -721,7 +723,8 @@ func TestCommandSide_ReactivateProject(t *testing.T) {
|
||||
eventFromEventPusher(
|
||||
project.NewProjectRemovedEvent(context.Background(),
|
||||
&project.NewAggregate("project1", "org1").Aggregate,
|
||||
"project"),
|
||||
"project",
|
||||
nil),
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -900,7 +903,8 @@ func TestCommandSide_RemoveProject(t *testing.T) {
|
||||
eventFromEventPusher(
|
||||
project.NewProjectRemovedEvent(context.Background(),
|
||||
&project.NewAggregate("project1", "org1").Aggregate,
|
||||
"project"),
|
||||
"project",
|
||||
nil),
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -915,7 +919,7 @@ func TestCommandSide_RemoveProject(t *testing.T) {
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "project remove, ok",
|
||||
name: "project remove, without entityConstraints, ok",
|
||||
fields: fields{
|
||||
eventstore: eventstoreExpect(
|
||||
t,
|
||||
@ -927,12 +931,15 @@ func TestCommandSide_RemoveProject(t *testing.T) {
|
||||
domain.PrivateLabelingSettingAllowLoginUserResourceOwnerPolicy),
|
||||
),
|
||||
),
|
||||
// no saml application events
|
||||
expectFilter(),
|
||||
expectPush(
|
||||
[]*repository.Event{
|
||||
eventFromEventPusher(
|
||||
project.NewProjectRemovedEvent(context.Background(),
|
||||
&project.NewAggregate("project1", "org1").Aggregate,
|
||||
"project"),
|
||||
"project",
|
||||
nil),
|
||||
),
|
||||
},
|
||||
uniqueConstraintsFromEventConstraint(project.NewRemoveProjectNameUniqueConstraint("project", "org1")),
|
||||
@ -950,6 +957,150 @@ func TestCommandSide_RemoveProject(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "project remove, with entityConstraints, ok",
|
||||
fields: fields{
|
||||
eventstore: eventstoreExpect(
|
||||
t,
|
||||
expectFilter(
|
||||
eventFromEventPusher(
|
||||
project.NewProjectAddedEvent(context.Background(),
|
||||
&project.NewAggregate("project1", "org1").Aggregate,
|
||||
"project", true, true, true,
|
||||
domain.PrivateLabelingSettingAllowLoginUserResourceOwnerPolicy),
|
||||
),
|
||||
),
|
||||
expectFilter(
|
||||
eventFromEventPusher(project.NewApplicationAddedEvent(context.Background(),
|
||||
&project.NewAggregate("project1", "org1").Aggregate,
|
||||
"app1",
|
||||
"app",
|
||||
)),
|
||||
eventFromEventPusher(
|
||||
project.NewSAMLConfigAddedEvent(context.Background(),
|
||||
&project.NewAggregate("project1", "org1").Aggregate,
|
||||
"app1",
|
||||
"https://test.com/saml/metadata",
|
||||
[]byte("<?xml version=\"1.0\"?>\n<md:EntityDescriptor xmlns:md=\"urn:oasis:names:tc:SAML:2.0:metadata\"\n validUntil=\"2022-08-26T14:08:16Z\"\n cacheDuration=\"PT604800S\"\n entityID=\"https://test.com/saml/metadata\">\n <md:SPSSODescriptor AuthnRequestsSigned=\"false\" WantAssertionsSigned=\"false\" protocolSupportEnumeration=\"urn:oasis:names:tc:SAML:2.0:protocol\">\n <md:NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified</md:NameIDFormat>\n <md:AssertionConsumerService Binding=\"urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST\"\n Location=\"https://test.com/saml/acs\"\n index=\"1\" />\n \n </md:SPSSODescriptor>\n</md:EntityDescriptor>"),
|
||||
"http://localhost:8080/saml/metadata",
|
||||
),
|
||||
),
|
||||
),
|
||||
expectPush(
|
||||
[]*repository.Event{
|
||||
eventFromEventPusher(
|
||||
project.NewProjectRemovedEvent(context.Background(),
|
||||
&project.NewAggregate("project1", "org1").Aggregate,
|
||||
"project",
|
||||
[]*eventstore.EventUniqueConstraint{
|
||||
project.NewRemoveSAMLConfigEntityIDUniqueConstraint("https://test.com/saml/metadata"),
|
||||
}),
|
||||
),
|
||||
},
|
||||
uniqueConstraintsFromEventConstraint(project.NewRemoveProjectNameUniqueConstraint("project", "org1")),
|
||||
uniqueConstraintsFromEventConstraint(project.NewRemoveSAMLConfigEntityIDUniqueConstraint("https://test.com/saml/metadata")),
|
||||
),
|
||||
),
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
projectID: "project1",
|
||||
resourceOwner: "org1",
|
||||
},
|
||||
res: res{
|
||||
want: &domain.ObjectDetails{
|
||||
ResourceOwner: "org1",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "project remove, with multiple entityConstraints, ok",
|
||||
fields: fields{
|
||||
eventstore: eventstoreExpect(
|
||||
t,
|
||||
expectFilter(
|
||||
eventFromEventPusher(
|
||||
project.NewProjectAddedEvent(context.Background(),
|
||||
&project.NewAggregate("project1", "org1").Aggregate,
|
||||
"project", true, true, true,
|
||||
domain.PrivateLabelingSettingAllowLoginUserResourceOwnerPolicy),
|
||||
),
|
||||
),
|
||||
expectFilter(
|
||||
eventFromEventPusher(project.NewApplicationAddedEvent(context.Background(),
|
||||
&project.NewAggregate("project1", "org1").Aggregate,
|
||||
"app1",
|
||||
"app",
|
||||
)),
|
||||
eventFromEventPusher(
|
||||
project.NewSAMLConfigAddedEvent(context.Background(),
|
||||
&project.NewAggregate("project1", "org1").Aggregate,
|
||||
"app1",
|
||||
"https://test1.com/saml/metadata",
|
||||
[]byte("<?xml version=\"1.0\"?>\n<md:EntityDescriptor xmlns:md=\"urn:oasis:names:tc:SAML:2.0:metadata\"\n validUntil=\"2022-08-26T14:08:16Z\"\n cacheDuration=\"PT604800S\"\n entityID=\"https://test.com/saml/metadata\">\n <md:SPSSODescriptor AuthnRequestsSigned=\"false\" WantAssertionsSigned=\"false\" protocolSupportEnumeration=\"urn:oasis:names:tc:SAML:2.0:protocol\">\n <md:NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified</md:NameIDFormat>\n <md:AssertionConsumerService Binding=\"urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST\"\n Location=\"https://test.com/saml/acs\"\n index=\"1\" />\n \n </md:SPSSODescriptor>\n</md:EntityDescriptor>"),
|
||||
"",
|
||||
),
|
||||
),
|
||||
eventFromEventPusher(project.NewApplicationAddedEvent(context.Background(),
|
||||
&project.NewAggregate("project1", "org1").Aggregate,
|
||||
"app2",
|
||||
"app",
|
||||
)),
|
||||
eventFromEventPusher(
|
||||
project.NewSAMLConfigAddedEvent(context.Background(),
|
||||
&project.NewAggregate("project1", "org1").Aggregate,
|
||||
"app2",
|
||||
"https://test2.com/saml/metadata",
|
||||
[]byte("<?xml version=\"1.0\"?>\n<md:EntityDescriptor xmlns:md=\"urn:oasis:names:tc:SAML:2.0:metadata\"\n validUntil=\"2022-08-26T14:08:16Z\"\n cacheDuration=\"PT604800S\"\n entityID=\"https://test.com/saml/metadata\">\n <md:SPSSODescriptor AuthnRequestsSigned=\"false\" WantAssertionsSigned=\"false\" protocolSupportEnumeration=\"urn:oasis:names:tc:SAML:2.0:protocol\">\n <md:NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified</md:NameIDFormat>\n <md:AssertionConsumerService Binding=\"urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST\"\n Location=\"https://test.com/saml/acs\"\n index=\"1\" />\n \n </md:SPSSODescriptor>\n</md:EntityDescriptor>"),
|
||||
"",
|
||||
),
|
||||
),
|
||||
eventFromEventPusher(project.NewApplicationAddedEvent(context.Background(),
|
||||
&project.NewAggregate("project1", "org1").Aggregate,
|
||||
"app3",
|
||||
"app",
|
||||
)),
|
||||
eventFromEventPusher(
|
||||
project.NewSAMLConfigAddedEvent(context.Background(),
|
||||
&project.NewAggregate("project1", "org1").Aggregate,
|
||||
"app3",
|
||||
"https://test3.com/saml/metadata",
|
||||
[]byte("<?xml version=\"1.0\"?>\n<md:EntityDescriptor xmlns:md=\"urn:oasis:names:tc:SAML:2.0:metadata\"\n validUntil=\"2022-08-26T14:08:16Z\"\n cacheDuration=\"PT604800S\"\n entityID=\"https://test.com/saml/metadata\">\n <md:SPSSODescriptor AuthnRequestsSigned=\"false\" WantAssertionsSigned=\"false\" protocolSupportEnumeration=\"urn:oasis:names:tc:SAML:2.0:protocol\">\n <md:NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified</md:NameIDFormat>\n <md:AssertionConsumerService Binding=\"urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST\"\n Location=\"https://test.com/saml/acs\"\n index=\"1\" />\n \n </md:SPSSODescriptor>\n</md:EntityDescriptor>"),
|
||||
"",
|
||||
),
|
||||
),
|
||||
),
|
||||
expectPush(
|
||||
[]*repository.Event{
|
||||
eventFromEventPusher(
|
||||
project.NewProjectRemovedEvent(context.Background(),
|
||||
&project.NewAggregate("project1", "org1").Aggregate,
|
||||
"project",
|
||||
[]*eventstore.EventUniqueConstraint{
|
||||
project.NewRemoveSAMLConfigEntityIDUniqueConstraint("https://test1.com/saml/metadata"),
|
||||
project.NewRemoveSAMLConfigEntityIDUniqueConstraint("https://test2.com/saml/metadata"),
|
||||
project.NewRemoveSAMLConfigEntityIDUniqueConstraint("https://test3.com/saml/metadata"),
|
||||
}),
|
||||
),
|
||||
},
|
||||
uniqueConstraintsFromEventConstraint(project.NewRemoveProjectNameUniqueConstraint("project", "org1")),
|
||||
uniqueConstraintsFromEventConstraint(project.NewRemoveSAMLConfigEntityIDUniqueConstraint("https://test1.com/saml/metadata")),
|
||||
uniqueConstraintsFromEventConstraint(project.NewRemoveSAMLConfigEntityIDUniqueConstraint("https://test2.com/saml/metadata")),
|
||||
uniqueConstraintsFromEventConstraint(project.NewRemoveSAMLConfigEntityIDUniqueConstraint("https://test3.com/saml/metadata")),
|
||||
),
|
||||
),
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
projectID: "project1",
|
||||
resourceOwner: "org1",
|
||||
},
|
||||
res: res{
|
||||
want: &domain.ObjectDetails{
|
||||
ResourceOwner: "org1",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
|
@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
|
||||
"github.com/zitadel/logging"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/repository/idpconfig"
|
||||
@ -92,6 +93,10 @@ func (rm *UniqueConstraintReadModel) Reduce() error {
|
||||
rm.addUniqueConstraint(e.Aggregate().ID, e.AppID, project.NewAddApplicationUniqueConstraint(e.Name, e.Aggregate().ID))
|
||||
case *project.ApplicationChangedEvent:
|
||||
rm.changeUniqueConstraint(e.Aggregate().ID, e.AppID, project.NewAddApplicationUniqueConstraint(e.Name, e.Aggregate().ID))
|
||||
case *project.SAMLConfigAddedEvent:
|
||||
rm.addUniqueConstraint(e.Aggregate().ID, e.AppID, project.NewAddSAMLConfigEntityIDUniqueConstraint(e.EntityID))
|
||||
case *project.SAMLConfigChangedEvent:
|
||||
rm.addUniqueConstraint(e.Aggregate().ID, e.AppID, project.NewRemoveSAMLConfigEntityIDUniqueConstraint(e.EntityID))
|
||||
case *project.ApplicationRemovedEvent:
|
||||
rm.removeUniqueConstraint(e.Aggregate().ID, e.AppID, project.UniqueAppNameType)
|
||||
case *project.GrantAddedEvent:
|
||||
|
@ -133,6 +133,7 @@ func TestCommandSide_AddUserGrant(t *testing.T) {
|
||||
project.NewProjectRemovedEvent(context.Background(),
|
||||
&project.NewAggregate("project1", "org1").Aggregate,
|
||||
"projectname1",
|
||||
nil,
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -819,6 +820,7 @@ func TestCommandSide_ChangeUserGrant(t *testing.T) {
|
||||
project.NewProjectRemovedEvent(context.Background(),
|
||||
&project.NewAggregate("project1", "org1").Aggregate,
|
||||
"projectname1",
|
||||
nil,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -40,4 +40,6 @@ type KeyConfig struct {
|
||||
Size int
|
||||
PrivateKeyLifetime time.Duration
|
||||
PublicKeyLifetime time.Duration
|
||||
CertificateSize int
|
||||
CertificateLifetime time.Duration
|
||||
}
|
||||
|
@ -1,11 +1,16 @@
|
||||
package crypto
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"time"
|
||||
)
|
||||
|
||||
func GenerateKeyPair(bits int) (*rsa.PrivateKey, *rsa.PublicKey, error) {
|
||||
@ -24,6 +29,104 @@ func GenerateEncryptedKeyPair(bits int, alg EncryptionAlgorithm) (*CryptoValue,
|
||||
return EncryptKeys(privateKey, publicKey, alg)
|
||||
}
|
||||
|
||||
type CertificateInformations struct {
|
||||
SerialNumber *big.Int
|
||||
Organisation []string
|
||||
CommonName string
|
||||
NotBefore time.Time
|
||||
NotAfter time.Time
|
||||
KeyUsage x509.KeyUsage
|
||||
ExtKeyUsage []x509.ExtKeyUsage
|
||||
}
|
||||
|
||||
func GenerateEncryptedKeyPairWithCACertificate(bits int, keyAlg, certAlg EncryptionAlgorithm, informations *CertificateInformations) (*CryptoValue, *CryptoValue, *CryptoValue, error) {
|
||||
privateKey, publicKey, cert, err := GenerateCACertificate(bits, informations)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
encryptPriv, encryptPub, encryptCaCert, err := EncryptKeysAndCert(privateKey, publicKey, cert, keyAlg, certAlg)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
return encryptPriv, encryptPub, encryptCaCert, nil
|
||||
}
|
||||
|
||||
func GenerateEncryptedKeyPairWithCertificate(bits int, keyAlg, certAlg EncryptionAlgorithm, caPrivateKey *rsa.PrivateKey, caCertificate []byte, informations *CertificateInformations) (*CryptoValue, *CryptoValue, *CryptoValue, error) {
|
||||
privateKey, publicKey, cert, err := GenerateCertificate(bits, caPrivateKey, caCertificate, informations)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
encryptPriv, encryptPub, encryptCaCert, err := EncryptKeysAndCert(privateKey, publicKey, cert, keyAlg, certAlg)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
return encryptPriv, encryptPub, encryptCaCert, nil
|
||||
}
|
||||
|
||||
func GenerateCACertificate(bits int, informations *CertificateInformations) (*rsa.PrivateKey, *rsa.PublicKey, []byte, error) {
|
||||
return generateCertificate(bits, nil, nil, informations)
|
||||
}
|
||||
|
||||
func GenerateCertificate(bits int, caPrivateKey *rsa.PrivateKey, ca []byte, informations *CertificateInformations) (*rsa.PrivateKey, *rsa.PublicKey, []byte, error) {
|
||||
return generateCertificate(bits, caPrivateKey, ca, informations)
|
||||
}
|
||||
|
||||
func generateCertificate(bits int, caPrivateKey *rsa.PrivateKey, ca []byte, informations *CertificateInformations) (*rsa.PrivateKey, *rsa.PublicKey, []byte, error) {
|
||||
notBefore := time.Now()
|
||||
if !informations.NotBefore.IsZero() {
|
||||
notBefore = informations.NotBefore
|
||||
}
|
||||
cert := &x509.Certificate{
|
||||
SerialNumber: informations.SerialNumber,
|
||||
Subject: pkix.Name{
|
||||
CommonName: informations.CommonName,
|
||||
Organization: informations.Organisation,
|
||||
},
|
||||
NotBefore: notBefore,
|
||||
NotAfter: informations.NotAfter,
|
||||
KeyUsage: informations.KeyUsage,
|
||||
ExtKeyUsage: informations.ExtKeyUsage,
|
||||
}
|
||||
|
||||
certPrivKey, err := rsa.GenerateKey(rand.Reader, bits)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
|
||||
certBytes := make([]byte, 0)
|
||||
if ca == nil {
|
||||
cert.IsCA = true
|
||||
cert.BasicConstraintsValid = true
|
||||
|
||||
certBytes, err = x509.CreateCertificate(rand.Reader, cert, cert, &certPrivKey.PublicKey, certPrivKey)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
} else {
|
||||
caCert, err := x509.ParseCertificate(ca)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
|
||||
certBytes, err = x509.CreateCertificate(rand.Reader, cert, caCert, &certPrivKey.PublicKey, caPrivateKey)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
}
|
||||
|
||||
x509Cert, err := x509.ParseCertificate(certBytes)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
|
||||
certPem, err := CertificateToBytes(x509Cert)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
|
||||
return certPrivKey, &certPrivKey.PublicKey, certPem, nil
|
||||
}
|
||||
|
||||
func PrivateKeyToBytes(priv *rsa.PrivateKey) []byte {
|
||||
return pem.EncodeToMemory(
|
||||
&pem.Block{
|
||||
@ -101,3 +204,34 @@ func EncryptKeys(privateKey *rsa.PrivateKey, publicKey *rsa.PublicKey, alg Encry
|
||||
}
|
||||
return encryptedPrivateKey, encryptedPublicKey, nil
|
||||
}
|
||||
|
||||
func CertificateToBytes(cert *x509.Certificate) ([]byte, error) {
|
||||
certPem := new(bytes.Buffer)
|
||||
if err := pem.Encode(certPem, &pem.Block{
|
||||
Type: "CERTIFICATE",
|
||||
Bytes: cert.Raw,
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return certPem.Bytes(), nil
|
||||
}
|
||||
|
||||
func BytesToCertificate(data []byte) ([]byte, error) {
|
||||
block, _ := pem.Decode(data)
|
||||
if block == nil || block.Type != "CERTIFICATE" {
|
||||
return nil, fmt.Errorf("failed to decode PEM block containing public key")
|
||||
}
|
||||
return block.Bytes, nil
|
||||
}
|
||||
|
||||
func EncryptKeysAndCert(privateKey *rsa.PrivateKey, publicKey *rsa.PublicKey, cert []byte, keyAlg, certAlg EncryptionAlgorithm) (*CryptoValue, *CryptoValue, *CryptoValue, error) {
|
||||
encryptedPrivateKey, encryptedPublicKey, err := EncryptKeys(privateKey, publicKey, keyAlg)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
encryptedCertificate, err := Encrypt(cert, certAlg)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
return encryptedPrivateKey, encryptedPublicKey, encryptedCertificate, nil
|
||||
}
|
||||
|
40
internal/domain/application_saml.go
Normal file
@ -0,0 +1,40 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"github.com/zitadel/zitadel/internal/eventstore/v1/models"
|
||||
)
|
||||
|
||||
type SAMLApp struct {
|
||||
models.ObjectRoot
|
||||
|
||||
AppID string
|
||||
AppName string
|
||||
EntityID string
|
||||
Metadata []byte
|
||||
MetadataURL string
|
||||
|
||||
State AppState
|
||||
}
|
||||
|
||||
func (a *SAMLApp) GetApplicationName() string {
|
||||
return a.AppName
|
||||
}
|
||||
|
||||
func (a *SAMLApp) GetState() AppState {
|
||||
return a.State
|
||||
}
|
||||
|
||||
func (a *SAMLApp) GetMetadata() []byte {
|
||||
return a.Metadata
|
||||
}
|
||||
|
||||
func (a *SAMLApp) GetMetadataURL() string {
|
||||
return a.MetadataURL
|
||||
}
|
||||
|
||||
func (a *SAMLApp) IsValid() bool {
|
||||
if a.MetadataURL == "" && a.Metadata == nil {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|