diff --git a/.github/ISSUE_TEMPLATE/BUG_REPORT.yaml b/.github/ISSUE_TEMPLATE/BUG_REPORT.yaml index f45c602c87..a5a562ef71 100644 --- a/.github/ISSUE_TEMPLATE/BUG_REPORT.yaml +++ b/.github/ISSUE_TEMPLATE/BUG_REPORT.yaml @@ -1,7 +1,7 @@ name: Bug Report description: "Create a bug report to help us improve ZITADEL. Click [here](https://github.com/zitadel/zitadel/blob/main/CONTRIBUTING.md#product-management) to see how we process your issue." title: "[Bug]: " -labels: ["type: bug", "state: triage"] +labels: ["bug"] body: - type: markdown attributes: @@ -27,11 +27,16 @@ body: - Self-hosted validations: required: true -- type: textarea - id: description +- type: input + id: version attributes: - label: Describe the bug - description: A clear and concise description of what the bug is. + label: Version + description: Which version of ZITADEL are you using. +- type: textarea + id: impact + attributes: + label: Describe the problem caused by this bug + description: A clear and concise description of the problem you have and what the bug is. validations: required: true - type: textarea @@ -57,11 +62,6 @@ body: attributes: label: Expected behavior description: A clear and concise description of what you expected to happen. -- type: input - id: version - attributes: - label: Version - description: Which version of ZITADEL are you using. - type: textarea id: os attributes: diff --git a/.github/ISSUE_TEMPLATE/docs.yaml b/.github/ISSUE_TEMPLATE/docs.yaml new file mode 100644 index 0000000000..777692f11a --- /dev/null +++ b/.github/ISSUE_TEMPLATE/docs.yaml @@ -0,0 +1,31 @@ +name: 📄 Documentation +description: Create an issue for missing or wrong documentation. +title: +labels: ["docs"] +body: + - type: markdown + attributes: + value: | + Thanks for taking the time to fill out this issue. + - type: checkboxes + id: preflight + attributes: + label: Preflight Checklist + options: + - label: + I could not find a solution in the existing issues, docs, nor discussions + required: true + - label: + I have joined the [ZITADEL chat](https://zitadel.com/chat) + - type: textarea + id: docs + attributes: + label: Describe the docs your are missing or that are wrong + placeholder: As a [type of user], I want [some goal] so that [some reason]. + validations: + required: true + - type: textarea + id: additional + attributes: + label: Additional Context + description: Please add any other infos that could be useful. diff --git a/.github/ISSUE_TEMPLATE/improvement.yaml b/.github/ISSUE_TEMPLATE/improvement.yaml new file mode 100644 index 0000000000..1cd8647552 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/improvement.yaml @@ -0,0 +1,55 @@ +name: 🛠️ Improvement +description: +title: +labels: ["improvement"] +body: + - type: markdown + attributes: + value: | + Thanks for taking the time to fill out this improvement request + - type: checkboxes + id: preflight + attributes: + label: Preflight Checklist + options: + - label: + I could not find a solution in the existing issues, docs, nor discussions + required: true + - label: + I have joined the [ZITADEL chat](https://zitadel.com/chat) + - type: textarea + id: problem + attributes: + label: Describe your problem + description: Please describe your problem this improvement is supposed to solve. + placeholder: Describe the problem you have + validations: + required: true + - type: textarea + id: solution + attributes: + label: Describe your ideal solution + description: Which solution do you propose? + placeholder: As a [type of user], I want [some goal] so that [some reason]. + validations: + required: true + - type: input + id: version + attributes: + label: Version + description: Which version of the ZITADEL are you using. + - type: dropdown + id: environment + attributes: + label: Environment + description: How do you use ZITADEL? + options: + - ZITADEL Cloud + - Self-hosted + validations: + required: true + - type: textarea + id: additional + attributes: + label: Additional Context + description: Please add any other infos that could be useful. diff --git a/.github/ISSUE_TEMPLATE/proposal.yaml b/.github/ISSUE_TEMPLATE/proposal.yaml new file mode 100644 index 0000000000..29b08db5cb --- /dev/null +++ b/.github/ISSUE_TEMPLATE/proposal.yaml @@ -0,0 +1,55 @@ +name: 💡 Proposal / Feature request +description: +title: +labels: ["enhancement"] +body: + - type: markdown + attributes: + value: | + Thanks for taking the time to fill out this proposal / feature reqeust + - type: checkboxes + id: preflight + attributes: + label: Preflight Checklist + options: + - label: + I could not find a solution in the existing issues, docs, nor discussions + required: true + - label: + I have joined the [ZITADEL chat](https://zitadel.com/chat) + - type: textarea + id: problem + attributes: + label: Describe your problem + description: Please describe your problem this proposal / feature is supposed to solve. + placeholder: Describe the problem you have. + validations: + required: true + - type: textarea + id: solution + attributes: + label: Describe your ideal solution + description: Which solution do you propose? + placeholder: As a [type of user], I want [some goal] so that [some reason]. + validations: + required: true + - type: input + id: version + attributes: + label: Version + description: Which version of ZITADEL are you using. + - type: dropdown + id: environment + attributes: + label: Environment + description: How do you use ZITADEL? + options: + - ZITADEL Cloud + - Self-hosted + validations: + required: true + - type: textarea + id: additional + attributes: + label: Additional Context + description: Please add any other infos that could be useful. diff --git a/.github/ISSUE_TEMPLATE/task.md b/.github/ISSUE_TEMPLATE/task.md deleted file mode 100644 index 7ea32464e3..0000000000 --- a/.github/ISSUE_TEMPLATE/task.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -name: User Story -about: A user story is a brief description of a feature that has to be implemented from the perspective of the end user. -title: '' -assignees: '' - ---- - -As a [type of user], I want [some goal] so that [some reason]. - -```[tasklist] -### Acceptance Criteria -- [ ] ... -``` diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml index 2347e029ad..23e35ceb66 100644 --- a/.github/workflows/integration.yml +++ b/.github/workflows/integration.yml @@ -43,7 +43,7 @@ jobs: go run main.go init --config internal/integration/config/zitadel.yaml --config internal/integration/config/${INTEGRATION_DB_FLAVOR}.yaml go run main.go setup --masterkeyFromEnv --config internal/integration/config/zitadel.yaml --config internal/integration/config/${INTEGRATION_DB_FLAVOR}.yaml - name: Run integration tests - run: go test -tags=integration -race -parallel 1 -v -coverprofile=profile.cov -coverpkg=./internal/...,./cmd/... ./internal/integration ./internal/api/grpc/... + run: go test -tags=integration -race -p 1 -v -coverprofile=profile.cov -coverpkg=./internal/...,./cmd/... ./internal/integration ./internal/api/grpc/... - name: Publish go coverage uses: codecov/codecov-action@v3.1.0 with: diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 00238fd516..06be1797a1 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -208,7 +208,7 @@ export INTEGRATION_DB_FLAVOR="cockroach" ZITADEL_MASTERKEY="MasterkeyNeedsToHave docker compose -f internal/integration/config/docker-compose.yaml up --wait ${INTEGRATION_DB_FLAVOR} go run main.go init --config internal/integration/config/zitadel.yaml --config internal/integration/config/${INTEGRATION_DB_FLAVOR}.yaml go run main.go setup --masterkeyFromEnv --config internal/integration/config/zitadel.yaml --config internal/integration/config/${INTEGRATION_DB_FLAVOR}.yaml -go test -tags=integration -race -parallel 1 ./internal/integration ./internal/api/grpc/... +go test -count 1 -tags=integration -race -p 1 ./internal/integration ./internal/api/grpc/... docker compose -f internal/integration/config/docker-compose.yaml down ``` diff --git a/README.md b/README.md index bfabb60be5..d929274373 100644 --- a/README.md +++ b/README.md @@ -155,7 +155,9 @@ Use [Console](https://zitadel.com/docs/guides/manage/console/overview) or our [A ## Security -See the policy [here](./SECURITY.md) +See the policy [here](./SECURITY.md). + +[Technical Advisories](https://zitadel.com/docs/support/technical_advisory) are published regarding major issues with the ZITADEL platform that could potentially impact security or stability in production environments. ## License diff --git a/console/src/app/modules/idp-table/idp-table.component.html b/console/src/app/modules/idp-table/idp-table.component.html index 1110232853..2990f196dc 100644 --- a/console/src/app/modules/idp-table/idp-table.component.html +++ b/console/src/app/modules/idp-table/idp-table.component.html @@ -13,7 +13,7 @@ {{ 'IDP.AVAILABILITY' | translate }} - + {{ 'IDP.NAME' | translate }} - + {{ idp?.name }} {{ 'IDP.TYPE' | translate }} - +
@@ -87,7 +87,7 @@ {{ 'IDP.STATE' | translate }} - + {{ 'IDP.CREATIONDATE' | translate }} - + {{ idp.details.creationDate | timestampToDate | localizedDate : 'dd. MMM, HH:mm' }} {{ 'IDP.CHANGEDATE' | translate }} - + {{ idp.details.changeDate | timestampToDate | localizedDate : 'dd. MMM, HH:mm' }} {{ 'IDP.OWNER' | translate }} - + {{ 'IDP.OWNERTYPES.' + idp.owner | translate }} @@ -140,7 +140,7 @@ " mat-icon-button matTooltip="{{ 'IDP.SETAVAILABLE' | translate }}" - (click)="addIdp(idp)" + (click)="addIdp(idp); $event.stopPropagation()" > @@ -160,7 +160,7 @@ " mat-icon-button matTooltip="{{ 'IDP.SETUNAVAILABLE' | translate }}" - (click)="removeIdp(idp)" + (click)="removeIdp(idp); $event.stopPropagation()" > @@ -185,7 +185,7 @@ mat-icon-button color="warn" matTooltip="{{ 'ACTIONS.REMOVE' | translate }}" - (click)="deleteIdp(idp)" + (click)="deleteIdp(idp); $event.stopPropagation()" > @@ -194,7 +194,7 @@ - +
diff --git a/console/src/app/modules/idp-table/idp-table.component.ts b/console/src/app/modules/idp-table/idp-table.component.ts index 4719f282a5..6df954413a 100644 --- a/console/src/app/modules/idp-table/idp-table.component.ts +++ b/console/src/app/modules/idp-table/idp-table.component.ts @@ -2,7 +2,7 @@ import { SelectionModel } from '@angular/cdk/collections'; import { Component, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core'; import { MatLegacyDialog as MatDialog } from '@angular/material/legacy-dialog'; import { MatLegacyTableDataSource as MatTableDataSource } from '@angular/material/legacy-table'; -import { RouterLink } from '@angular/router'; +import { Router, RouterLink } from '@angular/router'; import { TranslateService } from '@ngx-translate/core'; import { Duration } from 'google-protobuf/google/protobuf/duration_pb'; import { BehaviorSubject, Observable } from 'rxjs'; @@ -31,6 +31,8 @@ import { AdminService } from 'src/app/services/admin.service'; import { ManagementService } from 'src/app/services/mgmt.service'; import { ToastService } from 'src/app/services/toast.service'; +import { OverlayWorkflowService } from 'src/app/services/overlay/overlay-workflow.service'; +import { ContextChangedWorkflowOverlays } from 'src/app/services/overlay/workflows'; import { PageEvent, PaginatorComponent } from '../paginator/paginator.component'; import { PolicyComponentServiceType } from '../policies/policy-component-types.enum'; import { WarnDialogComponent } from '../warn-dialog/warn-dialog.component'; @@ -60,7 +62,13 @@ export class IdpTableComponent implements OnInit { public IDPStylingType: any = IDPStylingType; public loginPolicy!: LoginPolicy.AsObject; - constructor(public translate: TranslateService, private toast: ToastService, private dialog: MatDialog) { + constructor( + private workflowService: OverlayWorkflowService, + public translate: TranslateService, + private toast: ToastService, + private dialog: MatDialog, + private router: Router, + ) { this.selection.changed.subscribe(() => { this.changedSelection.emit(this.selection.selected); }); @@ -241,6 +249,16 @@ export class IdpTableComponent implements OnInit { } } + navigateToIDP(row: Provider.AsObject) { + this.router.navigate(this.routerLinkForRow(row)).then(() => { + if (this.serviceType === PolicyComponentServiceType.MGMT && row.owner === IDPOwnerType.IDP_OWNER_TYPE_SYSTEM) { + setTimeout(() => { + this.workflowService.startWorkflow(ContextChangedWorkflowOverlays, null); + }, 1000); + } + }); + } + private async getIdps(): Promise { switch (this.serviceType) { case PolicyComponentServiceType.MGMT: diff --git a/console/src/app/services/overlay/workflows.ts b/console/src/app/services/overlay/workflows.ts index 1c6c8263bb..6f9c11e568 100644 --- a/console/src/app/services/overlay/workflows.ts +++ b/console/src/app/services/overlay/workflows.ts @@ -66,3 +66,17 @@ export const OrgContextChangedWorkflowOverlays: CnslOverlay[] = [ }, }, ]; + +export const ContextChangedWorkflowOverlays: CnslOverlay[] = [ + { + id: 'contextswitcher', + origin: 'orgbutton', + toHighlight: ['orgbutton'], + content: { + i18nText: 'OVERLAYS.SWITCHEDTOINSTANCE.TEXT', + }, + requirements: { + permission: ['iam.read'], + }, + }, +]; diff --git a/console/src/assets/i18n/de.json b/console/src/assets/i18n/de.json index 68d81ac777..832ce451bb 100644 --- a/console/src/assets/i18n/de.json +++ b/console/src/assets/i18n/de.json @@ -204,6 +204,9 @@ }, "CONTEXTCHANGED": { "TEXT": "Achtung! Soeben wurde die Organisation gewechselt." + }, + "SWITCHEDTOINSTANCE": { + "TEXT": "Soeben wurde die Ansicht auf Instanz gewechselt." } }, "FILTER": { diff --git a/console/src/assets/i18n/en.json b/console/src/assets/i18n/en.json index 2d27939ee0..d6a19679e5 100644 --- a/console/src/assets/i18n/en.json +++ b/console/src/assets/i18n/en.json @@ -204,7 +204,10 @@ "TEXT": "This navigation changes based on your selected organization above or your instance" }, "CONTEXTCHANGED": { - "TEXT": "Attention! The organization context has changed." + "TEXT": "The organization context has changed." + }, + "SWITCHEDTOINSTANCE": { + "TEXT": "The view just changed to instance!" } }, "FILTER": { diff --git a/console/src/assets/i18n/es.json b/console/src/assets/i18n/es.json index 0fea6748ab..8e1164d092 100644 --- a/console/src/assets/i18n/es.json +++ b/console/src/assets/i18n/es.json @@ -205,6 +205,9 @@ }, "CONTEXTCHANGED": { "TEXT": "¡Atención! El contexto de la organización ha cambiado." + }, + "SWITCHEDTOINSTANCE": { + "TEXT": "¡La vista acaba de cambiar a instancia!" } }, "FILTER": { diff --git a/console/src/assets/i18n/fr.json b/console/src/assets/i18n/fr.json index e24116fd80..07e0829e83 100644 --- a/console/src/assets/i18n/fr.json +++ b/console/src/assets/i18n/fr.json @@ -204,6 +204,9 @@ }, "CONTEXTCHANGED": { "TEXT": "Attention ! Le contexte de l'organisation a changé." + }, + "SWITCHEDTOINSTANCE": { + "TEXT": "La vue vient de changer en instance !" } }, "FILTER": { diff --git a/console/src/assets/i18n/it.json b/console/src/assets/i18n/it.json index 4d18722afb..e025b48399 100644 --- a/console/src/assets/i18n/it.json +++ b/console/src/assets/i18n/it.json @@ -204,6 +204,9 @@ }, "CONTEXTCHANGED": { "TEXT": "Attenzione! L'organizzazione è appena stata cambiata." + }, + "SWITCHEDTOINSTANCE": { + "TEXT": "La visualizzazione è appena stata modificata in istanza!" } }, "FILTER": { diff --git a/console/src/assets/i18n/ja.json b/console/src/assets/i18n/ja.json index 2ba3863ad4..6dea6b8db1 100644 --- a/console/src/assets/i18n/ja.json +++ b/console/src/assets/i18n/ja.json @@ -205,6 +205,9 @@ }, "CONTEXTCHANGED": { "TEXT": "注意! 組織のコンテキストが変更されました。" + }, + "SWITCHEDTOINSTANCE": { + "TEXT": "ビューがインスタンスに変更されました。" } }, "FILTER": { diff --git a/console/src/assets/i18n/pl.json b/console/src/assets/i18n/pl.json index 0dd538ba30..3d9a3f8254 100644 --- a/console/src/assets/i18n/pl.json +++ b/console/src/assets/i18n/pl.json @@ -204,6 +204,9 @@ }, "CONTEXTCHANGED": { "TEXT": "Uwaga! Kontekst organizacji uległ zmianie." + }, + "SWITCHEDTOINSTANCE": { + "TEXT": "Widok właśnie zmienił się na instancję!" } }, "FILTER": { diff --git a/console/src/assets/i18n/zh.json b/console/src/assets/i18n/zh.json index dbc8dcc199..b8f735d517 100644 --- a/console/src/assets/i18n/zh.json +++ b/console/src/assets/i18n/zh.json @@ -204,6 +204,9 @@ }, "CONTEXTCHANGED": { "TEXT": "注意!组织环境发生了变化。" + }, + "SWITCHEDTOINSTANCE": { + "TEXT": "视图刚刚更改为实例!" } }, "FILTER": { diff --git a/docs/docs/apis/openidoauth/endpoints.mdx b/docs/docs/apis/openidoauth/endpoints.mdx index 8c8a3dc48b..8e9b8b2314 100644 --- a/docs/docs/apis/openidoauth/endpoints.mdx +++ b/docs/docs/apis/openidoauth/endpoints.mdx @@ -1,5 +1,5 @@ --- -title: Endpoints +title: OpenID Connect Endpoints --- import Tabs from "@theme/Tabs"; @@ -101,7 +101,7 @@ no additional parameters required | state | Opaque value used to maintain state between the request and the callback. Used for Cross-Site Request Forgery (CSRF) mitigation as well, therefore highly **recommended**. | | ui_locales | Spaces delimited list of preferred locales for the login UI, e.g. `de-CH de en`. If none is provided or matches the possible locales provided by the login UI, the `accept-language` header of the browser will be taken into account. | -### Successful Code Response +### Successful code response When your `response_type` was `code` and no error occurred, the following response will be returned: @@ -110,7 +110,7 @@ When your `response_type` was `code` and no error occurred, the following respon | code | Opaque string which will be necessary to request tokens on the token endpoint | | state | Unmodified `state` parameter from the request | -### Successful Implicit Response +### Successful implicit response When your `response_type` was either `it_token` or `id_token token` and no error occurred, the following response will be returned: @@ -123,7 +123,7 @@ When your `response_type` was either `it_token` or `id_token token` and no error | scope | Scopes of the `access_token`. These might differ from the provided `scope` parameter. | | state | Unmodified `state` parameter from the request | -### Error Response +### Error response Regardless of the authorization flow chosen, if an error occurs the following response will be returned to the redirect_uri. @@ -158,11 +158,11 @@ The token_endpoint will as the name suggests return various tokens (access, id a When using [`authorization_code`](#authorization-code-grant-code-exchange) flow call this endpoint after receiving the code from the authorization_endpoint. When using [`refresh_token`](#authorization-code-grant-code-exchange) or [`urn:ietf:params:oauth:grant-type:jwt-bearer` (JWT Profile)](#jwt-profile-grant) you will call this endpoint directly. -### Authorization Code Grant (Code Exchange) +### Authorization code grant (Code Exchange) As mention above, when using `authorization_code` grant, this endpoint will be your second request for authorizing a user with its user agent (browser). -#### Required request Parameters +#### Required request parameters | Parameter | Description | | ------------ | ------------------------------------------------------------------------------------------------------------- | @@ -229,9 +229,9 @@ Send a client assertion as JWT for us to validate the signature against the regi | refresh_token | An opaque token. Only returned if `offline_access` scope was requested | | token_type | Type of the `access_token`. Value is always `Bearer` | -### JWT Profile Grant +### JWT profile grant -#### Required request Parameters +#### Required request parameters | Parameter | Description | | ---------- | ----------------------------------------------------------------------------------------------------------------------- | @@ -247,7 +247,7 @@ curl --request POST \ --data assertion=eyJhbGciOiJSUzI1Ni... ``` -#### Successful JWT Profile response {#token-jwt-response} +#### Successful JWT profile response {#token-jwt-response} | Property | Description | | ------------ | ------------------------------------------------------------------------------------- | @@ -257,12 +257,12 @@ curl --request POST \ | scope | Scopes of the `access_token`. These might differ from the provided `scope` parameter. | | token_type | Type of the `access_token`. Value is always `Bearer` | -### Refresh Token Grant +### Refresh token grant To request a new `access_token` without user interaction, you can use the `refresh_token` grant. See [offline_access Scope](scopes#standard-scopes) for how to request a `refresh_token` in the authorization request. -#### Required request Parameters +#### Required request parameters | Parameter | Description | | ------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | @@ -325,9 +325,9 @@ Send a `client_assertion` as JWT for us to validate the signature against the re | refresh_token | An new opaque refresh_token. | | token_type | Type of the `access_token`. Value is always `Bearer` | -### Client Credentials Grant +### Client credentials grant -#### Required request Parameters +#### Required request parameters | Parameter | Description | | ---------- | ----------------------------------------------------------------------------------------------------------------------- | @@ -363,7 +363,7 @@ curl --request POST \ --data scope=openid profile ``` -#### Successful Client Credentials response {#token-client-credentials-response} +#### Successful client credentials response {#token-client-credentials-response} | Property | Description | | ------------ | ------------------------------------------------------------------------------------- | @@ -584,8 +584,27 @@ If both parameters are provided, they must be equal. {your_domain}/oauth/v2/keys -> Be aware that these keys can be rotated without any prior notice. We will however make sure that a proper `kid` is set with each key! +The endpoint returns a JSON Web Key Set (JWKS) containing the public keys that can be used to locally validate JWTs you received from ZITADEL. +The alternative would be to validate tokens with the [introspection endpoint](#introspection_endpoint). -## OAuth 2.0 Metadata +### Key rotation + +Keys are automatically rotated on a regular basis or on demand, meaning keys can change in irregular intervals. +ZITADEL ensures that a proper `kid` is set with each key. + +:::info Keys rotate without prior notice +Be aware that these keys can be rotated without any prior notice. +::: + +### Caching + +You can optimize performance of your clients by caching the response from the keys endpoint. +We recommend to regularly update the cached response, since the [keys can be rotated without prior notice](#key-rotation). +You could also combine caching with a risk-based on-demand refresh when a critical operation is executed. + +Without caching you will call this endpoint on each request. +This might result in being rate limited for a large number of requests that come from the same backend. + +## OAuth 2.0 metadata **ZITADEL** does not yet provide a OAuth 2.0 Metadata endpoint but instead provides a [OpenID Connect Discovery Endpoint](https://openid.net/specs/openid-connect-discovery-1_0.html). diff --git a/docs/docs/apis/saml/endpoints.md b/docs/docs/apis/saml/endpoints.md index 4cd186510c..eeae326d9a 100644 --- a/docs/docs/apis/saml/endpoints.md +++ b/docs/docs/apis/saml/endpoints.md @@ -1,8 +1,8 @@ --- -title: Endpoints +title: SAML endpoints --- -## SAML 2.0 Metadata +## SAML 2.0 metadata The SAML Metadata is located within the issuer domain. This would give us {your_domain}/saml/v2/metadata. @@ -11,14 +11,14 @@ 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 +## 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 +## SSO endpoint {your_domain}/saml/v2/SSO @@ -40,7 +40,7 @@ spec.** [Bindings for the OASIS Security Assertion Markup Language (SAML) V2.0 | 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 +### Successful response Depending on the content of the request the response comes back in the requested binding, but the content is the same. @@ -51,7 +51,7 @@ Depending on the content of the request the response comes back in the requested | 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 +### 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. diff --git a/docs/docs/concepts/architecture/software.md b/docs/docs/concepts/architecture/software.md index 29dcb2c1ea..565221774e 100644 --- a/docs/docs/concepts/architecture/software.md +++ b/docs/docs/concepts/architecture/software.md @@ -147,5 +147,5 @@ The storage layer of ZITADEL is responsible for multiple things. For example: ZITADEL currently supports CockroachDB as first choice of storage due to its perfect match for ZITADELs needs. Postgres is currently in [Beta](/docs/support/software-release-cycles-support#beta) and will be [Enterprise Supported](/docs/support/software-release-cycles-support#partially-supported) afterwards. Beta state will be removed as soon as [automated tests](https://github.com/zitadel/zitadel/issues/5741) are implemented. -Make sure to read our [Production Guide](./self-hosting/manage/production#prefer-cockroachdb) before you decide to use it. +Make sure to read our [Production Guide](/docs/self-hosting/manage/production#prefer-cockroachdb) before you decide to use it. diff --git a/docs/docs/concepts/architecture/solution.md b/docs/docs/concepts/architecture/solution.md index 176c151732..9b0656c985 100644 --- a/docs/docs/concepts/architecture/solution.md +++ b/docs/docs/concepts/architecture/solution.md @@ -11,7 +11,7 @@ Depending on your projects needs our general recommendation is to run ZITADEL an Consult the [CockroachDB documentation](https://www.cockroachlabs.com/docs/) for more details or use the [CockroachCloud Service](https://www.cockroachlabs.com/docs/cockroachcloud/create-an-account.html) Postgres is currently in [Beta](/docs/support/software-release-cycles-support#beta) and will be [Enterprise Supported](/docs/support/software-release-cycles-support#partially-supported) afterwards. Beta state will be removed as soon as [automated tests](https://github.com/zitadel/zitadel/issues/5741) are implemented. -Make sure to read our [Production Guide](./self-hosting/manage/production#prefer-cockroachdb) before you decide to use it. +Make sure to read our [Production Guide](/self-hosting/manage/production#prefer-cockroachdb) before you decide to use it. ## Scalability diff --git a/docs/docs/concepts/features/audit-trail.md b/docs/docs/concepts/features/audit-trail.md index bf1992453d..12cd77606e 100644 --- a/docs/docs/concepts/features/audit-trail.md +++ b/docs/docs/concepts/features/audit-trail.md @@ -4,7 +4,7 @@ title: Audit Trail ZITADEL provides you with an built-in audit trail to track all changes and events over an unlimited period of time. Most other solutions replace a historic record and track changes in a separate log when information is updated. -ZITADEL only ever appends data in an [Eventstore](https://zitadel.com/docs/concepts/eventstore), keeping all historic record. +ZITADEL only ever appends data in an [Eventstore](/docs/concepts/eventstore/overview), keeping all historic record. The audit trail itself is identical to the state, since ZITADEL calculates the state from all the past changes. ![Example of events that happen for a profile change and a login](/img/concepts/audit-trail/audit-log-events.png) diff --git a/docs/docs/examples/sdks.md b/docs/docs/examples/sdks.md index 6c70dba163..fc868a08a3 100644 --- a/docs/docs/examples/sdks.md +++ b/docs/docs/examples/sdks.md @@ -94,5 +94,4 @@ You might want to check out the following links to find a good library: - [awesome-auth](https://github.com/casbin/awesome-auth) - [OpenID General References](https://openid.net/developers/libraries/) -- [OpenID certified libraries](https://openid.net/developers/certified/) -- [OpenID uncertified libraries](https://openid.net/developers/uncertified/) \ No newline at end of file +- [OpenID certified developer tools](https://openid.net/certified-open-id-developer-tools/) \ No newline at end of file diff --git a/docs/docs/guides/migrate/introduction.md b/docs/docs/guides/migrate/introduction.md index 887a374a23..6ac351f433 100644 --- a/docs/docs/guides/migrate/introduction.md +++ b/docs/docs/guides/migrate/introduction.md @@ -8,7 +8,7 @@ The individual guides in this section should give you an overview of things to c When moving from a previous auth solution to ZITADEL, it is important to note that some decisions and features are unique to ZITADEL. Without duplicating too much content here are some important features and patterns to consider in terms of solution architecture. -You can read more about the basic structure and important concepts of ZITADEL in our [concepts section](https://zitadel.com/docs/concepts/introduction). +You can read more about the basic structure and important concepts of ZITADEL in our [concepts section](/docs/concepts/). ## Multi-Tenancy Architecture diff --git a/docs/docs/legal/acceptable-use-policy.md b/docs/docs/legal/acceptable-use-policy.md index 683bca3653..5fb99138aa 100644 --- a/docs/docs/legal/acceptable-use-policy.md +++ b/docs/docs/legal/acceptable-use-policy.md @@ -3,8 +3,6 @@ title: Acceptable Use Policy custom_edit_url: null --- -## Introduction - This policy is an annex to the [Terms of Service](terms-of-service) and clarifies your obligations while using our Services. ## Use diff --git a/docs/docs/legal/policies/account-lockout-policy.md b/docs/docs/legal/policies/account-lockout-policy.md new file mode 100644 index 0000000000..14463392d3 --- /dev/null +++ b/docs/docs/legal/policies/account-lockout-policy.md @@ -0,0 +1,62 @@ +--- +title: Account Lockout Policy +custom_edit_url: null +--- + +This policy is an annex to the [Terms of Service](../terms-of-service) that clarifies your obligations and our procedure handling requests where you can't get access to your ZITADEL Cloud services and data. This policy is applicable to situations where we, ZITADEL, need to restore your access for a otherwise available service and not in cases where the services are unavailable. + +## Why to do we have this policy? + +Users may not be able to access our services anymore due to loss of credentials or misconfiguration. +In certain circumstances it might not be possible to recover the credentials through a self-service flow (eg, loss of 2FA credentials) or access the system to undo the configuration that caused the issue. +These cases might require help from our support, so you can regain access to your data. + +We will require some initial information and conditions to be able to assist you, and will require further information to handle the request. +We also keep the right to refuse any such request without providing a reason, in case you can't provide the requested information. + +## Scope + +In scope of this policy are requests to recover + +- ZITADEL Cloud account (customer portal) +- Manager accounts to a specific instance +- Undo configuration changes resulting in lockout (eg, misconfigured Action) + +Out of scope are requests to recover access + +- Where you have to option to ask another Admin/Manager +- by end-users who should ask an Admin/Manager instead +- self-hosted instances + +## Process + +Before you send a request to restore access to your account, please make sure that can't ask your manager/admin or another manager/admin to recover access. + +### ZITADEL Cloud account + +If you need to recover your ZITADEL Cloud account for the customer portal, please send an email to [support@zitadel.com](mailto:support@zitadel.com?subject=ZITADEL%20Cloud%20account%20lockout): + +- State clearly in the subject line that this is related to an account lockout for a ZITADEL Cloud account +- The sender's email address must match the verified email address of the account owner +- State the reason why you're not able to recover the account yourself + +Please allow us time to validate your request. +Our support will get back to you to request additional information for verification. + +### Manager access to an Instance + +If you need to recover a Manager account to an instance, please make sure you can't recover the account via another user or service user with Manager permissions. + +Please visit the [support page in the customer portal](https://zitadel.cloud/admin/support): + +- State clearly in the subject line that this is related to an account lockout the affected instance +- State the reason why you're not able to recover the account yourself + +Please allow us time to validate your request. +Our support will get back to you to request additional information for verification. + +## Entry into force + +This policy is valid from May 31, 2023. + +Last revised May 31, 2023 \ No newline at end of file diff --git a/docs/docs/legal/rate-limit-policy.md b/docs/docs/legal/rate-limit-policy.md index 8cae76e99a..48c78b6a48 100644 --- a/docs/docs/legal/rate-limit-policy.md +++ b/docs/docs/legal/rate-limit-policy.md @@ -2,7 +2,6 @@ title: Rate Limit Policy custom_edit_url: null --- -## Introduction This policy is an annex to the [Terms of Service](terms-of-service) and clarifies your obligations while using our Services, specifically how we will use rate limiting to enforce certain aspects of our [Acceptable Use Policy](acceptable-use-policy). diff --git a/docs/docs/legal/vulnerability-disclosure-policy.mdx b/docs/docs/legal/vulnerability-disclosure-policy.mdx index 7b6cc85ebd..74d04258e3 100644 --- a/docs/docs/legal/vulnerability-disclosure-policy.mdx +++ b/docs/docs/legal/vulnerability-disclosure-policy.mdx @@ -3,8 +3,6 @@ title: Vulnerability Disclosure Policy custom_edit_url: null --- -## Introduction - At ZITADEL we are extremely grateful for security aware people who disclose vulnerabilities to us and the open source community. All reports will be investigated by our team and we will work with you closely to validate and fix vulnerabilities reported to us. @@ -91,6 +89,6 @@ In case we have confirmed your report, we may compensate you, given prior writte ## Entry into force -This privacy policy is valid from March 16, 2023. +This policy is valid from March 16, 2023. Last revised March 16, 2023 diff --git a/docs/docs/self-hosting/deploy/kubernetes.mdx b/docs/docs/self-hosting/deploy/kubernetes.mdx index 7d3385666b..fa214d5774 100644 --- a/docs/docs/self-hosting/deploy/kubernetes.mdx +++ b/docs/docs/self-hosting/deploy/kubernetes.mdx @@ -10,49 +10,61 @@ Installation and configuration details are described in the [open source ZITADEL By default, the chart installs a secure and highly available ZITADEL instance. For running an easily testable, insecure, non-HA ZITADEL instance, run the following commands. -## Helm -### Add the helm repositories for CockroachDB and ZITADEL +## Add the Helm Repositories for CockroachDB and ZITADEL ```bash helm repo add cockroachdb https://charts.cockroachdb.com/ helm repo add zitadel https://charts.zitadel.com ``` -### Install zitadel +After you have your repositories added, +you can setup ZITADEL and either +- initialize an [IAM owner who is a human user](#setup-zitadel-and-a-human-admin) or +- initialize an [IAM owner who is a service account](#setup-zitadel-and-a-service-account-admin) -#### Install an insecure cockroachdb and zitadel release that works with localhost +## Setup ZITADEL and a Human Admin ```bash -# CockroachDB +# Install CockroachDB helm install crdb cockroachdb/cockroachdb \ --set fullnameOverride=crdb \ --set single-node=true \ --set statefulset.replicas=1 -# ZITADEL +# Install ZITADEL helm install my-zitadel zitadel/zitadel \ --set zitadel.masterkey="MasterkeyNeedsToHave32Characters" \ --set zitadel.configmapConfig.ExternalSecure=false \ --set zitadel.configmapConfig.TLS.Enabled=false \ --set zitadel.secretConfig.Database.cockroach.User.Password="a-zitadel-db-user-password" \ --set replicaCount=1 + +# Make ZITADEL locally accessible +kubectl port-forward svc/my-zitadel 8080 ``` -#### Install an insecure zitadel release that works with localhost with a service account +## Setup ZITADEL and a Service Account Admin -!!!Caution!!! With this setup you only get a service account with a key and no admin account where you can login directly into ZITADEL. +With this setup, you don't create a human user that has the IAM_OWNER role. +Instead, you create a service account that has the IAM_OWNER role. +ZITADEL will also create a key for your, with which you can authenticate to the ZITADEL API. +For example, you can install ZITADEL and seemlessly provision ZITADEL resources after installation using [Terraform](/docs/guides/manage/terraform/basics.md). + +:::caution +With this setup you only get a key for a service account. Logging in at ZITADEL using the login screen is not possible until you create a user with the ZITADEL API. +::: ```bash -# CockroachDB +# Install CockroachDB helm install crdb cockroachdb/cockroachdb \ --set fullnameOverride=crdb \ --set single-node=true \ --set statefulset.replicas=1 -# ZITADEL +# Install ZITADEL helm install --namespace zitadel --create-namespace my-zitadel zitadel/zitadel \ --set zitadel.masterkey="MasterkeyNeedsToHave32Characters" \ --set zitadel.configmapConfig.ExternalSecure=false \ @@ -63,20 +75,15 @@ helm install --namespace zitadel --create-namespace my-zitadel zitadel/zitadel \ --set zitadel.configmapConfig.FirstInstance.Org.Machine.Machine.Username="zitadel-admin-sa" \ --set zitadel.configmapConfig.FirstInstance.Org.Machine.Machine.Name="Admin" \ --set zitadel.configmapConfig.FirstInstance.Org.Machine.MachineKey.Type=1 + +# Make ZITADEL locally accessible +kubectl port-forward svc/my-zitadel 8080 ``` -When helm is done, you get a command to retrieve your machine key, which is saved as a kubernetes secret, for example: +When Helm is done, you can print your service account key from a Kubernetes secret: ```bash kubectl -n zitadel get secret zitadel-admin-sa -o jsonpath='{ .data.zitadel-admin-sa\.json }' | base64 -D ``` -This key can be used to provision resources with for example [Terraform](/docs/guides/manage/terraform/basics.md). - -### Forward the ZITADEL service port to your local machine - -```bash -kubectl port-forward svc/my-zitadel 8080:8080 -``` - diff --git a/docs/docs/self-hosting/deploy/overview.mdx b/docs/docs/self-hosting/deploy/overview.mdx index 8797c43594..d7ba3c92ea 100644 --- a/docs/docs/self-hosting/deploy/overview.mdx +++ b/docs/docs/self-hosting/deploy/overview.mdx @@ -14,7 +14,7 @@ Choose your platform and run ZITADEL with the most minimal configuration possibl ## Prerequisites - For test environments, ZITADEL does not need many resources, 1 CPU and 512MB memory are more than enough. (With more CPU, the password hashing might be faster) -- A CockroachDB or Postgresql as only needed storage. Make sure to read our [Production Guide](./self-hosting/manage/production#prefer-cockroachdb) before you decide to use Postgresql. +- A CockroachDB or Postgresql as only needed storage. Make sure to read our [Production Guide](/docs/self-hosting/manage/production#prefer-cockroachdb) before you decide to use Postgresql. ) ## Releases diff --git a/docs/docs/self-hosting/manage/database/_postgres.mdx b/docs/docs/self-hosting/manage/database/_postgres.mdx index ace58ef328..d012ac0807 100644 --- a/docs/docs/self-hosting/manage/database/_postgres.mdx +++ b/docs/docs/self-hosting/manage/database/_postgres.mdx @@ -10,7 +10,7 @@ Be aware that PostgreSQL is only [Enterprise Supported](/docs/support/software-r ::: If you want to use a PostgreSQL database instead of CockroachDB you can [overwrite the default configuration](../configure/configure.mdx). -Make sure to read our [Production Guide](./self-hosting/manage/production#prefer-cockroachdb) before you decide to use it. +Make sure to read our [Production Guide](/docs/self-hosting/manage/production#prefer-cockroachdb) before you decide to use it. Currently versions >= 14 are supported. diff --git a/docs/docs/self-hosting/manage/productionchecklist.md b/docs/docs/self-hosting/manage/productionchecklist.md index e6f7d1bac4..2f02361b77 100644 --- a/docs/docs/self-hosting/manage/productionchecklist.md +++ b/docs/docs/self-hosting/manage/productionchecklist.md @@ -9,13 +9,15 @@ To apply best practices to your production setup we created a step by step check - [ ] Make use of configuration management tools such as Terraform to provision all of the below - [ ] Use a secrets manager to store your confidential information -- [ ] Reduce the manual interaction with your platform to an absolute minimum +- [ ] Reduce the manual interaction with your platform to an absolute minimum + #### HA Setup + - [ ] High Availability for ZITADEL containers - [ ] Use a container orchestrator such as Kubernetes - [ ] Use serverless platform such as Knative or a hyperscaler equivalent (e.g. CloudRun from Google) - [ ] Split `zitadel init` and `zitadel setup` for fast start-up times when [scaling](/docs/self-hosting/manage/updating_scaling) ZITADEL -- [ ] High Availability for database +- [ ] High Availability for database - [ ] Follow the [Production Checklist](https://www.cockroachlabs.com/docs/stable/recommended-production-settings.html) for CockroachDB if you selfhost the database or use [CockroachDB cloud](https://www.cockroachlabs.com/docs/cockroachcloud/create-an-account.html) - [ ] Configure backups on a regular basis for the database - [ ] Test the restore scenarios before going live @@ -26,12 +28,14 @@ To apply best practices to your production setup we created a step by step check - [ ] Web Application Firewall #### Networking + - [ ] Use a Layer 7 Web Application Firewall to secure ZITADEL that supports **[HTTP/2](/docs/self-hosting/manage/http2)** - [ ] Limit the access by IP addresses if needed - - [ ] Secure the access by rate limits for specific endpoints (e.g. API vs frontend) to secure availability on high load. See the [ZITADEL Cloud rate limits](https://zitadel.com/docs/apis/ratelimits) for reference. - - [ ] Check that your firewall also filters IPv6 traffic``` + - [ ] Secure the access by rate limits for specific endpoints (e.g. API vs frontend) to secure availability on high load. See the [ZITADEL Cloud rate limits](/docs/legal/rate-limit-policy) for reference. + - [ ] Check that your firewall also filters IPv6 traffic ### ZITADEL configuration + - [ ] Configure a valid [SMTP Server](/docs/guides/manage/console/instance-settings#smtp) and test the email delivery - [ ] Add [Custom Branding](/docs/guides/manage/customize/branding) if required - [ ] Configure a valid [SMS Service](/docs/guides/manage/console/instance-settings#sms) such as Twilio if needed @@ -40,12 +44,14 @@ To apply best practices to your production setup we created a step by step check - [ ] Declare and apply zitadel configuration using the zitadel terraform [provider](https://github.com/zitadel/terraform-provider-zitadel) ### Security + - [ ] Use a FQDN and a trusted valid certificate for external [TLS](/docs/self-hosting/manage/tls_modes#http2) connections - [ ] Create service accounts for applications that interact with ZITADEL's APIs - [ ] Make use of a CDN service to decrease the load for static assets served by ZITADEL - [ ] Make use of a [security scanner](https://owasp.org/www-community/Vulnerability_Scanning_Tools) to test your application and deployment environment ### Monitoring + Use an appropriate monitoring solution to have an overview about your ZITADEL instance. In particular you may want to watch out for things like: - [ ] CPU and memory of ZITADEL and the database diff --git a/docs/docs/support/technical_advisory.mdx b/docs/docs/support/technical_advisory.mdx index c866c32934..7fba134201 100644 --- a/docs/docs/support/technical_advisory.mdx +++ b/docs/docs/support/technical_advisory.mdx @@ -28,6 +28,13 @@ We understand that these advisories may include breaking changes, and we aim to +## Subscribe to our Mailing List + +If you want to stay up to date on our technical advisories, we recommend subscribing to the mailing list. +Go to the subscription form and add your email address. + +As ZITADEL Cloud customer, you can also login to the ZITADEL Customer Portal and enable the Technical Advisory Notifications in your settings. + ## Categories ### Breaking Behaviour Change diff --git a/docs/docs/support/troubleshooting.md b/docs/docs/support/troubleshooting.md index 05e986fd43..8027152c34 100644 --- a/docs/docs/support/troubleshooting.md +++ b/docs/docs/support/troubleshooting.md @@ -37,6 +37,42 @@ If you're self hosting with a custom domain, you need to instruct ZITADEL to use You can find further instructions in our guide about [custom domains](https://zitadel.com/docs/self-hosting/manage/custom-domain). We also provide a guide on how to [configure](https://zitadel.com/docs/self-hosting/manage/configure) ZITADEL with variables from files or environment variables. +## Invalid audience + +`invalid audience (APP-Zxfako)` + +This error message refers to the audience claim (`aud`) of your token. +This claim identifies the audience, i.e. the resource server, that this token is intended for. +If a resource server does not identify itself with a value in the "aud" claim when this claim is present, then the must be rejected (see [RFC7519](https://www.rfc-editor.org/rfc/rfc7519#section-4.1.3) for more details). + +You might encounter this error message from ZITADEL, typically when you authenticated with a client in one project and trying to access an application in another project. +You need add a specific [reserved scope](http://localhost:3000/docs/apis/openidoauth/scopes#reserved-scopes) to add the projectID to the audience of the access token. + +The two scenarios should help you troubleshoot this issue: + +### Frontend to Backend + +You have one project for your frontend application and one project for your backend application. +End-users authenticate to an application in your frontend project. +The frontend then sends requests to the backend, validates the token with ZITADEL's introspection endpoint, and returns a payload to the frontend. +The backend returns the error `invalid audience (APP-Zxfako)`. + +You must add the scope `urn:zitadel:iam:org:project:id:{projectId}:aud` to the auth request that is send from the front end. +Replace `projectId` with the projectId of your backend. + +### Accessing ZITADEL's APIs + +You have a project for a frontend application. +The application should also access the API of your ZITADEL, for example to pull a list of all users and display them on a user page. +End-users authenticate to the application in the frontend project, but when calling the management API you get the error `invalid audience (APP-Zxfako)`. + +You must add the scope `urn:zitadel:iam:org:project:id:zitadel:aud` to the auth request that is send from the front end. + +When accessing your ZITADEL instance's APIs they act as a resource server. +You can check the Console or via API and see that when you open your default organization there exists a project "ZITADEL" that contains different applications for each API and the Console. +Like in the scenario above the access token requires to have an `aud` claim that includes the "ZITADEL" project. +Instead of `urn:zitadel:iam:org:project:id:zitadel:aud` you could also use `urn:zitadel:iam:org:project:id:{projectId}:aud`, where `projectId` is the projectId of the Project "ZITADEL". + ## WebFinger requirement for Tailscale The WebFinger requirement and setup is a step a user has to take outside of their IdP set-up. WebFinger is a protocol which supports the ability for OIDC issuer discovery, and we use it to prove that the user has administrative control over the domain and to retrieve the issuer. This is a requirement we have in place for all users, regardless of their IdP, who use custom OIDC with Tailscale. diff --git a/docs/sidebars.js b/docs/sidebars.js index 3615dee280..cace2f404d 100644 --- a/docs/sidebars.js +++ b/docs/sidebars.js @@ -150,6 +150,23 @@ module.exports = { "guides/integrate/logout", ], }, + { + type: "category", + label: "Authenticate service users", + link: { + type: "generated-index", + title: "Authenticate Service Users", + slug: "/guides/integrate/serviceusers", + description: + "How to authenticate service users for machine-to-machine (M2M) communication between services. You also need to authenticate service users to access ZITADEL's APIs.", + }, + collapsed: true, + items: [ + "guides/integrate/private-key-jwt", + "guides/integrate/client-credentials", + "guides/integrate/pat", + ], + }, { type: "category", label: "Configure identity providers", @@ -179,21 +196,9 @@ module.exports = { collapsed: true, items: [ { - type: "category", - label: "Authenticate service users", - link: { - type: "generated-index", - title: "Authenticate Service Users", - slug: "/guides/integrate/serviceusers", - description: - "How to authenticate service users", - }, - collapsed: true, - items: [ - "guides/integrate/private-key-jwt", - "guides/integrate/client-credentials", - "guides/integrate/pat", - ], + type: 'link', + label: 'Authenticate service users', + href: '/guides/integrate/serviceusers', }, "guides/integrate/access-zitadel-apis", "guides/integrate/access-zitadel-system-api", @@ -587,10 +592,17 @@ module.exports = { type: "category", label: "Policies", collapsed: false, + link: { + type: "generated-index", + title: "Policies", + slug: "/legal/policies", + description: "Policies and guidelines in addition to our terms of services.", + }, items: [ "legal/privacy-policy", "legal/acceptable-use-policy", "legal/rate-limit-policy", + "legal/policies/account-lockout-policy", "legal/vulnerability-disclosure-policy", ], }, diff --git a/docs/src/pages/index.js b/docs/src/pages/index.js index 544f43cbb2..0e88b075ac 100644 --- a/docs/src/pages/index.js +++ b/docs/src/pages/index.js @@ -72,7 +72,7 @@ const features = [ description="" /> > 2]; - base64 += chars[((bytes[i] & 3) << 4) | (bytes[i + 1] >> 4)]; - base64 += chars[((bytes[i + 1] & 15) << 2) | (bytes[i + 2] >> 6)]; - base64 += chars[bytes[i + 2] & 63]; - } - - if ((len % 3) === 2) { - base64 = base64.substring(0, base64.length - 1) + "="; - } else if (len % 3 === 1) { - base64 = base64.substring(0, base64.length - 2) + "=="; - } - - return base64; -} - -function decode(base64) { - let bufferLength = base64.length * 0.75, - len = base64.length, i, p = 0, - encoded1, encoded2, encoded3, encoded4; - - if (base64[base64.length - 1] === "=") { - bufferLength--; - if (base64[base64.length - 2] === "=") { - bufferLength--; - } - } - - let arraybuffer = new ArrayBuffer(bufferLength), - bytes = new Uint8Array(arraybuffer); - - for (i = 0; i < len; i += 4) { - encoded1 = lookup[base64.charCodeAt(i)]; - encoded2 = lookup[base64.charCodeAt(i + 1)]; - encoded3 = lookup[base64.charCodeAt(i + 2)]; - encoded4 = lookup[base64.charCodeAt(i + 3)]; - - bytes[p++] = (encoded1 << 2) | (encoded2 >> 4); - bytes[p++] = ((encoded2 & 15) << 4) | (encoded3 >> 2); - bytes[p++] = ((encoded3 & 3) << 6) | (encoded4 & 63); - } - - return arraybuffer; -} \ No newline at end of file diff --git a/internal/api/ui/login/static/resources/scripts/utils.js b/internal/api/ui/login/static/resources/scripts/utils.js new file mode 100644 index 0000000000..1dc7589a4b --- /dev/null +++ b/internal/api/ui/login/static/resources/scripts/utils.js @@ -0,0 +1,63 @@ +function coerceToBase64Url(thing, name) { + // Array or ArrayBuffer to Uint8Array + if (Array.isArray(thing)) { + thing = Uint8Array.from(thing); + } + + if (thing instanceof ArrayBuffer) { + thing = new Uint8Array(thing); + } + + // Uint8Array to base64 + if (thing instanceof Uint8Array) { + var str = ""; + var len = thing.byteLength; + + for (var i = 0; i < len; i++) { + str += String.fromCharCode(thing[i]); + } + thing = window.btoa(str); + } + + if (typeof thing !== "string") { + throw new Error("could not coerce '" + name + "' to string"); + } + + // base64 to base64url + // NOTE: "=" at the end of challenge is optional, strip it off here + thing = thing.replace(/\+/g, "-").replace(/\//g, "_").replace(/=*$/g, ""); + + return thing; +} + +function coerceToArrayBuffer(thing, name) { + if (typeof thing === "string") { + // base64url to base64 + thing = thing.replace(/-/g, "+").replace(/_/g, "/"); + + // base64 to Uint8Array + var str = window.atob(thing); + var bytes = new Uint8Array(str.length); + for (var i = 0; i < str.length; i++) { + bytes[i] = str.charCodeAt(i); + } + thing = bytes; + } + + // Array to Uint8Array + if (Array.isArray(thing)) { + thing = new Uint8Array(thing); + } + + // Uint8Array to ArrayBuffer + if (thing instanceof Uint8Array) { + thing = thing.buffer; + } + + // error if none of the above worked + if (!(thing instanceof ArrayBuffer)) { + throw new TypeError("could not coerce '" + name + "' to ArrayBuffer"); + } + + return thing; +} diff --git a/internal/api/ui/login/static/resources/scripts/webauthn.js b/internal/api/ui/login/static/resources/scripts/webauthn.js index e333c819c9..e8d9c54f41 100644 --- a/internal/api/ui/login/static/resources/scripts/webauthn.js +++ b/internal/api/ui/login/static/resources/scripts/webauthn.js @@ -1,31 +1,28 @@ function checkWebauthnSupported(button, func) { - let support = document.getElementsByClassName("wa-support"); - let noSupport = document.getElementsByClassName("wa-no-support"); - if (!window.PublicKeyCredential) { - for (let item of noSupport) { - item.classList.remove('hidden'); - } - for (let item of support) { - item.classList.add('hidden'); - } - return; + let support = document.getElementsByClassName("wa-support"); + let noSupport = document.getElementsByClassName("wa-no-support"); + if (!window.PublicKeyCredential) { + for (let item of noSupport) { + item.classList.remove("hidden"); } - document.getElementById(button).addEventListener('click', func); + for (let item of support) { + item.classList.add("hidden"); + } + return; + } + document.getElementById(button).addEventListener("click", func); } function webauthnError(error) { - let err = document.getElementById('wa-error'); - err.getElementsByClassName('cause')[0].innerText = error.message; - err.classList.remove('hidden'); + let err = document.getElementById("wa-error"); + err.getElementsByClassName("cause")[0].innerText = error.message; + err.classList.remove("hidden"); } -function bufferDecode(value) { - return decode(value); +function bufferDecode(value, name) { + return coerceToArrayBuffer(value, name); } -function bufferEncode(value) { - return encode(value) - .replace(/\+/g, "-") - .replace(/\//g, "_") - .replace(/=/g, ""); +function bufferEncode(value, name) { + return coerceToBase64Url(value, name); } diff --git a/internal/api/ui/login/static/resources/scripts/webauthn_login.js b/internal/api/ui/login/static/resources/scripts/webauthn_login.js index 3b0542799a..4265d76e62 100644 --- a/internal/api/ui/login/static/resources/scripts/webauthn_login.js +++ b/internal/api/ui/login/static/resources/scripts/webauthn_login.js @@ -1,41 +1,54 @@ -document.addEventListener('DOMContentLoaded', checkWebauthnSupported('btn-login', login)); +document.addEventListener( + "DOMContentLoaded", + checkWebauthnSupported("btn-login", login) +); function login() { - document.getElementById('wa-error').classList.add('hidden'); + document.getElementById("wa-error").classList.add("hidden"); - let makeAssertionOptions = JSON.parse(atob(document.getElementsByName('credentialAssertionData')[0].value)); - makeAssertionOptions.publicKey.challenge = bufferDecode(makeAssertionOptions.publicKey.challenge); - makeAssertionOptions.publicKey.allowCredentials.forEach(function (listItem) { - listItem.id = bufferDecode(listItem.id) - }); - navigator.credentials.get({ - publicKey: makeAssertionOptions.publicKey - }).then(function (credential) { - verifyAssertion(credential); - }).catch(function (err) { - webauthnError(err); + let makeAssertionOptions = JSON.parse( + atob(document.getElementsByName("credentialAssertionData")[0].value) + ); + makeAssertionOptions.publicKey.challenge = bufferDecode( + makeAssertionOptions.publicKey.challenge, + "publicKey.challenge" + ); + makeAssertionOptions.publicKey.allowCredentials.forEach(function (listItem) { + listItem.id = bufferDecode(listItem.id, "publicKey.allowCredentials.id"); + }); + navigator.credentials + .get({ + publicKey: makeAssertionOptions.publicKey, + }) + .then(function (credential) { + verifyAssertion(credential); + }) + .catch(function (err) { + webauthnError(err); }); } function verifyAssertion(assertedCredential) { - let authData = new Uint8Array(assertedCredential.response.authenticatorData); - let clientDataJSON = new Uint8Array(assertedCredential.response.clientDataJSON); - let rawId = new Uint8Array(assertedCredential.rawId); - let sig = new Uint8Array(assertedCredential.response.signature); - let userHandle = new Uint8Array(assertedCredential.response.userHandle); + let authData = new Uint8Array(assertedCredential.response.authenticatorData); + let clientDataJSON = new Uint8Array( + assertedCredential.response.clientDataJSON + ); + let rawId = new Uint8Array(assertedCredential.rawId); + let sig = new Uint8Array(assertedCredential.response.signature); + let userHandle = new Uint8Array(assertedCredential.response.userHandle); - let data = JSON.stringify({ - id: assertedCredential.id, - rawId: bufferEncode(rawId), - type: assertedCredential.type, - response: { - authenticatorData: bufferEncode(authData), - clientDataJSON: bufferEncode(clientDataJSON), - signature: bufferEncode(sig), - userHandle: bufferEncode(userHandle), - }, - }) + let data = JSON.stringify({ + id: assertedCredential.id, + rawId: bufferEncode(rawId), + type: assertedCredential.type, + response: { + authenticatorData: bufferEncode(authData), + clientDataJSON: bufferEncode(clientDataJSON), + signature: bufferEncode(sig), + userHandle: bufferEncode(userHandle), + }, + }); - document.getElementsByName('credentialData')[0].value = btoa(data); - document.getElementsByTagName('form')[0].submit(); -} \ No newline at end of file + document.getElementsByName("credentialData")[0].value = btoa(data); + document.getElementsByTagName("form")[0].submit(); +} diff --git a/internal/api/ui/login/static/resources/scripts/webauthn_register.js b/internal/api/ui/login/static/resources/scripts/webauthn_register.js index 153f6f1abf..7ce875905b 100644 --- a/internal/api/ui/login/static/resources/scripts/webauthn_register.js +++ b/internal/api/ui/login/static/resources/scripts/webauthn_register.js @@ -1,42 +1,61 @@ -document.addEventListener('DOMContentLoaded', checkWebauthnSupported('btn-register', registerCredential)); +document.addEventListener( + "DOMContentLoaded", + checkWebauthnSupported("btn-register", registerCredential) +); function registerCredential() { - document.getElementById('wa-error').classList.add('hidden'); + document.getElementById("wa-error").classList.add("hidden"); - let opt = JSON.parse(atob(document.getElementsByName('credentialCreationData')[0].value)); - opt.publicKey.challenge = bufferDecode(opt.publicKey.challenge); - opt.publicKey.user.id = bufferDecode(opt.publicKey.user.id); - if (opt.publicKey.excludeCredentials) { - for (let i = 0; i < opt.publicKey.excludeCredentials.length; i++) { - if (opt.publicKey.excludeCredentials[i].id !== null) { - opt.publicKey.excludeCredentials[i].id = bufferDecode(opt.publicKey.excludeCredentials[i].id); - } - } + let opt = JSON.parse( + atob(document.getElementsByName("credentialCreationData")[0].value) + ); + opt.publicKey.challenge = bufferDecode( + opt.publicKey.challenge, + "publicKey.challenge" + ); + opt.publicKey.user.id = bufferDecode( + opt.publicKey.user.id, + "publicKey.user.id" + ); + if (opt.publicKey.excludeCredentials) { + for (let i = 0; i < opt.publicKey.excludeCredentials.length; i++) { + if (opt.publicKey.excludeCredentials[i].id !== null) { + opt.publicKey.excludeCredentials[i].id = bufferDecode( + opt.publicKey.excludeCredentials[i].id, + "publicKey.excludeCredentials" + ); + } } - navigator.credentials.create({ - publicKey: opt.publicKey - }).then(function (credential) { - createCredential(credential); - }).catch(function (err) { - webauthnError(err); + } + navigator.credentials + .create({ + publicKey: opt.publicKey, + }) + .then(function (credential) { + createCredential(credential); + }) + .catch(function (err) { + webauthnError(err); }); } function createCredential(newCredential) { - let attestationObject = new Uint8Array(newCredential.response.attestationObject); - let clientDataJSON = new Uint8Array(newCredential.response.clientDataJSON); - let rawId = new Uint8Array(newCredential.rawId); + let attestationObject = new Uint8Array( + newCredential.response.attestationObject + ); + let clientDataJSON = new Uint8Array(newCredential.response.clientDataJSON); + let rawId = new Uint8Array(newCredential.rawId); - let data = JSON.stringify({ - id: newCredential.id, - rawId: bufferEncode(rawId), - type: newCredential.type, - response: { - attestationObject: bufferEncode(attestationObject), - clientDataJSON: bufferEncode(clientDataJSON), - }, - }); + let data = JSON.stringify({ + id: newCredential.id, + rawId: bufferEncode(rawId), + type: newCredential.type, + response: { + attestationObject: bufferEncode(attestationObject), + clientDataJSON: bufferEncode(clientDataJSON), + }, + }); - document.getElementsByName('credentialData')[0].value = btoa(data); - document.getElementsByTagName('form')[0].submit(); -} \ No newline at end of file + document.getElementsByName("credentialData")[0].value = btoa(data); + document.getElementsByTagName("form")[0].submit(); +} diff --git a/internal/api/ui/login/static/resources/themes/zitadel/css/zitadel.css b/internal/api/ui/login/static/resources/themes/zitadel/css/zitadel.css index acbae860e9..6f7a3e9723 100644 --- a/internal/api/ui/login/static/resources/themes/zitadel/css/zitadel.css +++ b/internal/api/ui/login/static/resources/themes/zitadel/css/zitadel.css @@ -218,7 +218,7 @@ body.waiting * { footer { width: 100%; box-sizing: border-box; - background: rgba(0, 0, 0, 0.1254901961); + background: #00000020; min-height: 50px; display: flex; align-items: center; @@ -759,7 +759,7 @@ i { letter-spacing: 0.05em; font-size: 12px; white-space: nowrap; - box-shadow: 0 0 3px rgba(0, 0, 0, 0.1019607843); + box-shadow: 0 0 3px #0000001a; width: fit-content; line-height: 1rem; } @@ -1211,7 +1211,7 @@ i { footer { width: 100%; box-sizing: border-box; - background: rgba(0, 0, 0, 0.1254901961); + background: #00000020; min-height: 50px; display: flex; align-items: center; @@ -1752,7 +1752,7 @@ i { letter-spacing: 0.05em; font-size: 12px; white-space: nowrap; - box-shadow: 0 0 3px rgba(0, 0, 0, 0.1019607843); + box-shadow: 0 0 3px #0000001a; width: fit-content; line-height: 1rem; } diff --git a/internal/api/ui/login/static/resources/themes/zitadel/css/zitadel.css.map b/internal/api/ui/login/static/resources/themes/zitadel/css/zitadel.css.map index c1bd90d0c0..0fa129cf1f 100644 --- a/internal/api/ui/login/static/resources/themes/zitadel/css/zitadel.css.map +++ b/internal/api/ui/login/static/resources/themes/zitadel/css/zitadel.css.map @@ -1 +1 @@ -{"version":3,"sourceRoot":"","sources":["../../scss/styles/vars.scss","../../scss/main.scss","../../scss/styles/footer/footer.scss","../../scss/styles/header/header.scss","../../scss/styles/button/button.scss","../../scss/styles/button/button_base.scss","../../scss/styles/input/input.scss","../../scss/styles/input/input_base.scss","../../scss/styles/label/label.scss","../../scss/styles/label/label_base.scss","../../scss/styles/radio/radio_base.scss","../../scss/styles/radio/radio.scss","../../scss/styles/a/a.scss","../../scss/styles/identity_provider/identity_provider.scss","../../scss/styles/identity_provider/identity_provider_base.scss","../../scss/styles/error/error.scss","../../scss/styles/qrcode/qrcode.scss","../../scss/styles/container/container.scss","../../scss/styles/account_selection/account_selection.scss","../../scss/styles/avatar/avatar.scss","../../scss/styles/checkbox/checkbox.scss","../../scss/styles/checkbox/checkbox_base.scss","../../scss/styles/select/select.scss","../../scss/styles/select/select_base.scss","../../scss/styles/list/list_base.scss","../../scss/styles/typography/faces/ailerons_font_faces.scss","../../scss/styles/typography/faces/lato_font_faces.scss","../../scss/styles/typography/faces/roboto_font_faces.scss","../../scss/styles/typography/faces/raleway_font_faces.scss","../../scss/styles/typography/faces/pt_sans_font_faces.scss","../../scss/styles/success_label/success_label.scss","../../scss/styles/mfa/mfa.scss","../../scss/styles/mfa/mfa_base.scss","../../scss/styles/register/register.scss","../../scss/styles/animations.scss","../../scss/styles/typography/typography.scss","../../scss/styles/core/core.scss","../../scss/styles/header/header_theme.scss","../../scss/styles/button/button_theme.scss","../../scss/styles/elevation/elevation.scss","../../scss/styles/input/input_theme.scss","../../scss/styles/radio/radio_theme.scss","../../scss/styles/checkbox/checkbox_theme.scss","../../scss/styles/label/label_theme.scss","../../scss/styles/footer/footer_theme.scss","../../scss/styles/a/a_theme.scss","../../scss/styles/error/error_theme.scss","../../scss/styles/qrcode/qrcode_theme.scss","../../scss/styles/container/container_theme.scss","../../scss/styles/account_selection/account_selection_theme.scss","../../scss/styles/avatar/avatar_theme.scss","../../scss/styles/select/select_theme.scss","../../scss/styles/list/list_theme.scss","../../scss/styles/identity_provider/identity_provider_theme.scss","../../scss/styles/success_label/success_label_theme.scss","../../scss/styles/mfa/mfa_theme.scss"],"names":[],"mappings":";AAAA;EACE;EACA;EACA;EACA;EAEA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EAEA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EAEA;EAEA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EAEA;EAEA;EAEA;EACA;EACA;EACA;EACA;EAEA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EAEA;EACA;EAEA;EACA;EACA;EACA;EACA;EACA;EAEA;EACA;AAAA;AAAA;EAIA;EAEA;EACA;EACA;EACA;AAAA;AAAA;EAGA;EACA;EACA;EACA;AAAA;AAAA;EAIA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EAEA;EACA;EAEA;EACA;EACA;EACA;EACA;EACA;EAEA;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EAEA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EAEA;EAEA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EAEA;EAEA;EAEA;EACA;EACA;EACA;EACA;EAEA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EAEA;EACA;EAEA;EACA;EACA;EACA;EACA;EACA;EAEA;EACA;AAAA;AAAA;EAIA;EAEA;EACA;EACA;EACA;AAAA;AAAA;EAGA;EACA;EACA;EACA;AAAA;AAAA;AAIA;EACA;EACA;EAEA;EACA;EACA;EACA;EACA;EACA;;;AC/NF;EACI;EACA;EACA;EACA;;AAEA;EACI;;AAGJ;EACI;;;AAIR;EACI;;;AAGJ;EACI;;;AChBJ;EACE;EACA;EACA;EACA,YAPc;EAQd;EACA;EACA,SATe;;AAWf;EACE;;AAGF;EACE;EACA;EACA;;AAGF;EAnBF;IAoBI;IACA;IACA;IACA;;;AAGF;EACE;;AAEA;EACE;EACA;EACA;;AAEA;EALF;IAMI;;;AAIJ;EACE;EACA;EACA;;;AC3CN;EACE;EACA;EACA,QALkB;EAMlB,SAPmB;EAQnB;EACA;EACA;EACA;;AAEA;EACE;EACA;EACA;EACA;EACA;EACA;;;ACjBJ;ECkBE;EACA;EACA;EACA;EACA;EACA;EAGA;EACA;EACA;EACA;EACA;EAGA,QAjCkB;EAkClB,WAnCqB;EAoCrB,aAlCuB;EAmCvB,SAtCmB;EAuCnB,eAnCyB;EAqCzB;EACA;;AAEA;EACE;;AAGF;EACE;;;AD3CJ;ECcE;EACA;EACA;EACA;EACA;EACA;EAGA;EACA;EACA;EACA;EACA;EAGA,QAjCkB;EAkClB,WAnCqB;EAoCrB,aAlCuB;EAmCvB,SAtCmB;EAuCnB,eAnCyB;EAqCzB;EACA;EAgBA;;AAdA;EACE;;AAGF;EACE;;;ADvCJ;EACE;EACA;EACA,OCCqB;EDArB;EACA;EACA,aCFqB;EDGrB,eCF8B;;ADI9B;EACE,aCJ0B;;;ADQ9B;EACE;EACA,SCf2B;EDgB3B,aCjB+B;;;ADoBjC;EACE;EACA,YC3B4B;;;AD+B5B;EACE;;;AEnCJ;AAAA;ECOI;EACA;EACA,sBAXsB;EAYtB;EACA;EACA;EACA;EACA,eAZsB;EAatB;EACA;EACA;EACA,cAfqB;EAgBrB,QAlBoB;EAmBpB,SArBgB;EAsBhB;EACA;EACA,QAvBe;;AAyBf;AAAA;EACI,WAtB0B;EAuB1B;;;ADrBR;AAAA;ECCI;EACA;EACA,sBAXsB;EAYtB;EACA;EACA;EACA;EACA,eAZsB;EAatB;EACA;EACA;EACA,cAfqB;EAgBrB,QAlBoB;EAmBpB,SArBgB;EAsBhB;EACA;EACA,QAvBe;;AAyBf;AAAA;EACI,WAtB0B;EAuB1B;;;ADhBR;EACE;;AAEA;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AEvBJ;ECGI;EACA,WANkB;EAOlB;EACA,QAPe;EAQf,aAPoB;;ADGtB;EACE;;;AEEJ;EACE;IACE;;EAGF;IACE;;EAGF;IACE;;;AChBJ;EDqBE;EACA;EACA;;AAEA;EACE;;AAGF;EACE;EACA,QA9Ba;EA+Bb;EACA,SA7BsB;EA8BtB;EACA;EACA;EACA;EACA;EACA,WAzCkB;EA0ClB;;AAEA;EAEE;EACA;EACA;EACA;EACA;;AAGF;EACE;EACA;EACA,OArDW;EAsDX,QAtDW;EAuDX;EACA;;AAGF;EACE;EACA;EACA,OA7DmB;EA8DnB,QA9DmB;EA+DnB;;AAIJ;EACE;EACA;EACA;EACA;EACA;EACA;;AAEA;EACE;;AAGF;EACE;;AAGF;EACE;;AAOA;EACE;EACA;;AAGF;EACE;;AAKN;AAAA;AAAA;EAGE;;;AE7GJ;EACE;EACA;EACA;;AAEA;EACE;;AAGF;EACE;EACA;;;ACTJ;ECOI;EACA,QAVa;EAWb;EACA;EACA;EACA;EACA,SAdc;EAed,eAboB;EAcpB;EACA;;AAEA;EACI;EACA;;AAGJ;EACI,aAxB4B;EAyB5B;EACA;EACA;;AAIA;EACI;EACA;EACA;EACA;EACA;EACA;EACA;;AAKJ;EACI;EACA;EACA;EACA;EACA;EACA;EACA;;AAKJ;EACI;EACA;EACA;EACA;EACA;EACA;EACA;;;AChEZ;EACI;EACA;EACA;;AACA;EACI;EACA;;;AAIR;EACI;;;ACXJ;EACI;EACA;EACA;EACA;;AAEA;EACI;EACA;EACA;;;ACHR;EACE,WAPwB;EAQxB;EACA;EACA,eAN4B;EAO5B;;AAEA;EAPF;IAQI,YAXuB;;;;AAe3B;EACE;EACA;EACA,QAnBqB;EAoBrB,SArBsB;EAsBtB;EACA;;AAGE;EACE;;AAGF;EACE;;AAIJ;EACE;EACA;EACA;EACA;;AAIA;EACE;;AAGF;EACE;;AAIJ;EACE;EACA;EACA;;AAIA;EACE;;AAGF;EACE;;AAIJ;EACE;;AAEA;EACE;EACA;;AAEF;EACE;EACA;EACA;EACA;EACA;;AAEA;EACE;EACA;;AAGF;EACE;;AAGF;EACE;;AAEA;EACE;EACA;EACA;EACA;;AAMR;EACE;EACA;EACA;EACA;;AAGF;EACE;EACA;;AAGF;EACE;EACA;;AAEA;EACE;EACA;;;AAKN;EACE;;;AC7HF;EACE;EACA;EACA;;AAEA;EACE;EACA;EACA;EACA;EACA;;AAEA;EACE;;AAGF;EACE;;AAGF;EACE;EACA;EACA;EACA;;AAEA;EACE;EACA;EACA;EACA;EACA;;AAGF;EACE;EACA;EACA;EACA;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAIJ;EACE;;AAGF;EACE;;AAEA;EACE;EACA;;;AC/DR;EACI,QAHc;EAId,OAJc;EAKd;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGJ;EACI;EACA;EACA;;;ACrBJ;ECCE;EACA;EACA;EACA,WANuB;EAOvB;EACA;;AAEA;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAIF;EACE;EACA;EACA;EACA;EACA,WA5BqB;EA6BrB;;AAEA;EACE;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;ACtDN;ECCE;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EACE;;AAGF;EACE;;;ACfA;EACI;EACA;;AAEA;EACI;;AAIR;EACI;EACA;EACA;EACA;EACA;;AAEA;EACI;EACA;;AAKJ;EACI;EACA;EACA;EACA;EACA;EACA;;AAIR;EACI;EACA;;;ACpCR;EACI;EACA;;ACFJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AC7DJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;;ACzEJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AC9GJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;ACtBJ;EACE;EACA;;;ACFF;ECDE;EACA;EACA;EACA;;AAEA;EACE;EACA;EACA;EACA;EACA;;AAEA;EACE;EACA;EACA;EACA;;AAEA;EACE;EACA;EACA;EACA;EACA;;AAGF;EACE;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;;;A9BlCR;EACE;EACA;EACA;EACA,YAPc;EAQd;EACA;EACA,SATe;;AAWf;EACE;;AAGF;EACE;EACA;EACA;;AAGF;EAnBF;IAoBI;IACA;IACA;IACA;;;AAGF;EACE;;AAEA;EACE;EACA;EACA;;AAEA;EALF;IAMI;;;AAIJ;EACE;EACA;EACA;;;AC3CN;EACE;EACA;EACA,QALkB;EAMlB,SAPmB;EAQnB;EACA;EACA;EACA;;AAEA;EACE;EACA;EACA;EACA;EACA;EACA;;;ACjBJ;ECkBE;EACA;EACA;EACA;EACA;EACA;EAGA;EACA;EACA;EACA;EACA;EAGA,QAjCkB;EAkClB,WAnCqB;EAoCrB,aAlCuB;EAmCvB,SAtCmB;EAuCnB,eAnCyB;EAqCzB;EACA;;AAEA;EACE;;AAGF;EACE;;;AD3CJ;ECcE;EACA;EACA;EACA;EACA;EACA;EAGA;EACA;EACA;EACA;EACA;EAGA,QAjCkB;EAkClB,WAnCqB;EAoCrB,aAlCuB;EAmCvB,SAtCmB;EAuCnB,eAnCyB;EAqCzB;EACA;EAgBA;;AAdA;EACE;;AAGF;EACE;;;ADvCJ;EACE;EACA;EACA,OCCqB;EDArB;EACA;EACA,aCFqB;EDGrB,eCF8B;;ADI9B;EACE,aCJ0B;;;ADQ9B;EACE;EACA,SCf2B;EDgB3B,aCjB+B;;;ADoBjC;EACE;EACA,YC3B4B;;;AD+B5B;EACE;;;AEnCJ;AAAA;ECOI;EACA;EACA,sBAXsB;EAYtB;EACA;EACA;EACA;EACA,eAZsB;EAatB;EACA;EACA;EACA,cAfqB;EAgBrB,QAlBoB;EAmBpB,SArBgB;EAsBhB;EACA;EACA,QAvBe;;AAyBf;AAAA;EACI,WAtB0B;EAuB1B;;;ADrBR;AAAA;ECCI;EACA;EACA,sBAXsB;EAYtB;EACA;EACA;EACA;EACA,eAZsB;EAatB;EACA;EACA;EACA,cAfqB;EAgBrB,QAlBoB;EAmBpB,SArBgB;EAsBhB;EACA;EACA,QAvBe;;AAyBf;AAAA;EACI,WAtB0B;EAuB1B;;;ADhBR;EACE;;AAEA;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AEvBJ;ECGI;EACA,WANkB;EAOlB;EACA,QAPe;EAQf,aAPoB;;ADGtB;EACE;;;AEEJ;EACE;IACE;;EAGF;IACE;;EAGF;IACE;;;AChBJ;EDqBE;EACA;EACA;;AAEA;EACE;;AAGF;EACE;EACA,QA9Ba;EA+Bb;EACA,SA7BsB;EA8BtB;EACA;EACA;EACA;EACA;EACA,WAzCkB;EA0ClB;;AAEA;EAEE;EACA;EACA;EACA;EACA;;AAGF;EACE;EACA;EACA,OArDW;EAsDX,QAtDW;EAuDX;EACA;;AAGF;EACE;EACA;EACA,OA7DmB;EA8DnB,QA9DmB;EA+DnB;;AAIJ;EACE;EACA;EACA;EACA;EACA;EACA;;AAEA;EACE;;AAGF;EACE;;AAGF;EACE;;AAOA;EACE;EACA;;AAGF;EACE;;AAKN;AAAA;AAAA;EAGE;;;AE7GJ;EACE;EACA;EACA;;AAEA;EACE;;AAGF;EACE;EACA;;;ACTJ;ECOI;EACA,QAVa;EAWb;EACA;EACA;EACA;EACA,SAdc;EAed,eAboB;EAcpB;EACA;;AAEA;EACI;EACA;;AAGJ;EACI,aAxB4B;EAyB5B;EACA;EACA;;AAIA;EACI;EACA;EACA;EACA;EACA;EACA;EACA;;AAKJ;EACI;EACA;EACA;EACA;EACA;EACA;EACA;;AAKJ;EACI;EACA;EACA;EACA;EACA;EACA;EACA;;;AChEZ;EACI;EACA;EACA;;AACA;EACI;EACA;;;AAIR;EACI;;;ACXJ;EACI;EACA;EACA;EACA;;AAEA;EACI;EACA;EACA;;;ACHR;EACE,WAPwB;EAQxB;EACA;EACA,eAN4B;EAO5B;;AAEA;EAPF;IAQI,YAXuB;;;;AAe3B;EACE;EACA;EACA,QAnBqB;EAoBrB,SArBsB;EAsBtB;EACA;;AAGE;EACE;;AAGF;EACE;;AAIJ;EACE;EACA;EACA;EACA;;AAIA;EACE;;AAGF;EACE;;AAIJ;EACE;EACA;EACA;;AAIA;EACE;;AAGF;EACE;;AAIJ;EACE;;AAEA;EACE;EACA;;AAEF;EACE;EACA;EACA;EACA;EACA;;AAEA;EACE;EACA;;AAGF;EACE;;AAGF;EACE;;AAEA;EACE;EACA;EACA;EACA;;AAMR;EACE;EACA;EACA;EACA;;AAGF;EACE;EACA;;AAGF;EACE;EACA;;AAEA;EACE;EACA;;;AAKN;EACE;;;AC7HF;EACE;EACA;EACA;;AAEA;EACE;EACA;EACA;EACA;EACA;;AAEA;EACE;;AAGF;EACE;;AAGF;EACE;EACA;EACA;EACA;;AAEA;EACE;EACA;EACA;EACA;EACA;;AAGF;EACE;EACA;EACA;EACA;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAIJ;EACE;;AAGF;EACE;;AAEA;EACE;EACA;;;AC/DR;EACI,QAHc;EAId,OAJc;EAKd;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGJ;EACI;EACA;EACA;;;ACrBJ;ECCE;EACA;EACA;EACA,WANuB;EAOvB;EACA;;AAEA;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAIF;EACE;EACA;EACA;EACA;EACA,WA5BqB;EA6BrB;;AAEA;EACE;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;ACtDN;ECCE;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EACE;;AAGF;EACE;;;ACfA;EACI;EACA;;AAEA;EACI;;AAIR;EACI;EACA;EACA;EACA;EACA;;AAEA;EACI;EACA;;AAKJ;EACI;EACA;EACA;EACA;EACA;EACA;;AAIR;EACI;EACA;;;ACpCR;EACI;EACA;;ACFJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AC7DJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;;ACzEJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AC9GJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;ACtBJ;EACE;EACA;;;ACFF;ECDE;EACA;EACA;EACA;;AAEA;EACE;EACA;EACA;EACA;EACA;;AAEA;EACE;EACA;EACA;EACA;;AAEA;EACE;EACA;EACA;EACA;EACA;;AAGF;EACE;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;;;APtCR;EACI;EACA;;ACFJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AC7DJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;;ACzEJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AC9GJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AZlBJ;EACE,WAPwB;EAQxB;EACA;EACA,eAN4B;EAO5B;;AAEA;EAPF;IAQI,YAXuB;;;;AAe3B;EACE;EACA;EACA,QAnBqB;EAoBrB,SArBsB;EAsBtB;EACA;;AAGE;EACE;;AAGF;EACE;;AAIJ;EACE;EACA;EACA;EACA;;AAIA;EACE;;AAGF;EACE;;AAIJ;EACE;EACA;EACA;;AAIA;EACE;;AAGF;EACE;;AAIJ;EACE;;AAEA;EACE;EACA;;AAEF;EACE;EACA;EACA;EACA;EACA;;AAEA;EACE;EACA;;AAGF;EACE;;AAGF;EACE;;AAEA;EACE;EACA;EACA;EACA;;AAMR;EACE;EACA;EACA;EACA;;AAGF;EACE;EACA;;AAGF;EACE;EACA;;AAEA;EACE;EACA;;;AAKN;EACE;;;AgB9HE;EACI;EACA;EACA;;AAEA;EACI;EACA;EACA;EACA;;AAIR;EACI;;;A9BdR;EACE;EACA;EACA,QALkB;EAMlB,SAPmB;EAQnB;EACA;EACA;EACA;;AAEA;EACE;EACA;EACA;EACA;EACA;EACA;;;A+BnBJ;EACI;IACE;;EAGF;IACE;;EAGF;IACE;;EAGF;IACE;;;AAIN;EACI;EACA;;;ACqIA;EANE;EACA,aAlEY;EAGd;EAsEE;;;AAGF;EAXE;EACA,aAlEY;EAGd;EA2EE;;;AAGF;EAhBE;EACA,aAlEY;EAGd;EAgFE;;;AAGF;EArBE;EACA,aAlEY;EAGd;EAqFE;;;AAGF;EA1BE;EACA,aAlEY;EAGd;;;AA4FA;EA9BE;EACA,aAlEY;EAGd;;AA+FE;EACE;;;AAIJ;EAtCE;EACA,aAlEY;EAGd;;;AAwGA;EA1CE;EACA,aAlEY;EAGd;;;AA4GA;EA9CE;EACA,aAlEY;EAGd;;;AAgHA;EAlDE;EACA,aAlEY;EAGd;;;AAoHA;EAtDE;EACA,aAlEY;EAGd;EAsHI;;;AAGJ;EA3DE;EACA,aAlEY;EAGd;EA2HE;;;AAGF;EAhEE;EACA,aAlEY;EAGd;EAgIE;;;AAGF;EArEE;EACA,aAlEY;EAGd;EAqIE;;;AAGF;EA1EE;EACA,aAlEY;EAGd;EA0IE;;;ACvNF;EACE;EACA;;;AAKA;EACE;;;ACTJ;EACI;EACA;;;ACAN;AAAA;AAAA;EAGE;EACA;;AA0HF;AAAA;AAAA;EACE,OAPM;;AASR;AAAA;AAAA;EACE,OAVM;;AAYR;AAAA;AAAA;EACE,OAbM;;AAoBN;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;EACE;;;AApIJ;AAAA;EAEE;;;AAGF;EACE;;;AAGF;AAAA;EC2GA;EDxGE;;;AAGF;EACE;;;AAqGF;EACE,OAPM;;AASR;EACE,OAVM;;AAYR;EACE,OAbM;;AAoBN;EACE;;AA/GF;EACE;;AAGF;EACE;;AAGF;EACE;;;AAIJ;EACE;EACA;;AAiFF;EACE,OAPM;;AASR;EACE,OAVM;;AAYR;EACE,OAbM;;AAoBN;EACE;;AAMJ;EACE;;AAEF;EACE;;AAEF;EACE;;AAOA;EACE;;AAhHF;EACE;;;AAIJ;AAAA;ECsEA;;;ADjEA;ECiEA;;AD9DE;EC8DF;;ADtDE;ECsDF;;;AC9HA;AAAA;AAAA;EAGE;;;AAGF;AAAA;AAAA;EAGE;EACA;;AAEA;AAAA;AAAA;EACE;;AAGF;AAAA;AAAA;EACE;;AAGF;AAAA;AAAA;EACE;;AAIF;AAAA;AAAA;EACE;;;AAIJ;AAAA;AAAA;EAGE;;;AChCI;EACI;;AAGJ;EACI;;;AAIR;EACI;;AAEA;EACI;;AAGJ;EACI;EACA;;AAIA;EACI;;AAIA;EACI;;AAGJ;EACI;;AAIR;EACI;;;ACtCd;EACE;EACA;EACA;EACA;EACA;EACA;EACA;;;AAKA;EACE;EACA;;AAGF;EACE;EACA;EACA;;;AAMF;EACE;EACA;;AAGF;EACE;;;AC7BF;EACE;;;ACFJ;EACE;EACA;EACA;;AAEA;EALF;IAMI;IACA;;;AAGF;EACE;;AAGF;EACE;EACA;EACA;;;ACpBJ;EACE;;AAEA;EAEE;;;ACJF;EACE;;;ACAE;EACI;;AAGJ;EACI;;AAGJ;EACI;;;ACTJ;EACI;;;AAKJ;EACI;;;ACTV;EACE;EACA;EACA;;AAEA;EACE;EACA;EACA;;AAEA;EACE;;AAGF;EACE;;AAGF;EACE;;AAGF;EACE;EACA;;AAGF;EACE;;AAEA;EACE;EACA;;AAGF;EACE;EACA;;;ACnBR;EACE,kBAPM;;AASR;EACE,kBAVM;;AAYR;EACE,kBAbM;;AAoBN;EAME,kBALY;;;AChCd;EACI;;;ACCJ;AAAA;EACE;;AAIA;AAAA;EACE;;AAGF;AAAA;EACE;;;ACbN;EACE;EACA;;AAEA;Ed2HF;;AcvHE;EduHF;;AcnHE;EACE;EACA;;AAGF;EACE;EACA;;AAGF;EACE;EACA;;;AAKF;EACE;;;AC7BJ;EACE;EACA;;;ACAQ;EACE;EACA;;AAKN;EACE;;AAGF;EACE;EACA;EACA","file":"zitadel.css"} +{"version":3,"sourceRoot":"","sources":["../../scss/styles/vars.scss","../../scss/main.scss","../../scss/styles/footer/footer.scss","../../scss/styles/header/header.scss","../../scss/styles/button/button.scss","../../scss/styles/button/button_base.scss","../../scss/styles/input/input.scss","../../scss/styles/input/input_base.scss","../../scss/styles/label/label.scss","../../scss/styles/label/label_base.scss","../../scss/styles/radio/radio_base.scss","../../scss/styles/radio/radio.scss","../../scss/styles/a/a.scss","../../scss/styles/identity_provider/identity_provider.scss","../../scss/styles/identity_provider/identity_provider_base.scss","../../scss/styles/error/error.scss","../../scss/styles/qrcode/qrcode.scss","../../scss/styles/container/container.scss","../../scss/styles/account_selection/account_selection.scss","../../scss/styles/avatar/avatar.scss","../../scss/styles/checkbox/checkbox.scss","../../scss/styles/checkbox/checkbox_base.scss","../../scss/styles/select/select.scss","../../scss/styles/select/select_base.scss","../../scss/styles/list/list_base.scss","../../scss/styles/typography/faces/ailerons_font_faces.scss","../../scss/styles/typography/faces/lato_font_faces.scss","../../scss/styles/typography/faces/roboto_font_faces.scss","../../scss/styles/typography/faces/raleway_font_faces.scss","../../scss/styles/typography/faces/pt_sans_font_faces.scss","../../scss/styles/success_label/success_label.scss","../../scss/styles/mfa/mfa.scss","../../scss/styles/mfa/mfa_base.scss","../../scss/styles/register/register.scss","../../scss/styles/animations.scss","../../scss/styles/typography/typography.scss","../../scss/styles/core/core.scss","../../scss/styles/header/header_theme.scss","../../scss/styles/button/button_theme.scss","../../scss/styles/elevation/elevation.scss","../../scss/styles/input/input_theme.scss","../../scss/styles/radio/radio_theme.scss","../../scss/styles/checkbox/checkbox_theme.scss","../../scss/styles/label/label_theme.scss","../../scss/styles/footer/footer_theme.scss","../../scss/styles/a/a_theme.scss","../../scss/styles/error/error_theme.scss","../../scss/styles/qrcode/qrcode_theme.scss","../../scss/styles/container/container_theme.scss","../../scss/styles/account_selection/account_selection_theme.scss","../../scss/styles/avatar/avatar_theme.scss","../../scss/styles/select/select_theme.scss","../../scss/styles/list/list_theme.scss","../../scss/styles/identity_provider/identity_provider_theme.scss","../../scss/styles/success_label/success_label_theme.scss","../../scss/styles/mfa/mfa_theme.scss"],"names":[],"mappings":";AAAA;EACE;EACA;EACA;EACA;EAEA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EAEA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EAEA;EAEA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EAEA;EAEA;EAEA;EACA;EACA;EACA;EACA;EAEA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EAEA;EACA;EAEA;EACA;EACA;EACA;EACA;EACA;EAEA;EACA;AAAA;AAAA;EAIA;EAEA;EACA;EACA;EACA;AAAA;AAAA;EAGA;EACA;EACA;EACA;AAAA;AAAA;EAIA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EAEA;EACA;EAEA;EACA;EACA;EACA;EACA;EACA;EAEA;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EAEA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EAEA;EAEA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EAEA;EAEA;EAEA;EACA;EACA;EACA;EACA;EAEA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EAEA;EACA;EAEA;EACA;EACA;EACA;EACA;EACA;EAEA;EACA;AAAA;AAAA;EAIA;EAEA;EACA;EACA;EACA;AAAA;AAAA;EAGA;EACA;EACA;EACA;AAAA;AAAA;AAIA;EACA;EACA;EAEA;EACA;EACA;EACA;EACA;EACA;;;AC/NF;EACI;EACA;EACA;EACA;;AAEA;EACI;;AAGJ;EACI;;;AAIR;EACI;;;AAGJ;EACI;;;AChBJ;EACE;EACA;EACA;EACA,YAPc;EAQd;EACA;EACA,SATe;;AAWf;EACE;;AAGF;EACE;EACA;EACA;;AAGF;EAnBF;IAoBI;IACA;IACA;IACA;;;AAGF;EACE;;AAEA;EACE;EACA;EACA;;AAEA;EALF;IAMI;;;AAIJ;EACE;EACA;EACA;;;AC3CN;EACE;EACA;EACA,QALkB;EAMlB,SAPmB;EAQnB;EACA;EACA;EACA;;AAEA;EACE;EACA;EACA;EACA;EACA;EACA;;;ACjBJ;ECkBE;EACA;EACA;EACA;EACA;EACA;EAGA;EACA;EACA;EACA;EACA;EAGA,QAjCkB;EAkClB,WAnCqB;EAoCrB,aAlCuB;EAmCvB,SAtCmB;EAuCnB,eAnCyB;EAqCzB;EACA;;AAEA;EACE;;AAGF;EACE;;;AD3CJ;ECcE;EACA;EACA;EACA;EACA;EACA;EAGA;EACA;EACA;EACA;EACA;EAGA,QAjCkB;EAkClB,WAnCqB;EAoCrB,aAlCuB;EAmCvB,SAtCmB;EAuCnB,eAnCyB;EAqCzB;EACA;EAgBA;;AAdA;EACE;;AAGF;EACE;;;ADvCJ;EACE;EACA;EACA,OCCqB;EDArB;EACA;EACA,aCFqB;EDGrB,eCF8B;;ADI9B;EACE,aCJ0B;;;ADQ9B;EACE;EACA,SCf2B;EDgB3B,aCjB+B;;;ADoBjC;EACE;EACA,YC3B4B;;;AD+B5B;EACE;;;AEnCJ;AAAA;ECOI;EACA;EACA,sBAXsB;EAYtB;EACA;EACA;EACA;EACA,eAZsB;EAatB;EACA;EACA;EACA,cAfqB;EAgBrB,QAlBoB;EAmBpB,SArBgB;EAsBhB;EACA;EACA,QAvBe;;AAyBf;AAAA;EACI,WAtB0B;EAuB1B;;;ADrBR;AAAA;ECCI;EACA;EACA,sBAXsB;EAYtB;EACA;EACA;EACA;EACA,eAZsB;EAatB;EACA;EACA;EACA,cAfqB;EAgBrB,QAlBoB;EAmBpB,SArBgB;EAsBhB;EACA;EACA,QAvBe;;AAyBf;AAAA;EACI,WAtB0B;EAuB1B;;;ADhBR;EACE;;AAEA;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AEvBJ;ECGI;EACA,WANkB;EAOlB;EACA,QAPe;EAQf,aAPoB;;ADGtB;EACE;;;AEEJ;EACE;IACE;;EAGF;IACE;;EAGF;IACE;;;AChBJ;EDqBE;EACA;EACA;;AAEA;EACE;;AAGF;EACE;EACA,QA9Ba;EA+Bb;EACA,SA7BsB;EA8BtB;EACA;EACA;EACA;EACA;EACA,WAzCkB;EA0ClB;;AAEA;EAEE;EACA;EACA;EACA;EACA;;AAGF;EACE;EACA;EACA,OArDW;EAsDX,QAtDW;EAuDX;EACA;;AAGF;EACE;EACA;EACA,OA7DmB;EA8DnB,QA9DmB;EA+DnB;;AAIJ;EACE;EACA;EACA;EACA;EACA;EACA;;AAEA;EACE;;AAGF;EACE;;AAGF;EACE;;AAOA;EACE;EACA;;AAGF;EACE;;AAKN;AAAA;AAAA;EAGE;;;AE7GJ;EACE;EACA;EACA;;AAEA;EACE;;AAGF;EACE;EACA;;;ACTJ;ECOI;EACA,QAVa;EAWb;EACA;EACA;EACA;EACA,SAdc;EAed,eAboB;EAcpB;EACA;;AAEA;EACI;EACA;;AAGJ;EACI,aAxB4B;EAyB5B;EACA;EACA;;AAIA;EACI;EACA;EACA;EACA;EACA;EACA;EACA;;AAKJ;EACI;EACA;EACA;EACA;EACA;EACA;EACA;;AAKJ;EACI;EACA;EACA;EACA;EACA;EACA;EACA;;;AChEZ;EACI;EACA;EACA;;AACA;EACI;EACA;;;AAIR;EACI;;;ACXJ;EACI;EACA;EACA;EACA;;AAEA;EACI;EACA;EACA;;;ACHR;EACE,WAPwB;EAQxB;EACA;EACA,eAN4B;EAO5B;;AAEA;EAPF;IAQI,YAXuB;;;;AAe3B;EACE;EACA;EACA,QAnBqB;EAoBrB,SArBsB;EAsBtB;EACA;;AAGE;EACE;;AAGF;EACE;;AAIJ;EACE;EACA;EACA;EACA;;AAIA;EACE;;AAGF;EACE;;AAIJ;EACE;EACA;EACA;;AAIA;EACE;;AAGF;EACE;;AAIJ;EACE;;AAEA;EACE;EACA;;AAEF;EACE;EACA;EACA;EACA;EACA;;AAEA;EACE;EACA;;AAGF;EACE;;AAGF;EACE;;AAEA;EACE;EACA;EACA;EACA;;AAMR;EACE;EACA;EACA;EACA;;AAGF;EACE;EACA;;AAGF;EACE;EACA;;AAEA;EACE;EACA;;;AAKN;EACE;;;AC7HF;EACE;EACA;EACA;;AAEA;EACE;EACA;EACA;EACA;EACA;;AAEA;EACE;;AAGF;EACE;;AAGF;EACE;EACA;EACA;EACA;;AAEA;EACE;EACA;EACA;EACA;EACA;;AAGF;EACE;EACA;EACA;EACA;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAIJ;EACE;;AAGF;EACE;;AAEA;EACE;EACA;;;AC/DR;EACI,QAHc;EAId,OAJc;EAKd;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGJ;EACI;EACA;EACA;;;ACrBJ;ECCE;EACA;EACA;EACA,WANuB;EAOvB;EACA;;AAEA;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAIF;EACE;EACA;EACA;EACA;EACA,WA5BqB;EA6BrB;;AAEA;EACE;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;ACtDN;ECCE;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EACE;;AAGF;EACE;;;ACfA;EACI;EACA;;AAEA;EACI;;AAIR;EACI;EACA;EACA;EACA;EACA;;AAEA;EACI;EACA;;AAKJ;EACI;EACA;EACA;EACA;EACA;EACA;;AAIR;EACI;EACA;;;ACpCR;EACI;EACA;;ACFJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AC7DJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;;ACzEJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AC9GJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;ACtBJ;EACE;EACA;;;ACFF;ECDE;EACA;EACA;EACA;;AAEA;EACE;EACA;EACA;EACA;EACA;;AAEA;EACE;EACA;EACA;EACA;;AAEA;EACE;EACA;EACA;EACA;EACA;;AAGF;EACE;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;;;A9BlCR;EACE;EACA;EACA;EACA,YAPc;EAQd;EACA;EACA,SATe;;AAWf;EACE;;AAGF;EACE;EACA;EACA;;AAGF;EAnBF;IAoBI;IACA;IACA;IACA;;;AAGF;EACE;;AAEA;EACE;EACA;EACA;;AAEA;EALF;IAMI;;;AAIJ;EACE;EACA;EACA;;;AC3CN;EACE;EACA;EACA,QALkB;EAMlB,SAPmB;EAQnB;EACA;EACA;EACA;;AAEA;EACE;EACA;EACA;EACA;EACA;EACA;;;ACjBJ;ECkBE;EACA;EACA;EACA;EACA;EACA;EAGA;EACA;EACA;EACA;EACA;EAGA,QAjCkB;EAkClB,WAnCqB;EAoCrB,aAlCuB;EAmCvB,SAtCmB;EAuCnB,eAnCyB;EAqCzB;EACA;;AAEA;EACE;;AAGF;EACE;;;AD3CJ;ECcE;EACA;EACA;EACA;EACA;EACA;EAGA;EACA;EACA;EACA;EACA;EAGA,QAjCkB;EAkClB,WAnCqB;EAoCrB,aAlCuB;EAmCvB,SAtCmB;EAuCnB,eAnCyB;EAqCzB;EACA;EAgBA;;AAdA;EACE;;AAGF;EACE;;;ADvCJ;EACE;EACA;EACA,OCCqB;EDArB;EACA;EACA,aCFqB;EDGrB,eCF8B;;ADI9B;EACE,aCJ0B;;;ADQ9B;EACE;EACA,SCf2B;EDgB3B,aCjB+B;;;ADoBjC;EACE;EACA,YC3B4B;;;AD+B5B;EACE;;;AEnCJ;AAAA;ECOI;EACA;EACA,sBAXsB;EAYtB;EACA;EACA;EACA;EACA,eAZsB;EAatB;EACA;EACA;EACA,cAfqB;EAgBrB,QAlBoB;EAmBpB,SArBgB;EAsBhB;EACA;EACA,QAvBe;;AAyBf;AAAA;EACI,WAtB0B;EAuB1B;;;ADrBR;AAAA;ECCI;EACA;EACA,sBAXsB;EAYtB;EACA;EACA;EACA;EACA,eAZsB;EAatB;EACA;EACA;EACA,cAfqB;EAgBrB,QAlBoB;EAmBpB,SArBgB;EAsBhB;EACA;EACA,QAvBe;;AAyBf;AAAA;EACI,WAtB0B;EAuB1B;;;ADhBR;EACE;;AAEA;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AEvBJ;ECGI;EACA,WANkB;EAOlB;EACA,QAPe;EAQf,aAPoB;;ADGtB;EACE;;;AEEJ;EACE;IACE;;EAGF;IACE;;EAGF;IACE;;;AChBJ;EDqBE;EACA;EACA;;AAEA;EACE;;AAGF;EACE;EACA,QA9Ba;EA+Bb;EACA,SA7BsB;EA8BtB;EACA;EACA;EACA;EACA;EACA,WAzCkB;EA0ClB;;AAEA;EAEE;EACA;EACA;EACA;EACA;;AAGF;EACE;EACA;EACA,OArDW;EAsDX,QAtDW;EAuDX;EACA;;AAGF;EACE;EACA;EACA,OA7DmB;EA8DnB,QA9DmB;EA+DnB;;AAIJ;EACE;EACA;EACA;EACA;EACA;EACA;;AAEA;EACE;;AAGF;EACE;;AAGF;EACE;;AAOA;EACE;EACA;;AAGF;EACE;;AAKN;AAAA;AAAA;EAGE;;;AE7GJ;EACE;EACA;EACA;;AAEA;EACE;;AAGF;EACE;EACA;;;ACTJ;ECOI;EACA,QAVa;EAWb;EACA;EACA;EACA;EACA,SAdc;EAed,eAboB;EAcpB;EACA;;AAEA;EACI;EACA;;AAGJ;EACI,aAxB4B;EAyB5B;EACA;EACA;;AAIA;EACI;EACA;EACA;EACA;EACA;EACA;EACA;;AAKJ;EACI;EACA;EACA;EACA;EACA;EACA;EACA;;AAKJ;EACI;EACA;EACA;EACA;EACA;EACA;EACA;;;AChEZ;EACI;EACA;EACA;;AACA;EACI;EACA;;;AAIR;EACI;;;ACXJ;EACI;EACA;EACA;EACA;;AAEA;EACI;EACA;EACA;;;ACHR;EACE,WAPwB;EAQxB;EACA;EACA,eAN4B;EAO5B;;AAEA;EAPF;IAQI,YAXuB;;;;AAe3B;EACE;EACA;EACA,QAnBqB;EAoBrB,SArBsB;EAsBtB;EACA;;AAGE;EACE;;AAGF;EACE;;AAIJ;EACE;EACA;EACA;EACA;;AAIA;EACE;;AAGF;EACE;;AAIJ;EACE;EACA;EACA;;AAIA;EACE;;AAGF;EACE;;AAIJ;EACE;;AAEA;EACE;EACA;;AAEF;EACE;EACA;EACA;EACA;EACA;;AAEA;EACE;EACA;;AAGF;EACE;;AAGF;EACE;;AAEA;EACE;EACA;EACA;EACA;;AAMR;EACE;EACA;EACA;EACA;;AAGF;EACE;EACA;;AAGF;EACE;EACA;;AAEA;EACE;EACA;;;AAKN;EACE;;;AC7HF;EACE;EACA;EACA;;AAEA;EACE;EACA;EACA;EACA;EACA;;AAEA;EACE;;AAGF;EACE;;AAGF;EACE;EACA;EACA;EACA;;AAEA;EACE;EACA;EACA;EACA;EACA;;AAGF;EACE;EACA;EACA;EACA;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAIJ;EACE;;AAGF;EACE;;AAEA;EACE;EACA;;;AC/DR;EACI,QAHc;EAId,OAJc;EAKd;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGJ;EACI;EACA;EACA;;;ACrBJ;ECCE;EACA;EACA;EACA,WANuB;EAOvB;EACA;;AAEA;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAIF;EACE;EACA;EACA;EACA;EACA,WA5BqB;EA6BrB;;AAEA;EACE;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;ACtDN;ECCE;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EACE;;AAGF;EACE;;;ACfA;EACI;EACA;;AAEA;EACI;;AAIR;EACI;EACA;EACA;EACA;EACA;;AAEA;EACI;EACA;;AAKJ;EACI;EACA;EACA;EACA;EACA;EACA;;AAIR;EACI;EACA;;;ACpCR;EACI;EACA;;ACFJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AC7DJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;;ACzEJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AC9GJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;ACtBJ;EACE;EACA;;;ACFF;ECDE;EACA;EACA;EACA;;AAEA;EACE;EACA;EACA;EACA;EACA;;AAEA;EACE;EACA;EACA;EACA;;AAEA;EACE;EACA;EACA;EACA;EACA;;AAGF;EACE;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;;;APtCR;EACI;EACA;;ACFJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AC7DJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;;ACzEJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AC9GJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AZlBJ;EACE,WAPwB;EAQxB;EACA;EACA,eAN4B;EAO5B;;AAEA;EAPF;IAQI,YAXuB;;;;AAe3B;EACE;EACA;EACA,QAnBqB;EAoBrB,SArBsB;EAsBtB;EACA;;AAGE;EACE;;AAGF;EACE;;AAIJ;EACE;EACA;EACA;EACA;;AAIA;EACE;;AAGF;EACE;;AAIJ;EACE;EACA;EACA;;AAIA;EACE;;AAGF;EACE;;AAIJ;EACE;;AAEA;EACE;EACA;;AAEF;EACE;EACA;EACA;EACA;EACA;;AAEA;EACE;EACA;;AAGF;EACE;;AAGF;EACE;;AAEA;EACE;EACA;EACA;EACA;;AAMR;EACE;EACA;EACA;EACA;;AAGF;EACE;EACA;;AAGF;EACE;EACA;;AAEA;EACE;EACA;;;AAKN;EACE;;;AgB9HE;EACI;EACA;EACA;;AAEA;EACI;EACA;EACA;EACA;;AAIR;EACI;;;A9BdR;EACE;EACA;EACA,QALkB;EAMlB,SAPmB;EAQnB;EACA;EACA;EACA;;AAEA;EACE;EACA;EACA;EACA;EACA;EACA;;;A+BnBJ;EACI;IACE;;EAGF;IACE;;EAGF;IACE;;EAGF;IACE;;;AAIN;EACI;EACA;;;ACqIA;EANE;EACA,aAlEY;EAGd;EAsEE;;;AAGF;EAXE;EACA,aAlEY;EAGd;EA2EE;;;AAGF;EAhBE;EACA,aAlEY;EAGd;EAgFE;;;AAGF;EArBE;EACA,aAlEY;EAGd;EAqFE;;;AAGF;EA1BE;EACA,aAlEY;EAGd;;;AA4FA;EA9BE;EACA,aAlEY;EAGd;;AA+FE;EACE;;;AAIJ;EAtCE;EACA,aAlEY;EAGd;;;AAwGA;EA1CE;EACA,aAlEY;EAGd;;;AA4GA;EA9CE;EACA,aAlEY;EAGd;;;AAgHA;EAlDE;EACA,aAlEY;EAGd;;;AAoHA;EAtDE;EACA,aAlEY;EAGd;EAsHI;;;AAGJ;EA3DE;EACA,aAlEY;EAGd;EA2HE;;;AAGF;EAhEE;EACA,aAlEY;EAGd;EAgIE;;;AAGF;EArEE;EACA,aAlEY;EAGd;EAqIE;;;AAGF;EA1EE;EACA,aAlEY;EAGd;EA0IE;;;ACvNF;EACE;EACA;;;AAKA;EACE;;;ACTJ;EACI;EACA;;;ACAN;AAAA;AAAA;EAGE;EACA;;AA0HF;AAAA;AAAA;EACE,OAPM;;AASR;AAAA;AAAA;EACE,OAVM;;AAYR;AAAA;AAAA;EACE,OAbM;;AAoBN;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;EACE;;;AApIJ;AAAA;EAEE;;;AAGF;EACE;;;AAGF;AAAA;EC2GA;EDxGE;;;AAGF;EACE;;;AAqGF;EACE,OAPM;;AASR;EACE,OAVM;;AAYR;EACE,OAbM;;AAoBN;EACE;;AA/GF;EACE;;AAGF;EACE;;AAGF;EACE;;;AAIJ;EACE;EACA;;AAiFF;EACE,OAPM;;AASR;EACE,OAVM;;AAYR;EACE,OAbM;;AAoBN;EACE;;AAMJ;EACE;;AAEF;EACE;;AAEF;EACE;;AAOA;EACE;;AAhHF;EACE;;;AAIJ;AAAA;ECsEA;;;ADjEA;ECiEA;;AD9DE;EC8DF;;ADtDE;ECsDF;;;AC9HA;AAAA;AAAA;EAGE;;;AAGF;AAAA;AAAA;EAGE;EACA;;AAEA;AAAA;AAAA;EACE;;AAGF;AAAA;AAAA;EACE;;AAGF;AAAA;AAAA;EACE;;AAIF;AAAA;AAAA;EACE;;;AAIJ;AAAA;AAAA;EAGE;;;AChCI;EACI;;AAGJ;EACI;;;AAIR;EACI;;AAEA;EACI;;AAGJ;EACI;EACA;;AAIA;EACI;;AAIA;EACI;;AAGJ;EACI;;AAIR;EACI;;;ACtCd;EACE;EACA;EACA;EACA;EACA;EACA;EACA;;;AAKA;EACE;EACA;;AAGF;EACE;EACA;EACA;;;AAMF;EACE;EACA;;AAGF;EACE;;;AC7BF;EACE;;;ACFJ;EACE;EACA;EACA;;AAEA;EALF;IAMI;IACA;;;AAGF;EACE;;AAGF;EACE;EACA;EACA;;;ACpBJ;EACE;;AAEA;EAEE;;;ACJF;EACE;;;ACAE;EACI;;AAGJ;EACI;;AAGJ;EACI;;;ACTJ;EACI;;;AAKJ;EACI;;;ACTV;EACE;EACA;EACA;;AAEA;EACE;EACA;EACA;;AAEA;EACE;;AAGF;EACE;;AAGF;EACE;;AAGF;EACE;EACA;;AAGF;EACE;;AAEA;EACE;EACA;;AAGF;EACE;EACA;;;ACnBR;EACE,kBAPM;;AASR;EACE,kBAVM;;AAYR;EACE,kBAbM;;AAoBN;EAME,kBALY;;;AChCd;EACI;;;ACCJ;AAAA;EACE;;AAIA;AAAA;EACE;;AAGF;AAAA;EACE;;;ACbN;EACE;EACA;;AAEA;Ed2HF;;AcvHE;EduHF;;AcnHE;EACE;EACA;;AAGF;EACE;EACA;;AAGF;EACE;EACA;;;AAKF;EACE;;;AC7BJ;EACE;EACA;;;ACAQ;EACE;EACA;;AAKN;EACE;;AAGF;EACE;EACA;EACA","file":"zitadel.css"} \ No newline at end of file diff --git a/internal/api/ui/login/static/templates/mfa_init_u2f.html b/internal/api/ui/login/static/templates/mfa_init_u2f.html index 7d6013d113..826defaa39 100644 --- a/internal/api/ui/login/static/templates/mfa_init_u2f.html +++ b/internal/api/ui/login/static/templates/mfa_init_u2f.html @@ -41,7 +41,7 @@
- + diff --git a/internal/api/ui/login/static/templates/mfa_verification_u2f.html b/internal/api/ui/login/static/templates/mfa_verification_u2f.html index 28ece3002e..b020ae935e 100644 --- a/internal/api/ui/login/static/templates/mfa_verification_u2f.html +++ b/internal/api/ui/login/static/templates/mfa_verification_u2f.html @@ -41,7 +41,7 @@ {{ end }} - + diff --git a/internal/api/ui/login/static/templates/passwordless.html b/internal/api/ui/login/static/templates/passwordless.html index 45e54264b1..6a95b54079 100644 --- a/internal/api/ui/login/static/templates/passwordless.html +++ b/internal/api/ui/login/static/templates/passwordless.html @@ -37,7 +37,7 @@ - + diff --git a/internal/api/ui/login/static/templates/passwordless_registration.html b/internal/api/ui/login/static/templates/passwordless_registration.html index c5d6a1948f..5ba814d66f 100644 --- a/internal/api/ui/login/static/templates/passwordless_registration.html +++ b/internal/api/ui/login/static/templates/passwordless_registration.html @@ -45,7 +45,7 @@ - + diff --git a/internal/command/idp_model.go b/internal/command/idp_model.go index 036a08aa97..0849c5bd01 100644 --- a/internal/command/idp_model.go +++ b/internal/command/idp_model.go @@ -211,6 +211,12 @@ func (wm *OIDCIDPWriteModel) Reduce() error { wm.reduceAddedEvent(e) case *idp.OIDCIDPChangedEvent: wm.reduceChangedEvent(e) + case *idp.OIDCIDPMigratedAzureADEvent: + wm.State = domain.IDPStateMigrated + case *idp.OIDCIDPMigratedGoogleEvent: + wm.State = domain.IDPStateMigrated + case *idp.RemovedEvent: + wm.State = domain.IDPStateRemoved case *idpconfig.IDPConfigAddedEvent: wm.reduceIDPConfigAddedEvent(e) case *idpconfig.IDPConfigChangedEvent: @@ -397,6 +403,8 @@ func (wm *JWTIDPWriteModel) Reduce() error { wm.reduceAddedEvent(e) case *idp.JWTIDPChangedEvent: wm.reduceChangedEvent(e) + case *idp.RemovedEvent: + wm.State = domain.IDPStateRemoved case *idpconfig.IDPConfigAddedEvent: wm.reduceIDPConfigAddedEvent(e) case *idpconfig.IDPConfigChangedEvent: @@ -558,6 +566,8 @@ func (wm *AzureADIDPWriteModel) Reduce() error { switch e := event.(type) { case *idp.AzureADIDPAddedEvent: wm.reduceAddedEvent(e) + case *idp.OIDCIDPMigratedAzureADEvent: + wm.reduceAddedEvent(&e.AzureADIDPAddedEvent) case *idp.AzureADIDPChangedEvent: wm.reduceChangedEvent(e) case *idp.RemovedEvent: @@ -1195,6 +1205,8 @@ func (wm *GoogleIDPWriteModel) Reduce() error { wm.reduceAddedEvent(e) case *idp.GoogleIDPChangedEvent: wm.reduceChangedEvent(e) + case *idp.OIDCIDPMigratedGoogleEvent: + wm.reduceAddedEvent(&e.GoogleIDPAddedEvent) case *idp.RemovedEvent: wm.State = domain.IDPStateRemoved } diff --git a/internal/command/instance_idp.go b/internal/command/instance_idp.go index 9d2533a12f..ea6534970b 100644 --- a/internal/command/instance_idp.go +++ b/internal/command/instance_idp.go @@ -97,6 +97,40 @@ func (c *Commands) UpdateInstanceGenericOIDCProvider(ctx context.Context, id str return pushedEventsToObjectDetails(pushedEvents), nil } +func (c *Commands) MigrateInstanceGenericOIDCToAzureADProvider(ctx context.Context, id string, provider AzureADProvider) (*domain.ObjectDetails, error) { + return c.migrateInstanceGenericOIDC(ctx, id, provider) +} + +func (c *Commands) MigrateInstanceGenericOIDCToGoogleProvider(ctx context.Context, id string, provider GoogleProvider) (*domain.ObjectDetails, error) { + return c.migrateInstanceGenericOIDC(ctx, id, provider) +} + +func (c *Commands) migrateInstanceGenericOIDC(ctx context.Context, id string, provider interface{}) (*domain.ObjectDetails, error) { + instanceID := authz.GetInstance(ctx).InstanceID() + instanceAgg := instance.NewAggregate(instanceID) + writeModel := NewOIDCInstanceIDPWriteModel(instanceID, id) + + var validation preparation.Validation + switch p := provider.(type) { + case AzureADProvider: + validation = c.prepareMigrateInstanceOIDCToAzureADProvider(instanceAgg, writeModel, p) + case GoogleProvider: + validation = c.prepareMigrateInstanceOIDCToGoogleProvider(instanceAgg, writeModel, p) + default: + return nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-s9219", "Errors.IDPConfig.NotExisting") + } + + cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, validation) + if err != nil { + return nil, err + } + pushedEvents, err := c.eventstore.Push(ctx, cmds...) + if err != nil { + return nil, err + } + return pushedEventsToObjectDetails(pushedEvents), nil +} + func (c *Commands) AddInstanceJWTProvider(ctx context.Context, provider JWTProvider) (string, *domain.ObjectDetails, error) { instanceID := authz.GetInstance(ctx).InstanceID() instanceAgg := instance.NewAggregate(instanceID) @@ -552,7 +586,7 @@ func (c *Commands) prepareUpdateInstanceOAuthProvider(a *instance.Aggregate, wri return nil, err } if !writeModel.State.Exists() { - return nil, caos_errs.ThrowNotFound(nil, "INST-D3r1s", "Errors.Instance.IDPConfig.NotExisting") + return nil, caos_errs.ThrowNotFound(nil, "INST-D3r1s", "Errors.IDPConfig.NotExisting") } event, err := writeModel.NewChangedEvent( ctx, @@ -646,7 +680,7 @@ func (c *Commands) prepareUpdateInstanceOIDCProvider(a *instance.Aggregate, writ return nil, err } if !writeModel.State.Exists() { - return nil, caos_errs.ThrowNotFound(nil, "INST-Dg331", "Errors.Instance.IDPConfig.NotExisting") + return nil, caos_errs.ThrowNotFound(nil, "INST-Dg331", "Errors.IDPConfig.NotExisting") } event, err := writeModel.NewChangedEvent( ctx, @@ -669,6 +703,91 @@ func (c *Commands) prepareUpdateInstanceOIDCProvider(a *instance.Aggregate, writ } } +func (c *Commands) prepareMigrateInstanceOIDCToAzureADProvider(a *instance.Aggregate, writeModel *InstanceOIDCIDPWriteModel, provider AzureADProvider) preparation.Validation { + return func() (preparation.CreateCommands, error) { + if provider.Name = strings.TrimSpace(provider.Name); provider.Name == "" { + return nil, caos_errs.ThrowInvalidArgument(nil, "INST-sdf3g", "Errors.Invalid.Argument") + } + if provider.ClientID = strings.TrimSpace(provider.ClientID); provider.ClientID == "" { + return nil, caos_errs.ThrowInvalidArgument(nil, "INST-Fhbr2", "Errors.Invalid.Argument") + } + if provider.ClientSecret = strings.TrimSpace(provider.ClientSecret); provider.ClientSecret == "" { + return nil, caos_errs.ThrowInvalidArgument(nil, "INST-Dzh3g", "Errors.Invalid.Argument") + } + return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) { + events, err := filter(ctx, writeModel.Query()) + if err != nil { + return nil, err + } + writeModel.AppendEvents(events...) + if err = writeModel.Reduce(); err != nil { + return nil, err + } + if !writeModel.State.Exists() { + return nil, caos_errs.ThrowNotFound(nil, "INST-Dg29201", "Errors.IDPConfig.NotExisting") + } + secret, err := crypto.Encrypt([]byte(provider.ClientSecret), c.idpConfigEncryption) + if err != nil { + return nil, err + } + return []eventstore.Command{ + instance.NewOIDCIDPMigratedAzureADEvent( + ctx, + &a.Aggregate, + writeModel.ID, + provider.Name, + provider.ClientID, + secret, + provider.Scopes, + provider.Tenant, + provider.EmailVerified, + provider.IDPOptions, + ), + }, nil + }, nil + } +} + +func (c *Commands) prepareMigrateInstanceOIDCToGoogleProvider(a *instance.Aggregate, writeModel *InstanceOIDCIDPWriteModel, provider GoogleProvider) preparation.Validation { + return func() (preparation.CreateCommands, error) { + if provider.ClientID = strings.TrimSpace(provider.ClientID); provider.ClientID == "" { + return nil, caos_errs.ThrowInvalidArgument(nil, "INST-D3fvs", "Errors.Invalid.Argument") + } + if provider.ClientSecret = strings.TrimSpace(provider.ClientSecret); provider.ClientSecret == "" { + return nil, caos_errs.ThrowInvalidArgument(nil, "INST-W2vqs", "Errors.Invalid.Argument") + } + return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) { + events, err := filter(ctx, writeModel.Query()) + if err != nil { + return nil, err + } + writeModel.AppendEvents(events...) + if err = writeModel.Reduce(); err != nil { + return nil, err + } + if !writeModel.State.Exists() { + return nil, caos_errs.ThrowNotFound(nil, "INST-Dg29202", "Errors.IDPConfig.NotExisting") + } + secret, err := crypto.Encrypt([]byte(provider.ClientSecret), c.idpConfigEncryption) + if err != nil { + return nil, err + } + return []eventstore.Command{ + instance.NewOIDCIDPMigratedGoogleEvent( + ctx, + &a.Aggregate, + writeModel.ID, + provider.Name, + provider.ClientID, + secret, + provider.Scopes, + provider.IDPOptions, + ), + }, nil + }, nil + } +} + func (c *Commands) prepareAddInstanceJWTProvider(a *instance.Aggregate, writeModel *InstanceJWTIDPWriteModel, provider JWTProvider) preparation.Validation { return func() (preparation.CreateCommands, error) { if provider.Name = strings.TrimSpace(provider.Name); provider.Name == "" { @@ -742,7 +861,7 @@ func (c *Commands) prepareUpdateInstanceJWTProvider(a *instance.Aggregate, write return nil, err } if !writeModel.State.Exists() { - return nil, caos_errs.ThrowNotFound(nil, "INST-Bhju5", "Errors.Instance.IDPConfig.NotExisting") + return nil, caos_errs.ThrowNotFound(nil, "INST-Bhju5", "Errors.IDPConfig.NotExisting") } event, err := writeModel.NewChangedEvent( ctx, @@ -826,7 +945,7 @@ func (c *Commands) prepareUpdateInstanceAzureADProvider(a *instance.Aggregate, w return nil, err } if !writeModel.State.Exists() { - return nil, caos_errs.ThrowNotFound(nil, "INST-BHz3q", "Errors.Instance.IDPConfig.NotExisting") + return nil, caos_errs.ThrowNotFound(nil, "INST-BHz3q", "Errors.IDPConfig.NotExisting") } event, err := writeModel.NewChangedEvent( ctx, @@ -904,7 +1023,7 @@ func (c *Commands) prepareUpdateInstanceGitHubProvider(a *instance.Aggregate, wr return nil, err } if !writeModel.State.Exists() { - return nil, caos_errs.ThrowNotFound(nil, "INST-Dr1gs", "Errors.Instance.IDPConfig.NotExisting") + return nil, caos_errs.ThrowNotFound(nil, "INST-Dr1gs", "Errors.IDPConfig.NotExisting") } event, err := writeModel.NewChangedEvent( ctx, @@ -1007,7 +1126,7 @@ func (c *Commands) prepareUpdateInstanceGitHubEnterpriseProvider(a *instance.Agg return nil, err } if !writeModel.State.Exists() { - return nil, caos_errs.ThrowNotFound(nil, "INST-GBr42", "Errors.Instance.IDPConfig.NotExisting") + return nil, caos_errs.ThrowNotFound(nil, "INST-GBr42", "Errors.IDPConfig.NotExisting") } event, err := writeModel.NewChangedEvent( ctx, @@ -1086,7 +1205,7 @@ func (c *Commands) prepareUpdateInstanceGitLabProvider(a *instance.Aggregate, wr return nil, err } if !writeModel.State.Exists() { - return nil, caos_errs.ThrowNotFound(nil, "INST-HBReq", "Errors.Instance.IDPConfig.NotExisting") + return nil, caos_errs.ThrowNotFound(nil, "INST-HBReq", "Errors.IDPConfig.NotExisting") } event, err := writeModel.NewChangedEvent( ctx, @@ -1175,7 +1294,7 @@ func (c *Commands) prepareUpdateInstanceGitLabSelfHostedProvider(a *instance.Agg return nil, err } if !writeModel.State.Exists() { - return nil, caos_errs.ThrowNotFound(nil, "INST-D2tg1", "Errors.Instance.IDPConfig.NotExisting") + return nil, caos_errs.ThrowNotFound(nil, "INST-D2tg1", "Errors.IDPConfig.NotExisting") } event, err := writeModel.NewChangedEvent( ctx, @@ -1252,7 +1371,7 @@ func (c *Commands) prepareUpdateInstanceGoogleProvider(a *instance.Aggregate, wr return nil, err } if !writeModel.State.Exists() { - return nil, caos_errs.ThrowNotFound(nil, "INST-D3r1s", "Errors.Instance.IDPConfig.NotExisting") + return nil, caos_errs.ThrowNotFound(nil, "INST-D3r1s", "Errors.IDPConfig.NotExisting") } event, err := writeModel.NewChangedEvent( ctx, @@ -1371,7 +1490,7 @@ func (c *Commands) prepareUpdateInstanceLDAPProvider(a *instance.Aggregate, writ return nil, err } if !writeModel.State.Exists() { - return nil, caos_errs.ThrowNotFound(nil, "INST-ASF3F", "Errors.Instance.IDPConfig.NotExisting") + return nil, caos_errs.ThrowNotFound(nil, "INST-ASF3F", "Errors.IDPConfig.NotExisting") } event, err := writeModel.NewChangedEvent( ctx, @@ -1412,7 +1531,7 @@ func (c *Commands) prepareDeleteInstanceProvider(a *instance.Aggregate, id strin return nil, err } if !writeModel.State.Exists() { - return nil, caos_errs.ThrowNotFound(nil, "INST-Se3tg", "Errors.Instance.IDPConfig.NotExisting") + return nil, caos_errs.ThrowNotFound(nil, "INST-Se3tg", "Errors.IDPConfig.NotExisting") } return []eventstore.Command{instance.NewIDPRemovedEvent(ctx, &a.Aggregate, id)}, nil }, nil diff --git a/internal/command/instance_idp_model.go b/internal/command/instance_idp_model.go index 41c971cba3..a0e51648d5 100644 --- a/internal/command/instance_idp_model.go +++ b/internal/command/instance_idp_model.go @@ -113,6 +113,10 @@ func (wm *InstanceOIDCIDPWriteModel) AppendEvents(events ...eventstore.Event) { wm.OIDCIDPWriteModel.AppendEvents(&e.OIDCIDPChangedEvent) case *instance.IDPRemovedEvent: wm.OIDCIDPWriteModel.AppendEvents(&e.RemovedEvent) + case *instance.OIDCIDPMigratedAzureADEvent: + wm.OIDCIDPWriteModel.AppendEvents(&e.OIDCIDPMigratedAzureADEvent) + case *instance.OIDCIDPMigratedGoogleEvent: + wm.OIDCIDPWriteModel.AppendEvents(&e.OIDCIDPMigratedGoogleEvent) // old events case *instance.IDPConfigAddedEvent: @@ -141,6 +145,8 @@ func (wm *InstanceOIDCIDPWriteModel) Query() *eventstore.SearchQueryBuilder { instance.OIDCIDPAddedEventType, instance.OIDCIDPChangedEventType, instance.IDPRemovedEventType, + instance.OIDCIDPMigratedAzureADEventType, + instance.OIDCIDPMigratedGoogleEventType, ). EventData(map[string]interface{}{"id": wm.ID}). Or(). // old events @@ -305,6 +311,8 @@ func (wm *InstanceAzureADIDPWriteModel) AppendEvents(events ...eventstore.Event) wm.AzureADIDPWriteModel.AppendEvents(&e.AzureADIDPAddedEvent) case *instance.AzureADIDPChangedEvent: wm.AzureADIDPWriteModel.AppendEvents(&e.AzureADIDPChangedEvent) + case *instance.OIDCIDPMigratedAzureADEvent: + wm.AzureADIDPWriteModel.AppendEvents(&e.OIDCIDPMigratedAzureADEvent) case *instance.IDPRemovedEvent: wm.AzureADIDPWriteModel.AppendEvents(&e.RemovedEvent) default: @@ -322,6 +330,7 @@ func (wm *InstanceAzureADIDPWriteModel) Query() *eventstore.SearchQueryBuilder { EventTypes( instance.AzureADIDPAddedEventType, instance.AzureADIDPChangedEventType, + instance.OIDCIDPMigratedAzureADEventType, instance.IDPRemovedEventType, ). EventData(map[string]interface{}{"id": wm.ID}). @@ -655,6 +664,8 @@ func (wm *InstanceGoogleIDPWriteModel) AppendEvents(events ...eventstore.Event) wm.GoogleIDPWriteModel.AppendEvents(&e.GoogleIDPAddedEvent) case *instance.GoogleIDPChangedEvent: wm.GoogleIDPWriteModel.AppendEvents(&e.GoogleIDPChangedEvent) + case *instance.OIDCIDPMigratedGoogleEvent: + wm.GoogleIDPWriteModel.AppendEvents(&e.OIDCIDPMigratedGoogleEvent) case *instance.IDPRemovedEvent: wm.GoogleIDPWriteModel.AppendEvents(&e.RemovedEvent) } @@ -670,6 +681,7 @@ func (wm *InstanceGoogleIDPWriteModel) Query() *eventstore.SearchQueryBuilder { EventTypes( instance.GoogleIDPAddedEventType, instance.GoogleIDPChangedEventType, + instance.OIDCIDPMigratedGoogleEventType, instance.IDPRemovedEventType, ). EventData(map[string]interface{}{"id": wm.ID}). diff --git a/internal/command/instance_idp_test.go b/internal/command/instance_idp_test.go index ed2b5fcaf8..9b9198fd4a 100644 --- a/internal/command/instance_idp_test.go +++ b/internal/command/instance_idp_test.go @@ -1102,6 +1102,474 @@ func TestCommandSide_UpdateInstanceGenericOIDCIDP(t *testing.T) { } } +func TestCommandSide_MigrateInstanceGenericOIDCToAzureADProvider(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + secretCrypto crypto.EncryptionAlgorithm + } + type args struct { + ctx context.Context + id string + provider AzureADProvider + } + type res struct { + want *domain.ObjectDetails + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + + { + "invalid name", + fields{ + eventstore: eventstoreExpect(t), + }, + args{ + ctx: authz.WithInstanceID(context.Background(), "instance1"), + provider: AzureADProvider{}, + }, + res{ + err: func(err error) bool { + return errors.Is(err, caos_errors.ThrowInvalidArgument(nil, "INST-sdf3g", "")) + }, + }, + }, + { + "invalid client id", + fields{ + eventstore: eventstoreExpect(t), + }, + args{ + ctx: authz.WithInstanceID(context.Background(), "instance1"), + provider: AzureADProvider{ + Name: "name", + }, + }, + res{ + err: func(err error) bool { + return errors.Is(err, caos_errors.ThrowInvalidArgument(nil, "INST-Fhbr2", "")) + }, + }, + }, + { + "invalid client secret", + fields{ + eventstore: eventstoreExpect(t), + }, + args{ + ctx: authz.WithInstanceID(context.Background(), "instance1"), + provider: AzureADProvider{ + Name: "name", + ClientID: "clientID", + }, + }, + res{ + err: func(err error) bool { + return errors.Is(err, caos_errors.ThrowInvalidArgument(nil, "INST-Dzh3g", "")) + }, + }, + }, + { + name: "not found", + fields: fields{ + eventstore: eventstoreExpect(t, + expectFilter(), + ), + }, + args: args{ + ctx: authz.WithInstanceID(context.Background(), "instance1"), + id: "id1", + provider: AzureADProvider{ + Name: "name", + ClientID: "clientID", + ClientSecret: "clientSecret", + }, + }, + res: res{ + err: caos_errors.IsNotFound, + }, + }, + { + name: "migrate ok", + fields: fields{ + eventstore: eventstoreExpect(t, + expectFilter( + eventFromEventPusher( + instance.NewOIDCIDPAddedEvent(context.Background(), &instance.NewAggregate("instance1").Aggregate, + "id1", + "name", + "issuer", + "clientID", + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("clientSecret"), + }, + nil, + false, + idp.Options{}, + )), + ), + expectPush( + []*repository.Event{ + eventFromEventPusherWithInstanceID( + "instance1", + func() eventstore.Command { + event := instance.NewOIDCIDPMigratedAzureADEvent(context.Background(), &instance.NewAggregate("instance1").Aggregate, + "id1", + "name", + "clientID", + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("clientSecret"), + }, + nil, + "", + false, + idp.Options{}, + ) + return event + }(), + ), + }, + ), + ), + secretCrypto: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), + }, + args: args{ + ctx: authz.WithInstanceID(context.Background(), "instance1"), + id: "id1", + provider: AzureADProvider{ + Name: "name", + ClientID: "clientID", + ClientSecret: "clientSecret", + }, + }, + res: res{ + want: &domain.ObjectDetails{ResourceOwner: "instance1"}, + }, + }, + { + name: "migrate ok full", + fields: fields{ + eventstore: eventstoreExpect(t, + expectFilter( + eventFromEventPusher( + instance.NewOIDCIDPAddedEvent(context.Background(), &instance.NewAggregate("instance1").Aggregate, + "id1", + "name", + "issuer", + "clientID", + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("clientSecret"), + }, + nil, + false, + idp.Options{}, + )), + ), + expectPush( + []*repository.Event{ + eventFromEventPusherWithInstanceID( + "instance1", + func() eventstore.Command { + event := instance.NewOIDCIDPMigratedAzureADEvent(context.Background(), &instance.NewAggregate("instance1").Aggregate, + "id1", + "name", + "clientID", + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("clientSecret"), + }, + []string{"openid"}, + "tenant", + true, + idp.Options{ + IsCreationAllowed: true, + IsLinkingAllowed: true, + IsAutoCreation: true, + IsAutoUpdate: true, + }, + ) + return event + }(), + ), + }, + ), + ), + secretCrypto: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), + }, + args: args{ + ctx: authz.WithInstanceID(context.Background(), "instance1"), + id: "id1", + provider: AzureADProvider{ + Name: "name", + ClientID: "clientID", + ClientSecret: "clientSecret", + Scopes: []string{"openid"}, + Tenant: "tenant", + EmailVerified: true, + IDPOptions: idp.Options{ + IsCreationAllowed: true, + IsLinkingAllowed: true, + IsAutoCreation: true, + IsAutoUpdate: true, + }, + }, + }, + res: res{ + want: &domain.ObjectDetails{ResourceOwner: "instance1"}, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &Commands{ + eventstore: tt.fields.eventstore, + idpConfigEncryption: tt.fields.secretCrypto, + } + got, err := c.MigrateInstanceGenericOIDCToAzureADProvider(tt.args.ctx, tt.args.id, tt.args.provider) + if tt.res.err == nil { + assert.NoError(t, err) + } + if tt.res.err != nil && !tt.res.err(err) { + t.Errorf("got wrong err: %v ", err) + } + if tt.res.err == nil { + assert.Equal(t, tt.res.want, got) + } + }) + } +} + +func TestCommandSide_MigrateInstanceOIDCToGoogleIDP(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + secretCrypto crypto.EncryptionAlgorithm + } + type args struct { + ctx context.Context + id string + provider GoogleProvider + } + type res struct { + want *domain.ObjectDetails + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + "invalid clientID", + fields{ + eventstore: eventstoreExpect(t), + }, + args{ + ctx: authz.WithInstanceID(context.Background(), "instance1"), + provider: GoogleProvider{}, + }, + res{ + err: func(err error) bool { + return errors.Is(err, caos_errors.ThrowInvalidArgument(nil, "INST-D3fvs", "")) + }, + }, + }, + { + "invalid clientSecret", + fields{ + eventstore: eventstoreExpect(t), + }, + args{ + ctx: authz.WithInstanceID(context.Background(), "instance1"), + provider: GoogleProvider{ + ClientID: "clientID", + }, + }, + res{ + err: func(err error) bool { + return errors.Is(err, caos_errors.ThrowInvalidArgument(nil, "INST-W2vqs", "")) + }, + }, + }, + { + name: "not found", + fields: fields{ + eventstore: eventstoreExpect(t, + expectFilter(), + ), + }, + args: args{ + ctx: authz.WithInstanceID(context.Background(), "instance1"), + id: "id1", + provider: GoogleProvider{ + ClientID: "clientID", + ClientSecret: "clientSecret", + }, + }, + res: res{ + err: caos_errors.IsNotFound, + }, + }, + { + name: "migrate ok", + fields: fields{ + eventstore: eventstoreExpect(t, + expectFilter( + eventFromEventPusher( + instance.NewOIDCIDPAddedEvent(context.Background(), &instance.NewAggregate("instance1").Aggregate, + "id1", + "name", + "issuer", + "clientID", + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("clientSecret"), + }, + nil, + false, + idp.Options{}, + )), + ), + expectPush( + []*repository.Event{ + eventFromEventPusherWithInstanceID( + "instance1", + instance.NewOIDCIDPMigratedGoogleEvent(context.Background(), &instance.NewAggregate("instance1").Aggregate, + "id1", + "", + "clientID", + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("clientSecret"), + }, + nil, + idp.Options{}, + )), + }, + ), + ), + secretCrypto: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), + }, + args: args{ + ctx: authz.WithInstanceID(context.Background(), "instance1"), + id: "id1", + provider: GoogleProvider{ + ClientID: "clientID", + ClientSecret: "clientSecret", + }, + }, + res: res{ + want: &domain.ObjectDetails{ResourceOwner: "instance1"}, + }, + }, + { + name: "migrate ok full", + fields: fields{ + eventstore: eventstoreExpect(t, + expectFilter( + eventFromEventPusher( + instance.NewOIDCIDPAddedEvent(context.Background(), &instance.NewAggregate("instance1").Aggregate, + "id1", + "name", + "issuer", + "clientID", + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("clientSecret"), + }, + nil, + false, + idp.Options{}, + )), + ), + expectPush( + []*repository.Event{ + eventFromEventPusherWithInstanceID( + "instance1", + instance.NewOIDCIDPMigratedGoogleEvent(context.Background(), &instance.NewAggregate("instance1").Aggregate, + "id1", + "", + "clientID", + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("clientSecret"), + }, + []string{"openid"}, + idp.Options{ + IsCreationAllowed: true, + IsLinkingAllowed: true, + IsAutoCreation: true, + IsAutoUpdate: true, + }, + )), + }, + ), + ), + secretCrypto: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), + }, + args: args{ + ctx: authz.WithInstanceID(context.Background(), "instance1"), + id: "id1", + provider: GoogleProvider{ + ClientID: "clientID", + ClientSecret: "clientSecret", + Scopes: []string{"openid"}, + IDPOptions: idp.Options{ + IsCreationAllowed: true, + IsLinkingAllowed: true, + IsAutoCreation: true, + IsAutoUpdate: true, + }, + }, + }, + res: res{ + want: &domain.ObjectDetails{ResourceOwner: "instance1"}, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &Commands{ + eventstore: tt.fields.eventstore, + idpConfigEncryption: tt.fields.secretCrypto, + } + got, err := c.MigrateInstanceGenericOIDCToGoogleProvider(tt.args.ctx, tt.args.id, tt.args.provider) + if tt.res.err == nil { + assert.NoError(t, err) + } + if tt.res.err != nil && !tt.res.err(err) { + t.Errorf("got wrong err: %v ", err) + } + if tt.res.err == nil { + assert.Equal(t, tt.res.want, got) + } + }) + } +} + func TestCommandSide_AddInstanceAzureADIDP(t *testing.T) { type fields struct { eventstore *eventstore.Eventstore diff --git a/internal/command/org_idp.go b/internal/command/org_idp.go index e6e22c4473..2fc0bc0483 100644 --- a/internal/command/org_idp.go +++ b/internal/command/org_idp.go @@ -92,6 +92,39 @@ func (c *Commands) UpdateOrgGenericOIDCProvider(ctx context.Context, resourceOwn return pushedEventsToObjectDetails(pushedEvents), nil } +func (c *Commands) MigrateOrgGenericOIDCToAzureADProvider(ctx context.Context, resourceOwner, id string, provider AzureADProvider) (*domain.ObjectDetails, error) { + return c.migrateOrgGenericOIDC(ctx, resourceOwner, id, provider) +} + +func (c *Commands) MigrateOrgGenericOIDCToGoogleProvider(ctx context.Context, resourceOwner, id string, provider GoogleProvider) (*domain.ObjectDetails, error) { + return c.migrateOrgGenericOIDC(ctx, resourceOwner, id, provider) +} + +func (c *Commands) migrateOrgGenericOIDC(ctx context.Context, resourceOwner, id string, provider interface{}) (*domain.ObjectDetails, error) { + orgAgg := org.NewAggregate(resourceOwner) + writeModel := NewOIDCOrgIDPWriteModel(resourceOwner, id) + + var validation preparation.Validation + switch p := provider.(type) { + case AzureADProvider: + validation = c.prepareMigrateOrgOIDCToAzureADProvider(orgAgg, writeModel, p) + case GoogleProvider: + validation = c.prepareMigrateOrgOIDCToGoogleProvider(orgAgg, writeModel, p) + default: + return nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-s9s2919", "Errors.IDPConfig.NotExisting") + } + + cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, validation) + if err != nil { + return nil, err + } + pushedEvents, err := c.eventstore.Push(ctx, cmds...) + if err != nil { + return nil, err + } + return pushedEventsToObjectDetails(pushedEvents), nil +} + func (c *Commands) AddOrgJWTProvider(ctx context.Context, resourceOwner string, provider JWTProvider) (string, *domain.ObjectDetails, error) { orgAgg := org.NewAggregate(resourceOwner) id, err := c.idGenerator.Next() @@ -647,6 +680,91 @@ func (c *Commands) prepareUpdateOrgOIDCProvider(a *org.Aggregate, writeModel *Or } } +func (c *Commands) prepareMigrateOrgOIDCToAzureADProvider(a *org.Aggregate, writeModel *OrgOIDCIDPWriteModel, provider AzureADProvider) preparation.Validation { + return func() (preparation.CreateCommands, error) { + if provider.Name = strings.TrimSpace(provider.Name); provider.Name == "" { + return nil, caos_errs.ThrowInvalidArgument(nil, "ORG-sdf3g", "Errors.Invalid.Argument") + } + if provider.ClientID = strings.TrimSpace(provider.ClientID); provider.ClientID == "" { + return nil, caos_errs.ThrowInvalidArgument(nil, "ORG-Fhbr2", "Errors.Invalid.Argument") + } + if provider.ClientSecret = strings.TrimSpace(provider.ClientSecret); provider.ClientSecret == "" { + return nil, caos_errs.ThrowInvalidArgument(nil, "ORG-Dzh3g", "Errors.Invalid.Argument") + } + return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) { + events, err := filter(ctx, writeModel.Query()) + if err != nil { + return nil, err + } + writeModel.AppendEvents(events...) + if err = writeModel.Reduce(); err != nil { + return nil, err + } + if !writeModel.State.Exists() { + return nil, caos_errs.ThrowNotFound(nil, "INST-Dg239201", "Errors.Instance.IDPConfig.NotExisting") + } + secret, err := crypto.Encrypt([]byte(provider.ClientSecret), c.idpConfigEncryption) + if err != nil { + return nil, err + } + return []eventstore.Command{ + org.NewOIDCIDPMigratedAzureADEvent( + ctx, + &a.Aggregate, + writeModel.ID, + provider.Name, + provider.ClientID, + secret, + provider.Scopes, + provider.Tenant, + provider.EmailVerified, + provider.IDPOptions, + ), + }, nil + }, nil + } +} + +func (c *Commands) prepareMigrateOrgOIDCToGoogleProvider(a *org.Aggregate, writeModel *OrgOIDCIDPWriteModel, provider GoogleProvider) preparation.Validation { + return func() (preparation.CreateCommands, error) { + if provider.ClientID = strings.TrimSpace(provider.ClientID); provider.ClientID == "" { + return nil, caos_errs.ThrowInvalidArgument(nil, "ORG-D3fvs", "Errors.Invalid.Argument") + } + if provider.ClientSecret = strings.TrimSpace(provider.ClientSecret); provider.ClientSecret == "" { + return nil, caos_errs.ThrowInvalidArgument(nil, "ORG-W2vqs", "Errors.Invalid.Argument") + } + return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) { + events, err := filter(ctx, writeModel.Query()) + if err != nil { + return nil, err + } + writeModel.AppendEvents(events...) + if err = writeModel.Reduce(); err != nil { + return nil, err + } + if !writeModel.State.Exists() { + return nil, caos_errs.ThrowNotFound(nil, "INST-x09981", "Errors.Instance.IDPConfig.NotExisting") + } + secret, err := crypto.Encrypt([]byte(provider.ClientSecret), c.idpConfigEncryption) + if err != nil { + return nil, err + } + return []eventstore.Command{ + org.NewOIDCIDPMigratedGoogleEvent( + ctx, + &a.Aggregate, + writeModel.ID, + provider.Name, + provider.ClientID, + secret, + provider.Scopes, + provider.IDPOptions, + ), + }, nil + }, nil + } +} + func (c *Commands) prepareAddOrgJWTProvider(a *org.Aggregate, writeModel *OrgJWTIDPWriteModel, provider JWTProvider) preparation.Validation { return func() (preparation.CreateCommands, error) { if provider.Name = strings.TrimSpace(provider.Name); provider.Name == "" { diff --git a/internal/command/org_idp_model.go b/internal/command/org_idp_model.go index ca8011121c..5f121dbbf4 100644 --- a/internal/command/org_idp_model.go +++ b/internal/command/org_idp_model.go @@ -113,6 +113,10 @@ func (wm *OrgOIDCIDPWriteModel) AppendEvents(events ...eventstore.Event) { wm.OIDCIDPWriteModel.AppendEvents(&e.OIDCIDPAddedEvent) case *org.OIDCIDPChangedEvent: wm.OIDCIDPWriteModel.AppendEvents(&e.OIDCIDPChangedEvent) + case *org.OIDCIDPMigratedAzureADEvent: + wm.OIDCIDPWriteModel.AppendEvents(&e.OIDCIDPMigratedAzureADEvent) + case *org.OIDCIDPMigratedGoogleEvent: + wm.OIDCIDPWriteModel.AppendEvents(&e.OIDCIDPMigratedGoogleEvent) case *org.IDPRemovedEvent: wm.OIDCIDPWriteModel.AppendEvents(&e.RemovedEvent) @@ -142,6 +146,8 @@ func (wm *OrgOIDCIDPWriteModel) Query() *eventstore.SearchQueryBuilder { EventTypes( org.OIDCIDPAddedEventType, org.OIDCIDPChangedEventType, + org.OIDCIDPMigratedAzureADEventType, + org.OIDCIDPMigratedGoogleEventType, org.IDPRemovedEventType, ). EventData(map[string]interface{}{"id": wm.ID}). @@ -311,6 +317,8 @@ func (wm *OrgAzureADIDPWriteModel) AppendEvents(events ...eventstore.Event) { wm.AzureADIDPWriteModel.AppendEvents(&e.AzureADIDPAddedEvent) case *org.AzureADIDPChangedEvent: wm.AzureADIDPWriteModel.AppendEvents(&e.AzureADIDPChangedEvent) + case *org.OIDCIDPMigratedAzureADEvent: + wm.AzureADIDPWriteModel.AppendEvents(&e.OIDCIDPMigratedAzureADEvent) case *org.IDPRemovedEvent: wm.AzureADIDPWriteModel.AppendEvents(&e.RemovedEvent) default: @@ -328,6 +336,7 @@ func (wm *OrgAzureADIDPWriteModel) Query() *eventstore.SearchQueryBuilder { EventTypes( org.AzureADIDPAddedEventType, org.AzureADIDPChangedEventType, + org.OIDCIDPMigratedAzureADEventType, org.IDPRemovedEventType, ). EventData(map[string]interface{}{"id": wm.ID}). @@ -663,6 +672,8 @@ func (wm *OrgGoogleIDPWriteModel) AppendEvents(events ...eventstore.Event) { wm.GoogleIDPWriteModel.AppendEvents(&e.GoogleIDPAddedEvent) case *org.GoogleIDPChangedEvent: wm.GoogleIDPWriteModel.AppendEvents(&e.GoogleIDPChangedEvent) + case *org.OIDCIDPMigratedGoogleEvent: + wm.GoogleIDPWriteModel.AppendEvents(&e.OIDCIDPMigratedGoogleEvent) case *org.IDPRemovedEvent: wm.GoogleIDPWriteModel.AppendEvents(&e.RemovedEvent) default: @@ -680,6 +691,7 @@ func (wm *OrgGoogleIDPWriteModel) Query() *eventstore.SearchQueryBuilder { EventTypes( org.GoogleIDPAddedEventType, org.GoogleIDPChangedEventType, + org.OIDCIDPMigratedGoogleEventType, org.IDPRemovedEventType, ). EventData(map[string]interface{}{"id": wm.ID}). diff --git a/internal/command/org_idp_test.go b/internal/command/org_idp_test.go index 7110cdd1dd..063bb70b24 100644 --- a/internal/command/org_idp_test.go +++ b/internal/command/org_idp_test.go @@ -1119,6 +1119,474 @@ func TestCommandSide_UpdateOrgGenericOIDCIDP(t *testing.T) { } } +func TestCommandSide_MigrateOrgGenericOIDCToAzureADProvider(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + secretCrypto crypto.EncryptionAlgorithm + } + type args struct { + ctx context.Context + resourceOwner string + id string + provider AzureADProvider + } + type res struct { + want *domain.ObjectDetails + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + "invalid name", + fields{ + eventstore: eventstoreExpect(t), + }, + args{ + ctx: context.Background(), + resourceOwner: "org1", + provider: AzureADProvider{}, + }, + res{ + err: func(err error) bool { + return errors.Is(err, caos_errors.ThrowInvalidArgument(nil, "ORG-sdf3g", "")) + }, + }, + }, + { + "invalid client id", + fields{ + eventstore: eventstoreExpect(t), + }, + args{ + ctx: context.Background(), + resourceOwner: "org1", + provider: AzureADProvider{ + Name: "name", + }, + }, + res{ + err: func(err error) bool { + return errors.Is(err, caos_errors.ThrowInvalidArgument(nil, "ORG-Fhbr2", "")) + }, + }, + }, + { + "invalid client secret", + fields{ + eventstore: eventstoreExpect(t), + }, + args{ + ctx: context.Background(), + resourceOwner: "org1", + provider: AzureADProvider{ + Name: "name", + ClientID: "clientID", + }, + }, + res{ + err: func(err error) bool { + return errors.Is(err, caos_errors.ThrowInvalidArgument(nil, "ORG-Dzh3g", "")) + }, + }, + }, + { + name: "not found", + fields: fields{ + eventstore: eventstoreExpect(t, + expectFilter(), + ), + }, + args: args{ + ctx: context.Background(), + resourceOwner: "ro", + id: "id1", + provider: AzureADProvider{ + Name: "name", + ClientID: "clientID", + ClientSecret: "clientSecret", + }, + }, + res: res{ + err: caos_errors.IsNotFound, + }, + }, + { + name: "migrate ok", + fields: fields{ + eventstore: eventstoreExpect(t, + expectFilter( + eventFromEventPusher( + org.NewOIDCIDPAddedEvent(context.Background(), &org.NewAggregate("org1").Aggregate, + "id1", + "name", + "issuer", + "clientID", + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("clientSecret"), + }, + nil, + false, + idp.Options{}, + )), + ), + expectPush( + []*repository.Event{ + eventFromEventPusher( + func() eventstore.Command { + event := org.NewOIDCIDPMigratedAzureADEvent(context.Background(), &org.NewAggregate("org1").Aggregate, + "id1", + "name", + "clientID", + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("clientSecret"), + }, + nil, + "", + false, + idp.Options{}, + ) + return event + }(), + ), + }, + ), + ), + secretCrypto: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), + }, + args: args{ + ctx: context.Background(), + resourceOwner: "org1", + id: "id1", + provider: AzureADProvider{ + Name: "name", + ClientID: "clientID", + ClientSecret: "clientSecret", + }, + }, + res: res{ + want: &domain.ObjectDetails{ResourceOwner: "org1"}, + }, + }, + { + name: "migrate full ok", + fields: fields{ + eventstore: eventstoreExpect(t, + expectFilter( + eventFromEventPusher( + org.NewOIDCIDPAddedEvent(context.Background(), &org.NewAggregate("org1").Aggregate, + "id1", + "name", + "issuer", + "clientID", + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("clientSecret"), + }, + nil, + false, + idp.Options{}, + )), + ), + expectPush( + eventPusherToEvents( + org.NewOIDCIDPMigratedAzureADEvent(context.Background(), &org.NewAggregate("org1").Aggregate, + "id1", + "name", + "clientID", + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("clientSecret"), + }, + []string{"openid"}, + "tenant", + true, + idp.Options{ + IsCreationAllowed: true, + IsLinkingAllowed: true, + IsAutoCreation: true, + IsAutoUpdate: true, + }, + )), + ), + ), + secretCrypto: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), + }, + args: args{ + ctx: context.Background(), + resourceOwner: "org1", + id: "id1", + provider: AzureADProvider{ + Name: "name", + ClientID: "clientID", + ClientSecret: "clientSecret", + Scopes: []string{"openid"}, + Tenant: "tenant", + EmailVerified: true, + IDPOptions: idp.Options{ + IsCreationAllowed: true, + IsLinkingAllowed: true, + IsAutoCreation: true, + IsAutoUpdate: true, + }, + }, + }, + res: res{ + want: &domain.ObjectDetails{ResourceOwner: "org1"}, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &Commands{ + eventstore: tt.fields.eventstore, + idpConfigEncryption: tt.fields.secretCrypto, + } + got, err := c.MigrateOrgGenericOIDCToAzureADProvider(tt.args.ctx, tt.args.resourceOwner, tt.args.id, tt.args.provider) + if tt.res.err == nil { + assert.NoError(t, err) + } + if tt.res.err != nil && !tt.res.err(err) { + t.Errorf("got wrong err: %v ", err) + } + if tt.res.err == nil { + assert.Equal(t, tt.res.want, got) + } + }) + } +} + +func TestCommandSide_MigrateOrgOIDCToGoogleIDP(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + secretCrypto crypto.EncryptionAlgorithm + } + type args struct { + ctx context.Context + resourceOwner string + id string + provider GoogleProvider + } + type res struct { + want *domain.ObjectDetails + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + "invalid clientID", + fields{ + eventstore: eventstoreExpect(t), + }, + args{ + ctx: context.Background(), + resourceOwner: "org1", + id: "id1", + provider: GoogleProvider{}, + }, + res{ + err: func(err error) bool { + return errors.Is(err, caos_errors.ThrowInvalidArgument(nil, "ORG-D3fvs", "")) + }, + }, + }, + { + "invalid clientSecret", + fields{ + eventstore: eventstoreExpect(t), + }, + args{ + ctx: context.Background(), + resourceOwner: "org1", + id: "id1", + provider: GoogleProvider{ + ClientID: "clientID", + }, + }, + res{ + err: func(err error) bool { + return errors.Is(err, caos_errors.ThrowInvalidArgument(nil, "ORG-W2vqs", "")) + }, + }, + }, + { + "not found", + fields{ + eventstore: eventstoreExpect(t, + expectFilter(), + ), + }, + args{ + ctx: context.Background(), + resourceOwner: "org1", + id: "id1", + provider: GoogleProvider{ + ClientID: "clientID", + ClientSecret: "clientSecret", + }, + }, + res{ + err: caos_errors.IsNotFound, + }, + }, + { + name: "migrate ok", + fields: fields{ + eventstore: eventstoreExpect(t, + expectFilter( + eventFromEventPusher( + org.NewOIDCIDPAddedEvent(context.Background(), &org.NewAggregate("org1").Aggregate, + "id1", + "name", + "issuer", + "clientID", + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("clientSecret"), + }, + nil, + false, + idp.Options{}, + )), + ), + expectPush( + eventPusherToEvents( + org.NewOIDCIDPMigratedGoogleEvent(context.Background(), &org.NewAggregate("org1").Aggregate, + "id1", + "", + "clientID", + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("clientSecret"), + }, + nil, + idp.Options{}, + )), + ), + ), + secretCrypto: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), + }, + args: args{ + ctx: context.Background(), + resourceOwner: "org1", + id: "id1", + provider: GoogleProvider{ + ClientID: "clientID", + ClientSecret: "clientSecret", + }, + }, + res: res{ + want: &domain.ObjectDetails{ResourceOwner: "org1"}, + }, + }, + { + name: "migrate full ok", + fields: fields{ + eventstore: eventstoreExpect(t, + expectFilter( + eventFromEventPusher( + org.NewOIDCIDPAddedEvent(context.Background(), &org.NewAggregate("org1").Aggregate, + "id1", + "name", + "issuer", + "clientID", + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("clientSecret"), + }, + nil, + false, + idp.Options{}, + )), + ), + expectPush( + eventPusherToEvents( + org.NewOIDCIDPMigratedGoogleEvent(context.Background(), &org.NewAggregate("org1").Aggregate, + "id1", + "", + "clientID", + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("clientSecret"), + }, + []string{"openid"}, + idp.Options{ + IsCreationAllowed: true, + IsLinkingAllowed: true, + IsAutoCreation: true, + IsAutoUpdate: true, + }, + )), + ), + ), + secretCrypto: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), + }, + args: args{ + ctx: context.Background(), + resourceOwner: "org1", + id: "id1", + provider: GoogleProvider{ + ClientID: "clientID", + ClientSecret: "clientSecret", + Scopes: []string{"openid"}, + IDPOptions: idp.Options{ + IsCreationAllowed: true, + IsLinkingAllowed: true, + IsAutoCreation: true, + IsAutoUpdate: true, + }, + }, + }, + res: res{ + want: &domain.ObjectDetails{ResourceOwner: "org1"}, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &Commands{ + eventstore: tt.fields.eventstore, + idpConfigEncryption: tt.fields.secretCrypto, + } + got, err := c.MigrateOrgGenericOIDCToGoogleProvider(tt.args.ctx, tt.args.resourceOwner, tt.args.id, tt.args.provider) + if tt.res.err == nil { + assert.NoError(t, err) + } + if tt.res.err != nil && !tt.res.err(err) { + t.Errorf("got wrong err: %v ", err) + } + if tt.res.err == nil { + assert.Equal(t, tt.res.want, got) + } + }) + } +} + func TestCommandSide_AddOrgAzureADIDP(t *testing.T) { type fields struct { eventstore *eventstore.Eventstore diff --git a/internal/command/session.go b/internal/command/session.go index cb652ea194..2feac20e00 100644 --- a/internal/command/session.go +++ b/internal/command/session.go @@ -16,10 +16,10 @@ import ( "github.com/zitadel/zitadel/internal/telemetry/tracing" ) -type SessionCheck func(ctx context.Context, cmd *SessionChecks) error +type SessionCommand func(ctx context.Context, cmd *SessionCommands) error -type SessionChecks struct { - checks []SessionCheck +type SessionCommands struct { + cmds []SessionCommand sessionWriteModel *SessionWriteModel passwordWriteModel *HumanPasswordWriteModel @@ -29,9 +29,9 @@ type SessionChecks struct { now func() time.Time } -func (c *Commands) NewSessionChecks(checks []SessionCheck, session *SessionWriteModel) *SessionChecks { - return &SessionChecks{ - checks: checks, +func (c *Commands) NewSessionCommands(cmds []SessionCommand, session *SessionWriteModel) *SessionCommands { + return &SessionCommands{ + cmds: cmds, sessionWriteModel: session, eventstore: c.eventstore, userPasswordAlg: c.userPasswordAlg, @@ -41,8 +41,8 @@ func (c *Commands) NewSessionChecks(checks []SessionCheck, session *SessionWrite } // CheckUser defines a user check to be executed for a session update -func CheckUser(id string) SessionCheck { - return func(ctx context.Context, cmd *SessionChecks) error { +func CheckUser(id string) SessionCommand { + return func(ctx context.Context, cmd *SessionCommands) error { if cmd.sessionWriteModel.UserID != "" && id != "" && cmd.sessionWriteModel.UserID != id { return caos_errs.ThrowInvalidArgument(nil, "", "user change not possible") } @@ -51,8 +51,8 @@ func CheckUser(id string) SessionCheck { } // CheckPassword defines a password check to be executed for a session update -func CheckPassword(password string) SessionCheck { - return func(ctx context.Context, cmd *SessionChecks) error { +func CheckPassword(password string) SessionCommand { + return func(ctx context.Context, cmd *SessionCommands) error { if cmd.sessionWriteModel.UserID == "" { return caos_errs.ThrowPreconditionFailed(nil, "COMMAND-Sfw3f", "Errors.User.UserIDMissing") } @@ -80,17 +80,32 @@ func CheckPassword(password string) SessionCheck { } } -// Check will execute the checks specified and return an error on the first occurrence -func (s *SessionChecks) Check(ctx context.Context) error { - for _, check := range s.checks { - if err := check(ctx, s); err != nil { +// Exec will execute the commands specified and returns an error on the first occurrence +func (s *SessionCommands) Exec(ctx context.Context) error { + for _, cmd := range s.cmds { + if err := cmd(ctx, s); err != nil { return err } } return nil } -func (s *SessionChecks) commands(ctx context.Context) (string, []eventstore.Command, error) { +func (s *SessionCommands) gethumanWriteModel(ctx context.Context) (*HumanWriteModel, error) { + if s.sessionWriteModel.UserID == "" { + return nil, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-eeR2e", "Errors.User.UserIDMissing") + } + humanWriteModel := NewHumanWriteModel(s.sessionWriteModel.UserID, s.sessionWriteModel.ResourceOwner) + err := s.eventstore.FilterToQueryReducer(ctx, humanWriteModel) + if err != nil { + return nil, err + } + if humanWriteModel.UserState != domain.UserStateActive { + return nil, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-Df4b3", "Errors.ie4Ai.NotFound") + } + return humanWriteModel, nil +} + +func (s *SessionCommands) commands(ctx context.Context) (string, []eventstore.Command, error) { if len(s.sessionWriteModel.commands) == 0 { return "", nil, nil } @@ -103,7 +118,7 @@ func (s *SessionChecks) commands(ctx context.Context) (string, []eventstore.Comm return token, s.sessionWriteModel.commands, nil } -func (c *Commands) CreateSession(ctx context.Context, checks []SessionCheck, metadata map[string][]byte) (set *SessionChanged, err error) { +func (c *Commands) CreateSession(ctx context.Context, cmds []SessionCommand, metadata map[string][]byte) (set *SessionChanged, err error) { sessionID, err := c.idGenerator.Next() if err != nil { return nil, err @@ -113,12 +128,12 @@ func (c *Commands) CreateSession(ctx context.Context, checks []SessionCheck, met if err != nil { return nil, err } - cmd := c.NewSessionChecks(checks, sessionWriteModel) + cmd := c.NewSessionCommands(cmds, sessionWriteModel) cmd.sessionWriteModel.Start(ctx) return c.updateSession(ctx, cmd, metadata) } -func (c *Commands) UpdateSession(ctx context.Context, sessionID, sessionToken string, checks []SessionCheck, metadata map[string][]byte) (set *SessionChanged, err error) { +func (c *Commands) UpdateSession(ctx context.Context, sessionID, sessionToken string, cmds []SessionCommand, metadata map[string][]byte) (set *SessionChanged, err error) { sessionWriteModel := NewSessionWriteModel(sessionID, authz.GetCtxData(ctx).OrgID) err = c.eventstore.FilterToQueryReducer(ctx, sessionWriteModel) if err != nil { @@ -127,7 +142,7 @@ func (c *Commands) UpdateSession(ctx context.Context, sessionID, sessionToken st if err := c.sessionPermission(ctx, sessionWriteModel, sessionToken, domain.PermissionSessionWrite); err != nil { return nil, err } - cmd := c.NewSessionChecks(checks, sessionWriteModel) + cmd := c.NewSessionCommands(cmds, sessionWriteModel) return c.updateSession(ctx, cmd, metadata) } @@ -154,12 +169,12 @@ func (c *Commands) TerminateSession(ctx context.Context, sessionID, sessionToken return writeModelToObjectDetails(&sessionWriteModel.WriteModel), nil } -// updateSession execute the [SessionChecks] where new events will be created and as well as for metadata (changes) -func (c *Commands) updateSession(ctx context.Context, checks *SessionChecks, metadata map[string][]byte) (set *SessionChanged, err error) { +// updateSession execute the [SessionCommands] where new events will be created and as well as for metadata (changes) +func (c *Commands) updateSession(ctx context.Context, checks *SessionCommands, metadata map[string][]byte) (set *SessionChanged, err error) { if checks.sessionWriteModel.State == domain.SessionStateTerminated { return nil, caos_errs.ThrowPreconditionFailed(nil, "COMAND-SAjeh", "Errors.Session.Terminated") } - if err := checks.Check(ctx); err != nil { + if err := checks.Exec(ctx); err != nil { // TODO: how to handle failed checks (e.g. pw wrong) https://github.com/zitadel/zitadel/issues/5807 return nil, err } diff --git a/internal/command/session_model.go b/internal/command/session_model.go index e331693818..24edbfd2e4 100644 --- a/internal/command/session_model.go +++ b/internal/command/session_model.go @@ -6,10 +6,31 @@ import ( "time" "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/session" + usr_repo "github.com/zitadel/zitadel/internal/repository/user" ) +type PasskeyChallengeModel struct { + Challenge string + AllowedCrentialIDs [][]byte + UserVerification domain.UserVerificationRequirement +} + +func (p *PasskeyChallengeModel) WebAuthNLogin(human *domain.Human, credentialAssertionData []byte) (*domain.WebAuthNLogin, error) { + if p == nil { + return nil, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-Ioqu5", "Errors.Session.Passkey.NoChallenge") + } + return &domain.WebAuthNLogin{ + ObjectRoot: human.ObjectRoot, + CredentialAssertionData: credentialAssertionData, + Challenge: p.Challenge, + AllowedCredentialIDs: p.AllowedCrentialIDs, + UserVerification: p.UserVerification, + }, nil +} + type SessionWriteModel struct { eventstore.WriteModel @@ -17,9 +38,12 @@ type SessionWriteModel struct { UserID string UserCheckedAt time.Time PasswordCheckedAt time.Time + PasskeyCheckedAt time.Time Metadata map[string][]byte State domain.SessionState + PasskeyChallenge *PasskeyChallengeModel + commands []eventstore.Command aggregate *eventstore.Aggregate } @@ -44,6 +68,10 @@ func (wm *SessionWriteModel) Reduce() error { wm.reduceUserChecked(e) case *session.PasswordCheckedEvent: wm.reducePasswordChecked(e) + case *session.PasskeyChallengedEvent: + wm.reducePasskeyChallenged(e) + case *session.PasskeyCheckedEvent: + wm.reducePasskeyChecked(e) case *session.TokenSetEvent: wm.reduceTokenSet(e) case *session.TerminateEvent: @@ -62,6 +90,8 @@ func (wm *SessionWriteModel) Query() *eventstore.SearchQueryBuilder { session.AddedType, session.UserCheckedType, session.PasswordCheckedType, + session.PasskeyChallengedType, + session.PasskeyCheckedType, session.TokenSetType, session.MetadataSetType, session.TerminateType, @@ -87,6 +117,19 @@ func (wm *SessionWriteModel) reducePasswordChecked(e *session.PasswordCheckedEve wm.PasswordCheckedAt = e.CheckedAt } +func (wm *SessionWriteModel) reducePasskeyChallenged(e *session.PasskeyChallengedEvent) { + wm.PasskeyChallenge = &PasskeyChallengeModel{ + Challenge: e.Challenge, + AllowedCrentialIDs: e.AllowedCrentialIDs, + UserVerification: e.UserVerification, + } +} + +func (wm *SessionWriteModel) reducePasskeyChecked(e *session.PasskeyCheckedEvent) { + wm.PasskeyChallenge = nil + wm.PasskeyCheckedAt = e.CheckedAt +} + func (wm *SessionWriteModel) reduceTokenSet(e *session.TokenSetEvent) { wm.TokenID = e.TokenID } @@ -110,6 +153,17 @@ func (wm *SessionWriteModel) PasswordChecked(ctx context.Context, checkedAt time wm.commands = append(wm.commands, session.NewPasswordCheckedEvent(ctx, wm.aggregate, checkedAt)) } +func (wm *SessionWriteModel) PasskeyChallenged(ctx context.Context, challenge string, allowedCrentialIDs [][]byte, userVerification domain.UserVerificationRequirement) { + wm.commands = append(wm.commands, session.NewPasskeyChallengedEvent(ctx, wm.aggregate, challenge, allowedCrentialIDs, userVerification)) +} + +func (wm *SessionWriteModel) PasskeyChecked(ctx context.Context, checkedAt time.Time, tokenID string, signCount uint32) { + wm.commands = append(wm.commands, + session.NewPasskeyCheckedEvent(ctx, wm.aggregate, checkedAt), + usr_repo.NewHumanPasswordlessSignCountChangedEvent(ctx, wm.aggregate, tokenID, signCount), + ) +} + func (wm *SessionWriteModel) SetToken(ctx context.Context, tokenID string) { wm.commands = append(wm.commands, session.NewTokenSetEvent(ctx, wm.aggregate, tokenID)) } diff --git a/internal/command/session_passkey.go b/internal/command/session_passkey.go new file mode 100644 index 0000000000..d4664ca2fe --- /dev/null +++ b/internal/command/session_passkey.go @@ -0,0 +1,84 @@ +package command + +import ( + "context" + "encoding/json" + + "github.com/zitadel/zitadel/internal/domain" + caos_errs "github.com/zitadel/zitadel/internal/errors" +) + +type humanPasskeys struct { + human *domain.Human + tokens []*domain.WebAuthNToken +} + +func (s *SessionCommands) getHumanPasskeys(ctx context.Context) (*humanPasskeys, error) { + humanWritemodel, err := s.gethumanWriteModel(ctx) + if err != nil { + return nil, err + } + tokenReadModel, err := s.getHumanPasswordlessTokenReadModel(ctx) + if err != nil { + return nil, err + } + return &humanPasskeys{ + human: writeModelToHuman(humanWritemodel), + tokens: readModelToPasswordlessTokens(tokenReadModel), + }, nil +} + +func (s *SessionCommands) getHumanPasswordlessTokenReadModel(ctx context.Context) (*HumanPasswordlessTokensReadModel, error) { + tokenReadModel := NewHumanPasswordlessTokensReadModel(s.sessionWriteModel.UserID, s.sessionWriteModel.ResourceOwner) + err := s.eventstore.FilterToQueryReducer(ctx, tokenReadModel) + if err != nil { + return nil, err + } + return tokenReadModel, nil +} + +func (c *Commands) CreatePasskeyChallenge(userVerification domain.UserVerificationRequirement, dst json.Unmarshaler) SessionCommand { + return func(ctx context.Context, cmd *SessionCommands) error { + humanPasskeys, err := cmd.getHumanPasskeys(ctx) + if err != nil { + return err + } + webAuthNLogin, err := c.webauthnConfig.BeginLogin(ctx, humanPasskeys.human, userVerification, humanPasskeys.tokens...) + if err != nil { + return err + } + if err = json.Unmarshal(webAuthNLogin.CredentialAssertionData, dst); err != nil { + return caos_errs.ThrowInternal(err, "COMMAND-Yah6A", "Errors.Internal") + } + + cmd.sessionWriteModel.PasskeyChallenged(ctx, webAuthNLogin.Challenge, webAuthNLogin.AllowedCredentialIDs, webAuthNLogin.UserVerification) + return nil + } +} + +func (c *Commands) CheckPasskey(credentialAssertionData json.Marshaler) SessionCommand { + return func(ctx context.Context, cmd *SessionCommands) error { + credentialAssertionData, err := json.Marshal(credentialAssertionData) + if err != nil { + return caos_errs.ThrowInvalidArgument(err, "COMMAND-ohG2o", "todo") + } + humanPasskeys, err := cmd.getHumanPasskeys(ctx) + if err != nil { + return err + } + webAuthN, err := cmd.sessionWriteModel.PasskeyChallenge.WebAuthNLogin(humanPasskeys.human, credentialAssertionData) + if err != nil { + return err + } + keyID, signCount, err := c.webauthnConfig.FinishLogin(ctx, humanPasskeys.human, webAuthN, credentialAssertionData, humanPasskeys.tokens...) + if err != nil && keyID == nil { + return err + } + _, token := domain.GetTokenByKeyID(humanPasskeys.tokens, keyID) + if token == nil { + return caos_errs.ThrowPreconditionFailed(nil, "COMMAND-Aej7i", "Errors.User.WebAuthN.NotFound") + } + cmd.sessionWriteModel.PasskeyChecked(ctx, cmd.now(), token.WebAuthNTokenID, signCount) + return nil + } +} diff --git a/internal/command/session_passkeys_test.go b/internal/command/session_passkeys_test.go new file mode 100644 index 0000000000..fdc3245374 --- /dev/null +++ b/internal/command/session_passkeys_test.go @@ -0,0 +1,130 @@ +package command + +import ( + "context" + "io" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "golang.org/x/text/language" + + "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/eventstore/v1/models" + "github.com/zitadel/zitadel/internal/repository/org" + "github.com/zitadel/zitadel/internal/repository/user" +) + +func TestSessionCommands_getHumanPasskeys(t *testing.T) { + userAggr := &user.NewAggregate("user1", "org1").Aggregate + + type fields struct { + eventstore *eventstore.Eventstore + sessionWriteModel *SessionWriteModel + } + type res struct { + want *humanPasskeys + err error + } + tests := []struct { + name string + fields fields + res res + }{ + { + name: "missing UID", + fields: fields{ + eventstore: &eventstore.Eventstore{}, + sessionWriteModel: &SessionWriteModel{}, + }, + res: res{ + want: nil, + err: caos_errs.ThrowPreconditionFailed(nil, "COMMAND-eeR2e", "Errors.User.UserIDMissing"), + }, + }, + { + name: "passwordless filter error", + fields: fields{ + eventstore: eventstoreExpect(t, + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + userAggr, + "", "", "", "", "", language.Georgian, + domain.GenderDiverse, "", true, + ), + ), + ), + expectFilterError(io.ErrClosedPipe), + ), + sessionWriteModel: &SessionWriteModel{ + UserID: "user1", + }, + }, + res: res{ + want: nil, + err: io.ErrClosedPipe, + }, + }, + { + name: "ok", + fields: fields{ + eventstore: eventstoreExpect(t, + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + userAggr, + "", "", "", "", "", language.Georgian, + domain.GenderDiverse, "", true, + ), + ), + ), + expectFilter(eventFromEventPusher( + user.NewHumanWebAuthNAddedEvent(eventstore.NewBaseEventForPush( + context.Background(), &org.NewAggregate("org1").Aggregate, user.HumanPasswordlessTokenAddedType, + ), "111", "challenge"), + )), + ), + sessionWriteModel: &SessionWriteModel{ + UserID: "user1", + }, + }, + res: res{ + want: &humanPasskeys{ + human: &domain.Human{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "user1", + ResourceOwner: "org1", + }, + State: domain.UserStateActive, + Profile: &domain.Profile{ + PreferredLanguage: language.Georgian, + Gender: domain.GenderDiverse, + }, + Email: &domain.Email{}, + }, + tokens: []*domain.WebAuthNToken{{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "org1", + }, + WebAuthNTokenID: "111", + State: domain.MFAStateNotReady, + Challenge: "challenge", + }}, + }, + err: nil, + }, + }, + } + for _, tt := range tests { + s := &SessionCommands{ + eventstore: tt.fields.eventstore, + sessionWriteModel: tt.fields.sessionWriteModel, + } + got, err := s.getHumanPasskeys(context.Background()) + require.ErrorIs(t, err, tt.res.err) + assert.Equal(t, tt.res.want, got) + } +} diff --git a/internal/command/session_test.go b/internal/command/session_test.go index eb080480fa..b9e24b03eb 100644 --- a/internal/command/session_test.go +++ b/internal/command/session_test.go @@ -2,6 +2,7 @@ package command import ( "context" + "io" "testing" "time" @@ -21,6 +22,121 @@ import ( "github.com/zitadel/zitadel/internal/repository/user" ) +func TestSessionCommands_getHumanWriteModel(t *testing.T) { + userAggr := &user.NewAggregate("user1", "org1").Aggregate + + type fields struct { + eventstore *eventstore.Eventstore + sessionWriteModel *SessionWriteModel + } + type res struct { + want *HumanWriteModel + err error + } + tests := []struct { + name string + fields fields + res res + }{ + { + name: "missing UID", + fields: fields{ + eventstore: &eventstore.Eventstore{}, + sessionWriteModel: &SessionWriteModel{}, + }, + res: res{ + want: nil, + err: caos_errs.ThrowPreconditionFailed(nil, "COMMAND-eeR2e", "Errors.User.UserIDMissing"), + }, + }, + { + name: "filter error", + fields: fields{ + eventstore: eventstoreExpect(t, + expectFilterError(io.ErrClosedPipe), + ), + sessionWriteModel: &SessionWriteModel{ + UserID: "user1", + }, + }, + res: res{ + want: nil, + err: io.ErrClosedPipe, + }, + }, + { + name: "removed user", + fields: fields{ + eventstore: eventstoreExpect(t, + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + userAggr, + "", "", "", "", "", language.Georgian, + domain.GenderDiverse, "", true, + ), + ), + eventFromEventPusher( + user.NewUserRemovedEvent(context.Background(), + userAggr, + "", nil, true, + ), + ), + ), + ), + sessionWriteModel: &SessionWriteModel{ + UserID: "user1", + }, + }, + res: res{ + want: nil, + err: caos_errs.ThrowPreconditionFailed(nil, "COMMAND-Df4b3", "Errors.ie4Ai.NotFound"), + }, + }, + { + name: "ok", + fields: fields{ + eventstore: eventstoreExpect(t, + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + userAggr, + "", "", "", "", "", language.Georgian, + domain.GenderDiverse, "", true, + ), + ), + ), + ), + sessionWriteModel: &SessionWriteModel{ + UserID: "user1", + }, + }, + res: res{ + want: &HumanWriteModel{ + WriteModel: eventstore.WriteModel{ + AggregateID: "user1", + ResourceOwner: "org1", + Events: []eventstore.Event{}, + }, + PreferredLanguage: language.Georgian, + Gender: domain.GenderDiverse, + UserState: domain.UserStateActive, + }, + err: nil, + }, + }, + } + for _, tt := range tests { + s := &SessionCommands{ + eventstore: tt.fields.eventstore, + sessionWriteModel: tt.fields.sessionWriteModel, + } + got, err := s.gethumanWriteModel(context.Background()) + require.ErrorIs(t, err, tt.res.err) + assert.Equal(t, tt.res.want, got) + } +} + func TestCommands_CreateSession(t *testing.T) { type fields struct { eventstore *eventstore.Eventstore @@ -29,7 +145,7 @@ func TestCommands_CreateSession(t *testing.T) { } type args struct { ctx context.Context - checks []SessionCheck + checks []SessionCommand metadata map[string][]byte } type res struct { @@ -126,7 +242,7 @@ func TestCommands_UpdateSession(t *testing.T) { ctx context.Context sessionID string sessionToken string - checks []SessionCheck + checks []SessionCommand metadata map[string][]byte } type res struct { @@ -231,7 +347,7 @@ func TestCommands_updateSession(t *testing.T) { } type args struct { ctx context.Context - checks *SessionChecks + checks *SessionCommands metadata map[string][]byte } type res struct { @@ -251,7 +367,7 @@ func TestCommands_updateSession(t *testing.T) { }, args{ ctx: context.Background(), - checks: &SessionChecks{ + checks: &SessionCommands{ sessionWriteModel: &SessionWriteModel{State: domain.SessionStateTerminated}, }, }, @@ -266,10 +382,10 @@ func TestCommands_updateSession(t *testing.T) { }, args{ ctx: context.Background(), - checks: &SessionChecks{ + checks: &SessionCommands{ sessionWriteModel: NewSessionWriteModel("sessionID", "org1"), - checks: []SessionCheck{ - func(ctx context.Context, cmd *SessionChecks) error { + cmds: []SessionCommand{ + func(ctx context.Context, cmd *SessionCommands) error { return caos_errs.ThrowInternal(nil, "id", "check failed") }, }, @@ -286,9 +402,9 @@ func TestCommands_updateSession(t *testing.T) { }, args{ ctx: context.Background(), - checks: &SessionChecks{ + checks: &SessionCommands{ sessionWriteModel: NewSessionWriteModel("sessionID", "org1"), - checks: []SessionCheck{}, + cmds: []SessionCommand{}, }, }, res{ @@ -321,9 +437,9 @@ func TestCommands_updateSession(t *testing.T) { }, args{ ctx: context.Background(), - checks: &SessionChecks{ + checks: &SessionCommands{ sessionWriteModel: NewSessionWriteModel("sessionID", "org1"), - checks: []SessionCheck{ + cmds: []SessionCommand{ CheckUser("userID"), CheckPassword("password"), }, diff --git a/internal/command/user_machine_secret.go b/internal/command/user_machine_secret.go index 577e18d3b1..a7a91b247f 100644 --- a/internal/command/user_machine_secret.go +++ b/internal/command/user_machine_secret.go @@ -14,7 +14,6 @@ import ( ) type GenerateMachineSecret struct { - ClientID string ClientSecret string } @@ -53,7 +52,6 @@ func prepareGenerateMachineSecret(a *user.Aggregate, generator crypto.Generator, if !isUserStateExists(writeModel.UserState) { return nil, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-x8910n", "Errors.User.NotExisting") } - set.ClientID = writeModel.UserName clientSecret, secretString, err := domain.NewMachineClientSecret(generator) if err != nil { diff --git a/internal/command/user_machine_secret_test.go b/internal/command/user_machine_secret_test.go index 3965558e7a..490797b96d 100644 --- a/internal/command/user_machine_secret_test.go +++ b/internal/command/user_machine_secret_test.go @@ -137,7 +137,6 @@ func TestCommandSide_GenerateMachineSecret(t *testing.T) { ResourceOwner: "org1", }, secret: &GenerateMachineSecret{ - ClientID: "user1", ClientSecret: "a", }, }, @@ -157,7 +156,6 @@ func TestCommandSide_GenerateMachineSecret(t *testing.T) { } if tt.res.err == nil { assert.Equal(t, tt.res.want, got) - assert.Equal(t, tt.args.set.ClientID, tt.res.secret.ClientID) assert.Equal(t, tt.args.set.ClientSecret, tt.res.secret.ClientSecret) } }) diff --git a/internal/domain/idp.go b/internal/domain/idp.go index f276f8eb4d..d06fcdd1f7 100644 --- a/internal/domain/idp.go +++ b/internal/domain/idp.go @@ -9,6 +9,7 @@ const ( IDPStateActive IDPStateInactive IDPStateRemoved + IDPStateMigrated idpStateCount ) @@ -18,7 +19,7 @@ func (s IDPState) Valid() bool { } func (s IDPState) Exists() bool { - return s != IDPStateUnspecified && s != IDPStateRemoved + return s != IDPStateUnspecified && s != IDPStateRemoved && s != IDPStateMigrated } type IDPType int32 diff --git a/internal/eventstore/repository/sql/crdb.go b/internal/eventstore/repository/sql/crdb.go index f27936de4c..62cf1f92a5 100644 --- a/internal/eventstore/repository/sql/crdb.go +++ b/internal/eventstore/repository/sql/crdb.go @@ -140,7 +140,7 @@ func (db *CRDB) Push(ctx context.Context, events []*repository.Event, uniqueCons "aggregateType", event.AggregateType, "eventType", event.Type, "instanceID", event.InstanceID, - ).WithError(err).Info("query failed") + ).WithError(err).Debug("query failed") return caos_errs.ThrowInternal(err, "SQL-SBP37", "unable to create event") } } diff --git a/internal/integration/assert.go b/internal/integration/assert.go index 3361392987..e5105d8d23 100644 --- a/internal/integration/assert.go +++ b/internal/integration/assert.go @@ -35,7 +35,7 @@ func AssertDetails[D DetailsMsg](t testing.TB, exptected, actual D) { gotCD := gotDetails.GetChangeDate().AsTime() now := time.Now() - assert.WithinRange(t, gotCD, now.Add(-time.Second), now.Add(time.Second)) + assert.WithinRange(t, gotCD, now.Add(-time.Minute), now.Add(time.Minute)) assert.Equal(t, wantDetails.GetResourceOwner(), gotDetails.GetResourceOwner()) } diff --git a/internal/integration/client.go b/internal/integration/client.go new file mode 100644 index 0000000000..c21b42f67e --- /dev/null +++ b/internal/integration/client.go @@ -0,0 +1,77 @@ +package integration + +import ( + "context" + "fmt" + "time" + + "github.com/zitadel/logging" + "google.golang.org/grpc" + + "github.com/zitadel/zitadel/pkg/grpc/admin" + object "github.com/zitadel/zitadel/pkg/grpc/object/v2alpha" + session "github.com/zitadel/zitadel/pkg/grpc/session/v2alpha" + user "github.com/zitadel/zitadel/pkg/grpc/user/v2alpha" +) + +type Client struct { + CC *grpc.ClientConn + Admin admin.AdminServiceClient + UserV2 user.UserServiceClient + SessionV2 session.SessionServiceClient +} + +func newClient(cc *grpc.ClientConn) Client { + return Client{ + CC: cc, + Admin: admin.NewAdminServiceClient(cc), + UserV2: user.NewUserServiceClient(cc), + SessionV2: session.NewSessionServiceClient(cc), + } +} + +func (s *Tester) CreateHumanUser(ctx context.Context) *user.AddHumanUserResponse { + resp, err := s.Client.UserV2.AddHumanUser(ctx, &user.AddHumanUserRequest{ + Organisation: &object.Organisation{ + Org: &object.Organisation_OrgId{ + OrgId: s.Organisation.ID, + }, + }, + Profile: &user.SetHumanProfile{ + FirstName: "Mickey", + LastName: "Mouse", + }, + Email: &user.SetHumanEmail{ + Email: fmt.Sprintf("%d@mouse.com", time.Now().UnixNano()), + Verification: &user.SetHumanEmail_ReturnCode{ + ReturnCode: &user.ReturnEmailVerificationCode{}, + }, + }, + }) + logging.OnError(err).Fatal("create human user") + return resp +} + +func (s *Tester) RegisterUserPasskey(ctx context.Context, userID string) { + reg, err := s.Client.UserV2.CreatePasskeyRegistrationLink(ctx, &user.CreatePasskeyRegistrationLinkRequest{ + UserId: userID, + Medium: &user.CreatePasskeyRegistrationLinkRequest_ReturnCode{}, + }) + logging.OnError(err).Fatal("create user passkey") + + pkr, err := s.Client.UserV2.RegisterPasskey(ctx, &user.RegisterPasskeyRequest{ + UserId: userID, + Code: reg.GetCode(), + }) + logging.OnError(err).Fatal("create user passkey") + attestationResponse, err := s.WebAuthN.CreateAttestationResponse(pkr.GetPublicKeyCredentialCreationOptions()) + logging.OnError(err).Fatal("create user passkey") + + _, err = s.Client.UserV2.VerifyPasskeyRegistration(ctx, &user.VerifyPasskeyRegistrationRequest{ + UserId: userID, + PasskeyId: pkr.GetPasskeyId(), + PublicKeyCredential: attestationResponse, + PasskeyName: "nice name", + }) + logging.OnError(err).Fatal("create user passkey") +} diff --git a/internal/integration/integration.go b/internal/integration/integration.go index 67ef9d60b2..6ffec11e1d 100644 --- a/internal/integration/integration.go +++ b/internal/integration/integration.go @@ -29,6 +29,7 @@ import ( caos_errs "github.com/zitadel/zitadel/internal/errors" "github.com/zitadel/zitadel/internal/eventstore/v1/models" "github.com/zitadel/zitadel/internal/query" + "github.com/zitadel/zitadel/internal/webauthn" "github.com/zitadel/zitadel/pkg/grpc/admin" ) @@ -68,8 +69,9 @@ type Tester struct { Organisation *query.Org Users map[UserType]User - GRPCClientConn *grpc.ClientConn - wg sync.WaitGroup // used for shutdown + Client Client + WebAuthN *webauthn.Client + wg sync.WaitGroup // used for shutdown } const commandLine = `start --masterkeyFromEnv` @@ -90,7 +92,7 @@ func (s *Tester) createClientConn(ctx context.Context) { logging.OnError(err).Fatal("integration tester client dial") logging.New().WithField("target", target).Info("finished dialing grpc client conn") - s.GRPCClientConn = cc + s.Client = newClient(cc) err = s.pollHealth(ctx) logging.OnError(err).Fatal("integration tester health") } @@ -99,14 +101,12 @@ func (s *Tester) createClientConn(ctx context.Context) { // TODO: remove when we make the setup blocking on all // projections completed. func (s *Tester) pollHealth(ctx context.Context) (err error) { - client := admin.NewAdminServiceClient(s.GRPCClientConn) - for { err = func(ctx context.Context) error { ctx, cancel := context.WithTimeout(ctx, 5*time.Second) defer cancel() - _, err := client.Healthz(ctx, &admin.HealthzRequest{}) + _, err := s.Client.Admin.Healthz(ctx, &admin.HealthzRequest{}) return err }(ctx) if err == nil { @@ -182,7 +182,7 @@ func (s *Tester) WithSystemAuthorization(ctx context.Context, u UserType) contex // Done send an interrupt signal to cleanly shutdown the server. func (s *Tester) Done() { - err := s.GRPCClientConn.Close() + err := s.Client.CC.Close() logging.OnError(err).Error("integration tester client close") s.Shutdown <- os.Interrupt @@ -238,6 +238,7 @@ func NewTester(ctx context.Context) *Tester { } tester.createClientConn(ctx) tester.createSystemUser(ctx) + tester.WebAuthN = webauthn.NewClient(tester.Config.WebAuthNName, tester.Config.ExternalDomain, "https://"+tester.Host()) return tester } diff --git a/internal/query/projection/idp_template.go b/internal/query/projection/idp_template.go index 4e74e6f529..80fee7e479 100644 --- a/internal/query/projection/idp_template.go +++ b/internal/query/projection/idp_template.go @@ -347,6 +347,10 @@ func (p *idpTemplateProjection) reducers() []handler.AggregateReducer { Event: instance.OIDCIDPChangedEventType, Reduce: p.reduceOIDCIDPChanged, }, + { + Event: instance.OIDCIDPMigratedAzureADEventType, + Reduce: p.reduceOIDCIDPMigratedAzureAD, + }, { Event: instance.JWTIDPAddedEventType, Reduce: p.reduceJWTIDPAdded, @@ -755,6 +759,106 @@ func (p *idpTemplateProjection) reduceOIDCIDPChanged(event eventstore.Event) (*h ), nil } +func (p *idpTemplateProjection) reduceOIDCIDPMigratedAzureAD(event eventstore.Event) (*handler.Statement, error) { + var idpEvent idp.OIDCIDPMigratedAzureADEvent + switch e := event.(type) { + case *org.OIDCIDPMigratedAzureADEvent: + idpEvent = e.OIDCIDPMigratedAzureADEvent + case *instance.OIDCIDPMigratedAzureADEvent: + idpEvent = e.OIDCIDPMigratedAzureADEvent + default: + return nil, errors.ThrowInvalidArgumentf(nil, "HANDL-p1582ks", "reduce.wrong.event.type %v", []eventstore.EventType{org.OIDCIDPMigratedAzureADEventType, instance.OIDCIDPMigratedAzureADEventType}) + } + + return crdb.NewMultiStatement( + &idpEvent, + crdb.AddUpdateStatement( + []handler.Column{ + handler.NewCol(IDPTemplateChangeDateCol, idpEvent.CreationDate()), + handler.NewCol(IDPTemplateSequenceCol, idpEvent.Sequence()), + handler.NewCol(IDPTemplateNameCol, idpEvent.Name), + handler.NewCol(IDPTemplateTypeCol, domain.IDPTypeAzureAD), + handler.NewCol(IDPTemplateIsCreationAllowedCol, idpEvent.IsCreationAllowed), + handler.NewCol(IDPTemplateIsLinkingAllowedCol, idpEvent.IsLinkingAllowed), + handler.NewCol(IDPTemplateIsAutoCreationCol, idpEvent.IsAutoCreation), + handler.NewCol(IDPTemplateIsAutoUpdateCol, idpEvent.IsAutoUpdate), + }, + []handler.Condition{ + handler.NewCond(IDPTemplateIDCol, idpEvent.ID), + handler.NewCond(IDPTemplateInstanceIDCol, idpEvent.Aggregate().InstanceID), + }, + ), + crdb.AddDeleteStatement( + []handler.Condition{ + handler.NewCond(OIDCIDCol, idpEvent.ID), + handler.NewCond(OIDCInstanceIDCol, idpEvent.Aggregate().InstanceID), + }, + crdb.WithTableSuffix(IDPTemplateOIDCSuffix), + ), + crdb.AddCreateStatement( + []handler.Column{ + handler.NewCol(AzureADIDCol, idpEvent.ID), + handler.NewCol(AzureADInstanceIDCol, idpEvent.Aggregate().InstanceID), + handler.NewCol(AzureADClientIDCol, idpEvent.ClientID), + handler.NewCol(AzureADClientSecretCol, idpEvent.ClientSecret), + handler.NewCol(AzureADScopesCol, database.StringArray(idpEvent.Scopes)), + handler.NewCol(AzureADTenantCol, idpEvent.Tenant), + handler.NewCol(AzureADIsEmailVerified, idpEvent.IsEmailVerified), + }, + crdb.WithTableSuffix(IDPTemplateAzureADSuffix), + ), + ), nil +} + +func (p *idpTemplateProjection) reduceOIDCIDPMigratedGoogle(event eventstore.Event) (*handler.Statement, error) { + var idpEvent idp.OIDCIDPMigratedGoogleEvent + switch e := event.(type) { + case *org.OIDCIDPMigratedGoogleEvent: + idpEvent = e.OIDCIDPMigratedGoogleEvent + case *instance.OIDCIDPMigratedGoogleEvent: + idpEvent = e.OIDCIDPMigratedGoogleEvent + default: + return nil, errors.ThrowInvalidArgumentf(nil, "HANDL-p1582ks", "reduce.wrong.event.type %v", []eventstore.EventType{org.OIDCIDPMigratedGoogleEventType, instance.OIDCIDPMigratedGoogleEventType}) + } + + return crdb.NewMultiStatement( + &idpEvent, + crdb.AddUpdateStatement( + []handler.Column{ + handler.NewCol(IDPTemplateChangeDateCol, idpEvent.CreationDate()), + handler.NewCol(IDPTemplateSequenceCol, idpEvent.Sequence()), + handler.NewCol(IDPTemplateNameCol, idpEvent.Name), + handler.NewCol(IDPTemplateTypeCol, domain.IDPTypeGoogle), + handler.NewCol(IDPTemplateIsCreationAllowedCol, idpEvent.IsCreationAllowed), + handler.NewCol(IDPTemplateIsLinkingAllowedCol, idpEvent.IsLinkingAllowed), + handler.NewCol(IDPTemplateIsAutoCreationCol, idpEvent.IsAutoCreation), + handler.NewCol(IDPTemplateIsAutoUpdateCol, idpEvent.IsAutoUpdate), + }, + []handler.Condition{ + handler.NewCond(IDPTemplateIDCol, idpEvent.ID), + handler.NewCond(IDPTemplateInstanceIDCol, idpEvent.Aggregate().InstanceID), + }, + ), + crdb.AddDeleteStatement( + []handler.Condition{ + handler.NewCond(OIDCIDCol, idpEvent.ID), + handler.NewCond(OIDCInstanceIDCol, idpEvent.Aggregate().InstanceID), + }, + crdb.WithTableSuffix(IDPTemplateOIDCSuffix), + ), + crdb.AddCreateStatement( + []handler.Column{ + handler.NewCol(GoogleIDCol, idpEvent.ID), + handler.NewCol(GoogleInstanceIDCol, idpEvent.Aggregate().InstanceID), + handler.NewCol(GoogleClientIDCol, idpEvent.ClientID), + handler.NewCol(GoogleClientSecretCol, idpEvent.ClientSecret), + handler.NewCol(GoogleScopesCol, database.StringArray(idpEvent.Scopes)), + }, + crdb.WithTableSuffix(IDPTemplateGoogleSuffix), + ), + ), nil +} + func (p *idpTemplateProjection) reduceJWTIDPAdded(event eventstore.Event) (*handler.Statement, error) { var idpEvent idp.JWTIDPAddedEvent var idpOwnerType domain.IdentityProviderType diff --git a/internal/query/projection/idp_template_test.go b/internal/query/projection/idp_template_test.go index 64f3fa3a43..355020717e 100644 --- a/internal/query/projection/idp_template_test.go +++ b/internal/query/projection/idp_template_test.go @@ -2686,6 +2686,278 @@ func TestIDPTemplateProjection_reducesOIDC(t *testing.T) { }, }, }, + { + name: "instance reduceOIDCIDPMigratedAzureAD", + args: args{ + event: getEvent(testEvent( + repository.EventType(instance.OIDCIDPMigratedAzureADEventType), + instance.AggregateType, + []byte(`{ + "id": "idp-id", + "name": "name", + "client_id": "client_id", + "client_secret": { + "cryptoType": 0, + "algorithm": "RSA-265", + "keyId": "key-id" + }, + "tenant": "tenant", + "isEmailVerified": true, + "scopes": ["profile"], + "isCreationAllowed": true, + "isLinkingAllowed": true, + "isAutoCreation": true, + "isAutoUpdate": true +}`), + ), instance.OIDCIDPMigratedAzureADEventMapper), + }, + reduce: (&idpTemplateProjection{}).reduceOIDCIDPMigratedAzureAD, + want: wantReduce{ + aggregateType: eventstore.AggregateType("instance"), + sequence: 15, + previousSequence: 10, + executer: &testExecuter{ + executions: []execution{ + { + expectedStmt: "UPDATE projections.idp_templates5 SET (change_date, sequence, name, type, is_creation_allowed, is_linking_allowed, is_auto_creation, is_auto_update) = ($1, $2, $3, $4, $5, $6, $7, $8) WHERE (id = $9) AND (instance_id = $10)", + expectedArgs: []interface{}{ + anyArg{}, + uint64(15), + "name", + domain.IDPTypeAzureAD, + true, + true, + true, + true, + "idp-id", + "instance-id", + }, + }, + { + expectedStmt: "DELETE FROM projections.idp_templates5_oidc WHERE (idp_id = $1) AND (instance_id = $2)", + expectedArgs: []interface{}{ + "idp-id", + "instance-id", + }, + }, + { + expectedStmt: "INSERT INTO projections.idp_templates5_azure (idp_id, instance_id, client_id, client_secret, scopes, tenant, is_email_verified) VALUES ($1, $2, $3, $4, $5, $6, $7)", + expectedArgs: []interface{}{ + "idp-id", + "instance-id", + "client_id", + anyArg{}, + database.StringArray{"profile"}, + "tenant", + true, + }, + }, + }, + }, + }, + }, + { + name: "org reduceOIDCIDPMigratedAzureAD", + args: args{ + event: getEvent(testEvent( + repository.EventType(org.OIDCIDPMigratedAzureADEventType), + org.AggregateType, + []byte(`{ + "id": "idp-id", + "name": "name", + "client_id": "client_id", + "client_secret": { + "cryptoType": 0, + "algorithm": "RSA-265", + "keyId": "key-id" + }, + "tenant": "tenant", + "isEmailVerified": true, + "scopes": ["profile"], + "isCreationAllowed": true, + "isLinkingAllowed": true, + "isAutoCreation": true, + "isAutoUpdate": true +}`), + ), org.OIDCIDPMigratedAzureADEventMapper), + }, + reduce: (&idpTemplateProjection{}).reduceOIDCIDPMigratedAzureAD, + want: wantReduce{ + aggregateType: eventstore.AggregateType("org"), + sequence: 15, + previousSequence: 10, + executer: &testExecuter{ + executions: []execution{ + { + expectedStmt: "UPDATE projections.idp_templates5 SET (change_date, sequence, name, type, is_creation_allowed, is_linking_allowed, is_auto_creation, is_auto_update) = ($1, $2, $3, $4, $5, $6, $7, $8) WHERE (id = $9) AND (instance_id = $10)", + expectedArgs: []interface{}{ + anyArg{}, + uint64(15), + "name", + domain.IDPTypeAzureAD, + true, + true, + true, + true, + "idp-id", + "instance-id", + }, + }, + { + expectedStmt: "DELETE FROM projections.idp_templates5_oidc WHERE (idp_id = $1) AND (instance_id = $2)", + expectedArgs: []interface{}{ + "idp-id", + "instance-id", + }, + }, + { + expectedStmt: "INSERT INTO projections.idp_templates5_azure (idp_id, instance_id, client_id, client_secret, scopes, tenant, is_email_verified) VALUES ($1, $2, $3, $4, $5, $6, $7)", + expectedArgs: []interface{}{ + "idp-id", + "instance-id", + "client_id", + anyArg{}, + database.StringArray{"profile"}, + "tenant", + true, + }, + }, + }, + }, + }, + }, + { + name: "instance reduceOIDCIDPMigratedGoogle", + args: args{ + event: getEvent(testEvent( + repository.EventType(instance.OIDCIDPMigratedGoogleEventType), + instance.AggregateType, + []byte(`{ + "id": "idp-id", + "name": "name", + "clientId": "client_id", + "clientSecret": { + "cryptoType": 0, + "algorithm": "RSA-265", + "keyId": "key-id" + }, + "scopes": ["profile"], + "isCreationAllowed": true, + "isLinkingAllowed": true, + "isAutoCreation": true, + "isAutoUpdate": true +}`), + ), instance.OIDCIDPMigratedGoogleEventMapper), + }, + reduce: (&idpTemplateProjection{}).reduceOIDCIDPMigratedGoogle, + want: wantReduce{ + aggregateType: eventstore.AggregateType("instance"), + sequence: 15, + previousSequence: 10, + executer: &testExecuter{ + executions: []execution{ + { + expectedStmt: "UPDATE projections.idp_templates5 SET (change_date, sequence, name, type, is_creation_allowed, is_linking_allowed, is_auto_creation, is_auto_update) = ($1, $2, $3, $4, $5, $6, $7, $8) WHERE (id = $9) AND (instance_id = $10)", + expectedArgs: []interface{}{ + anyArg{}, + uint64(15), + "name", + domain.IDPTypeGoogle, + true, + true, + true, + true, + "idp-id", + "instance-id", + }, + }, + { + expectedStmt: "DELETE FROM projections.idp_templates5_oidc WHERE (idp_id = $1) AND (instance_id = $2)", + expectedArgs: []interface{}{ + "idp-id", + "instance-id", + }, + }, + { + expectedStmt: "INSERT INTO projections.idp_templates5_google (idp_id, instance_id, client_id, client_secret, scopes) VALUES ($1, $2, $3, $4, $5)", + expectedArgs: []interface{}{ + "idp-id", + "instance-id", + "client_id", + anyArg{}, + database.StringArray{"profile"}, + }, + }, + }, + }, + }, + }, + { + name: "org reduceOIDCIDPMigratedGoogle", + args: args{ + event: getEvent(testEvent( + repository.EventType(org.OIDCIDPMigratedGoogleEventType), + org.AggregateType, + []byte(`{ + "id": "idp-id", + "name": "name", + "clientId": "client_id", + "clientSecret": { + "cryptoType": 0, + "algorithm": "RSA-265", + "keyId": "key-id" + }, + "scopes": ["profile"], + "isCreationAllowed": true, + "isLinkingAllowed": true, + "isAutoCreation": true, + "isAutoUpdate": true +}`), + ), org.OIDCIDPMigratedGoogleEventMapper), + }, + reduce: (&idpTemplateProjection{}).reduceOIDCIDPMigratedGoogle, + want: wantReduce{ + aggregateType: eventstore.AggregateType("org"), + sequence: 15, + previousSequence: 10, + executer: &testExecuter{ + executions: []execution{ + { + expectedStmt: "UPDATE projections.idp_templates5 SET (change_date, sequence, name, type, is_creation_allowed, is_linking_allowed, is_auto_creation, is_auto_update) = ($1, $2, $3, $4, $5, $6, $7, $8) WHERE (id = $9) AND (instance_id = $10)", + expectedArgs: []interface{}{ + anyArg{}, + uint64(15), + "name", + domain.IDPTypeGoogle, + true, + true, + true, + true, + "idp-id", + "instance-id", + }, + }, + { + expectedStmt: "DELETE FROM projections.idp_templates5_oidc WHERE (idp_id = $1) AND (instance_id = $2)", + expectedArgs: []interface{}{ + "idp-id", + "instance-id", + }, + }, + { + expectedStmt: "INSERT INTO projections.idp_templates5_google (idp_id, instance_id, client_id, client_secret, scopes) VALUES ($1, $2, $3, $4, $5)", + expectedArgs: []interface{}{ + "idp-id", + "instance-id", + "client_id", + anyArg{}, + database.StringArray{"profile"}, + }, + }, + }, + }, + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/internal/query/projection/session.go b/internal/query/projection/session.go index 9858de4739..d40302a84f 100644 --- a/internal/query/projection/session.go +++ b/internal/query/projection/session.go @@ -13,7 +13,7 @@ import ( ) const ( - SessionsProjectionTable = "projections.sessions" + SessionsProjectionTable = "projections.sessions1" SessionColumnID = "id" SessionColumnCreationDate = "creation_date" @@ -26,6 +26,7 @@ const ( SessionColumnUserID = "user_id" SessionColumnUserCheckedAt = "user_checked_at" SessionColumnPasswordCheckedAt = "password_checked_at" + SessionColumnPasskeyCheckedAt = "passkey_checked_at" SessionColumnMetadata = "metadata" SessionColumnTokenID = "token_id" ) @@ -51,6 +52,7 @@ func newSessionProjection(ctx context.Context, config crdb.StatementHandlerConfi crdb.NewColumn(SessionColumnUserID, crdb.ColumnTypeText, crdb.Nullable()), crdb.NewColumn(SessionColumnUserCheckedAt, crdb.ColumnTypeTimestamp, crdb.Nullable()), crdb.NewColumn(SessionColumnPasswordCheckedAt, crdb.ColumnTypeTimestamp, crdb.Nullable()), + crdb.NewColumn(SessionColumnPasskeyCheckedAt, crdb.ColumnTypeTimestamp, crdb.Nullable()), crdb.NewColumn(SessionColumnMetadata, crdb.ColumnTypeJSONB, crdb.Nullable()), crdb.NewColumn(SessionColumnTokenID, crdb.ColumnTypeText, crdb.Nullable()), }, @@ -78,6 +80,10 @@ func (p *sessionProjection) reducers() []handler.AggregateReducer { Event: session.PasswordCheckedType, Reduce: p.reducePasswordChecked, }, + { + Event: session.PasskeyCheckedType, + Reduce: p.reducePasskeyChecked, + }, { Event: session.TokenSetType, Reduce: p.reduceTokenSet, @@ -165,6 +171,26 @@ func (p *sessionProjection) reducePasswordChecked(event eventstore.Event) (*hand ), nil } +func (p *sessionProjection) reducePasskeyChecked(event eventstore.Event) (*handler.Statement, error) { + e, ok := event.(*session.PasskeyCheckedEvent) + if !ok { + return nil, errors.ThrowInvalidArgumentf(nil, "HANDL-WieM4", "reduce.wrong.event.type %s", session.PasskeyCheckedType) + } + + return crdb.NewUpdateStatement( + e, + []handler.Column{ + handler.NewCol(SessionColumnChangeDate, e.CreationDate()), + handler.NewCol(SessionColumnSequence, e.Sequence()), + handler.NewCol(SessionColumnPasskeyCheckedAt, e.CheckedAt), + }, + []handler.Condition{ + handler.NewCond(SessionColumnID, e.Aggregate().ID), + handler.NewCond(SessionColumnInstanceID, e.Aggregate().InstanceID), + }, + ), nil +} + func (p *sessionProjection) reduceTokenSet(event eventstore.Event) (*handler.Statement, error) { e, ok := event.(*session.TokenSetEvent) if !ok { diff --git a/internal/query/projection/session_test.go b/internal/query/projection/session_test.go index 6cd3ae2e59..ecb96368c6 100644 --- a/internal/query/projection/session_test.go +++ b/internal/query/projection/session_test.go @@ -40,7 +40,7 @@ func TestSessionProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "INSERT INTO projections.sessions (id, instance_id, creation_date, change_date, resource_owner, state, sequence, creator) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)", + expectedStmt: "INSERT INTO projections.sessions1 (id, instance_id, creation_date, change_date, resource_owner, state, sequence, creator) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)", expectedArgs: []interface{}{ "agg-id", "instance-id", @@ -76,7 +76,7 @@ func TestSessionProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.sessions SET (change_date, sequence, user_id, user_checked_at) = ($1, $2, $3, $4) WHERE (id = $5) AND (instance_id = $6)", + expectedStmt: "UPDATE projections.sessions1 SET (change_date, sequence, user_id, user_checked_at) = ($1, $2, $3, $4) WHERE (id = $5) AND (instance_id = $6)", expectedArgs: []interface{}{ anyArg{}, anyArg{}, @@ -109,7 +109,7 @@ func TestSessionProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.sessions SET (change_date, sequence, password_checked_at) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)", + expectedStmt: "UPDATE projections.sessions1 SET (change_date, sequence, password_checked_at) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)", expectedArgs: []interface{}{ anyArg{}, anyArg{}, @@ -141,7 +141,7 @@ func TestSessionProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.sessions SET (change_date, sequence, token_id) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)", + expectedStmt: "UPDATE projections.sessions1 SET (change_date, sequence, token_id) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)", expectedArgs: []interface{}{ anyArg{}, anyArg{}, @@ -175,7 +175,7 @@ func TestSessionProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.sessions SET (change_date, sequence, metadata) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)", + expectedStmt: "UPDATE projections.sessions1 SET (change_date, sequence, metadata) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)", expectedArgs: []interface{}{ anyArg{}, anyArg{}, @@ -207,7 +207,7 @@ func TestSessionProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "DELETE FROM projections.sessions WHERE (id = $1) AND (instance_id = $2)", + expectedStmt: "DELETE FROM projections.sessions1 WHERE (id = $1) AND (instance_id = $2)", expectedArgs: []interface{}{ "agg-id", "instance-id", @@ -234,7 +234,7 @@ func TestSessionProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "DELETE FROM projections.sessions WHERE (instance_id = $1)", + expectedStmt: "DELETE FROM projections.sessions1 WHERE (instance_id = $1)", expectedArgs: []interface{}{ "agg-id", }, diff --git a/internal/query/session.go b/internal/query/session.go index ba30c64da6..2dfc622a7d 100644 --- a/internal/query/session.go +++ b/internal/query/session.go @@ -32,6 +32,7 @@ type Session struct { Creator string UserFactor SessionUserFactor PasswordFactor SessionPasswordFactor + PasskeyFactor SessionPasskeyFactor Metadata map[string][]byte } @@ -46,6 +47,10 @@ type SessionPasswordFactor struct { PasswordCheckedAt time.Time } +type SessionPasskeyFactor struct { + PasskeyCheckedAt time.Time +} + type SessionsSearchQueries struct { SearchRequest Queries []SearchQuery @@ -108,6 +113,10 @@ var ( name: projection.SessionColumnPasswordCheckedAt, table: sessionsTable, } + SessionColumnPasskeyCheckedAt = Column{ + name: projection.SessionColumnPasskeyCheckedAt, + table: sessionsTable, + } SessionColumnMetadata = Column{ name: projection.SessionColumnMetadata, table: sessionsTable, @@ -198,6 +207,7 @@ func prepareSessionQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuil LoginNameNameCol.identifier(), HumanDisplayNameCol.identifier(), SessionColumnPasswordCheckedAt.identifier(), + SessionColumnPasskeyCheckedAt.identifier(), SessionColumnMetadata.identifier(), SessionColumnToken.identifier(), ).From(sessionsTable.identifier()). @@ -212,6 +222,7 @@ func prepareSessionQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuil loginName sql.NullString displayName sql.NullString passwordCheckedAt sql.NullTime + passkeyCheckedAt sql.NullTime metadata database.Map[[]byte] token sql.NullString ) @@ -229,6 +240,7 @@ func prepareSessionQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuil &loginName, &displayName, &passwordCheckedAt, + &passkeyCheckedAt, &metadata, &token, ) @@ -245,6 +257,7 @@ func prepareSessionQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuil session.UserFactor.LoginName = loginName.String session.UserFactor.DisplayName = displayName.String session.PasswordFactor.PasswordCheckedAt = passwordCheckedAt.Time + session.PasskeyFactor.PasskeyCheckedAt = passkeyCheckedAt.Time session.Metadata = metadata return session, token.String, nil @@ -265,6 +278,7 @@ func prepareSessionsQuery(ctx context.Context, db prepareDatabase) (sq.SelectBui LoginNameNameCol.identifier(), HumanDisplayNameCol.identifier(), SessionColumnPasswordCheckedAt.identifier(), + SessionColumnPasskeyCheckedAt.identifier(), SessionColumnMetadata.identifier(), countColumn.identifier(), ).From(sessionsTable.identifier()). @@ -282,6 +296,7 @@ func prepareSessionsQuery(ctx context.Context, db prepareDatabase) (sq.SelectBui loginName sql.NullString displayName sql.NullString passwordCheckedAt sql.NullTime + passkeyCheckedAt sql.NullTime metadata database.Map[[]byte] ) @@ -298,6 +313,7 @@ func prepareSessionsQuery(ctx context.Context, db prepareDatabase) (sq.SelectBui &loginName, &displayName, &passwordCheckedAt, + &passkeyCheckedAt, &metadata, &sessions.Count, ) @@ -310,6 +326,7 @@ func prepareSessionsQuery(ctx context.Context, db prepareDatabase) (sq.SelectBui session.UserFactor.LoginName = loginName.String session.UserFactor.DisplayName = displayName.String session.PasswordFactor.PasswordCheckedAt = passwordCheckedAt.Time + session.PasskeyFactor.PasskeyCheckedAt = passkeyCheckedAt.Time session.Metadata = metadata sessions.Sessions = append(sessions.Sessions, session) diff --git a/internal/query/sessions_test.go b/internal/query/sessions_test.go index eeff881d80..03a5b21970 100644 --- a/internal/query/sessions_test.go +++ b/internal/query/sessions_test.go @@ -17,41 +17,43 @@ import ( ) var ( - expectedSessionQuery = regexp.QuoteMeta(`SELECT projections.sessions.id,` + - ` projections.sessions.creation_date,` + - ` projections.sessions.change_date,` + - ` projections.sessions.sequence,` + - ` projections.sessions.state,` + - ` projections.sessions.resource_owner,` + - ` projections.sessions.creator,` + - ` projections.sessions.user_id,` + - ` projections.sessions.user_checked_at,` + + expectedSessionQuery = regexp.QuoteMeta(`SELECT projections.sessions1.id,` + + ` projections.sessions1.creation_date,` + + ` projections.sessions1.change_date,` + + ` projections.sessions1.sequence,` + + ` projections.sessions1.state,` + + ` projections.sessions1.resource_owner,` + + ` projections.sessions1.creator,` + + ` projections.sessions1.user_id,` + + ` projections.sessions1.user_checked_at,` + ` projections.login_names2.login_name,` + ` projections.users8_humans.display_name,` + - ` projections.sessions.password_checked_at,` + - ` projections.sessions.metadata,` + - ` projections.sessions.token_id` + - ` FROM projections.sessions` + - ` LEFT JOIN projections.login_names2 ON projections.sessions.user_id = projections.login_names2.user_id AND projections.sessions.instance_id = projections.login_names2.instance_id` + - ` LEFT JOIN projections.users8_humans ON projections.sessions.user_id = projections.users8_humans.user_id AND projections.sessions.instance_id = projections.users8_humans.instance_id` + + ` projections.sessions1.password_checked_at,` + + ` projections.sessions1.passkey_checked_at,` + + ` projections.sessions1.metadata,` + + ` projections.sessions1.token_id` + + ` FROM projections.sessions1` + + ` LEFT JOIN projections.login_names2 ON projections.sessions1.user_id = projections.login_names2.user_id AND projections.sessions1.instance_id = projections.login_names2.instance_id` + + ` LEFT JOIN projections.users8_humans ON projections.sessions1.user_id = projections.users8_humans.user_id AND projections.sessions1.instance_id = projections.users8_humans.instance_id` + ` AS OF SYSTEM TIME '-1 ms'`) - expectedSessionsQuery = regexp.QuoteMeta(`SELECT projections.sessions.id,` + - ` projections.sessions.creation_date,` + - ` projections.sessions.change_date,` + - ` projections.sessions.sequence,` + - ` projections.sessions.state,` + - ` projections.sessions.resource_owner,` + - ` projections.sessions.creator,` + - ` projections.sessions.user_id,` + - ` projections.sessions.user_checked_at,` + + expectedSessionsQuery = regexp.QuoteMeta(`SELECT projections.sessions1.id,` + + ` projections.sessions1.creation_date,` + + ` projections.sessions1.change_date,` + + ` projections.sessions1.sequence,` + + ` projections.sessions1.state,` + + ` projections.sessions1.resource_owner,` + + ` projections.sessions1.creator,` + + ` projections.sessions1.user_id,` + + ` projections.sessions1.user_checked_at,` + ` projections.login_names2.login_name,` + ` projections.users8_humans.display_name,` + - ` projections.sessions.password_checked_at,` + - ` projections.sessions.metadata,` + + ` projections.sessions1.password_checked_at,` + + ` projections.sessions1.passkey_checked_at,` + + ` projections.sessions1.metadata,` + ` COUNT(*) OVER ()` + - ` FROM projections.sessions` + - ` LEFT JOIN projections.login_names2 ON projections.sessions.user_id = projections.login_names2.user_id AND projections.sessions.instance_id = projections.login_names2.instance_id` + - ` LEFT JOIN projections.users8_humans ON projections.sessions.user_id = projections.users8_humans.user_id AND projections.sessions.instance_id = projections.users8_humans.instance_id` + + ` FROM projections.sessions1` + + ` LEFT JOIN projections.login_names2 ON projections.sessions1.user_id = projections.login_names2.user_id AND projections.sessions1.instance_id = projections.login_names2.instance_id` + + ` LEFT JOIN projections.users8_humans ON projections.sessions1.user_id = projections.users8_humans.user_id AND projections.sessions1.instance_id = projections.users8_humans.instance_id` + ` AS OF SYSTEM TIME '-1 ms'`) sessionCols = []string{ @@ -67,6 +69,7 @@ var ( "login_name", "display_name", "password_checked_at", + "passkey_checked_at", "metadata", "token", } @@ -84,6 +87,7 @@ var ( "login_name", "display_name", "password_checked_at", + "passkey_checked_at", "metadata", "count", } @@ -133,6 +137,7 @@ func Test_SessionsPrepare(t *testing.T) { "login-name", "display-name", testNow, + testNow, []byte(`{"key": "dmFsdWU="}`), }, }, @@ -160,6 +165,9 @@ func Test_SessionsPrepare(t *testing.T) { PasswordFactor: SessionPasswordFactor{ PasswordCheckedAt: testNow, }, + PasskeyFactor: SessionPasskeyFactor{ + PasskeyCheckedAt: testNow, + }, Metadata: map[string][]byte{ "key": []byte("value"), }, @@ -188,6 +196,7 @@ func Test_SessionsPrepare(t *testing.T) { "login-name", "display-name", testNow, + testNow, []byte(`{"key": "dmFsdWU="}`), }, { @@ -203,6 +212,7 @@ func Test_SessionsPrepare(t *testing.T) { "login-name2", "display-name2", testNow, + testNow, []byte(`{"key": "dmFsdWU="}`), }, }, @@ -230,6 +240,9 @@ func Test_SessionsPrepare(t *testing.T) { PasswordFactor: SessionPasswordFactor{ PasswordCheckedAt: testNow, }, + PasskeyFactor: SessionPasskeyFactor{ + PasskeyCheckedAt: testNow, + }, Metadata: map[string][]byte{ "key": []byte("value"), }, @@ -251,6 +264,9 @@ func Test_SessionsPrepare(t *testing.T) { PasswordFactor: SessionPasswordFactor{ PasswordCheckedAt: testNow, }, + PasskeyFactor: SessionPasskeyFactor{ + PasskeyCheckedAt: testNow, + }, Metadata: map[string][]byte{ "key": []byte("value"), }, @@ -332,6 +348,7 @@ func Test_SessionPrepare(t *testing.T) { "login-name", "display-name", testNow, + testNow, []byte(`{"key": "dmFsdWU="}`), "tokenID", }, @@ -354,6 +371,9 @@ func Test_SessionPrepare(t *testing.T) { PasswordFactor: SessionPasswordFactor{ PasswordCheckedAt: testNow, }, + PasskeyFactor: SessionPasskeyFactor{ + PasskeyCheckedAt: testNow, + }, Metadata: map[string][]byte{ "key": []byte("value"), }, diff --git a/internal/repository/idp/oidc.go b/internal/repository/idp/oidc.go index ea3b63c674..09437d18e0 100644 --- a/internal/repository/idp/oidc.go +++ b/internal/repository/idp/oidc.go @@ -162,3 +162,93 @@ func OIDCIDPChangedEventMapper(event *repository.Event) (eventstore.Event, error return e, nil } + +type OIDCIDPMigratedAzureADEvent struct { + AzureADIDPAddedEvent +} + +func NewOIDCIDPMigratedAzureADEvent( + base *eventstore.BaseEvent, + id, + name, + clientID string, + clientSecret *crypto.CryptoValue, + scopes []string, + tenant string, + isEmailVerified bool, + options Options, +) *OIDCIDPMigratedAzureADEvent { + return &OIDCIDPMigratedAzureADEvent{ + AzureADIDPAddedEvent: AzureADIDPAddedEvent{ + BaseEvent: *base, + ID: id, + Name: name, + ClientID: clientID, + ClientSecret: clientSecret, + Scopes: scopes, + Tenant: tenant, + IsEmailVerified: isEmailVerified, + Options: options, + }, + } +} + +func (e *OIDCIDPMigratedAzureADEvent) Data() interface{} { + return e +} + +func (e *OIDCIDPMigratedAzureADEvent) UniqueConstraints() []*eventstore.EventUniqueConstraint { + return nil +} + +func OIDCIDPMigratedAzureADEventMapper(event *repository.Event) (eventstore.Event, error) { + e, err := AzureADIDPAddedEventMapper(event) + if err != nil { + return nil, err + } + + return &OIDCIDPMigratedAzureADEvent{AzureADIDPAddedEvent: *e.(*AzureADIDPAddedEvent)}, nil +} + +type OIDCIDPMigratedGoogleEvent struct { + GoogleIDPAddedEvent +} + +func NewOIDCIDPMigratedGoogleEvent( + base *eventstore.BaseEvent, + id, + name, + clientID string, + clientSecret *crypto.CryptoValue, + scopes []string, + options Options, +) *OIDCIDPMigratedGoogleEvent { + return &OIDCIDPMigratedGoogleEvent{ + GoogleIDPAddedEvent: GoogleIDPAddedEvent{ + BaseEvent: *base, + ID: id, + Name: name, + ClientID: clientID, + ClientSecret: clientSecret, + Scopes: scopes, + Options: options, + }, + } +} + +func (e *OIDCIDPMigratedGoogleEvent) Data() interface{} { + return e +} + +func (e *OIDCIDPMigratedGoogleEvent) UniqueConstraints() []*eventstore.EventUniqueConstraint { + return nil +} + +func OIDCIDPMigratedGoogleEventMapper(event *repository.Event) (eventstore.Event, error) { + e, err := GoogleIDPAddedEventMapper(event) + if err != nil { + return nil, err + } + + return &OIDCIDPMigratedGoogleEvent{GoogleIDPAddedEvent: *e.(*GoogleIDPAddedEvent)}, nil +} diff --git a/internal/repository/instance/eventstore.go b/internal/repository/instance/eventstore.go index b6d8923ecf..4161a3696a 100644 --- a/internal/repository/instance/eventstore.go +++ b/internal/repository/instance/eventstore.go @@ -74,6 +74,8 @@ func RegisterEventMappers(es *eventstore.Eventstore) { RegisterFilterEventMapper(AggregateType, OAuthIDPChangedEventType, OAuthIDPChangedEventMapper). RegisterFilterEventMapper(AggregateType, OIDCIDPAddedEventType, OIDCIDPAddedEventMapper). RegisterFilterEventMapper(AggregateType, OIDCIDPChangedEventType, OIDCIDPChangedEventMapper). + RegisterFilterEventMapper(AggregateType, OIDCIDPMigratedAzureADEventType, OIDCIDPMigratedAzureADEventMapper). + RegisterFilterEventMapper(AggregateType, OIDCIDPMigratedGoogleEventType, OIDCIDPMigratedGoogleEventMapper). RegisterFilterEventMapper(AggregateType, JWTIDPAddedEventType, JWTIDPAddedEventMapper). RegisterFilterEventMapper(AggregateType, JWTIDPChangedEventType, JWTIDPChangedEventMapper). RegisterFilterEventMapper(AggregateType, AzureADIDPAddedEventType, AzureADIDPAddedEventMapper). diff --git a/internal/repository/instance/idp.go b/internal/repository/instance/idp.go index 612933f0ee..b2d42d8eeb 100644 --- a/internal/repository/instance/idp.go +++ b/internal/repository/instance/idp.go @@ -15,6 +15,8 @@ const ( OAuthIDPChangedEventType eventstore.EventType = "instance.idp.oauth.changed" OIDCIDPAddedEventType eventstore.EventType = "instance.idp.oidc.added" OIDCIDPChangedEventType eventstore.EventType = "instance.idp.oidc.changed" + OIDCIDPMigratedAzureADEventType eventstore.EventType = "instance.idp.oidc.migrated.azure" + OIDCIDPMigratedGoogleEventType eventstore.EventType = "instance.idp.oidc.migrated.google" JWTIDPAddedEventType eventstore.EventType = "instance.idp.jwt.added" JWTIDPChangedEventType eventstore.EventType = "instance.idp.jwt.changed" AzureADIDPAddedEventType eventstore.EventType = "instance.idp.azure.added" @@ -198,6 +200,90 @@ func OIDCIDPChangedEventMapper(event *repository.Event) (eventstore.Event, error return &OIDCIDPChangedEvent{OIDCIDPChangedEvent: *e.(*idp.OIDCIDPChangedEvent)}, nil } +type OIDCIDPMigratedAzureADEvent struct { + idp.OIDCIDPMigratedAzureADEvent +} + +func NewOIDCIDPMigratedAzureADEvent( + ctx context.Context, + aggregate *eventstore.Aggregate, + id, + name, + clientID string, + clientSecret *crypto.CryptoValue, + scopes []string, + tenant string, + isEmailVerified bool, + options idp.Options, +) *OIDCIDPMigratedAzureADEvent { + return &OIDCIDPMigratedAzureADEvent{ + OIDCIDPMigratedAzureADEvent: *idp.NewOIDCIDPMigratedAzureADEvent( + eventstore.NewBaseEventForPush( + ctx, + aggregate, + OIDCIDPMigratedAzureADEventType, + ), + id, + name, + clientID, + clientSecret, + scopes, + tenant, + isEmailVerified, + options, + ), + } +} + +func OIDCIDPMigratedAzureADEventMapper(event *repository.Event) (eventstore.Event, error) { + e, err := idp.OIDCIDPMigratedAzureADEventMapper(event) + if err != nil { + return nil, err + } + + return &OIDCIDPMigratedAzureADEvent{OIDCIDPMigratedAzureADEvent: *e.(*idp.OIDCIDPMigratedAzureADEvent)}, nil +} + +type OIDCIDPMigratedGoogleEvent struct { + idp.OIDCIDPMigratedGoogleEvent +} + +func NewOIDCIDPMigratedGoogleEvent( + ctx context.Context, + aggregate *eventstore.Aggregate, + id, + name, + clientID string, + clientSecret *crypto.CryptoValue, + scopes []string, + options idp.Options, +) *OIDCIDPMigratedGoogleEvent { + return &OIDCIDPMigratedGoogleEvent{ + OIDCIDPMigratedGoogleEvent: *idp.NewOIDCIDPMigratedGoogleEvent( + eventstore.NewBaseEventForPush( + ctx, + aggregate, + OIDCIDPMigratedAzureADEventType, + ), + id, + name, + clientID, + clientSecret, + scopes, + options, + ), + } +} + +func OIDCIDPMigratedGoogleEventMapper(event *repository.Event) (eventstore.Event, error) { + e, err := idp.OIDCIDPMigratedGoogleEventMapper(event) + if err != nil { + return nil, err + } + + return &OIDCIDPMigratedGoogleEvent{OIDCIDPMigratedGoogleEvent: *e.(*idp.OIDCIDPMigratedGoogleEvent)}, nil +} + type JWTIDPAddedEvent struct { idp.JWTIDPAddedEvent } diff --git a/internal/repository/org/eventstore.go b/internal/repository/org/eventstore.go index 662bf77b4b..16332e08fd 100644 --- a/internal/repository/org/eventstore.go +++ b/internal/repository/org/eventstore.go @@ -83,6 +83,8 @@ func RegisterEventMappers(es *eventstore.Eventstore) { RegisterFilterEventMapper(AggregateType, OAuthIDPChangedEventType, OAuthIDPChangedEventMapper). RegisterFilterEventMapper(AggregateType, OIDCIDPAddedEventType, OIDCIDPAddedEventMapper). RegisterFilterEventMapper(AggregateType, OIDCIDPChangedEventType, OIDCIDPChangedEventMapper). + RegisterFilterEventMapper(AggregateType, OIDCIDPMigratedAzureADEventType, OIDCIDPMigratedAzureADEventMapper). + RegisterFilterEventMapper(AggregateType, OIDCIDPMigratedGoogleEventType, OIDCIDPMigratedGoogleEventMapper). RegisterFilterEventMapper(AggregateType, JWTIDPAddedEventType, JWTIDPAddedEventMapper). RegisterFilterEventMapper(AggregateType, JWTIDPChangedEventType, JWTIDPChangedEventMapper). RegisterFilterEventMapper(AggregateType, AzureADIDPAddedEventType, AzureADIDPAddedEventMapper). diff --git a/internal/repository/org/idp.go b/internal/repository/org/idp.go index 97b0ebffc1..6c6201db1b 100644 --- a/internal/repository/org/idp.go +++ b/internal/repository/org/idp.go @@ -15,6 +15,8 @@ const ( OAuthIDPChangedEventType eventstore.EventType = "org.idp.oauth.changed" OIDCIDPAddedEventType eventstore.EventType = "org.idp.oidc.added" OIDCIDPChangedEventType eventstore.EventType = "org.idp.oidc.changed" + OIDCIDPMigratedAzureADEventType eventstore.EventType = "org.idp.oidc.migrated.azure" + OIDCIDPMigratedGoogleEventType eventstore.EventType = "org.idp.oidc.migrated.google" JWTIDPAddedEventType eventstore.EventType = "org.idp.jwt.added" JWTIDPChangedEventType eventstore.EventType = "org.idp.jwt.changed" AzureADIDPAddedEventType eventstore.EventType = "org.idp.azure.added" @@ -198,6 +200,90 @@ func OIDCIDPChangedEventMapper(event *repository.Event) (eventstore.Event, error return &OIDCIDPChangedEvent{OIDCIDPChangedEvent: *e.(*idp.OIDCIDPChangedEvent)}, nil } +type OIDCIDPMigratedAzureADEvent struct { + idp.OIDCIDPMigratedAzureADEvent +} + +func NewOIDCIDPMigratedAzureADEvent( + ctx context.Context, + aggregate *eventstore.Aggregate, + id, + name, + clientID string, + clientSecret *crypto.CryptoValue, + scopes []string, + tenant string, + isEmailVerified bool, + options idp.Options, +) *OIDCIDPMigratedAzureADEvent { + return &OIDCIDPMigratedAzureADEvent{ + OIDCIDPMigratedAzureADEvent: *idp.NewOIDCIDPMigratedAzureADEvent( + eventstore.NewBaseEventForPush( + ctx, + aggregate, + OIDCIDPMigratedAzureADEventType, + ), + id, + name, + clientID, + clientSecret, + scopes, + tenant, + isEmailVerified, + options, + ), + } +} + +func OIDCIDPMigratedAzureADEventMapper(event *repository.Event) (eventstore.Event, error) { + e, err := idp.OIDCIDPMigratedAzureADEventMapper(event) + if err != nil { + return nil, err + } + + return &OIDCIDPMigratedAzureADEvent{OIDCIDPMigratedAzureADEvent: *e.(*idp.OIDCIDPMigratedAzureADEvent)}, nil +} + +type OIDCIDPMigratedGoogleEvent struct { + idp.OIDCIDPMigratedGoogleEvent +} + +func NewOIDCIDPMigratedGoogleEvent( + ctx context.Context, + aggregate *eventstore.Aggregate, + id, + name, + clientID string, + clientSecret *crypto.CryptoValue, + scopes []string, + options idp.Options, +) *OIDCIDPMigratedGoogleEvent { + return &OIDCIDPMigratedGoogleEvent{ + OIDCIDPMigratedGoogleEvent: *idp.NewOIDCIDPMigratedGoogleEvent( + eventstore.NewBaseEventForPush( + ctx, + aggregate, + OIDCIDPMigratedGoogleEventType, + ), + id, + name, + clientID, + clientSecret, + scopes, + options, + ), + } +} + +func OIDCIDPMigratedGoogleEventMapper(event *repository.Event) (eventstore.Event, error) { + e, err := idp.OIDCIDPMigratedGoogleEventMapper(event) + if err != nil { + return nil, err + } + + return &OIDCIDPMigratedGoogleEvent{OIDCIDPMigratedGoogleEvent: *e.(*idp.OIDCIDPMigratedGoogleEvent)}, nil +} + type JWTIDPAddedEvent struct { idp.JWTIDPAddedEvent } diff --git a/internal/repository/session/eventstore.go b/internal/repository/session/eventstore.go index 3dd1875da7..3253cc8566 100644 --- a/internal/repository/session/eventstore.go +++ b/internal/repository/session/eventstore.go @@ -6,6 +6,8 @@ func RegisterEventMappers(es *eventstore.Eventstore) { es.RegisterFilterEventMapper(AggregateType, AddedType, AddedEventMapper). RegisterFilterEventMapper(AggregateType, UserCheckedType, UserCheckedEventMapper). RegisterFilterEventMapper(AggregateType, PasswordCheckedType, PasswordCheckedEventMapper). + RegisterFilterEventMapper(AggregateType, PasskeyChallengedType, eventstore.GenericEventMapper[PasskeyChallengedEvent]). + RegisterFilterEventMapper(AggregateType, PasskeyCheckedType, eventstore.GenericEventMapper[PasskeyCheckedEvent]). RegisterFilterEventMapper(AggregateType, TokenSetType, TokenSetEventMapper). RegisterFilterEventMapper(AggregateType, MetadataSetType, MetadataSetEventMapper). RegisterFilterEventMapper(AggregateType, TerminateType, TerminateEventMapper) diff --git a/internal/repository/session/session.go b/internal/repository/session/session.go index 6883834426..887f056e86 100644 --- a/internal/repository/session/session.go +++ b/internal/repository/session/session.go @@ -5,19 +5,22 @@ import ( "encoding/json" "time" + "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" ) const ( - sessionEventPrefix = "session." - AddedType = sessionEventPrefix + "added" - UserCheckedType = sessionEventPrefix + "user.checked" - PasswordCheckedType = sessionEventPrefix + "password.checked" - TokenSetType = sessionEventPrefix + "token.set" - MetadataSetType = sessionEventPrefix + "metadata.set" - TerminateType = sessionEventPrefix + "terminated" + sessionEventPrefix = "session." + AddedType = sessionEventPrefix + "added" + UserCheckedType = sessionEventPrefix + "user.checked" + PasswordCheckedType = sessionEventPrefix + "password.checked" + PasskeyChallengedType = sessionEventPrefix + "passkey.challenged" + PasskeyCheckedType = sessionEventPrefix + "passkey.checked" + TokenSetType = sessionEventPrefix + "token.set" + MetadataSetType = sessionEventPrefix + "metadata.set" + TerminateType = sessionEventPrefix + "terminated" ) type AddedEvent struct { @@ -141,6 +144,78 @@ func PasswordCheckedEventMapper(event *repository.Event) (eventstore.Event, erro return added, nil } +type PasskeyChallengedEvent struct { + eventstore.BaseEvent `json:"-"` + + Challenge string `json:"challenge,omitempty"` + AllowedCrentialIDs [][]byte `json:"allowedCrentialIDs,omitempty"` + UserVerification domain.UserVerificationRequirement `json:"userVerification,omitempty"` +} + +func (e *PasskeyChallengedEvent) Data() interface{} { + return e +} + +func (e *PasskeyChallengedEvent) UniqueConstraints() []*eventstore.EventUniqueConstraint { + return nil +} + +func (e *PasskeyChallengedEvent) SetBaseEvent(base *eventstore.BaseEvent) { + e.BaseEvent = *base +} + +func NewPasskeyChallengedEvent( + ctx context.Context, + aggregate *eventstore.Aggregate, + challenge string, + allowedCrentialIDs [][]byte, + userVerification domain.UserVerificationRequirement, +) *PasskeyChallengedEvent { + return &PasskeyChallengedEvent{ + BaseEvent: *eventstore.NewBaseEventForPush( + ctx, + aggregate, + PasskeyChallengedType, + ), + Challenge: challenge, + AllowedCrentialIDs: allowedCrentialIDs, + UserVerification: userVerification, + } +} + +type PasskeyCheckedEvent struct { + eventstore.BaseEvent `json:"-"` + + CheckedAt time.Time `json:"checkedAt"` +} + +func (e *PasskeyCheckedEvent) Data() interface{} { + return e +} + +func (e *PasskeyCheckedEvent) UniqueConstraints() []*eventstore.EventUniqueConstraint { + return nil +} + +func (e *PasskeyCheckedEvent) SetBaseEvent(base *eventstore.BaseEvent) { + e.BaseEvent = *base +} + +func NewPasskeyCheckedEvent( + ctx context.Context, + aggregate *eventstore.Aggregate, + checkedAt time.Time, +) *PasswordCheckedEvent { + return &PasswordCheckedEvent{ + BaseEvent: *eventstore.NewBaseEventForPush( + ctx, + aggregate, + PasskeyCheckedType, + ), + CheckedAt: checkedAt, + } +} + type TokenSetEvent struct { eventstore.BaseEvent `json:"-"` diff --git a/internal/static/i18n/de.yaml b/internal/static/i18n/de.yaml index 441b6be368..6d830b2def 100644 --- a/internal/static/i18n/de.yaml +++ b/internal/static/i18n/de.yaml @@ -476,6 +476,8 @@ Errors: Terminated: Session bereits beendet Token: Invalid: Session Token ist ungültig + Passkey: + NoChallenge: Sitzung ohne Passkey-Herausforderung Intent: IDPMissing: IDP ID fehlt im Request SuccessURLMissing: Success URL fehlt im Request diff --git a/internal/static/i18n/en.yaml b/internal/static/i18n/en.yaml index 276ad07da8..88763384ee 100644 --- a/internal/static/i18n/en.yaml +++ b/internal/static/i18n/en.yaml @@ -476,6 +476,8 @@ Errors: Terminated: Session already terminated Token: Invalid: Session Token is invalid + Passkey: + NoChallenge: Session without passkey challenge Intent: IDPMissing: IDP ID is missing in the request SuccessURLMissing: Success URL is missing in the request diff --git a/internal/static/i18n/es.yaml b/internal/static/i18n/es.yaml index 757a730549..dbfa308c81 100644 --- a/internal/static/i18n/es.yaml +++ b/internal/static/i18n/es.yaml @@ -476,6 +476,8 @@ Errors: Terminated: Sesión ya terminada Token: Invalid: El identificador de sesión no es válido + Passkey: + NoChallenge: Sesión sin desafío de contraseña Intent: IDPMissing: Falta IDP en la solicitud SuccessURLMissing: Falta la URL de éxito en la solicitud diff --git a/internal/static/i18n/fr.yaml b/internal/static/i18n/fr.yaml index eb5b5471e3..9d4c03ed63 100644 --- a/internal/static/i18n/fr.yaml +++ b/internal/static/i18n/fr.yaml @@ -476,6 +476,8 @@ Errors: Terminated: La session est déjà terminée Token: Invalid: Le jeton de session n'est pas valide + Passkey: + NoChallenge: Session sans défi de clé d'accès Intent: IDPMissing: IDP manquant dans la requête SuccessURLMissing: Success URL absent de la requête diff --git a/internal/static/i18n/it.yaml b/internal/static/i18n/it.yaml index 7ce4200b76..113e99c308 100644 --- a/internal/static/i18n/it.yaml +++ b/internal/static/i18n/it.yaml @@ -476,6 +476,8 @@ Errors: Terminated: Sessione già terminata Token: Invalid: Il token della sessione non è valido + Passkey: + NoChallenge: Sessione senza sfida passkey Intent: IDPMissing: IDP mancante nella richiesta SuccessURLMissing: URL di successo mancante nella richiesta diff --git a/internal/static/i18n/ja.yaml b/internal/static/i18n/ja.yaml index 394880e116..fd2fd5abeb 100644 --- a/internal/static/i18n/ja.yaml +++ b/internal/static/i18n/ja.yaml @@ -465,6 +465,8 @@ Errors: Terminated: セッションはすでに終了しています Token: Invalid: セッショントークンが無効です + Passkey: + NoChallenge: パスキーチャレンジなしのセッション Intent: IDPMissing: リクエストにIDP IDが含まれていません SuccessURLMissing: リクエストに成功時の URL がありません diff --git a/internal/static/i18n/pl.yaml b/internal/static/i18n/pl.yaml index 3be75368b9..0c7e56ccdb 100644 --- a/internal/static/i18n/pl.yaml +++ b/internal/static/i18n/pl.yaml @@ -476,6 +476,8 @@ Errors: Terminated: Sesja już zakończona Token: Invalid: Token sesji jest nieprawidłowy + Passkey: + NoChallenge: Sesja bez wyzwania klucza Intent: IDPMissing: Brak identyfikatora IDP w żądaniu SuccessURLMissing: Brak adresu URL powodzenia w żądaniu diff --git a/internal/static/i18n/zh.yaml b/internal/static/i18n/zh.yaml index eb3a76c77f..3a1508e057 100644 --- a/internal/static/i18n/zh.yaml +++ b/internal/static/i18n/zh.yaml @@ -476,6 +476,8 @@ Errors: Terminated: 会话已经终止 Token: Invalid: 会话令牌是无效的 + Passkey: + NoChallenge: 没有密码挑战的会话 Intent: IDPMissing: 请求中缺少IDP ID SuccessURLMissing: 请求中缺少成功URL diff --git a/internal/webauthn/client.go b/internal/webauthn/client.go index ac7fbe766a..511378feed 100644 --- a/internal/webauthn/client.go +++ b/internal/webauthn/client.go @@ -4,6 +4,8 @@ import ( "fmt" "github.com/descope/virtualwebauthn" + "google.golang.org/protobuf/encoding/protojson" + "google.golang.org/protobuf/types/known/structpb" ) type Client struct { @@ -25,12 +27,40 @@ func NewClient(name, domain, origin string) *Client { } } -func (c *Client) CreateAttestationResponse(options []byte) ([]byte, error) { +func (c *Client) CreateAttestationResponse(optionsPb *structpb.Struct) (*structpb.Struct, error) { + options, err := protojson.Marshal(optionsPb) + if err != nil { + return nil, fmt.Errorf("webauthn.Client.CreateAttestationResponse: %w", err) + } parsedAttestationOptions, err := virtualwebauthn.ParseAttestationOptions(string(options)) if err != nil { return nil, fmt.Errorf("webauthn.Client.CreateAttestationResponse: %w", err) } - return []byte(virtualwebauthn.CreateAttestationResponse( + resp := new(structpb.Struct) + err = protojson.Unmarshal([]byte(virtualwebauthn.CreateAttestationResponse( c.rp, c.auth, c.credential, *parsedAttestationOptions, - )), nil + )), resp) + if err != nil { + return nil, fmt.Errorf("webauthn.Client.CreateAttestationResponse: %w", err) + } + return resp, nil +} + +func (c *Client) CreateAssertionResponse(optionsPb *structpb.Struct) (*structpb.Struct, error) { + options, err := protojson.Marshal(optionsPb) + if err != nil { + return nil, fmt.Errorf("webauthn.Client.CreateAssertionResponse: %w", err) + } + parsedAssertionOptions, err := virtualwebauthn.ParseAssertionOptions(string(options)) + if err != nil { + return nil, fmt.Errorf("webauthn.Client.CreateAssertionResponse: %w", err) + } + resp := new(structpb.Struct) + err = protojson.Unmarshal([]byte(virtualwebauthn.CreateAssertionResponse( + c.rp, c.auth, c.credential, *parsedAssertionOptions, + )), resp) + if err != nil { + return nil, fmt.Errorf("webauthn.Client.CreateAssertionResponse: %w", err) + } + return resp, nil } diff --git a/proto/zitadel/admin.proto b/proto/zitadel/admin.proto index 1424d7cd29..e50d3afb33 100644 --- a/proto/zitadel/admin.proto +++ b/proto/zitadel/admin.proto @@ -1342,6 +1342,24 @@ service AdminService { }; } + // Migrate an existing OIDC identity provider on the instance + rpc MigrateGenericOIDCProvider(MigrateGenericOIDCProviderRequest) returns (MigrateGenericOIDCProviderResponse) { + option (google.api.http) = { + post: "/idps/generic_oidc/{id}/_migrate" + body: "*" + }; + + option (zitadel.v1.auth_option) = { + permission: "iam.idp.write" + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + tags: "Identity Providers"; + summary: "Migrate Generic OIDC Identity Provider"; + description: ""; + }; + } + // Add a new JWT identity provider on the instance rpc AddJWTProvider(AddJWTProviderRequest) returns (AddJWTProviderResponse) { option (google.api.http) = { @@ -4828,6 +4846,23 @@ message UpdateGenericOIDCProviderResponse { zitadel.v1.ObjectDetails details = 1; } +message MigrateGenericOIDCProviderRequest{ + string id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"69629023906488334\""; + } + ]; + oneof template { + AddAzureADProviderRequest azure = 2; + AddGoogleProviderRequest google = 3; + } +} + +message MigrateGenericOIDCProviderResponse{ + zitadel.v1.ObjectDetails details = 1; +} + message AddJWTProviderRequest { string name = 1 [ (validate.rules).string = {min_len: 1, max_len: 200}, diff --git a/proto/zitadel/management.proto b/proto/zitadel/management.proto index cf055e4a55..b0d5bc97fe 100644 --- a/proto/zitadel/management.proto +++ b/proto/zitadel/management.proto @@ -6558,6 +6558,24 @@ service ManagementService { }; } + // Migrate an existing OIDC identity provider in the organization + rpc MigrateGenericOIDCProvider(MigrateGenericOIDCProviderRequest) returns (MigrateGenericOIDCProviderResponse) { + option (google.api.http) = { + post: "/idps/generic_oidc/{id}/_migrate" + body: "*" + }; + + option (zitadel.v1.auth_option) = { + permission: "org.idp.write" + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + tags: "Identity Providers"; + summary: "Migrate Generic OIDC Identity Provider"; + description: ""; + }; + } + // Add a new JWT identity provider in the organization rpc AddJWTProvider(AddJWTProviderRequest) returns (AddJWTProviderResponse) { option (google.api.http) = { @@ -11526,6 +11544,23 @@ message UpdateGenericOIDCProviderResponse { zitadel.v1.ObjectDetails details = 1; } +message MigrateGenericOIDCProviderRequest{ + string id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"69629023906488334\""; + } + ]; + oneof template { + AddAzureADProviderRequest azure = 2; + AddGoogleProviderRequest google = 3; + } +} + +message MigrateGenericOIDCProviderResponse{ + zitadel.v1.ObjectDetails details = 1; +} + message AddJWTProviderRequest { string name = 1 [(validate.rules).string = {min_len: 1, max_len: 200}]; string issuer = 2 [ diff --git a/proto/zitadel/session/v2alpha/challenge.proto b/proto/zitadel/session/v2alpha/challenge.proto new file mode 100644 index 0000000000..498cb729b4 --- /dev/null +++ b/proto/zitadel/session/v2alpha/challenge.proto @@ -0,0 +1,26 @@ +syntax = "proto3"; + +package zitadel.session.v2alpha; + +import "google/protobuf/struct.proto"; +import "protoc-gen-openapiv2/options/annotations.proto"; + +option go_package = "github.com/zitadel/zitadel/pkg/grpc/session/v2alpha;session"; + +enum ChallengeKind { + CHALLENGE_KIND_UNSPECIFIED = 0; + CHALLENGE_KIND_PASSKEY = 1; +} + +message Challenges { + message Passkey { + google.protobuf.Struct public_key_credential_request_options = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "Options for Assertion Generaration (dictionary PublicKeyCredentialRequestOptions). Generated helper methods transform the field to JSON, for use in a WebauthN client. See also: https://www.w3.org/TR/webauthn/#dictdef-publickeycredentialrequestoptions" + example: "{\"publicKey\":{\"allowCredentials\":[{\"id\":\"ATmqBg-99qyOZk2zloPdJQyS2R7IkFT7v9Hoos_B_nM\",\"type\":\"public-key\"}],\"challenge\":\"GAOHYz2jE69kJMYo6Laij8yWw9-dKKgbViNhfuy0StA\",\"rpId\":\"localhost\",\"timeout\":300000,\"userVerification\":\"required\"}}" + } + ]; + } + + optional Passkey passkey = 1; +} diff --git a/proto/zitadel/session/v2alpha/session.proto b/proto/zitadel/session/v2alpha/session.proto index 279863d14b..79f2030864 100644 --- a/proto/zitadel/session/v2alpha/session.proto +++ b/proto/zitadel/session/v2alpha/session.proto @@ -2,7 +2,6 @@ syntax = "proto3"; package zitadel.session.v2alpha; -import "google/api/field_behavior.proto"; import "google/protobuf/timestamp.proto"; import "protoc-gen-openapiv2/options/annotations.proto"; import "validate/validate.proto"; @@ -45,6 +44,7 @@ message Session { message Factors { UserFactor user = 1; PasswordFactor password = 2; + PasskeyFactor passkey = 3; } message UserFactor { @@ -78,6 +78,14 @@ message PasswordFactor { ]; } +message PasskeyFactor { + google.protobuf.Timestamp verified_at = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "\"time when the passkey challenge was last checked\""; + } + ]; +} + message SearchQuery { oneof query { option (validate.required) = true; diff --git a/proto/zitadel/session/v2alpha/session_service.proto b/proto/zitadel/session/v2alpha/session_service.proto index e564621074..0ee4b05d30 100644 --- a/proto/zitadel/session/v2alpha/session_service.proto +++ b/proto/zitadel/session/v2alpha/session_service.proto @@ -5,9 +5,11 @@ package zitadel.session.v2alpha; import "zitadel/object/v2alpha/object.proto"; import "zitadel/protoc_gen_zitadel/v2/options.proto"; +import "zitadel/session/v2alpha/challenge.proto"; import "zitadel/session/v2alpha/session.proto"; import "google/api/annotations.proto"; import "google/api/field_behavior.proto"; +import "google/protobuf/struct.proto"; import "protoc-gen-openapiv2/options/annotations.proto"; import "validate/validate.proto"; @@ -242,6 +244,7 @@ message CreateSessionRequest{ description: "\"custom key value list to be stored on the session\""; } ]; + repeated ChallengeKind challenges = 3; } message CreateSessionResponse{ @@ -257,6 +260,7 @@ message CreateSessionResponse{ description: "\"token of the session, which is required for further updates of the session or the request other resources\""; } ]; + Challenges challenges = 4; } message SetSessionRequest{ @@ -287,6 +291,7 @@ message SetSessionRequest{ description: "\"custom key value list to be stored on the session\""; } ]; + repeated ChallengeKind challenges = 5; } message SetSessionResponse{ @@ -296,6 +301,7 @@ message SetSessionResponse{ description: "\"token of the session, which is required for further updates of the session or the request other resources\""; } ]; + Challenges challenges = 3; } message DeleteSessionRequest{ @@ -330,6 +336,11 @@ message Checks { description: "\"Checks the password and updates the session on success. Requires that the user is already checked, either in the previous or the same request.\""; } ]; + optional CheckPasskey passkey = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "\"Checks the public key credential issued by the passkey client. Requires that the user is already checked and a passkey challenge to be requested, in any previous request.\""; + } + ]; } message CheckUser { @@ -363,3 +374,15 @@ message CheckPassword { } ]; } + +message CheckPasskey { + google.protobuf.Struct credential_assertion_data = 1 [ + (validate.rules).message.required = true, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "JSON representation of public key credential issued by the passkey client"; + min_length: 55; + max_length: 1048576; //1 MB + } + ]; +} diff --git a/proto/zitadel/user/v2alpha/user_service.proto b/proto/zitadel/user/v2alpha/user_service.proto index 1af446f09a..9522cd96a4 100644 --- a/proto/zitadel/user/v2alpha/user_service.proto +++ b/proto/zitadel/user/v2alpha/user_service.proto @@ -12,6 +12,7 @@ import "zitadel/user/v2alpha/user.proto"; import "google/api/annotations.proto"; import "google/api/field_behavior.proto"; import "google/protobuf/duration.proto"; +import "google/protobuf/struct.proto"; import "protoc-gen-openapiv2/options/annotations.proto"; import "validate/validate.proto"; @@ -429,10 +430,10 @@ message RegisterPasskeyResponse{ example: "\"fabde5c8-c13f-481d-a90b-5e59a001a076\"" } ]; - bytes public_key_credential_creation_options = 3 [ + google.protobuf.Struct public_key_credential_creation_options = 3 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - description: "json representation of public key credential creation options used by the passkey client" - example: "\"eyJwdWJsaWNLZXkiOnsiY2hhbGxlbmdlIoplfZm4vM21qSzBPdjltN2x6VWhnclYyejFJSlVzZnpLd0Z1TytWTWtzRW1Icz0iLCJycCI6eyJuYW1lIjoiWklUQURFTCIsImlkIjoiYWNtZS1nem9lNHgueml0YWRlbC5jbG91ZCJ9LCJ1c2VyIjp7Im5hbWUiOiJ0ZXN0dXNlcjU1QGFjbWUueml0YWRlbC5jbG91ZCIsImRpc3BsYXlOYW1lIjoiVGVzdCBUZXN0IiwiaWQiOiJNVGd5TVRVMk1qWTBNakk1TXpBMk5qSTEifSwicHViS2V5Q3JlZFBhcmFtcyI6W3sidHlwZSI6InB1YmxpYy1rZXkiLCJhbGciOi03fSx7InR5cGUiOiJwdWJsaWMta2V5IiwiYWxnIjotMzV9LHsidHlwZSI6InB1YmxpYy1rZXkiLCJhbGciOi0zNn0seyJ0eXBlIjoicHVibGljLWtleSIsImFsZyI6LTI1N30seyJ0eXBlIjoicHVibGljLWtleSIsImFsZyI6LTI1OH0seyJ0eXBlIjoicHVibGljLWtleSIsImFsZyI6LTI1OX0seyJ0eXBlIjoicHVibGljLWtleSIsImFsZyI6LTM3fSx7InR5cGUiOiJwdWJsaWMta2V5IiwiYWxnIjotMzh9LHsidHlwZSI6InB1YmxpYy1rZXkiLCJhbGciOi0zOX0seyJ0eXBlIjoicHVibGljLWtleSIsImFsZyI6LTh9XSwiYXV0aGVudGljYXRvclNlbGVjdGlvbiI6eyJ1c2VyVmVyaWZpY2F0aW9uIjoiZGlzY291cmFnZWQifn2ilGltZW91dCI6NjAwMDAsImF0dGVzdGF0aW9uIjoibm9uZSJ9fQ==\"" + description: "Options for Credential Creation (dictionary PublicKeyCredentialCreationOptions). Generated helper methods transform the field to JSON, for use in a WebauthN client. See also: https://www.w3.org/TR/webauthn/#dictdef-publickeycredentialcreationoptions" + example: "{\"publicKey\":{\"attestation\":\"none\",\"authenticatorSelection\":{\"userVerification\":\"required\"},\"challenge\":\"XaMYwWOZ5hj6pwtwJJlpcI-ExkO5TxevBMG4R8DoKQQ\",\"excludeCredentials\":[{\"id\":\"tVp1QfYhT8DkyEHVrv7blnpAo2YJzbZgZNBf7zPs6CI\",\"type\":\"public-key\"}],\"pubKeyCredParams\":[{\"alg\":-7,\"type\":\"public-key\"}],\"rp\":{\"id\":\"localhost\",\"name\":\"ZITADEL\"},\"timeout\":300000,\"user\":{\"displayName\":\"Tim Mohlmann\",\"id\":\"MjE1NTk4MDAwNDY0OTk4OTQw\",\"name\":\"tim\"}}}" } ]; } @@ -456,11 +457,12 @@ message VerifyPasskeyRegistrationRequest{ example: "\"fabde5c8-c13f-481d-a90b-5e59a001a076\""; } ]; - bytes public_key_credential = 3 [ - (validate.rules).bytes = {min_len: 55, max_len: 1048576}, + google.protobuf.Struct public_key_credential = 3 [ + (validate.rules).message.required = true, (google.api.field_behavior) = REQUIRED, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - description: "JSON representation of public key credential issued by the passkey client"; + description: "PublicKeyCredential Interface. Generated helper methods populate the field from JSON created by a WebauthN client. See also: https://www.w3.org/TR/webauthn/#publickeycredential"; + example: "{\"type\":\"public-key\",\"id\":\"pawVarF4xPxLFmfCnRkwXWeTrKGzabcAi92LEI1WC00\",\"rawId\":\"pawVarF4xPxLFmfCnRkwXWeTrKGzabcAi92LEI1WC00\",\"response\":{\"attestationObject\":\"o2NmbXRmcGFja2VkZ2F0dFN0bXSiY2FsZyZjc2lnWEcwRQIgRKS3VpeE9tfExXRzkoUKnG4rQWPvtSSt4YtDGgTx32oCIQDPey-2YJ4uIg-QCM4jj6aE2U3tgMFM_RP7Efx6xRu3JGhhdXRoRGF0YVikSZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2NFAAAAADju76085Yhmlt1CEOHkwLQAIKWsFWqxeMT8SxZnwp0ZMF1nk6yhs2m3AIvdixCNVgtNpQECAyYgASFYIMGUDSP2FAQn2MIfPMy7cyB_Y30VqixVgGULTBtFjfRiIlggjUGfQo3_-CrMmH3S-ZQkFKWKnNBQEAMkFtG-9A4zqW0\",\"clientDataJSON\":\"eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoiQlhXdHh0WGxJeFZZa0pHT1dVaUVmM25zby02aXZKdWw2YmNmWHdMVlFIayIsIm9yaWdpbiI6Imh0dHBzOi8vbG9jYWxob3N0OjgwODAifQ\"}}"; min_length: 55; max_length: 1048576; //1 MB }