:aud'`
+- Also, we want to include user info inside the token to avoid calling the user info endpoint, so go to Token Settings in the front-end app and select User Info inside ID Token.
+- After a successful authentication, ZITADEL generates an access token and an ID token.
+- The front-end application receives these tokens and stores them securely (e.g., in browser storage).
+
+### Token exchange and user information
+
+- Once the frontend obtains the tokens, it can extract certain information from the ID token itself (e.g., user ID, email, etc.) without making an additional request.
+- If more user information is required, the frontend can use the access token to call the ZITADEL User Info endpoint. This endpoint provides additional user details, such as name, profile picture, etc.
+
+### Back-end API communication
+
+- To communicate with the back-end API, the front-end includes the access token in the Authorization header of API requests.
+- The back-end API receives the request and needs to validate and authorize the token before processing the request.
+- The API performs token introspection using ZITADEL's introspection endpoint to validate the access token. This API uses Basic Authentication to invoke the [introspection endpoint](/docs/apis/openidoauth/endpoints#introspection_endpoint), which means it sends its client ID and client secret along with the access token received.
+- If the token is valid and active, the API proceeds to handle the requested action or fetch data from the underlying data sources.
diff --git a/docs/docs/legal/_piid-table.mdx b/docs/docs/legal/_piid-table.mdx
index 6677201b3..4fc3c3618 100644
--- a/docs/docs/legal/_piid-table.mdx
+++ b/docs/docs/legal/_piid-table.mdx
@@ -8,7 +8,7 @@
| Basic data |
- - Surname and first name
+ - Family and given name
- Email addresses
- User name
diff --git a/docs/docs/legal/privacy-policy.mdx b/docs/docs/legal/privacy-policy.mdx
index 91b713834..e922117e9 100644
--- a/docs/docs/legal/privacy-policy.mdx
+++ b/docs/docs/legal/privacy-policy.mdx
@@ -21,7 +21,7 @@ Switzerland
Our representative in the EU is
-**VGS Datenschutzpartner UG**
+**VGS Datenschutzpartner GmbH**
Am Kaiserkai 69
20457 Hamburg
Germany
diff --git a/docs/docs/self-hosting/deploy/compose.mdx b/docs/docs/self-hosting/deploy/compose.mdx
index 563cdf946..57edf063c 100644
--- a/docs/docs/self-hosting/deploy/compose.mdx
+++ b/docs/docs/self-hosting/deploy/compose.mdx
@@ -8,6 +8,8 @@ import DockerComposeSaSource from '!!raw-loader!./docker-compose-sa.yaml'
import Disclaimer from './_disclaimer.mdx'
import DefaultUser from './_defaultuser.mdx'
import Next from './_next.mdx'
+import NoteInstanceNotFound from './troubleshooting/_note_instance_not_found.mdx';
+
The setup is tested against Docker version 20.10.17 and Docker Compose version v2.2.3
@@ -29,6 +31,8 @@ docker compose up --detach
+
+
## VideoGuide
diff --git a/docs/docs/self-hosting/deploy/knative.mdx b/docs/docs/self-hosting/deploy/knative.mdx
index 54f22ab17..e2a67ccb6 100644
--- a/docs/docs/self-hosting/deploy/knative.mdx
+++ b/docs/docs/self-hosting/deploy/knative.mdx
@@ -5,6 +5,8 @@ title: Knative
import Disclaimer from './_disclaimer.mdx'
import DefaultUser from './_defaultuser.mdx'
import Next from './_next.mdx'
+import NoteInstanceNotFound from './troubleshooting/_note_instance_not_found.mdx';
+
## Install Knative
@@ -31,7 +33,7 @@ kn service create zitadel \
--env ZITADEL_EXTERNALPORT=80 \
--env ZITADEL_TLS_ENABLED=false \
--env ZITADEL_EXTERNALDOMAIN=zitadel.default.127.0.0.1.sslip.io \
---arg "start-from-init" --arg "--masterkey" --arg "MasterkeyNeedsToHave32Characters"
+--arg "start-from-init" --arg "--masterkey" --arg "MasterkeyNeedsToHave32Characters"
```
### Knavite yaml
@@ -59,6 +61,8 @@ If you didn't configure something else, this is the default IAM admin users logi
* username: zitadel-admin@zitadel.zitadel.default.127.0.0.1.sslip.io
* password: Password1!
+
+
## VideoGuide
diff --git a/docs/docs/self-hosting/deploy/kubernetes.mdx b/docs/docs/self-hosting/deploy/kubernetes.mdx
index fa214d577..bca072326 100644
--- a/docs/docs/self-hosting/deploy/kubernetes.mdx
+++ b/docs/docs/self-hosting/deploy/kubernetes.mdx
@@ -5,6 +5,8 @@ title: Kubernetes
import Disclaimer from './_disclaimer.mdx'
import DefaultUser from './_defaultuser.mdx'
import Next from './_next.mdx'
+import NoteInstanceNotFound from './troubleshooting/_note_instance_not_found.mdx';
+
Installation and configuration details are described in the [open source ZITADEL charts repo](https://github.com/zitadel/zitadel-charts).
By default, the chart installs a secure and highly available ZITADEL instance.
@@ -46,6 +48,8 @@ kubectl port-forward svc/my-zitadel 8080
+
+
## Setup ZITADEL and a Service Account Admin
With this setup, you don't create a human user that has the IAM_OWNER role.
diff --git a/docs/docs/self-hosting/deploy/linux.mdx b/docs/docs/self-hosting/deploy/linux.mdx
index c1b959793..a6ed2e43c 100644
--- a/docs/docs/self-hosting/deploy/linux.mdx
+++ b/docs/docs/self-hosting/deploy/linux.mdx
@@ -5,6 +5,8 @@ title: Linux
import Disclaimer from './_disclaimer.mdx'
import DefaultUser from './_defaultuser.mdx'
import Next from './_next.mdx'
+import NoteInstanceNotFound from './troubleshooting/_note_instance_not_found.mdx';
+
## Install CockroachDB
@@ -38,6 +40,8 @@ ZITADEL_EXTERNALSECURE=false zitadel start-from-init --masterkey "MasterkeyNeeds
+
+
## VideoGuide
@@ -54,3 +58,5 @@ This key can be used to provision resources with for example [Terraform](/docs/g
+
+
diff --git a/docs/docs/self-hosting/deploy/loadbalancing-example/example-traefik.yaml b/docs/docs/self-hosting/deploy/loadbalancing-example/example-traefik.yaml
index 4b4f5a35c..c16f74a46 100644
--- a/docs/docs/self-hosting/deploy/loadbalancing-example/example-traefik.yaml
+++ b/docs/docs/self-hosting/deploy/loadbalancing-example/example-traefik.yaml
@@ -21,7 +21,6 @@ providers:
filename: /etc/traefik/traefik.yaml
http:
-
middlewares:
zitadel:
headers:
@@ -29,7 +28,7 @@ http:
allowedHosts:
- 'my.domain'
customRequestHeaders:
- :authority: 'my.domain'
+ authority: 'my.domain'
redirect-to-https:
redirectScheme:
scheme: https
diff --git a/docs/docs/self-hosting/deploy/loadbalancing-example/loadbalancing-example.mdx b/docs/docs/self-hosting/deploy/loadbalancing-example/loadbalancing-example.mdx
index e4a668b0f..9f49d1f5c 100644
--- a/docs/docs/self-hosting/deploy/loadbalancing-example/loadbalancing-example.mdx
+++ b/docs/docs/self-hosting/deploy/loadbalancing-example/loadbalancing-example.mdx
@@ -8,6 +8,7 @@ import ExampleTraefikSource from '!!raw-loader!./example-traefik.yaml'
import ExampleZITADELConfigSource from '!!raw-loader!./example-zitadel-config.yaml'
import ExampleZITADELSecretsSource from '!!raw-loader!./example-zitadel-secrets.yaml'
import ExampleZITADELInitStepsSource from '!!raw-loader!./example-zitadel-init-steps.yaml'
+import NoteInstanceNotFound from '../troubleshooting/_note_instance_not_found.mdx';
With this example configuration, you create a near production environment for ZITADEL with [Docker Compose](https://docs.docker.com/compose/).
@@ -61,7 +62,7 @@ export ZITADEL_MASTERKEY="$(tr -dc A-Za-z0-9
+
## Troubleshooting
You can connect to cockroach like this: `docker exec -it loadbalancing-example-my-cockroach-db-1 cockroach sql --host my-cockroach-db --certs-dir /cockroach/certs/`
diff --git a/docs/docs/self-hosting/deploy/macos.mdx b/docs/docs/self-hosting/deploy/macos.mdx
index f00bdd393..53719ef99 100644
--- a/docs/docs/self-hosting/deploy/macos.mdx
+++ b/docs/docs/self-hosting/deploy/macos.mdx
@@ -5,6 +5,7 @@ title: MacOS
import Disclaimer from './_disclaimer.mdx'
import DefaultUser from './_defaultuser.mdx'
import Next from './_next.mdx'
+import NoteInstanceNotFound from './troubleshooting/_note_instance_not_found.mdx';
## Install CockroachDB
@@ -44,6 +45,7 @@ ZITADEL_EXTERNALSECURE=false zitadel start-from-init --masterkey "MasterkeyNeeds
```
+
## VideoGuide
diff --git a/docs/docs/self-hosting/deploy/troubleshooting/_instance_not_found.mdx b/docs/docs/self-hosting/deploy/troubleshooting/_instance_not_found.mdx
new file mode 100644
index 000000000..98785275b
--- /dev/null
+++ b/docs/docs/self-hosting/deploy/troubleshooting/_instance_not_found.mdx
@@ -0,0 +1,5 @@
+`ID=QUERY-n0wng Message=Instance not found`
+
+If you're self hosting with a custom domain, you need to instruct ZITADEL to use the `ExternalDomain`.
+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.
diff --git a/docs/docs/self-hosting/deploy/troubleshooting/_note_instance_not_found.mdx b/docs/docs/self-hosting/deploy/troubleshooting/_note_instance_not_found.mdx
new file mode 100644
index 000000000..c194800e6
--- /dev/null
+++ b/docs/docs/self-hosting/deploy/troubleshooting/_note_instance_not_found.mdx
@@ -0,0 +1,5 @@
+:::note
+This guide is based on a local setup.
+If you encounter an error "Instance Not Found" please read the following section:
+[Instance not found](/docs/self-hosting/deploy/troubleshooting#instance-not-found)
+:::
\ No newline at end of file
diff --git a/docs/docs/self-hosting/deploy/troubleshooting/troubleshooting.mdx b/docs/docs/self-hosting/deploy/troubleshooting/troubleshooting.mdx
new file mode 100644
index 000000000..aa5b2ed2e
--- /dev/null
+++ b/docs/docs/self-hosting/deploy/troubleshooting/troubleshooting.mdx
@@ -0,0 +1,10 @@
+---
+title: Troubleshoot
+---
+
+import InstanceNotFound from '/docs/self-hosting/deploy/troubleshooting/_instance_not_found.mdx';
+
+## Instance not found
+
+
+
\ No newline at end of file
diff --git a/docs/docs/self-hosting/manage/production.md b/docs/docs/self-hosting/manage/production.md
index 0f3169fa0..f00e7fe0e 100644
--- a/docs/docs/self-hosting/manage/production.md
+++ b/docs/docs/self-hosting/manage/production.md
@@ -61,6 +61,26 @@ Instead, your execution environment should provide tooling for managing logs in
This includes tasks like rotating files, routing, collecting, archiving and cleaning-up.
For example, systemd has journald and kubernetes has fluentd and fluentbit.
+## Telemetry
+
+If you want to have some data about reached usage milestones pushed to external systems, enable telemetry in the ZITADEL configuration.
+
+The following table describes the milestones that are sent to the endpoints:
+
+| Trigger | Description |
+|-----------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------|
+| A virtual instance is created. | This data point is also sent when the first instance is automatically created during the ZITADEL binaries setup phase in a self-hosting scenario. |
+| An authentication succeeded for the first time on an instance. | This is the first authentication with the instances automatically created admin user during the instance setup, which can be a human or a machine. |
+| A project is created for the first time in a virtual instance. | The ZITADEL project that is automatically created during the instance setup is omitted. |
+| An application is created for the first time in a virtual instance. | The applications in the ZITADEL project that are automatically created during the instance setup are omitted. |
+| An authentication succeeded for the first time in a virtal instances application. | This is the first authentication using a ZITADEL application that is not created during the instance setup phase. |
+| A virtual instance is deleted. | This data point is sent when a virtual instance is deleted via ZITADELs system API |
+
+
+ZITADEL pushes the metrics by projecting certain events.
+Therefore, you can configure delivery guarantees not in the Telemetry section of the ZITADEL configuration,
+but in the Projections.Customizations.Telemetry section
+
## Database
### Prefer CockroachDB
diff --git a/docs/docs/self-hosting/manage/tls_modes.mdx b/docs/docs/self-hosting/manage/tls_modes.mdx
index bb67ee90f..dc34dca07 100644
--- a/docs/docs/self-hosting/manage/tls_modes.mdx
+++ b/docs/docs/self-hosting/manage/tls_modes.mdx
@@ -2,8 +2,8 @@
title: TLS Modes
---
-To allow ZITADEL to be run on any kind of infrastrucute it allows to configure on how tho handle TLS connections.
-There are three mode of operation: `external`, `enabled`, `disabled`.
+To allow ZITADEL to be run on any kind of infrastructure it allows to configure on how tho handle TLS connections.
+There are three mode of operation: `external`, `enabled`, `disabled`.
Generally this command is set as argument while starting ZITADEL. For example like this:
@@ -11,18 +11,18 @@ Generally this command is set as argument while starting ZITADEL. For example li
zitadel start-from-init --masterkey "MasterkeyNeedsToHave32Characters" --tlsMode disabled
```
-## External
+## External
The mode `external` allows you to configure ZITADEL in such a way that it will instruct its clients to use https.
However ZITADEL delegates the management of TLS connections to a reverseproxy, web application firewall or a service mesh.
## Enabled
-When using the mode `enabled` ZITADEL is setup to await incoming connections in an encrypted fashion.
+When using the mode `enabled` ZITADEL is setup to await incoming connections in an encrypted fashion.
Wether it is from a client directly, a reverseproxy or web application firewall.
This allows http connections to be secured at the transport level the whole way.
-If you use the mode `enabled` you need to configure ZITADEL with the nessecary TLS settings.
+If you use the mode `enabled` you need to configure ZITADEL with the necessary TLS settings.
```yaml
TLS:
@@ -31,17 +31,17 @@ TLS:
# either directly or by a path to the corresponding file
Enabled: true
# Path to the private key of the TLS certificate, it will be loaded into the Key
- # and overwrite any exising value
+ # and overwrite any existing value
KeyPath: #/path/to/key/file.pem
# Private key of the TLS certificate (KeyPath will this overwrite, if specified)
Key: #
# Path to the certificate for the TLS connection, it will be loaded into the Cert
- # and overwrite any exising value
+ # and overwrite any existing value
CertPath: #/path/to/cert/file.pem
# Certificate for the TLS connection (CertPath will this overwrite, if specified)
Cert: #
```
-
+
## Disabled
With the mode `disabled` ZITADEL is instructed to await all connections with plain http without TLS.
@@ -55,4 +55,4 @@ Be aware this is not a secure setup and should only be used for test systems!
## HTTP/2
To allow ZITADEL to function properly please make sure that HTTP/2 is enabled. If you are using the mode `external` or `disabled` make sure to verify h2c compatibilty.
-You can read more about how ZITADEL utilizes in our [HTTP/2 docs](/self-hosting/manage/http2).
\ No newline at end of file
+You can read more about how ZITADEL utilizes in our [HTTP/2 docs](/self-hosting/manage/http2).
diff --git a/docs/docs/support/troubleshooting.md b/docs/docs/support/troubleshooting.mdx
similarity index 91%
rename from docs/docs/support/troubleshooting.md
rename to docs/docs/support/troubleshooting.mdx
index 8027152c3..76d320463 100644
--- a/docs/docs/support/troubleshooting.md
+++ b/docs/docs/support/troubleshooting.mdx
@@ -29,14 +29,6 @@ To make sure, that your new settings will trigger, please restart your mobile ph
Do you still face this issue? Please contact us, and we will help you find out what the problem is.
-## Instance not found
-
-`ID=QUERY-n0wng Message=Instance not found`
-
-If you're self hosting with a custom domain, you need to instruct ZITADEL to use the `ExternalDomain`.
-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)`
diff --git a/docs/docusaurus.config.js b/docs/docusaurus.config.js
index ccac55b08..c7131a226 100644
--- a/docs/docusaurus.config.js
+++ b/docs/docusaurus.config.js
@@ -274,6 +274,13 @@ module.exports = {
groupPathsBy: "tag",
},
},
+ oidc: {
+ specPath: ".artifacts/openapi/zitadel/oidc/v2alpha/oidc_service.swagger.json",
+ outputDir: "docs/apis/resources/oidc_service",
+ sidebarOptions: {
+ groupPathsBy: "tag",
+ },
+ },
settings: {
specPath: ".artifacts/openapi/zitadel/settings/v2alpha/settings_service.swagger.json",
outputDir: "docs/apis/resources/settings_service",
diff --git a/docs/sidebars.js b/docs/sidebars.js
index 755a57085..a90e7a0ad 100644
--- a/docs/sidebars.js
+++ b/docs/sidebars.js
@@ -150,6 +150,22 @@ module.exports = {
"guides/integrate/logout",
],
},
+ {
+ type: "category",
+ label: "Token introspection",
+ link: {
+ type: "generated-index",
+ title: "Token introspection",
+ slug: "/guides/integrate/token-introspection",
+ description:
+ "Token introspection is the process of checking whether an access token is valid and can be used to access protected resources. You have an API that acts as an OAuth resource server and can be accessed by user-facing applications. To validate an access token by calling the ZITADEL introspection API, you can use the JSON Web Token (JWT) Profile (recommended) or Basic Authentication for token introspection. It's crucial to understand that the API is entirely separate from the front end. The API shouldn’t concern itself with the token type received. Instead, it's about how the API chooses to call the introspection endpoint, either through JWT Profile or Basic Authentication. Many APIs assume they might receive a JWT and attempt to verify it based on signature or expiration. However, with ZITADEL, you can send either a JWT or an opaque Bearer token from the client end to the API. This flexibility is one of ZITADEL's standout features.",
+ },
+ collapsed: true,
+ items: [
+ "guides/integrate/token-introspection/private-key-jwt",
+ "guides/integrate/token-introspection/basic-auth",
+ ],
+ },
{
type: "category",
label: "Authenticate service users",
@@ -169,20 +185,23 @@ module.exports = {
},
{
type: "category",
- label: "Build your own Login-UI",
+ label: "Build your own login UI",
link: {
type: "generated-index",
- title: "Build your own Login-UI",
+ title: "Build your own login UI",
slug: "/guides/integrate/login-ui",
description:
- "In the following guides you will learn how to create your own login ui with our APIs. The different scenarios like username/password, external identity provider, etc will be shown.",
+ "In the following guides you will learn how to create your own login UI with our APIs. The different scenarios like username/password, external identity provider, etc. will be shown."
},
collapsed: true,
items: [
"guides/integrate/login-ui/username-password",
"guides/integrate/login-ui/external-login",
+ "guides/integrate/login-ui/passkey",
+ "guides/integrate/login-ui/mfa",
"guides/integrate/login-ui/select-account",
+ "guides/integrate/login-ui/password-reset",
"guides/integrate/login-ui/logout",
"guides/integrate/login-ui/oidc-standard"
],
@@ -206,6 +225,7 @@ module.exports = {
"guides/integrate/identity-providers/gitlab",
"guides/integrate/identity-providers/ldap",
"guides/integrate/identity-providers/openldap",
+ "guides/integrate/identity-providers/migrate",
"guides/integrate/identity-providers/google-oidc",
"guides/integrate/identity-providers/azuread-oidc",
],
@@ -293,6 +313,7 @@ module.exports = {
"guides/solution-scenarios/saas",
"guides/solution-scenarios/domain-discovery",
"guides/solution-scenarios/configurations",
+ "guides/solution-scenarios/frontend-calling-backend-API",
],
},
{
@@ -464,6 +485,20 @@ module.exports = {
},
items: require("./docs/apis/resources/session_service/sidebar.js"),
},
+ {
+ type: "category",
+ label: "OIDC lifecycle (Alpha)",
+ link: {
+ type: "generated-index",
+ title: "OIDC service API (Alpha)",
+ slug: "/apis/resources/oidc_service",
+ description:
+ "Get OIDC Auth Request details and create callback URLs.\n"+
+ "\n"+
+ "This project is in alpha state. It can AND will continue breaking until the services provide the same functionality as the current login.",
+ },
+ items: require("./docs/apis/resources/oidc_service/sidebar.js"),
+ },
{
type: "category",
label: "Settings lifecycle (alpha)",
@@ -554,7 +589,8 @@ module.exports = {
"self-hosting/deploy/compose",
"self-hosting/deploy/knative",
"self-hosting/deploy/kubernetes",
- "self-hosting/deploy/loadbalancing-example/loadbalancing-example"
+ "self-hosting/deploy/loadbalancing-example/loadbalancing-example",
+ "self-hosting/deploy/troubleshooting/troubleshooting"
],
},
{
diff --git a/docs/static/img/guides/integrate/token-introspection-basic-auth-1.png b/docs/static/img/guides/integrate/token-introspection-basic-auth-1.png
new file mode 100644
index 000000000..38c25a4b1
Binary files /dev/null and b/docs/static/img/guides/integrate/token-introspection-basic-auth-1.png differ
diff --git a/docs/static/img/guides/integrate/token-introspection-basic-auth-2.png b/docs/static/img/guides/integrate/token-introspection-basic-auth-2.png
new file mode 100644
index 000000000..6915f4b8f
Binary files /dev/null and b/docs/static/img/guides/integrate/token-introspection-basic-auth-2.png differ
diff --git a/docs/static/img/guides/integrate/token-introspection-basic-auth-3.png b/docs/static/img/guides/integrate/token-introspection-basic-auth-3.png
new file mode 100644
index 000000000..a0a18f206
Binary files /dev/null and b/docs/static/img/guides/integrate/token-introspection-basic-auth-3.png differ
diff --git a/docs/static/img/guides/integrate/token-introspection-basic-auth-4.png b/docs/static/img/guides/integrate/token-introspection-basic-auth-4.png
new file mode 100644
index 000000000..ac3fa06d2
Binary files /dev/null and b/docs/static/img/guides/integrate/token-introspection-basic-auth-4.png differ
diff --git a/docs/static/img/guides/integrate/token-introspection-basic-auth-5.png b/docs/static/img/guides/integrate/token-introspection-basic-auth-5.png
new file mode 100644
index 000000000..a6dbe546a
Binary files /dev/null and b/docs/static/img/guides/integrate/token-introspection-basic-auth-5.png differ
diff --git a/docs/static/img/guides/integrate/token-introspection-basic-auth-6.png b/docs/static/img/guides/integrate/token-introspection-basic-auth-6.png
new file mode 100644
index 000000000..05c58384b
Binary files /dev/null and b/docs/static/img/guides/integrate/token-introspection-basic-auth-6.png differ
diff --git a/docs/static/img/guides/integrate/token-introspection-basic-auth-7.png b/docs/static/img/guides/integrate/token-introspection-basic-auth-7.png
new file mode 100644
index 000000000..c3b984383
Binary files /dev/null and b/docs/static/img/guides/integrate/token-introspection-basic-auth-7.png differ
diff --git a/docs/static/img/guides/integrate/token-introspection-jwt-profile-1.png b/docs/static/img/guides/integrate/token-introspection-jwt-profile-1.png
new file mode 100644
index 000000000..1025eb869
Binary files /dev/null and b/docs/static/img/guides/integrate/token-introspection-jwt-profile-1.png differ
diff --git a/docs/static/img/guides/integrate/token-introspection-jwt-profile-10.png b/docs/static/img/guides/integrate/token-introspection-jwt-profile-10.png
new file mode 100644
index 000000000..05c58384b
Binary files /dev/null and b/docs/static/img/guides/integrate/token-introspection-jwt-profile-10.png differ
diff --git a/docs/static/img/guides/integrate/token-introspection-jwt-profile-11.png b/docs/static/img/guides/integrate/token-introspection-jwt-profile-11.png
new file mode 100644
index 000000000..c3b984383
Binary files /dev/null and b/docs/static/img/guides/integrate/token-introspection-jwt-profile-11.png differ
diff --git a/docs/static/img/guides/integrate/token-introspection-jwt-profile-2.png b/docs/static/img/guides/integrate/token-introspection-jwt-profile-2.png
new file mode 100644
index 000000000..1bb592763
Binary files /dev/null and b/docs/static/img/guides/integrate/token-introspection-jwt-profile-2.png differ
diff --git a/docs/static/img/guides/integrate/token-introspection-jwt-profile-3.png b/docs/static/img/guides/integrate/token-introspection-jwt-profile-3.png
new file mode 100644
index 000000000..d4deeb6e9
Binary files /dev/null and b/docs/static/img/guides/integrate/token-introspection-jwt-profile-3.png differ
diff --git a/docs/static/img/guides/integrate/token-introspection-jwt-profile-4.png b/docs/static/img/guides/integrate/token-introspection-jwt-profile-4.png
new file mode 100644
index 000000000..7063c251e
Binary files /dev/null and b/docs/static/img/guides/integrate/token-introspection-jwt-profile-4.png differ
diff --git a/docs/static/img/guides/integrate/token-introspection-jwt-profile-5.png b/docs/static/img/guides/integrate/token-introspection-jwt-profile-5.png
new file mode 100644
index 000000000..9f5bddb7d
Binary files /dev/null and b/docs/static/img/guides/integrate/token-introspection-jwt-profile-5.png differ
diff --git a/docs/static/img/guides/integrate/token-introspection-jwt-profile-6.png b/docs/static/img/guides/integrate/token-introspection-jwt-profile-6.png
new file mode 100644
index 000000000..d213eaee9
Binary files /dev/null and b/docs/static/img/guides/integrate/token-introspection-jwt-profile-6.png differ
diff --git a/docs/static/img/guides/integrate/token-introspection-jwt-profile-7.png b/docs/static/img/guides/integrate/token-introspection-jwt-profile-7.png
new file mode 100644
index 000000000..d783c92cd
Binary files /dev/null and b/docs/static/img/guides/integrate/token-introspection-jwt-profile-7.png differ
diff --git a/docs/static/img/guides/integrate/token-introspection-jwt-profile-8.png b/docs/static/img/guides/integrate/token-introspection-jwt-profile-8.png
new file mode 100644
index 000000000..47299c092
Binary files /dev/null and b/docs/static/img/guides/integrate/token-introspection-jwt-profile-8.png differ
diff --git a/docs/static/img/guides/integrate/token-introspection-jwt-profile-9.png b/docs/static/img/guides/integrate/token-introspection-jwt-profile-9.png
new file mode 100644
index 000000000..e20a66449
Binary files /dev/null and b/docs/static/img/guides/integrate/token-introspection-jwt-profile-9.png differ
diff --git a/docs/static/img/guides/login-ui/passkey-login-flow.png b/docs/static/img/guides/login-ui/passkey-login-flow.png
new file mode 100644
index 000000000..638b58d6d
Binary files /dev/null and b/docs/static/img/guides/login-ui/passkey-login-flow.png differ
diff --git a/docs/static/img/guides/login-ui/passkey-registration-flow.png b/docs/static/img/guides/login-ui/passkey-registration-flow.png
new file mode 100644
index 000000000..9dc1a47fc
Binary files /dev/null and b/docs/static/img/guides/login-ui/passkey-registration-flow.png differ
diff --git a/docs/static/img/guides/login-ui/password-reset-flow.png b/docs/static/img/guides/login-ui/password-reset-flow.png
new file mode 100644
index 000000000..56e7f8eff
Binary files /dev/null and b/docs/static/img/guides/login-ui/password-reset-flow.png differ
diff --git a/docs/static/img/guides/login-ui/register-totp-flow.png b/docs/static/img/guides/login-ui/register-totp-flow.png
new file mode 100644
index 000000000..75a4da5c1
Binary files /dev/null and b/docs/static/img/guides/login-ui/register-totp-flow.png differ
diff --git a/docs/static/img/guides/login-ui/register-u2f-flow.png b/docs/static/img/guides/login-ui/register-u2f-flow.png
new file mode 100644
index 000000000..2e1c39d02
Binary files /dev/null and b/docs/static/img/guides/login-ui/register-u2f-flow.png differ
diff --git a/docs/static/img/guides/solution-scenarios/frontend-calling-backend-API_1.png b/docs/static/img/guides/solution-scenarios/frontend-calling-backend-API_1.png
new file mode 100644
index 000000000..14577545a
Binary files /dev/null and b/docs/static/img/guides/solution-scenarios/frontend-calling-backend-API_1.png differ
diff --git a/docs/vercel.json b/docs/vercel.json
index 9a977c840..a42f8c252 100644
--- a/docs/vercel.json
+++ b/docs/vercel.json
@@ -3,7 +3,7 @@
"rewrites": [
{
"source": "/docs/proxy/js/script.js",
- "destination": "https://plausible.io/js/script.tagged-events.outbound-links.js"
+ "destination": "https://plausible.io/js/script.tagged-events.pageview-props.outbound-links.js"
},
{
"source": "/docs/proxy/api/event",
@@ -15,6 +15,10 @@
}
],
"redirects": [
- { "source": "/", "destination": "/docs" }
+ { "source": "/", "destination": "/docs" },
+ { "source": "/docs/apis/mgmt/:slug*", "destination": "/docs/apis/resources/mgmt/:slug*", "permanent": true },
+ { "source": "/docs/apis/auth/:slug*", "destination": "/docs/apis/resources/auth/:slug*", "permanent": true },
+ { "source": "/docs/apis/system/:slug*", "destination": "/docs/apis/resources/system/:slug*", "permanent": true },
+ { "source": "/docs/apis/admin/:slug*", "destination": "/docs/apis/resources/admin/:slug*", "permanent": true }
]
}
diff --git a/e2e/config/host.docker.internal/docker-compose.yaml b/e2e/config/host.docker.internal/docker-compose.yaml
index dbedbeee9..0bcac1a93 100644
--- a/e2e/config/host.docker.internal/docker-compose.yaml
+++ b/e2e/config/host.docker.internal/docker-compose.yaml
@@ -1,18 +1,22 @@
version: '3.8'
services:
- zitadel:
- extends:
- file: '../localhost/docker-compose.yaml'
- service: 'zitadel'
- volumes:
- - ./zitadel.yaml:/zitadel.yaml
db:
extends:
file: '../localhost/docker-compose.yaml'
service: 'db'
+ zitadel:
+ depends_on:
+ db:
+ condition: 'service_healthy'
+ extends:
+ file: '../localhost/docker-compose.yaml'
+ service: 'zitadel'
+ volumes:
+ - ./zitadel.yaml:/zitadel.yaml
+
prepare:
image: node:18-alpine3.15
working_dir: /e2e
diff --git a/e2e/cypress/e2e/projects/projects.cy.ts b/e2e/cypress/e2e/projects/projects.cy.ts
index 36768a153..101608289 100644
--- a/e2e/cypress/e2e/projects/projects.cy.ts
+++ b/e2e/cypress/e2e/projects/projects.cy.ts
@@ -1,19 +1,23 @@
import { Context } from 'support/commands';
import { ensureProjectDoesntExist, ensureProjectExists } from '../../support/api/projects';
+import { ensureOrgExists } from 'support/api/orgs';
describe('projects', () => {
beforeEach(() => {
cy.context().as('ctx');
});
+ const defaultOrg = 'e2eorgnewdefault';
const testProjectNameCreate = 'e2eprojectcreate';
const testProjectNameDelete = 'e2eprojectdelete';
describe('add project', () => {
beforeEach(`ensure it doesn't exist already`, () => {
cy.get('@ctx').then((ctx) => {
- ensureProjectDoesntExist(ctx.api, testProjectNameCreate);
- cy.visit(`/projects`);
+ ensureOrgExists(ctx, defaultOrg).then(() => {
+ ensureProjectDoesntExist(ctx.api, testProjectNameCreate);
+ cy.visit(`/projects`);
+ });
});
});
@@ -27,10 +31,49 @@ describe('projects', () => {
it('should configure a project to assert roles on authentication');
});
+ describe('create project grant', () => {
+ const testRoleName = 'e2eroleundertestname';
+
+ beforeEach('ensure it exists', () => {
+ cy.get('@ctx').then((ctx) => {
+ ensureProjectExists(ctx.api, testProjectNameCreate).as('projectId');
+ cy.get('@projectId').then((projectId) => {
+ cy.visit(`/projects/${projectId}`);
+ });
+ });
+ });
+
+ it('should add a role', () => {
+ cy.get('[data-e2e="sidenav-element-roles"]').click();
+ cy.get('[data-e2e="add-new-role"]').click();
+ cy.get('[formcontrolname="key"]').type(testRoleName);
+ cy.get('[formcontrolname="displayName"]').type('e2eroleundertestdisplay');
+ cy.get('[formcontrolname="group"]').type('e2eroleundertestgroup');
+ cy.get('[data-e2e="save-button"]').click();
+ cy.shouldConfirmSuccess();
+ cy.contains('tr', testRoleName);
+ });
+
+ it('should add a project grant', () => {
+ const rowSelector = `tr:contains(${testRoleName})`;
+
+ cy.get('[data-e2e="sidenav-element-projectgrants"]').click();
+ cy.get('[data-e2e="create-project-grant-button"]').click();
+ cy.get('[data-e2e="add-org-input"]').type(defaultOrg);
+ cy.get('mat-option').contains(defaultOrg).click();
+ cy.get('button').should('be.enabled');
+ cy.get('[data-e2e="project-grant-continue"]').first().click();
+ cy.get(rowSelector).find('input').click({ force: true });
+ cy.get('[data-e2e="save-project-grant-button"]').click();
+ cy.contains('tr', defaultOrg);
+ cy.contains('tr', testRoleName);
+ });
+ });
+
describe('edit project', () => {
beforeEach('ensure it exists', () => {
cy.get('@ctx').then((ctx) => {
- ensureProjectExists(ctx.api, testProjectNameDelete);
+ ensureProjectExists(ctx.api, testProjectNameDelete).as('projectId');
cy.visit(`/projects`);
});
});
diff --git a/e2e/cypress/e2e/quotas/quotas.cy.ts b/e2e/cypress/e2e/quotas/quotas.cy.ts
index ada143105..e3b650479 100644
--- a/e2e/cypress/e2e/quotas/quotas.cy.ts
+++ b/e2e/cypress/e2e/quotas/quotas.cy.ts
@@ -130,12 +130,12 @@ describe('quotas', () => {
expect(res.status).to.equal(429);
});
// visit limited console
- cy.visit('/users/me');
- cy.contains('#authenticated-requests-exhausted-dialog button', 'Continue').click();
- const upgradeInstancePage = `https://example.com/instances/${ctx.instanceId}`;
- cy.origin(upgradeInstancePage, { args: { upgradeInstancePage } }, ({ upgradeInstancePage }) => {
- cy.location('href').should('equal', upgradeInstancePage);
- });
+ // cy.visit('/users/me');
+ // cy.contains('#authenticated-requests-exhausted-dialog button', 'Continue').click();
+ // const upgradeInstancePage = `https://example.com/instances/${ctx.instanceId}`;
+ // cy.origin(upgradeInstancePage, { args: { upgradeInstancePage } }, ({ upgradeInstancePage }) => {
+ // cy.location('href').should('equal', upgradeInstancePage);
+ // });
// upgrade instance
ensureQuotaIsRemoved(ctx, Unit.AuthenticatedRequests);
// visit upgraded console again
diff --git a/e2e/cypress/support/login/users.ts b/e2e/cypress/support/login/users.ts
index d3a1a08c1..db230b1f5 100644
--- a/e2e/cypress/support/login/users.ts
+++ b/e2e/cypress/support/login/users.ts
@@ -82,6 +82,8 @@ export function login(
onAuthenticated ? onAuthenticated() : null;
+ cy.wait(1000);
+
cy.visit('/');
cy.get('[data-e2e=authenticated-welcome]', {
diff --git a/go.mod b/go.mod
index 74ff7c774..18077ccc0 100644
--- a/go.mod
+++ b/go.mod
@@ -1,13 +1,12 @@
module github.com/zitadel/zitadel
-go 1.19
+go 1.20
require (
cloud.google.com/go/storage v1.30.1
github.com/BurntSushi/toml v1.2.1
github.com/DATA-DOG/go-sqlmock v1.5.0
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/trace v1.13.0
- github.com/Masterminds/sprig v2.22.0+incompatible
github.com/Masterminds/squirrel v1.5.4
github.com/VictoriaMetrics/fastcache v1.12.1
github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b
@@ -15,14 +14,15 @@ require (
github.com/benbjohnson/clock v1.3.0
github.com/boombuler/barcode v1.0.1
github.com/cockroachdb/cockroach-go/v2 v2.3.3
+ github.com/common-nighthawk/go-figure v0.0.0-20210622060536-734e95fb86be
github.com/descope/virtualwebauthn v1.0.2
github.com/dop251/goja v0.0.0-20230402114112-623f9dda9079
github.com/dop251/goja_nodejs v0.0.0-20230322100729-2550c7b6c124
github.com/drone/envsubst v1.0.3
github.com/envoyproxy/protoc-gen-validate v0.10.1
+ github.com/fatih/color v1.13.0
github.com/go-ldap/ldap/v3 v3.4.4
github.com/go-webauthn/webauthn v0.8.2
- github.com/golang/glog v1.1.1
github.com/golang/mock v1.6.0
github.com/golang/protobuf v1.5.3
github.com/gorilla/csrf v1.7.1
@@ -59,7 +59,8 @@ require (
github.com/superseriousbusiness/exifremove v0.0.0-20210330092427-6acd27eac203
github.com/ttacon/libphonenumber v1.2.1
github.com/zitadel/logging v0.3.4
- github.com/zitadel/oidc/v2 v2.6.3
+ github.com/zitadel/oidc/v2 v2.7.0
+ github.com/zitadel/passwap v0.2.0
github.com/zitadel/saml v0.0.11
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.40.0
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.40.0
@@ -71,16 +72,15 @@ require (
go.opentelemetry.io/otel/sdk v1.14.0
go.opentelemetry.io/otel/sdk/metric v0.37.0
go.opentelemetry.io/otel/trace v1.14.0
- golang.org/x/crypto v0.9.0
- golang.org/x/net v0.10.0
- golang.org/x/oauth2 v0.8.0
- golang.org/x/sync v0.1.0
- golang.org/x/text v0.9.0
- golang.org/x/tools v0.7.0
- google.golang.org/api v0.115.0
- google.golang.org/genproto v0.0.0-20230403163135-c38d8f061ccd
- google.golang.org/grpc v1.54.0
- google.golang.org/protobuf v1.30.0
+ golang.org/x/crypto v0.11.0
+ golang.org/x/net v0.12.0
+ golang.org/x/oauth2 v0.10.0
+ golang.org/x/sync v0.2.0
+ golang.org/x/text v0.11.0
+ google.golang.org/api v0.126.0
+ google.golang.org/genproto/googleapis/api v0.0.0-20230530153820-e85fd2cbaebc
+ google.golang.org/grpc v1.55.0
+ google.golang.org/protobuf v1.31.0
gopkg.in/square/go-jose.v2 v2.6.0
sigs.k8s.io/yaml v1.3.0
)
@@ -91,25 +91,29 @@ require (
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-sql-driver/mysql v1.6.0 // indirect
github.com/go-webauthn/revoke v0.1.9 // indirect
+ github.com/golang/glog v1.1.1 // indirect
github.com/google/go-tpm v0.3.3 // indirect
github.com/google/pprof v0.0.0-20230323073829-e72429f035bd // indirect
+ github.com/google/s2a-go v0.1.4 // indirect
github.com/gorilla/websocket v1.4.2 // indirect
github.com/klauspost/cpuid/v2 v2.2.4 // indirect
+ github.com/mattn/go-colorable v0.1.12 // indirect
+ github.com/mattn/go-isatty v0.0.17 // indirect
github.com/pelletier/go-toml/v2 v2.0.7 // indirect
github.com/smartystreets/assertions v1.0.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.14.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
+ google.golang.org/genproto v0.0.0-20230530153820-e85fd2cbaebc // indirect
+ google.golang.org/genproto/googleapis/rpc v0.0.0-20230530153820-e85fd2cbaebc // indirect
)
require (
- cloud.google.com/go v0.110.0 // indirect
- cloud.google.com/go/compute v1.19.0 // indirect
+ cloud.google.com/go v0.110.2 // indirect
+ cloud.google.com/go/compute v1.20.1 // indirect
cloud.google.com/go/compute/metadata v0.2.3 // indirect
cloud.google.com/go/iam v1.0.0 // indirect
cloud.google.com/go/trace v1.9.0 // indirect
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect
- github.com/Masterminds/goutils v1.1.1 // indirect
- github.com/Masterminds/semver v1.5.0 // indirect
github.com/amdonov/xmlsig v0.1.0 // indirect
github.com/beevik/etree v1.1.0 // indirect
github.com/beorn7/perks v1.0.1 // indirect
@@ -130,7 +134,6 @@ require (
github.com/felixge/httpsnoop v1.0.3 // indirect
github.com/fsnotify/fsnotify v1.6.0 // indirect
github.com/fxamacker/cbor/v2 v2.4.0 // indirect
- github.com/ghodss/yaml v1.0.0 // indirect
github.com/go-asn1-ber/asn1-ber v1.5.4 // indirect
github.com/go-errors/errors v1.4.2 // indirect
github.com/go-sourcemap/sourcemap v2.1.3+incompatible // indirect
@@ -144,13 +147,11 @@ require (
github.com/google/go-cmp v0.5.9 // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.2.3 // indirect
- github.com/googleapis/gax-go/v2 v2.8.0 // indirect
+ github.com/googleapis/gax-go/v2 v2.11.0 // indirect
github.com/gorilla/handlers v1.5.1 // indirect
github.com/h2non/filetype v1.1.3 // indirect
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
- github.com/huandu/xstrings v1.4.0 // indirect
- github.com/imdario/mergo v0.3.15 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jackc/chunkreader/v2 v2.0.1 // indirect
github.com/jackc/pgio v1.0.0 // indirect
@@ -170,8 +171,6 @@ require (
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
github.com/minio/md5-simd v1.1.2 // indirect
github.com/minio/sha256-simd v1.0.0 // indirect
- github.com/mitchellh/copystructure v1.2.0 // indirect
- github.com/mitchellh/reflectwalk v1.0.2 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/muesli/clusters v0.0.0-20200529215643-2700303c1762 // indirect
@@ -195,8 +194,7 @@ require (
go.opencensus.io v0.24.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.14.0 // indirect
go.opentelemetry.io/proto/otlp v0.19.0 // indirect
- golang.org/x/mod v0.10.0 // indirect
- golang.org/x/sys v0.8.0
+ golang.org/x/sys v0.10.0
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect
google.golang.org/appengine v1.6.7 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
diff --git a/go.sum b/go.sum
index 162c573e2..e69bad2e3 100644
--- a/go.sum
+++ b/go.sum
@@ -17,16 +17,16 @@ cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHOb
cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI=
cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk=
cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY=
-cloud.google.com/go v0.110.0 h1:Zc8gqp3+a9/Eyph2KDmcGaPtbKRIoqq4YTlL4NMD0Ys=
-cloud.google.com/go v0.110.0/go.mod h1:SJnCLqQ0FCFGSZMUNUf84MV3Aia54kn7pi8st7tMzaY=
+cloud.google.com/go v0.110.2 h1:sdFPBr6xG9/wkBbfhmUz/JmZC7X6LavQgcrVINrKiVA=
+cloud.google.com/go v0.110.2/go.mod h1:k04UEeEtb6ZBRTv3dZz4CeJC3jKGxyhl0sAiVVquxiw=
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
-cloud.google.com/go/compute v1.19.0 h1:+9zda3WGgW1ZSTlVppLCYFIr48Pa35q1uG2N1itbCEQ=
-cloud.google.com/go/compute v1.19.0/go.mod h1:rikpw2y+UMidAe9tISo04EHNOIf42RLYF/q8Bs93scU=
+cloud.google.com/go/compute v1.20.1 h1:6aKEtlUiwEpJzM001l0yFkpXmUVXaN8W+fbkb2AZNbg=
+cloud.google.com/go/compute v1.20.1/go.mod h1:4tCnrn48xsqlwSAiLf1HXMQk8CONslYbdiEZc9FEIbM=
cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY=
cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA=
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
@@ -67,14 +67,8 @@ github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.37.0 h1:k5x4SiDgKS8wVQO/ww1fnoh5gwYEg6Wsi+1z5kB9uDM=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.37.0/go.mod h1:oEccMakRmMNrayCPR+5OmZE/aeXmTPzUtmomEXIPBdI=
github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0=
-github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
-github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
-github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww=
-github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y=
github.com/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030IGemrRc=
github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
-github.com/Masterminds/sprig v2.22.0+incompatible h1:z4yfnGrZ7netVz+0EDJ0Wi+5VZCSYp4Z0m2dk6cEM60=
-github.com/Masterminds/sprig v2.22.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o=
github.com/Masterminds/squirrel v1.5.4 h1:uUcX/aBc8O7Fg9kaISIUsHXdKuqehiXAMQTYX8afzqM=
github.com/Masterminds/squirrel v1.5.4/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
@@ -156,6 +150,8 @@ github.com/cockroachdb/cockroach-go/v2 v2.3.3 h1:fNmtG6XhoA1DhdDCIu66YyGSsNb1szj
github.com/cockroachdb/cockroach-go/v2 v2.3.3/go.mod h1:1wNJ45eSXW9AnOc3skntW9ZUZz6gxrQK3cOj3rK+BC8=
github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8=
github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI=
+github.com/common-nighthawk/go-figure v0.0.0-20210622060536-734e95fb86be h1:J5BL2kskAlV9ckgEsNQXscjIaLiOYiZ75d4e94E6dcQ=
+github.com/common-nighthawk/go-figure v0.0.0-20210622060536-734e95fb86be/go.mod h1:mk5IQ+Y0ZeO87b858TlA645sVcEcbiX6YqP98kt+7+w=
github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
@@ -247,6 +243,8 @@ github.com/envoyproxy/protoc-gen-validate v0.10.1/go.mod h1:DRjgyB0I43LtJapqN6Ni
github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5 h1:Yzb9+7DPaBjB8zlTR87/ElzFsnQfuHnVUVqpZZIcV5Y=
github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
+github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w=
+github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/felixge/httpsnoop v1.0.3 h1:s/nj+GCswXYzN5v2DpNMuMQYe+0DDwt5WVCU6CWBdXk=
github.com/felixge/httpsnoop v1.0.3/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
@@ -258,7 +256,6 @@ github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
github.com/fxamacker/cbor/v2 v2.4.0 h1:ri0ArlOR+5XunOP8CRUowT0pSJOwhW098ZCUyskZD88=
github.com/fxamacker/cbor/v2 v2.4.0/go.mod h1:TA1xS00nchWmaBnEIxPSE5oHLuJBAVvqrtAnWBwBCVo=
-github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
@@ -417,6 +414,8 @@ github.com/google/pprof v0.0.0-20230207041349-798e818bf904/go.mod h1:uglQLonpP8q
github.com/google/pprof v0.0.0-20230323073829-e72429f035bd h1:r8yyd+DJDmsUhGrRBxH5Pj7KeFK5l+Y3FsgT8keqKtk=
github.com/google/pprof v0.0.0-20230323073829-e72429f035bd/go.mod h1:79YE0hCXdHag9sBkw2o+N/YnZtTkXi0UT9Nnixa5eYk=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
+github.com/google/s2a-go v0.1.4 h1:1kZ/sQM3srePvKs3tXAvQzo66XfcReoqFpIpIccE7Oc=
+github.com/google/s2a-go v0.1.4/go.mod h1:Ej+mSEMGRnqRzjc7VtF+jdBwYG5fuJfiZ8ELkjEwM0A=
github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
@@ -425,8 +424,8 @@ github.com/googleapis/enterprise-certificate-proxy v0.2.3 h1:yk9/cqRKtT9wXZSsRH9
github.com/googleapis/enterprise-certificate-proxy v0.2.3/go.mod h1:AwSRAtLfXpU5Nm3pW+v7rGDHp09LsPtGY9MduiEsR9k=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
-github.com/googleapis/gax-go/v2 v2.8.0 h1:UBtEZqx1bjXtOQ5BVTkuYghXrr3N4V123VKJK67vJZc=
-github.com/googleapis/gax-go/v2 v2.8.0/go.mod h1:4orTrqY6hXxxaUL4LHIPl6lGo8vAE38/qKbhSAKP6QI=
+github.com/googleapis/gax-go/v2 v2.11.0 h1:9V9PWXEsWnPpQhu/PeQIkS4eGzMlTLGgt80cUUI8Ki4=
+github.com/googleapis/gax-go/v2 v2.11.0/go.mod h1:DxmR61SGKkGLa2xigwuZIQpkCI2S5iydzRfb3peWZJI=
github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
@@ -491,14 +490,10 @@ github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0m
github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I=
github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
-github.com/huandu/xstrings v1.4.0 h1:D17IlohoQq4UcpqD7fDk80P7l+lwAmlFaBHgOipl2FU=
-github.com/huandu/xstrings v1.4.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
github.com/hudl/fargo v1.3.0/go.mod h1:y3CKSmjA+wD2gak7sUSXTAoopbhU08POFhmITJgmKTg=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/ianlancetaylor/demangle v0.0.0-20220319035150-800ac71e25c2/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w=
-github.com/imdario/mergo v0.3.15 h1:M8XP7IuFNsqUx6VPK2P9OSmsYsI/YFaGil0uD21V3dM=
-github.com/imdario/mergo v0.3.15/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY=
github.com/improbable-eng/grpc-web v0.15.0 h1:BN+7z6uNXZ1tQGcNAuaU1YjsLTApzkjt2tzCixLaUPQ=
github.com/improbable-eng/grpc-web v0.15.0/go.mod h1:1sy9HKV4Jt9aEs9JSnkWlRJPuPtwNr0l57L4f878wP8=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
@@ -639,12 +634,17 @@ github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3v
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
+github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
+github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40=
+github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
-github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
+github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
+github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng=
+github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mattn/go-sqlite3 v1.14.0/go.mod h1:JIl7NbARA7phWnGvh0LKTyg7S9BA+6gx71ShQilpsus=
github.com/mattn/go-sqlite3 v2.0.3+incompatible h1:gXHsfypPkaMZrKbD5209QV9jbUTJKjyR5WD3HYQSd+U=
@@ -660,8 +660,6 @@ github.com/minio/minio-go/v7 v7.0.50/go.mod h1:IbbodHyjUAguneyucUaahv+VMNs/EOTV9
github.com/minio/sha256-simd v1.0.0 h1:v1ta+49hkWZyvaKwrQB8elexRqm6Y0aMLjCNsrYxo6g=
github.com/minio/sha256-simd v1.0.0/go.mod h1:OuYzVNI5vcoYIAmbIvHPl3N3jUzVedXbKy5RFepssQM=
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
-github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
-github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
@@ -671,8 +669,6 @@ github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:F
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
-github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
-github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@@ -900,8 +896,10 @@ github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5t
github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
github.com/zitadel/logging v0.3.4 h1:9hZsTjMMTE3X2LUi0xcF9Q9EdLo+FAezeu52ireBbHM=
github.com/zitadel/logging v0.3.4/go.mod h1:aPpLQhE+v6ocNK0TWrBrd363hZ95KcI17Q1ixAQwZF0=
-github.com/zitadel/oidc/v2 v2.6.3 h1:YY87cAcdI+3voZqcRU2RGz3Pxky/2KsjDmYDVb6EgWw=
-github.com/zitadel/oidc/v2 v2.6.3/go.mod h1:2LrbdKYLSgKxXBfct56ev4e186J7TXotlZxb6tExOO4=
+github.com/zitadel/oidc/v2 v2.7.0 h1:IGX4EDk6tegTjUSsZDWeTfLseFU0BdJ/Glf1tgys2lU=
+github.com/zitadel/oidc/v2 v2.7.0/go.mod h1:zkUkVJS0sDVy9m0UA9RgO3f8i/C0rtjvXU36UJj7T+0=
+github.com/zitadel/passwap v0.2.0 h1:rkYrax9hfRIpVdXJ7pS8JHkQOhuQTdZQxEhsY0dFFrU=
+github.com/zitadel/passwap v0.2.0/go.mod h1:KRTL4LL8ugJIn2xLoQYZf5t4kDyr7w41uq3XqvUlO6w=
github.com/zitadel/saml v0.0.11 h1:kObucnBrcu1PHCO7RGT0iVeuJL/5I50gUgr40S41nMs=
github.com/zitadel/saml v0.0.11/go.mod h1:YGWAvPZRv4DbEZ78Ht/2P0AWzGn+6WGhFf90PMXl0Po=
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
@@ -980,11 +978,12 @@ golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm
golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
+golang.org/x/crypto v0.0.0-20220314234659-1baeb1ce4c0b/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
-golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g=
-golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0=
+golang.org/x/crypto v0.11.0 h1:6Ewdq3tDic1mg5xRO4milcWCfMVQhI4NkqWWvqejpuA=
+golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@@ -1022,8 +1021,6 @@ golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
-golang.org/x/mod v0.10.0 h1:lFO9qtOdlre5W1jxS3r/4szv2/6iXxScdzjoBMXNhYk=
-golang.org/x/mod v0.10.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -1074,8 +1071,8 @@ golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qx
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.4.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
-golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M=
-golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
+golang.org/x/net v0.12.0 h1:cfawfvKITfUsFCeJIHJrbSxpeu/E81khclypR0GVT50=
+golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@@ -1086,8 +1083,8 @@ golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ
golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
-golang.org/x/oauth2 v0.8.0 h1:6dkIjl3j3LtZ/O3sTgZTMsLKSftL/B8Zgq4huOIIUu8=
-golang.org/x/oauth2 v0.8.0/go.mod h1:yr7u4HXZRm1R1kBWqr/xKNqewf0plRYoB7sla+BCIXE=
+golang.org/x/oauth2 v0.10.0 h1:zHCpF2Khkwy4mMB4bv0U37YtJdTGW8jI0glAApi0Kh8=
+golang.org/x/oauth2 v0.10.0/go.mod h1:kTpgurOux7LqtuxjuyZa4Gj2gdezIt/jQtGnNFfypQI=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -1100,8 +1097,8 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
-golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.2.0 h1:PUR+T4wwASmuSTYdKjYHI5TD22Wy5ogLU5qZCOLxBrI=
+golang.org/x/sync v0.2.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -1160,7 +1157,9 @@ golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210629170331-7dc0b73dc9fb/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220207234003-57398862261d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -1168,11 +1167,12 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU=
-golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA=
+golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
@@ -1191,8 +1191,8 @@ golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
-golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
-golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
+golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4=
+golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
@@ -1257,8 +1257,6 @@ golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4f
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
-golang.org/x/tools v0.7.0 h1:W4OVu8VVOaIO0yzWMNdepAulS7YfoS3Zabrm8DOXXU4=
-golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s=
golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -1287,8 +1285,8 @@ google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz513
google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg=
google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE=
google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8=
-google.golang.org/api v0.115.0 h1:6FFkVvStt4YqXSx3azKyzj7fXerGnVlLJ/eud01nBDE=
-google.golang.org/api v0.115.0/go.mod h1:9cD4/t6uvd9naoEJFA+M96d0IuB6BqFuyhpw68+mRGg=
+google.golang.org/api v0.126.0 h1:q4GJq+cAdMAC7XP7njvQ4tvohGLiSlytuL4BQxbIZ+o=
+google.golang.org/api v0.126.0/go.mod h1:mBwVAtz+87bEN6CbA1GtZPDOqY2R5ONPqJeIlvyo4Aw=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
@@ -1339,8 +1337,12 @@ google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6D
google.golang.org/genproto v0.0.0-20210126160654-44e461bb6506/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
-google.golang.org/genproto v0.0.0-20230403163135-c38d8f061ccd h1:sLpv7bNL1AsX3fdnWh9WVh7ejIzXdOc1RRHGeAmeStU=
-google.golang.org/genproto v0.0.0-20230403163135-c38d8f061ccd/go.mod h1:UUQDJDOlWu4KYeJZffbWgBkS1YFobzKbLVfK69pe0Ak=
+google.golang.org/genproto v0.0.0-20230530153820-e85fd2cbaebc h1:8DyZCyvI8mE1IdLy/60bS+52xfymkE72wv1asokgtao=
+google.golang.org/genproto v0.0.0-20230530153820-e85fd2cbaebc/go.mod h1:xZnkP7mREFX5MORlOPEzLMr+90PPZQ2QWzrVTWfAq64=
+google.golang.org/genproto/googleapis/api v0.0.0-20230530153820-e85fd2cbaebc h1:kVKPf/IiYSBWEWtkIn6wZXwWGCnLKcC8oWfZvXjsGnM=
+google.golang.org/genproto/googleapis/api v0.0.0-20230530153820-e85fd2cbaebc/go.mod h1:vHYtlOoi6TsQ3Uk2yxR7NI5z8uoV+3pZtR4jmHIkRig=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20230530153820-e85fd2cbaebc h1:XSJ8Vk1SWuNr8S18z1NZSziL0CPIXLCCMDOEFtHBOFc=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20230530153820-e85fd2cbaebc/go.mod h1:66JfowdXAEgad5O9NnYcsNPLCPZJD++2L9X0PCMODrA=
google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.0/go.mod h1:chYK+tFQF0nDUGJgXMSgLCQk3phJEuONr2DCgLDdAQM=
@@ -1367,8 +1369,9 @@ google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAG
google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=
google.golang.org/grpc v1.42.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU=
-google.golang.org/grpc v1.54.0 h1:EhTqbhiYeixwWQtAEZAxmV9MGqcjEU2mFx52xCzNyag=
-google.golang.org/grpc v1.54.0/go.mod h1:PUSEXI6iWghWaB6lXM4knEgpJNu2qUcKfDtNci3EC2g=
+google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ=
+google.golang.org/grpc v1.55.0 h1:3Oj82/tFSCeUrRTg/5E/7d/W5A1tj6Ky1ABAuZuv5ag=
+google.golang.org/grpc v1.55.0/go.mod h1:iYEXKGkEBhg1PjZQvoYEVPTDkHo1/bjTnfwTeGONTY8=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
@@ -1382,8 +1385,8 @@ google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlba
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
-google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
-google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
+google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
+google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
diff --git a/internal/api/assets/asset.go b/internal/api/assets/asset.go
index f570b7dd9..08ba7130a 100644
--- a/internal/api/assets/asset.go
+++ b/internal/api/assets/asset.go
@@ -190,6 +190,10 @@ func DownloadHandleFunc(s AssetsService, downloader Downloader) func(http.Respon
return
}
if err = GetAsset(w, r, resourceOwner, objectName, s.Storage()); err != nil {
+ if strings.Contains(err.Error(), "DATAB-pCP8P") {
+ s.ErrorHandler()(w, r, err, http.StatusNotFound)
+ return
+ }
s.ErrorHandler()(w, r, err, http.StatusInternalServerError)
}
}
diff --git a/internal/api/assets/generator/asset_generator.go b/internal/api/assets/generator/asset_generator.go
index a30112a82..51268634c 100644
--- a/internal/api/assets/generator/asset_generator.go
+++ b/internal/api/assets/generator/asset_generator.go
@@ -128,7 +128,9 @@ func GenerateAssetHandler(configFilePath, handlerPrefix string, authz, router, d
logging.Log("ASSETS-Bfd41").OnError(err).Fatal("cannot generate docs")
}
-const authzTmpl = `package {{.GoPkgName}}
+const authzTmpl = `// Code generated by assets generator. DO NOT EDIT.
+
+package {{.GoPkgName}}
import (
"github.com/zitadel/zitadel/internal/api/authz"
@@ -159,7 +161,9 @@ var {{.Name}}_AuthMethods = authz.MethodMapping {
}
`
-const routerTmpl = `package {{.GoPkgName}}
+const routerTmpl = `// Code generated by assets generator. DO NOT EDIT.
+
+package {{.GoPkgName}}
import (
"github.com/gorilla/mux"
diff --git a/internal/api/authz/context_mock.go b/internal/api/authz/context_mock.go
index e7b5c4017..6badf1586 100644
--- a/internal/api/authz/context_mock.go
+++ b/internal/api/authz/context_mock.go
@@ -4,11 +4,11 @@ import "context"
func NewMockContext(instanceID, orgID, userID string) context.Context {
ctx := context.WithValue(context.Background(), dataKey, CtxData{UserID: userID, OrgID: orgID})
- return context.WithValue(ctx, instanceKey, instanceID)
+ return context.WithValue(ctx, instanceKey, &instance{id: instanceID})
}
func NewMockContextWithPermissions(instanceID, orgID, userID string, permissions []string) context.Context {
ctx := context.WithValue(context.Background(), dataKey, CtxData{UserID: userID, OrgID: orgID})
- ctx = context.WithValue(ctx, instanceKey, instanceID)
+ ctx = context.WithValue(ctx, instanceKey, &instance{id: instanceID})
return context.WithValue(ctx, requestPermissionsKey, permissions)
}
diff --git a/internal/api/authz/token.go b/internal/api/authz/token.go
index b56d042da..615543093 100644
--- a/internal/api/authz/token.go
+++ b/internal/api/authz/token.go
@@ -20,7 +20,8 @@ import (
const (
BearerPrefix = "Bearer "
- SessionTokenFormat = "sess_%s:%s"
+ SessionTokenPrefix = "sess_"
+ SessionTokenFormat = SessionTokenPrefix + "%s:%s"
)
type TokenVerifier struct {
diff --git a/internal/api/call/duration.go b/internal/api/call/duration.go
index 24573aa36..b3334de95 100644
--- a/internal/api/call/duration.go
+++ b/internal/api/call/duration.go
@@ -9,12 +9,18 @@ type durationKey struct{}
var key *durationKey = (*durationKey)(nil)
-// WithTimestamp sets [time.Now()] adds the call field to the context
-// if it's not already set
+// WithTimestamp sets [time.Now()] to the call field in the context
+// if it's not already set.
func WithTimestamp(parent context.Context) context.Context {
if parent.Value(key) != nil {
return parent
}
+ return ResetTimestamp(parent)
+}
+
+// ResetTimestamp sets [time.Now()] to the call field in the context,
+// overwriting any previously set call timestamp.
+func ResetTimestamp(parent context.Context) context.Context {
return context.WithValue(parent, key, time.Now())
}
diff --git a/internal/api/grpc/admin/export.go b/internal/api/grpc/admin/export.go
index bd5387364..678b81048 100644
--- a/internal/api/grpc/admin/export.go
+++ b/internal/api/grpc/admin/export.go
@@ -430,6 +430,7 @@ func (s *Server) getLoginPolicy(ctx context.Context, orgID string, orgIDPs []str
AllowRegister: queriedLogin.AllowRegister,
AllowExternalIdp: queriedLogin.AllowExternalIDPs,
ForceMfa: queriedLogin.ForceMFA,
+ ForceMfaLocalOnly: queriedLogin.ForceMFALocalOnly,
PasswordlessType: policy_pb.PasswordlessType(queriedLogin.PasswordlessType),
HidePasswordReset: queriedLogin.HidePasswordReset,
IgnoreUnknownUsernames: queriedLogin.IgnoreUnknownUsernames,
@@ -577,15 +578,14 @@ func (s *Server) getUsers(ctx context.Context, org string, withPasswords bool, w
}
if withPasswords {
ctx, pwspan := tracing.NewSpan(ctx)
- hashedPassword, hashAlgorithm, err := s.query.GetHumanPassword(ctx, org, user.ID)
+ encodedHash, err := s.query.GetHumanPassword(ctx, org, user.ID)
pwspan.EndWithError(err)
if err != nil && !caos_errors.IsNotFound(err) {
return nil, nil, nil, nil, err
}
- if err == nil && hashedPassword != nil {
+ if err == nil && encodedHash != "" {
dataUser.User.HashedPassword = &management_pb.ImportHumanUserRequest_HashedPassword{
- Value: string(hashedPassword),
- Algorithm: hashAlgorithm,
+ Value: encodedHash,
}
}
}
diff --git a/internal/api/grpc/admin/iam_settings.go b/internal/api/grpc/admin/iam_settings.go
index cfbf28563..a4be248ee 100644
--- a/internal/api/grpc/admin/iam_settings.go
+++ b/internal/api/grpc/admin/iam_settings.go
@@ -18,6 +18,7 @@ func (s *Server) ListSecretGenerators(ctx context.Context, req *admin_pb.ListSec
return nil, err
}
return &admin_pb.ListSecretGeneratorsResponse{
+ Result: SecretGeneratorsToPb(result.SecretGenerators),
Details: object.ToListDetails(result.Count, result.Sequence, result.Timestamp),
}, nil
}
diff --git a/internal/api/grpc/admin/iam_settings_converter.go b/internal/api/grpc/admin/iam_settings_converter.go
index c93f8fbf4..7dfd1a381 100644
--- a/internal/api/grpc/admin/iam_settings_converter.go
+++ b/internal/api/grpc/admin/iam_settings_converter.go
@@ -62,6 +62,14 @@ func UpdateSecretGeneratorToConfig(req *admin_pb.UpdateSecretGeneratorRequest) *
}
}
+func SecretGeneratorsToPb(generators []*query.SecretGenerator) []*settings_pb.SecretGenerator {
+ list := make([]*settings_pb.SecretGenerator, len(generators))
+ for i, generator := range generators {
+ list[i] = SecretGeneratorToPb(generator)
+ }
+ return list
+}
+
func SecretGeneratorToPb(generator *query.SecretGenerator) *settings_pb.SecretGenerator {
mapped := &settings_pb.SecretGenerator{
Length: uint32(generator.Length),
diff --git a/internal/api/grpc/admin/login_policy_converter.go b/internal/api/grpc/admin/login_policy_converter.go
index 24fee2d00..68c1f879d 100644
--- a/internal/api/grpc/admin/login_policy_converter.go
+++ b/internal/api/grpc/admin/login_policy_converter.go
@@ -14,6 +14,7 @@ func updateLoginPolicyToCommand(p *admin_pb.UpdateLoginPolicyRequest) *command.C
AllowRegister: p.AllowRegister,
AllowExternalIDP: p.AllowExternalIdp,
ForceMFA: p.ForceMfa,
+ ForceMFALocalOnly: p.ForceMfaLocalOnly,
PasswordlessType: policy_grpc.PasswordlessTypeToDomain(p.PasswordlessType),
HidePasswordReset: p.HidePasswordReset,
IgnoreUnknownUsernames: p.IgnoreUnknownUsernames,
diff --git a/internal/api/grpc/management/policy_label_converter.go b/internal/api/grpc/management/policy_label_converter.go
index 3ad0b3329..9c399cfe3 100644
--- a/internal/api/grpc/management/policy_label_converter.go
+++ b/internal/api/grpc/management/policy_label_converter.go
@@ -10,9 +10,11 @@ func AddLabelPolicyToDomain(p *mgmt_pb.AddCustomLabelPolicyRequest) *domain.Labe
PrimaryColor: p.PrimaryColor,
BackgroundColor: p.BackgroundColor,
WarnColor: p.WarnColor,
+ FontColor: p.FontColor,
PrimaryColorDark: p.PrimaryColorDark,
BackgroundColorDark: p.BackgroundColorDark,
WarnColorDark: p.WarnColorDark,
+ FontColorDark: p.FontColorDark,
HideLoginNameSuffix: p.HideLoginNameSuffix,
DisableWatermark: p.DisableWatermark,
}
diff --git a/internal/api/grpc/management/policy_login_converter.go b/internal/api/grpc/management/policy_login_converter.go
index 2e41d1c8f..0e000e427 100644
--- a/internal/api/grpc/management/policy_login_converter.go
+++ b/internal/api/grpc/management/policy_login_converter.go
@@ -15,6 +15,7 @@ func AddLoginPolicyToCommand(p *mgmt_pb.AddCustomLoginPolicyRequest) *command.Ad
AllowRegister: p.AllowRegister,
AllowExternalIDP: p.AllowExternalIdp,
ForceMFA: p.ForceMfa,
+ ForceMFALocalOnly: p.ForceMfaLocalOnly,
PasswordlessType: policy_grpc.PasswordlessTypeToDomain(p.PasswordlessType),
HidePasswordReset: p.HidePasswordReset,
IgnoreUnknownUsernames: p.IgnoreUnknownUsernames,
@@ -49,6 +50,7 @@ func updateLoginPolicyToCommand(p *mgmt_pb.UpdateCustomLoginPolicyRequest) *comm
AllowRegister: p.AllowRegister,
AllowExternalIDP: p.AllowExternalIdp,
ForceMFA: p.ForceMfa,
+ ForceMFALocalOnly: p.ForceMfaLocalOnly,
PasswordlessType: policy_grpc.PasswordlessTypeToDomain(p.PasswordlessType),
HidePasswordReset: p.HidePasswordReset,
IgnoreUnknownUsernames: p.IgnoreUnknownUsernames,
diff --git a/internal/api/grpc/management/user.go b/internal/api/grpc/management/user.go
index 7b1b109ed..5375ce5fb 100644
--- a/internal/api/grpc/management/user.go
+++ b/internal/api/grpc/management/user.go
@@ -854,7 +854,7 @@ func (s *Server) ListPersonalAccessTokens(ctx context.Context, req *mgmt_pb.List
}
func (s *Server) AddPersonalAccessToken(ctx context.Context, req *mgmt_pb.AddPersonalAccessTokenRequest) (*mgmt_pb.AddPersonalAccessTokenResponse, error) {
- scopes := []string{oidc.ScopeOpenID, z_oidc.ScopeUserMetaData, z_oidc.ScopeResourceOwner}
+ scopes := []string{oidc.ScopeOpenID, oidc.ScopeProfile, z_oidc.ScopeUserMetaData, z_oidc.ScopeResourceOwner}
pat := AddPersonalAccessTokenRequestToCommand(req, authz.GetCtxData(ctx).OrgID, scopes, domain.UserTypeMachine)
details, err := s.command.AddPersonalAccessToken(ctx, pat)
if err != nil {
diff --git a/internal/api/grpc/management/user_converter.go b/internal/api/grpc/management/user_converter.go
index 3c969c2a1..a3f7760b0 100644
--- a/internal/api/grpc/management/user_converter.go
+++ b/internal/api/grpc/management/user_converter.go
@@ -121,9 +121,7 @@ func ImportHumanUserRequestToDomain(req *mgmt_pb.ImportHumanUserRequest) (human
human.Password.ChangeRequired = req.PasswordChangeRequired
}
- if req.HashedPassword != nil && req.HashedPassword.Value != "" && req.HashedPassword.Algorithm != "" {
- human.HashedPassword = domain.NewHashedPassword(req.HashedPassword.Value, req.HashedPassword.Algorithm)
- }
+ human.HashedPassword = req.GetHashedPassword().GetValue()
links = make([]*domain.UserIDPLink, len(req.Idps))
for i, idp := range req.Idps {
links[i] = &domain.UserIDPLink{
diff --git a/internal/api/grpc/management/user_integration_test.go b/internal/api/grpc/management/user_integration_test.go
new file mode 100644
index 000000000..c1682a5cc
--- /dev/null
+++ b/internal/api/grpc/management/user_integration_test.go
@@ -0,0 +1,87 @@
+//go:build integration
+
+package management_test
+
+import (
+ "context"
+ "os"
+ "testing"
+ "time"
+
+ "github.com/zitadel/zitadel/internal/integration"
+ "github.com/zitadel/zitadel/pkg/grpc/management"
+)
+
+var (
+ CTX context.Context
+ Tester *integration.Tester
+ Client management.ManagementServiceClient
+)
+
+func TestMain(m *testing.M) {
+ os.Exit(func() int {
+ ctx, errCtx, cancel := integration.Contexts(3 * time.Minute)
+ defer cancel()
+
+ Tester = integration.NewTester(ctx)
+ defer Tester.Done()
+
+ CTX, _ = Tester.WithAuthorization(ctx, integration.OrgOwner), errCtx
+ Client = Tester.Client.Mgmt
+ return m.Run()
+ }())
+}
+
+// TestImport_and_Get reproduces https://github.com/zitadel/zitadel/issues/5808
+// which led to consistency issues due the call timestamp not being
+// updated after a bulk Trigger.
+// This test Imports a user and directly tries to Get it, 100 times in a loop.
+// When the bug still existed, some (between 1 to 7 out of 100)
+// Get calls would return a Not Found error.
+
+/* Test disabled because it breaks the pipeline.
+func TestImport_and_Get(t *testing.T) {
+ const N = 100
+ var misses int
+
+ for i := 0; i < N; i++ {
+ firstName := strconv.Itoa(i)
+ t.Run(firstName, func(t *testing.T) {
+ // create unique names.
+ lastName := strconv.FormatInt(time.Now().Unix(), 10)
+ userName := strings.Join([]string{firstName, lastName}, "_")
+ email := strings.Join([]string{userName, "zitadel.com"}, "@")
+
+ res, err := Client.ImportHumanUser(CTX, &management.ImportHumanUserRequest{
+ UserName: userName,
+ Profile: &management.ImportHumanUserRequest_Profile{
+ FirstName: firstName,
+ LastName: lastName,
+ PreferredLanguage: language.Afrikaans.String(),
+ Gender: user.Gender_GENDER_DIVERSE,
+ },
+ Email: &management.ImportHumanUserRequest_Email{
+ Email: email,
+ IsEmailVerified: true,
+ },
+ })
+ require.NoError(t, err)
+
+ _, err = Client.GetUserByID(CTX, &management.GetUserByIDRequest{Id: res.GetUserId()})
+
+ if s, ok := status.FromError(err); ok {
+ if s == nil {
+ return
+ }
+ if s.Code() == codes.NotFound {
+ t.Log(s)
+ misses++
+ return
+ }
+ }
+ require.NoError(t, err) // catch and fail on any other error
+ })
+ }
+ assert.Zerof(t, misses, "Not Found errors %d out of %d", misses, N)
+}
+*/
diff --git a/internal/api/grpc/metadata/metadata.go b/internal/api/grpc/metadata/metadata.go
index 731dda98a..e54007e09 100644
--- a/internal/api/grpc/metadata/metadata.go
+++ b/internal/api/grpc/metadata/metadata.go
@@ -70,5 +70,5 @@ func MetadataQueryToQuery(query *meta_pb.MetadataQuery) (query.SearchQuery, erro
}
func MetadataKeyQueryToQuery(q *meta_pb.MetadataKeyQuery) (query.SearchQuery, error) {
- return query.NewUserMetadataKeySearchQuery(q.Key, object.TextMethodToQuery(q.Method))
+ return query.NewOrgMetadataKeySearchQuery(q.Key, object.TextMethodToQuery(q.Method))
}
diff --git a/internal/api/grpc/oidc/v2/oidc.go b/internal/api/grpc/oidc/v2/oidc.go
new file mode 100644
index 000000000..4675dfc6f
--- /dev/null
+++ b/internal/api/grpc/oidc/v2/oidc.go
@@ -0,0 +1,204 @@
+package oidc
+
+import (
+ "context"
+
+ "github.com/zitadel/logging"
+ "github.com/zitadel/oidc/v2/pkg/op"
+ "google.golang.org/protobuf/types/known/durationpb"
+ "google.golang.org/protobuf/types/known/timestamppb"
+
+ "github.com/zitadel/zitadel/internal/api/authz"
+ "github.com/zitadel/zitadel/internal/api/grpc/object/v2"
+ "github.com/zitadel/zitadel/internal/api/http"
+ "github.com/zitadel/zitadel/internal/api/oidc"
+ "github.com/zitadel/zitadel/internal/domain"
+ "github.com/zitadel/zitadel/internal/errors"
+ "github.com/zitadel/zitadel/internal/query"
+ oidc_pb "github.com/zitadel/zitadel/pkg/grpc/oidc/v2alpha"
+)
+
+func (s *Server) GetAuthRequest(ctx context.Context, req *oidc_pb.GetAuthRequestRequest) (*oidc_pb.GetAuthRequestResponse, error) {
+ authRequest, err := s.query.AuthRequestByID(ctx, true, req.GetAuthRequestId(), true)
+ if err != nil {
+ logging.WithError(err).Error("query authRequest by ID")
+ return nil, err
+ }
+ return &oidc_pb.GetAuthRequestResponse{
+ AuthRequest: authRequestToPb(authRequest),
+ }, nil
+}
+
+func authRequestToPb(a *query.AuthRequest) *oidc_pb.AuthRequest {
+ pba := &oidc_pb.AuthRequest{
+ Id: a.ID,
+ CreationDate: timestamppb.New(a.CreationDate),
+ ClientId: a.ClientID,
+ Scope: a.Scope,
+ RedirectUri: a.RedirectURI,
+ Prompt: promptsToPb(a.Prompt),
+ UiLocales: a.UiLocales,
+ LoginHint: a.LoginHint,
+ HintUserId: a.HintUserID,
+ }
+ if a.MaxAge != nil {
+ pba.MaxAge = durationpb.New(*a.MaxAge)
+ }
+ return pba
+}
+
+func promptsToPb(promps []domain.Prompt) []oidc_pb.Prompt {
+ out := make([]oidc_pb.Prompt, len(promps))
+ for i, p := range promps {
+ out[i] = promptToPb(p)
+ }
+ return out
+}
+
+func promptToPb(p domain.Prompt) oidc_pb.Prompt {
+ switch p {
+ case domain.PromptUnspecified:
+ return oidc_pb.Prompt_PROMPT_UNSPECIFIED
+ case domain.PromptNone:
+ return oidc_pb.Prompt_PROMPT_NONE
+ case domain.PromptLogin:
+ return oidc_pb.Prompt_PROMPT_LOGIN
+ case domain.PromptConsent:
+ return oidc_pb.Prompt_PROMPT_CONSENT
+ case domain.PromptSelectAccount:
+ return oidc_pb.Prompt_PROMPT_SELECT_ACCOUNT
+ case domain.PromptCreate:
+ return oidc_pb.Prompt_PROMPT_CREATE
+ default:
+ return oidc_pb.Prompt_PROMPT_UNSPECIFIED
+ }
+}
+
+func (s *Server) CreateCallback(ctx context.Context, req *oidc_pb.CreateCallbackRequest) (*oidc_pb.CreateCallbackResponse, error) {
+ switch v := req.GetCallbackKind().(type) {
+ case *oidc_pb.CreateCallbackRequest_Error:
+ return s.failAuthRequest(ctx, req.GetAuthRequestId(), v.Error)
+ case *oidc_pb.CreateCallbackRequest_Session:
+ return s.linkSessionToAuthRequest(ctx, req.GetAuthRequestId(), v.Session)
+ default:
+ return nil, errors.ThrowUnimplementedf(nil, "OIDCv2-zee7A", "verification oneOf %T in method CreateCallback not implemented", v)
+ }
+}
+
+func (s *Server) failAuthRequest(ctx context.Context, authRequestID string, ae *oidc_pb.AuthorizationError) (*oidc_pb.CreateCallbackResponse, error) {
+ details, aar, err := s.command.FailAuthRequest(ctx, authRequestID, errorReasonToDomain(ae.GetError()))
+ if err != nil {
+ return nil, err
+ }
+ authReq := &oidc.AuthRequestV2{CurrentAuthRequest: aar}
+ callback, err := oidc.CreateErrorCallbackURL(authReq, errorReasonToOIDC(ae.GetError()), ae.GetErrorDescription(), ae.GetErrorUri(), s.op)
+ if err != nil {
+ return nil, err
+ }
+ return &oidc_pb.CreateCallbackResponse{
+ Details: object.DomainToDetailsPb(details),
+ CallbackUrl: callback,
+ }, nil
+}
+
+func (s *Server) linkSessionToAuthRequest(ctx context.Context, authRequestID string, session *oidc_pb.Session) (*oidc_pb.CreateCallbackResponse, error) {
+ details, aar, err := s.command.LinkSessionToAuthRequest(ctx, authRequestID, session.GetSessionId(), session.GetSessionToken(), true)
+ if err != nil {
+ return nil, err
+ }
+ authReq := &oidc.AuthRequestV2{CurrentAuthRequest: aar}
+ ctx = op.ContextWithIssuer(ctx, http.BuildOrigin(authz.GetInstance(ctx).RequestedHost(), s.externalSecure))
+ var callback string
+ if aar.ResponseType == domain.OIDCResponseTypeCode {
+ callback, err = oidc.CreateCodeCallbackURL(ctx, authReq, s.op)
+ } else {
+ callback, err = oidc.CreateTokenCallbackURL(ctx, authReq, s.op)
+ }
+ if err != nil {
+ return nil, err
+ }
+ return &oidc_pb.CreateCallbackResponse{
+ Details: object.DomainToDetailsPb(details),
+ CallbackUrl: callback,
+ }, nil
+}
+
+func errorReasonToDomain(errorReason oidc_pb.ErrorReason) domain.OIDCErrorReason {
+ switch errorReason {
+ case oidc_pb.ErrorReason_ERROR_REASON_UNSPECIFIED:
+ return domain.OIDCErrorReasonUnspecified
+ case oidc_pb.ErrorReason_ERROR_REASON_INVALID_REQUEST:
+ return domain.OIDCErrorReasonInvalidRequest
+ case oidc_pb.ErrorReason_ERROR_REASON_UNAUTHORIZED_CLIENT:
+ return domain.OIDCErrorReasonUnauthorizedClient
+ case oidc_pb.ErrorReason_ERROR_REASON_ACCESS_DENIED:
+ return domain.OIDCErrorReasonAccessDenied
+ case oidc_pb.ErrorReason_ERROR_REASON_UNSUPPORTED_RESPONSE_TYPE:
+ return domain.OIDCErrorReasonUnsupportedResponseType
+ case oidc_pb.ErrorReason_ERROR_REASON_INVALID_SCOPE:
+ return domain.OIDCErrorReasonInvalidScope
+ case oidc_pb.ErrorReason_ERROR_REASON_SERVER_ERROR:
+ return domain.OIDCErrorReasonServerError
+ case oidc_pb.ErrorReason_ERROR_REASON_TEMPORARY_UNAVAILABLE:
+ return domain.OIDCErrorReasonTemporaryUnavailable
+ case oidc_pb.ErrorReason_ERROR_REASON_INTERACTION_REQUIRED:
+ return domain.OIDCErrorReasonInteractionRequired
+ case oidc_pb.ErrorReason_ERROR_REASON_LOGIN_REQUIRED:
+ return domain.OIDCErrorReasonLoginRequired
+ case oidc_pb.ErrorReason_ERROR_REASON_ACCOUNT_SELECTION_REQUIRED:
+ return domain.OIDCErrorReasonAccountSelectionRequired
+ case oidc_pb.ErrorReason_ERROR_REASON_CONSENT_REQUIRED:
+ return domain.OIDCErrorReasonConsentRequired
+ case oidc_pb.ErrorReason_ERROR_REASON_INVALID_REQUEST_URI:
+ return domain.OIDCErrorReasonInvalidRequestURI
+ case oidc_pb.ErrorReason_ERROR_REASON_INVALID_REQUEST_OBJECT:
+ return domain.OIDCErrorReasonInvalidRequestObject
+ case oidc_pb.ErrorReason_ERROR_REASON_REQUEST_NOT_SUPPORTED:
+ return domain.OIDCErrorReasonRequestNotSupported
+ case oidc_pb.ErrorReason_ERROR_REASON_REQUEST_URI_NOT_SUPPORTED:
+ return domain.OIDCErrorReasonRequestURINotSupported
+ case oidc_pb.ErrorReason_ERROR_REASON_REGISTRATION_NOT_SUPPORTED:
+ return domain.OIDCErrorReasonRegistrationNotSupported
+ default:
+ return domain.OIDCErrorReasonUnspecified
+ }
+}
+
+func errorReasonToOIDC(reason oidc_pb.ErrorReason) string {
+ switch reason {
+ case oidc_pb.ErrorReason_ERROR_REASON_INVALID_REQUEST:
+ return "invalid_request"
+ case oidc_pb.ErrorReason_ERROR_REASON_UNAUTHORIZED_CLIENT:
+ return "unauthorized_client"
+ case oidc_pb.ErrorReason_ERROR_REASON_ACCESS_DENIED:
+ return "access_denied"
+ case oidc_pb.ErrorReason_ERROR_REASON_UNSUPPORTED_RESPONSE_TYPE:
+ return "unsupported_response_type"
+ case oidc_pb.ErrorReason_ERROR_REASON_INVALID_SCOPE:
+ return "invalid_scope"
+ case oidc_pb.ErrorReason_ERROR_REASON_TEMPORARY_UNAVAILABLE:
+ return "temporarily_unavailable"
+ case oidc_pb.ErrorReason_ERROR_REASON_INTERACTION_REQUIRED:
+ return "interaction_required"
+ case oidc_pb.ErrorReason_ERROR_REASON_LOGIN_REQUIRED:
+ return "login_required"
+ case oidc_pb.ErrorReason_ERROR_REASON_ACCOUNT_SELECTION_REQUIRED:
+ return "account_selection_required"
+ case oidc_pb.ErrorReason_ERROR_REASON_CONSENT_REQUIRED:
+ return "consent_required"
+ case oidc_pb.ErrorReason_ERROR_REASON_INVALID_REQUEST_URI:
+ return "invalid_request_uri"
+ case oidc_pb.ErrorReason_ERROR_REASON_INVALID_REQUEST_OBJECT:
+ return "invalid_request_object"
+ case oidc_pb.ErrorReason_ERROR_REASON_REQUEST_NOT_SUPPORTED:
+ return "request_not_supported"
+ case oidc_pb.ErrorReason_ERROR_REASON_REQUEST_URI_NOT_SUPPORTED:
+ return "request_uri_not_supported"
+ case oidc_pb.ErrorReason_ERROR_REASON_REGISTRATION_NOT_SUPPORTED:
+ return "registration_not_supported"
+ case oidc_pb.ErrorReason_ERROR_REASON_UNSPECIFIED, oidc_pb.ErrorReason_ERROR_REASON_SERVER_ERROR:
+ fallthrough
+ default:
+ return "server_error"
+ }
+}
diff --git a/internal/api/grpc/oidc/v2/oidc_integration_test.go b/internal/api/grpc/oidc/v2/oidc_integration_test.go
new file mode 100644
index 000000000..b48bfc083
--- /dev/null
+++ b/internal/api/grpc/oidc/v2/oidc_integration_test.go
@@ -0,0 +1,254 @@
+//go:build integration
+
+package oidc_test
+
+import (
+ "context"
+ "net/url"
+ "os"
+ "regexp"
+ "testing"
+ "time"
+
+ "github.com/muhlemmer/gu"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+
+ "github.com/zitadel/zitadel/internal/integration"
+ object "github.com/zitadel/zitadel/pkg/grpc/object/v2alpha"
+ oidc_pb "github.com/zitadel/zitadel/pkg/grpc/oidc/v2alpha"
+ session "github.com/zitadel/zitadel/pkg/grpc/session/v2alpha"
+ user "github.com/zitadel/zitadel/pkg/grpc/user/v2alpha"
+)
+
+var (
+ CTX context.Context
+ Tester *integration.Tester
+ Client oidc_pb.OIDCServiceClient
+ User *user.AddHumanUserResponse
+)
+
+const (
+ redirectURI = "oidcintegrationtest://callback"
+ redirectURIImplicit = "http://localhost:9999/callback"
+ logoutRedirectURI = "oidcintegrationtest://logged-out"
+)
+
+func TestMain(m *testing.M) {
+ os.Exit(func() int {
+ ctx, _, cancel := integration.Contexts(5 * time.Minute)
+ defer cancel()
+
+ Tester = integration.NewTester(ctx)
+ defer Tester.Done()
+ Client = Tester.Client.OIDCv2
+
+ CTX = Tester.WithAuthorization(ctx, integration.OrgOwner)
+ User = Tester.CreateHumanUser(CTX)
+ return m.Run()
+ }())
+}
+
+func TestServer_GetAuthRequest(t *testing.T) {
+ project, err := Tester.CreateProject(CTX)
+ require.NoError(t, err)
+ client, err := Tester.CreateOIDCNativeClient(CTX, redirectURI, logoutRedirectURI, project.GetId())
+ require.NoError(t, err)
+ authRequestID, err := Tester.CreateOIDCAuthRequest(client.GetClientId(), Tester.Users[integration.FirstInstanceUsersKey][integration.OrgOwner].ID, redirectURI)
+ require.NoError(t, err)
+ now := time.Now()
+
+ tests := []struct {
+ name string
+ AuthRequestID string
+ want *oidc_pb.GetAuthRequestResponse
+ wantErr bool
+ }{
+ {
+ name: "Not found",
+ AuthRequestID: "123",
+ wantErr: true,
+ },
+ {
+ name: "success",
+ AuthRequestID: authRequestID,
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ got, err := Client.GetAuthRequest(CTX, &oidc_pb.GetAuthRequestRequest{
+ AuthRequestId: tt.AuthRequestID,
+ })
+ if tt.wantErr {
+ require.Error(t, err)
+ return
+ }
+ require.NoError(t, err)
+ authRequest := got.GetAuthRequest()
+ assert.NotNil(t, authRequest)
+ assert.Equal(t, authRequestID, authRequest.GetId())
+ assert.WithinRange(t, authRequest.GetCreationDate().AsTime(), now.Add(-time.Second), now.Add(time.Second))
+ assert.Contains(t, authRequest.GetScope(), "openid")
+ })
+ }
+}
+
+func TestServer_CreateCallback(t *testing.T) {
+ project, err := Tester.CreateProject(CTX)
+ require.NoError(t, err)
+ client, err := Tester.CreateOIDCNativeClient(CTX, redirectURI, logoutRedirectURI, project.GetId())
+ require.NoError(t, err)
+ sessionResp, err := Tester.Client.SessionV2.CreateSession(CTX, &session.CreateSessionRequest{
+ Checks: &session.Checks{
+ User: &session.CheckUser{
+ Search: &session.CheckUser_UserId{
+ UserId: Tester.Users[integration.FirstInstanceUsersKey][integration.OrgOwner].ID,
+ },
+ },
+ },
+ })
+ require.NoError(t, err)
+
+ tests := []struct {
+ name string
+ req *oidc_pb.CreateCallbackRequest
+ AuthError string
+ want *oidc_pb.CreateCallbackResponse
+ wantURL *url.URL
+ wantErr bool
+ }{
+ {
+ name: "Not found",
+ req: &oidc_pb.CreateCallbackRequest{
+ AuthRequestId: "123",
+ CallbackKind: &oidc_pb.CreateCallbackRequest_Session{
+ Session: &oidc_pb.Session{
+ SessionId: sessionResp.GetSessionId(),
+ SessionToken: sessionResp.GetSessionToken(),
+ },
+ },
+ },
+ wantErr: true,
+ },
+ {
+ name: "session not found",
+ req: &oidc_pb.CreateCallbackRequest{
+ AuthRequestId: func() string {
+ authRequestID, err := Tester.CreateOIDCAuthRequest(client.GetClientId(), Tester.Users[integration.FirstInstanceUsersKey][integration.OrgOwner].ID, redirectURI)
+ require.NoError(t, err)
+ return authRequestID
+ }(),
+ CallbackKind: &oidc_pb.CreateCallbackRequest_Session{
+ Session: &oidc_pb.Session{
+ SessionId: "foo",
+ SessionToken: "bar",
+ },
+ },
+ },
+ wantErr: true,
+ },
+ {
+ name: "session token invalid",
+ req: &oidc_pb.CreateCallbackRequest{
+ AuthRequestId: func() string {
+ authRequestID, err := Tester.CreateOIDCAuthRequest(client.GetClientId(), Tester.Users[integration.FirstInstanceUsersKey][integration.OrgOwner].ID, redirectURI)
+ require.NoError(t, err)
+ return authRequestID
+ }(),
+ CallbackKind: &oidc_pb.CreateCallbackRequest_Session{
+ Session: &oidc_pb.Session{
+ SessionId: sessionResp.GetSessionId(),
+ SessionToken: "bar",
+ },
+ },
+ },
+ wantErr: true,
+ },
+ {
+ name: "fail callback",
+ req: &oidc_pb.CreateCallbackRequest{
+ AuthRequestId: func() string {
+ authRequestID, err := Tester.CreateOIDCAuthRequest(client.GetClientId(), Tester.Users[integration.FirstInstanceUsersKey][integration.OrgOwner].ID, redirectURI)
+ require.NoError(t, err)
+ return authRequestID
+ }(),
+ CallbackKind: &oidc_pb.CreateCallbackRequest_Error{
+ Error: &oidc_pb.AuthorizationError{
+ Error: oidc_pb.ErrorReason_ERROR_REASON_ACCESS_DENIED,
+ ErrorDescription: gu.Ptr("nope"),
+ ErrorUri: gu.Ptr("https://example.com/docs"),
+ },
+ },
+ },
+ want: &oidc_pb.CreateCallbackResponse{
+ CallbackUrl: regexp.QuoteMeta(`oidcintegrationtest://callback?error=access_denied&error_description=nope&error_uri=https%3A%2F%2Fexample.com%2Fdocs&state=state`),
+ Details: &object.Details{
+ ResourceOwner: Tester.Instance.InstanceID(),
+ },
+ },
+ wantErr: false,
+ },
+ {
+ name: "code callback",
+ req: &oidc_pb.CreateCallbackRequest{
+ AuthRequestId: func() string {
+ authRequestID, err := Tester.CreateOIDCAuthRequest(client.GetClientId(), Tester.Users[integration.FirstInstanceUsersKey][integration.OrgOwner].ID, redirectURI)
+ require.NoError(t, err)
+ return authRequestID
+ }(),
+ CallbackKind: &oidc_pb.CreateCallbackRequest_Session{
+ Session: &oidc_pb.Session{
+ SessionId: sessionResp.GetSessionId(),
+ SessionToken: sessionResp.GetSessionToken(),
+ },
+ },
+ },
+ want: &oidc_pb.CreateCallbackResponse{
+ CallbackUrl: `oidcintegrationtest:\/\/callback\?code=(.*)&state=state`,
+ Details: &object.Details{
+ ResourceOwner: Tester.Instance.InstanceID(),
+ },
+ },
+ wantErr: false,
+ },
+ {
+ name: "implicit",
+ req: &oidc_pb.CreateCallbackRequest{
+ AuthRequestId: func() string {
+ client, err := Tester.CreateOIDCImplicitFlowClient(CTX, redirectURIImplicit)
+ require.NoError(t, err)
+ authRequestID, err := Tester.CreateOIDCAuthRequestImplicit(client.GetClientId(), Tester.Users[integration.FirstInstanceUsersKey][integration.OrgOwner].ID, redirectURIImplicit)
+ require.NoError(t, err)
+ return authRequestID
+ }(),
+ CallbackKind: &oidc_pb.CreateCallbackRequest_Session{
+ Session: &oidc_pb.Session{
+ SessionId: sessionResp.GetSessionId(),
+ SessionToken: sessionResp.GetSessionToken(),
+ },
+ },
+ },
+ want: &oidc_pb.CreateCallbackResponse{
+ CallbackUrl: `http:\/\/localhost:9999\/callback#access_token=(.*)&expires_in=(.*)&id_token=(.*)&state=state&token_type=Bearer`,
+ Details: &object.Details{
+ ResourceOwner: Tester.Instance.InstanceID(),
+ },
+ },
+ wantErr: false,
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ got, err := Client.CreateCallback(CTX, tt.req)
+ if tt.wantErr {
+ require.Error(t, err)
+ return
+ }
+ require.NoError(t, err)
+ integration.AssertDetails(t, tt.want, got)
+ if tt.want != nil {
+ assert.Regexp(t, regexp.MustCompile(tt.want.CallbackUrl), got.GetCallbackUrl())
+ }
+ })
+ }
+}
diff --git a/internal/api/grpc/oidc/v2/oidc_test.go b/internal/api/grpc/oidc/v2/oidc_test.go
new file mode 100644
index 000000000..91d795a00
--- /dev/null
+++ b/internal/api/grpc/oidc/v2/oidc_test.go
@@ -0,0 +1,150 @@
+package oidc
+
+import (
+ "testing"
+ "time"
+
+ "github.com/muhlemmer/gu"
+ "github.com/stretchr/testify/assert"
+ "google.golang.org/protobuf/proto"
+ "google.golang.org/protobuf/types/known/durationpb"
+ "google.golang.org/protobuf/types/known/timestamppb"
+
+ "github.com/zitadel/zitadel/internal/domain"
+ "github.com/zitadel/zitadel/internal/query"
+ oidc_pb "github.com/zitadel/zitadel/pkg/grpc/oidc/v2alpha"
+)
+
+func Test_authRequestToPb(t *testing.T) {
+ now := time.Now()
+ arg := &query.AuthRequest{
+ ID: "authID",
+ CreationDate: now,
+ ClientID: "clientID",
+ Scope: []string{"a", "b", "c"},
+ RedirectURI: "callbackURI",
+ Prompt: []domain.Prompt{
+ domain.PromptUnspecified,
+ domain.PromptNone,
+ domain.PromptLogin,
+ domain.PromptConsent,
+ domain.PromptSelectAccount,
+ domain.PromptCreate,
+ 999,
+ },
+ UiLocales: []string{"en", "fi"},
+ LoginHint: gu.Ptr("foo@bar.com"),
+ MaxAge: gu.Ptr(time.Minute),
+ HintUserID: gu.Ptr("userID"),
+ }
+ want := &oidc_pb.AuthRequest{
+ Id: "authID",
+ CreationDate: timestamppb.New(now),
+ ClientId: "clientID",
+ RedirectUri: "callbackURI",
+ Prompt: []oidc_pb.Prompt{
+ oidc_pb.Prompt_PROMPT_UNSPECIFIED,
+ oidc_pb.Prompt_PROMPT_NONE,
+ oidc_pb.Prompt_PROMPT_LOGIN,
+ oidc_pb.Prompt_PROMPT_CONSENT,
+ oidc_pb.Prompt_PROMPT_SELECT_ACCOUNT,
+ oidc_pb.Prompt_PROMPT_CREATE,
+ oidc_pb.Prompt_PROMPT_UNSPECIFIED,
+ },
+ UiLocales: []string{"en", "fi"},
+ Scope: []string{"a", "b", "c"},
+ LoginHint: gu.Ptr("foo@bar.com"),
+ MaxAge: durationpb.New(time.Minute),
+ HintUserId: gu.Ptr("userID"),
+ }
+ got := authRequestToPb(arg)
+ if !proto.Equal(want, got) {
+ t.Errorf("authRequestToPb() =\n%v\nwant\n%v\n", got, want)
+ }
+}
+
+func Test_errorReasonToOIDC(t *testing.T) {
+ tests := []struct {
+ reason oidc_pb.ErrorReason
+ want string
+ }{
+ {
+ reason: oidc_pb.ErrorReason_ERROR_REASON_UNSPECIFIED,
+ want: "server_error",
+ },
+ {
+ reason: oidc_pb.ErrorReason_ERROR_REASON_INVALID_REQUEST,
+ want: "invalid_request",
+ },
+ {
+ reason: oidc_pb.ErrorReason_ERROR_REASON_UNAUTHORIZED_CLIENT,
+ want: "unauthorized_client",
+ },
+ {
+ reason: oidc_pb.ErrorReason_ERROR_REASON_ACCESS_DENIED,
+ want: "access_denied",
+ },
+ {
+ reason: oidc_pb.ErrorReason_ERROR_REASON_UNSUPPORTED_RESPONSE_TYPE,
+ want: "unsupported_response_type",
+ },
+ {
+ reason: oidc_pb.ErrorReason_ERROR_REASON_INVALID_SCOPE,
+ want: "invalid_scope",
+ },
+ {
+ reason: oidc_pb.ErrorReason_ERROR_REASON_SERVER_ERROR,
+ want: "server_error",
+ },
+ {
+ reason: oidc_pb.ErrorReason_ERROR_REASON_TEMPORARY_UNAVAILABLE,
+ want: "temporarily_unavailable",
+ },
+ {
+ reason: oidc_pb.ErrorReason_ERROR_REASON_INTERACTION_REQUIRED,
+ want: "interaction_required",
+ },
+ {
+ reason: oidc_pb.ErrorReason_ERROR_REASON_LOGIN_REQUIRED,
+ want: "login_required",
+ },
+ {
+ reason: oidc_pb.ErrorReason_ERROR_REASON_ACCOUNT_SELECTION_REQUIRED,
+ want: "account_selection_required",
+ },
+ {
+ reason: oidc_pb.ErrorReason_ERROR_REASON_CONSENT_REQUIRED,
+ want: "consent_required",
+ },
+ {
+ reason: oidc_pb.ErrorReason_ERROR_REASON_INVALID_REQUEST_URI,
+ want: "invalid_request_uri",
+ },
+ {
+ reason: oidc_pb.ErrorReason_ERROR_REASON_INVALID_REQUEST_OBJECT,
+ want: "invalid_request_object",
+ },
+ {
+ reason: oidc_pb.ErrorReason_ERROR_REASON_REQUEST_NOT_SUPPORTED,
+ want: "request_not_supported",
+ },
+ {
+ reason: oidc_pb.ErrorReason_ERROR_REASON_REQUEST_URI_NOT_SUPPORTED,
+ want: "request_uri_not_supported",
+ },
+ {
+ reason: oidc_pb.ErrorReason_ERROR_REASON_REGISTRATION_NOT_SUPPORTED,
+ want: "registration_not_supported",
+ },
+ {
+ reason: 99999,
+ want: "server_error",
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.reason.String(), func(t *testing.T) {
+ got := errorReasonToOIDC(tt.reason)
+ assert.Equal(t, tt.want, got)
+ })
+ }
+}
diff --git a/internal/api/grpc/oidc/v2/server.go b/internal/api/grpc/oidc/v2/server.go
new file mode 100644
index 000000000..f15401d1b
--- /dev/null
+++ b/internal/api/grpc/oidc/v2/server.go
@@ -0,0 +1,59 @@
+package oidc
+
+import (
+ "github.com/zitadel/oidc/v2/pkg/op"
+ "google.golang.org/grpc"
+
+ "github.com/zitadel/zitadel/internal/api/authz"
+ "github.com/zitadel/zitadel/internal/api/grpc/server"
+ "github.com/zitadel/zitadel/internal/command"
+ "github.com/zitadel/zitadel/internal/query"
+ oidc_pb "github.com/zitadel/zitadel/pkg/grpc/oidc/v2alpha"
+)
+
+var _ oidc_pb.OIDCServiceServer = (*Server)(nil)
+
+type Server struct {
+ oidc_pb.UnimplementedOIDCServiceServer
+ command *command.Commands
+ query *query.Queries
+
+ op op.OpenIDProvider
+ externalSecure bool
+}
+
+type Config struct{}
+
+func CreateServer(
+ command *command.Commands,
+ query *query.Queries,
+ op op.OpenIDProvider,
+ externalSecure bool,
+) *Server {
+ return &Server{
+ command: command,
+ query: query,
+ op: op,
+ externalSecure: externalSecure,
+ }
+}
+
+func (s *Server) RegisterServer(grpcServer *grpc.Server) {
+ oidc_pb.RegisterOIDCServiceServer(grpcServer, s)
+}
+
+func (s *Server) AppName() string {
+ return oidc_pb.OIDCService_ServiceDesc.ServiceName
+}
+
+func (s *Server) MethodPrefix() string {
+ return oidc_pb.OIDCService_ServiceDesc.ServiceName
+}
+
+func (s *Server) AuthMethods() authz.MethodMapping {
+ return oidc_pb.OIDCService_AuthMethods
+}
+
+func (s *Server) RegisterGateway() server.RegisterGatewayFunc {
+ return oidc_pb.RegisterOIDCServiceHandler
+}
diff --git a/internal/api/grpc/policy/login_policy.go b/internal/api/grpc/policy/login_policy.go
index 3cd191a91..986fd9da2 100644
--- a/internal/api/grpc/policy/login_policy.go
+++ b/internal/api/grpc/policy/login_policy.go
@@ -18,6 +18,7 @@ func ModelLoginPolicyToPb(policy *query.LoginPolicy) *policy_pb.LoginPolicy {
AllowRegister: policy.AllowRegister,
AllowExternalIdp: policy.AllowExternalIDPs,
ForceMfa: policy.ForceMFA,
+ ForceMfaLocalOnly: policy.ForceMFALocalOnly,
PasswordlessType: ModelPasswordlessTypeToPb(policy.PasswordlessType),
HidePasswordReset: policy.HidePasswordReset,
IgnoreUnknownUsernames: policy.IgnoreUnknownUsernames,
diff --git a/internal/api/grpc/session/v2/session.go b/internal/api/grpc/session/v2/session.go
index 8ac29a488..e842476c2 100644
--- a/internal/api/grpc/session/v2/session.go
+++ b/internal/api/grpc/session/v2/session.go
@@ -16,7 +16,7 @@ import (
)
func (s *Server) GetSession(ctx context.Context, req *session.GetSessionRequest) (*session.GetSessionResponse, error) {
- res, err := s.query.SessionByID(ctx, req.GetSessionId(), req.GetSessionToken())
+ res, err := s.query.SessionByID(ctx, true, req.GetSessionId(), req.GetSessionToken())
if err != nil {
return nil, err
}
@@ -156,10 +156,11 @@ func userFactorToPb(factor query.SessionUserFactor) *session.UserFactor {
return nil
}
return &session.UserFactor{
- VerifiedAt: timestamppb.New(factor.UserCheckedAt),
- Id: factor.UserID,
- LoginName: factor.LoginName,
- DisplayName: factor.DisplayName,
+ VerifiedAt: timestamppb.New(factor.UserCheckedAt),
+ Id: factor.UserID,
+ LoginName: factor.LoginName,
+ DisplayName: factor.DisplayName,
+ OrganisationId: factor.ResourceOwner,
}
}
diff --git a/internal/api/grpc/session/v2/session_integration_test.go b/internal/api/grpc/session/v2/session_integration_test.go
index 6c3c2720d..949d78ded 100644
--- a/internal/api/grpc/session/v2/session_integration_test.go
+++ b/internal/api/grpc/session/v2/session_integration_test.go
@@ -4,14 +4,15 @@ package session_test
import (
"context"
+ "fmt"
"os"
"testing"
"time"
+ "github.com/muhlemmer/gu"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
- "google.golang.org/grpc/codes"
- "google.golang.org/grpc/status"
+ "google.golang.org/grpc/metadata"
"github.com/zitadel/zitadel/internal/integration"
object "github.com/zitadel/zitadel/pkg/grpc/object/v2alpha"
@@ -20,11 +21,10 @@ import (
)
var (
- CTX context.Context
- Tester *integration.Tester
- Client session.SessionServiceClient
- User *user.AddHumanUserResponse
- GenericOAuthIDPID string
+ CTX context.Context
+ Tester *integration.Tester
+ Client session.SessionServiceClient
+ User *user.AddHumanUserResponse
)
func TestMain(m *testing.M) {
@@ -36,38 +36,24 @@ func TestMain(m *testing.M) {
defer Tester.Done()
Client = Tester.Client.SessionV2
- CTX, _ = Tester.WithSystemAuthorization(ctx, integration.OrgOwner), errCtx
+ CTX, _ = Tester.WithAuthorization(ctx, integration.OrgOwner), errCtx
User = Tester.CreateHumanUser(CTX)
+ Tester.SetUserPassword(CTX, User.GetUserId(), integration.UserPassword)
Tester.RegisterUserPasskey(CTX, User.GetUserId())
return m.Run()
}())
}
-func verifyCurrentSession(t testing.TB, id, token string, sequence uint64, window time.Duration, metadata map[string][]byte, factors ...wantFactor) (s *session.Session) {
+func verifyCurrentSession(t testing.TB, id, token string, sequence uint64, window time.Duration, metadata map[string][]byte, factors ...wantFactor) *session.Session {
require.NotEmpty(t, id)
require.NotEmpty(t, token)
-retry:
- for {
- resp, err := Client.GetSession(CTX, &session.GetSessionRequest{
- SessionId: id,
- SessionToken: &token,
- })
- if err == nil {
- s = resp.GetSession()
- break retry
- }
- if code := status.Convert(err).Code(); code == codes.NotFound || code == codes.PermissionDenied {
- select {
- case <-CTX.Done():
- t.Fatal(CTX.Err(), err)
- case <-time.After(time.Second):
- t.Log("retrying GetSession")
- continue
- }
- }
- require.NoError(t, err)
- }
+ resp, err := Client.GetSession(CTX, &session.GetSessionRequest{
+ SessionId: id,
+ SessionToken: &token,
+ })
+ require.NoError(t, err)
+ s := resp.GetSession()
assert.Equal(t, id, s.GetId())
assert.WithinRange(t, s.GetCreationDate().AsTime(), time.Now().Add(-window), time.Now().Add(window))
@@ -395,3 +381,51 @@ func TestServer_SetSession_flow(t *testing.T) {
verifyCurrentSession(t, createResp.GetSessionId(), resp.GetSessionToken(), resp.GetDetails().GetSequence(), time.Minute, nil, wantFactors...)
})
}
+
+func Test_ZITADEL_API_missing_authentication(t *testing.T) {
+ // create new, empty session
+ createResp, err := Client.CreateSession(CTX, &session.CreateSessionRequest{Domain: Tester.Config.ExternalDomain})
+ require.NoError(t, err)
+
+ ctx := metadata.AppendToOutgoingContext(context.Background(), "Authorization", fmt.Sprintf("Bearer %s", createResp.GetSessionToken()))
+ sessionResp, err := Tester.Client.SessionV2.GetSession(ctx, &session.GetSessionRequest{SessionId: createResp.GetSessionId()})
+ require.Error(t, err)
+ require.Nil(t, sessionResp)
+}
+
+func Test_ZITADEL_API_missing_mfa(t *testing.T) {
+ id, token, _, _ := Tester.CreatePasswordSession(t, CTX, User.GetUserId(), integration.UserPassword)
+
+ ctx := metadata.AppendToOutgoingContext(context.Background(), "Authorization", fmt.Sprintf("Bearer %s", token))
+ sessionResp, err := Tester.Client.SessionV2.GetSession(ctx, &session.GetSessionRequest{SessionId: id})
+ require.Error(t, err)
+ require.Nil(t, sessionResp)
+}
+
+func Test_ZITADEL_API_success(t *testing.T) {
+ id, token, _, _ := Tester.CreatePasskeySession(t, CTX, User.GetUserId())
+
+ ctx := metadata.AppendToOutgoingContext(context.Background(), "Authorization", fmt.Sprintf("Bearer %s", token))
+ sessionResp, err := Tester.Client.SessionV2.GetSession(ctx, &session.GetSessionRequest{SessionId: id})
+ require.NoError(t, err)
+ require.NotNil(t, id, sessionResp.GetSession().GetFactors().GetPasskey().GetVerifiedAt().AsTime())
+}
+
+func Test_ZITADEL_API_session_not_found(t *testing.T) {
+ id, token, _, _ := Tester.CreatePasskeySession(t, CTX, User.GetUserId())
+
+ // test session token works
+ ctx := metadata.AppendToOutgoingContext(context.Background(), "Authorization", fmt.Sprintf("Bearer %s", token))
+ _, err := Tester.Client.SessionV2.GetSession(ctx, &session.GetSessionRequest{SessionId: id})
+ require.NoError(t, err)
+
+ //terminate the session and test it does not work anymore
+ _, err = Tester.Client.SessionV2.DeleteSession(CTX, &session.DeleteSessionRequest{
+ SessionId: id,
+ SessionToken: gu.Ptr(token),
+ })
+ require.NoError(t, err)
+ ctx = metadata.AppendToOutgoingContext(context.Background(), "Authorization", fmt.Sprintf("Bearer %s", token))
+ _, err = Tester.Client.SessionV2.GetSession(ctx, &session.GetSessionRequest{SessionId: id})
+ require.Error(t, err)
+}
diff --git a/internal/api/grpc/session/v2/session_test.go b/internal/api/grpc/session/v2/session_test.go
index b8ae8df3a..410ab43dc 100644
--- a/internal/api/grpc/session/v2/session_test.go
+++ b/internal/api/grpc/session/v2/session_test.go
@@ -46,6 +46,7 @@ func Test_sessionsToPb(t *testing.T) {
UserCheckedAt: past,
LoginName: "donald",
DisplayName: "donald duck",
+ ResourceOwner: "org1",
},
Metadata: map[string][]byte{"hello": []byte("world")},
},
@@ -62,6 +63,7 @@ func Test_sessionsToPb(t *testing.T) {
UserCheckedAt: past,
LoginName: "donald",
DisplayName: "donald duck",
+ ResourceOwner: "org1",
},
PasswordFactor: query.SessionPasswordFactor{
PasswordCheckedAt: past,
@@ -81,6 +83,7 @@ func Test_sessionsToPb(t *testing.T) {
UserCheckedAt: past,
LoginName: "donald",
DisplayName: "donald duck",
+ ResourceOwner: "org1",
},
PasskeyFactor: query.SessionPasskeyFactor{
PasskeyCheckedAt: past,
@@ -105,10 +108,11 @@ func Test_sessionsToPb(t *testing.T) {
Sequence: 123,
Factors: &session.Factors{
User: &session.UserFactor{
- VerifiedAt: timestamppb.New(past),
- Id: "345",
- LoginName: "donald",
- DisplayName: "donald duck",
+ VerifiedAt: timestamppb.New(past),
+ Id: "345",
+ LoginName: "donald",
+ DisplayName: "donald duck",
+ OrganisationId: "org1",
},
},
Metadata: map[string][]byte{"hello": []byte("world")},
@@ -120,10 +124,11 @@ func Test_sessionsToPb(t *testing.T) {
Sequence: 123,
Factors: &session.Factors{
User: &session.UserFactor{
- VerifiedAt: timestamppb.New(past),
- Id: "345",
- LoginName: "donald",
- DisplayName: "donald duck",
+ VerifiedAt: timestamppb.New(past),
+ Id: "345",
+ LoginName: "donald",
+ DisplayName: "donald duck",
+ OrganisationId: "org1",
},
Password: &session.PasswordFactor{
VerifiedAt: timestamppb.New(past),
@@ -138,10 +143,11 @@ func Test_sessionsToPb(t *testing.T) {
Sequence: 123,
Factors: &session.Factors{
User: &session.UserFactor{
- VerifiedAt: timestamppb.New(past),
- Id: "345",
- LoginName: "donald",
- DisplayName: "donald duck",
+ VerifiedAt: timestamppb.New(past),
+ Id: "345",
+ LoginName: "donald",
+ DisplayName: "donald duck",
+ OrganisationId: "org1",
},
Passkey: &session.PasskeyFactor{
VerifiedAt: timestamppb.New(past),
diff --git a/internal/api/grpc/settings/v2/settings_converter.go b/internal/api/grpc/settings/v2/settings_converter.go
index 42ba4767c..4330613d0 100644
--- a/internal/api/grpc/settings/v2/settings_converter.go
+++ b/internal/api/grpc/settings/v2/settings_converter.go
@@ -8,7 +8,6 @@ import (
settings "github.com/zitadel/zitadel/pkg/grpc/settings/v2alpha"
)
-// TODO: ?
func loginSettingsToPb(current *query.LoginPolicy) *settings.LoginSettings {
multi := make([]settings.MultiFactorType, len(current.MultiFactors))
for i, typ := range current.MultiFactors {
@@ -24,6 +23,7 @@ func loginSettingsToPb(current *query.LoginPolicy) *settings.LoginSettings {
AllowRegister: current.AllowRegister,
AllowExternalIdp: current.AllowExternalIDPs,
ForceMfa: current.ForceMFA,
+ ForceMfaLocalOnly: current.ForceMFALocalOnly,
PasskeysType: passkeysTypeToPb(current.PasswordlessType),
HidePasswordReset: current.HidePasswordReset,
IgnoreUnknownUsernames: current.IgnoreUnknownUsernames,
diff --git a/internal/api/grpc/settings/v2/settings_converter_test.go b/internal/api/grpc/settings/v2/settings_converter_test.go
index 11ebc0f71..e81c0a70c 100644
--- a/internal/api/grpc/settings/v2/settings_converter_test.go
+++ b/internal/api/grpc/settings/v2/settings_converter_test.go
@@ -25,6 +25,7 @@ func Test_loginSettingsToPb(t *testing.T) {
AllowRegister: true,
AllowExternalIDPs: true,
ForceMFA: true,
+ ForceMFALocalOnly: true,
PasswordlessType: domain.PasswordlessTypeAllowed,
HidePasswordReset: true,
IgnoreUnknownUsernames: true,
@@ -52,6 +53,7 @@ func Test_loginSettingsToPb(t *testing.T) {
AllowRegister: true,
AllowExternalIdp: true,
ForceMfa: true,
+ ForceMfaLocalOnly: true,
PasskeysType: settings.PasskeysType_PASSKEYS_TYPE_ALLOWED,
HidePasswordReset: true,
IgnoreUnknownUsernames: true,
diff --git a/internal/api/grpc/system/instance_converter.go b/internal/api/grpc/system/instance_converter.go
index 9aeea3e2c..271dc1520 100644
--- a/internal/api/grpc/system/instance_converter.go
+++ b/internal/api/grpc/system/instance_converter.go
@@ -111,7 +111,7 @@ func createInstancePbToAddMachine(req *system_pb.CreateInstanceRequest_Machine,
if defaultMachine.Pat != nil || req.PersonalAccessToken != nil {
pat := command.AddPat{
// Scopes are currently static and can not be overwritten
- Scopes: []string{oidc.ScopeOpenID, z_oidc.ScopeUserMetaData, z_oidc.ScopeResourceOwner},
+ Scopes: []string{oidc.ScopeOpenID, oidc.ScopeProfile, z_oidc.ScopeUserMetaData, z_oidc.ScopeResourceOwner},
}
if req.GetPersonalAccessToken().GetExpirationDate().IsValid() {
pat.ExpirationDate = req.PersonalAccessToken.ExpirationDate.AsTime()
diff --git a/internal/api/grpc/user/v2/user.go b/internal/api/grpc/user/v2/user.go
index 9e28b5029..b115d83eb 100644
--- a/internal/api/grpc/user/v2/user.go
+++ b/internal/api/grpc/user/v2/user.go
@@ -48,10 +48,6 @@ func addUserRequestToAddHuman(req *user.AddHumanUserRequest) (*command.AddHuman,
return nil, err
}
}
- bcryptedPassword, err := hashedPasswordToCommand(req.GetHashedPassword())
- if err != nil {
- return nil, err
- }
passwordChangeRequired := req.GetPassword().GetChangeRequired() || req.GetHashedPassword().GetChangeRequired()
metadata := make([]*command.AddMetadataEntry, len(req.Metadata))
for i, metadataEntry := range req.Metadata {
@@ -85,7 +81,7 @@ func addUserRequestToAddHuman(req *user.AddHumanUserRequest) (*command.AddHuman,
Gender: genderToDomain(req.GetProfile().GetGender()),
Phone: command.Phone{}, // TODO: add as soon as possible
Password: req.GetPassword().GetPassword(),
- BcryptedPassword: bcryptedPassword,
+ EncodedPasswordHash: req.GetHashedPassword().GetHash(),
PasswordChangeRequired: passwordChangeRequired,
Passwordless: false,
Register: false,
@@ -109,17 +105,6 @@ func genderToDomain(gender user.Gender) domain.Gender {
}
}
-func hashedPasswordToCommand(hashed *user.HashedPassword) (string, error) {
- if hashed == nil {
- return "", nil
- }
- // we currently only handle bcrypt
- if hashed.GetAlgorithm() != "bcrypt" {
- return "", errors.ThrowInvalidArgument(nil, "USER-JDk4t", "Errors.InvalidArgument")
- }
- return hashed.GetHash(), nil
-}
-
func (s *Server) AddIDPLink(ctx context.Context, req *user.AddIDPLinkRequest) (_ *user.AddIDPLinkResponse, err error) {
orgID := authz.GetCtxData(ctx).OrgID
details, err := s.command.AddUserIDPLink(ctx, req.UserId, orgID, &domain.UserIDPLink{
diff --git a/internal/api/grpc/user/v2/user_integration_test.go b/internal/api/grpc/user/v2/user_integration_test.go
index 40efe6195..a23ebbad9 100644
--- a/internal/api/grpc/user/v2/user_integration_test.go
+++ b/internal/api/grpc/user/v2/user_integration_test.go
@@ -38,7 +38,7 @@ func TestMain(m *testing.M) {
Tester = integration.NewTester(ctx)
defer Tester.Done()
- CTX, ErrCTX = Tester.WithSystemAuthorization(ctx, integration.OrgOwner), errCtx
+ CTX, ErrCTX = Tester.WithAuthorization(ctx, integration.OrgOwner), errCtx
Client = Tester.Client.UserV2
return m.Run()
}())
@@ -392,6 +392,79 @@ func TestServer_AddHumanUser(t *testing.T) {
},
},
},
+ {
+ name: "hashed password",
+ args: args{
+ CTX,
+ &user.AddHumanUserRequest{
+ Organisation: &object.Organisation{
+ Org: &object.Organisation_OrgId{
+ OrgId: Tester.Organisation.ID,
+ },
+ },
+ Profile: &user.SetHumanProfile{
+ FirstName: "Donald",
+ LastName: "Duck",
+ NickName: gu.Ptr("Dukkie"),
+ DisplayName: gu.Ptr("Donald Duck"),
+ PreferredLanguage: gu.Ptr("en"),
+ Gender: user.Gender_GENDER_DIVERSE.Enum(),
+ },
+ Email: &user.SetHumanEmail{},
+ Metadata: []*user.SetMetadataEntry{
+ {
+ Key: "somekey",
+ Value: []byte("somevalue"),
+ },
+ },
+ PasswordType: &user.AddHumanUserRequest_HashedPassword{
+ HashedPassword: &user.HashedPassword{
+ Hash: "$2y$12$hXUrnqdq1RIIYZ2HPytIIe5lXdIvbhqrTvdPsSF7o.jFh817Z6lwm",
+ },
+ },
+ },
+ },
+ want: &user.AddHumanUserResponse{
+ Details: &object.Details{
+ ChangeDate: timestamppb.Now(),
+ ResourceOwner: Tester.Organisation.ID,
+ },
+ },
+ },
+ {
+ name: "unsupported hashed password",
+ args: args{
+ CTX,
+ &user.AddHumanUserRequest{
+ Organisation: &object.Organisation{
+ Org: &object.Organisation_OrgId{
+ OrgId: Tester.Organisation.ID,
+ },
+ },
+ Profile: &user.SetHumanProfile{
+ FirstName: "Donald",
+ LastName: "Duck",
+ NickName: gu.Ptr("Dukkie"),
+ DisplayName: gu.Ptr("Donald Duck"),
+ PreferredLanguage: gu.Ptr("en"),
+ Gender: user.Gender_GENDER_DIVERSE.Enum(),
+ },
+ Email: &user.SetHumanEmail{},
+ Metadata: []*user.SetMetadataEntry{
+ {
+ Key: "somekey",
+ Value: []byte("somevalue"),
+ },
+ },
+ PasswordType: &user.AddHumanUserRequest_HashedPassword{
+ HashedPassword: &user.HashedPassword{
+ Hash: "$scrypt$ln=16,r=8,p=1$cmFuZG9tc2FsdGlzaGFyZA$Rh+NnJNo1I6nRwaNqbDm6kmADswD1+7FTKZ7Ln9D8nQ",
+ },
+ },
+ },
+ },
+ wantErr: true,
+ },
}
for i, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
@@ -454,7 +527,7 @@ func TestServer_AddIDPLink(t *testing.T) {
args: args{
CTX,
&user.AddIDPLinkRequest{
- UserId: Tester.Users[integration.OrgOwner].ID,
+ UserId: Tester.Users[integration.FirstInstanceUsersKey][integration.OrgOwner].ID,
IdpLink: &user.IDPLink{
IdpId: "idpID",
UserId: "userID",
@@ -470,7 +543,7 @@ func TestServer_AddIDPLink(t *testing.T) {
args: args{
CTX,
&user.AddIDPLinkRequest{
- UserId: Tester.Users[integration.OrgOwner].ID,
+ UserId: Tester.Users[integration.FirstInstanceUsersKey][integration.OrgOwner].ID,
IdpLink: &user.IDPLink{
IdpId: idpID,
UserId: "userID",
@@ -540,7 +613,7 @@ func TestServer_StartIdentityProviderFlow(t *testing.T) {
ResourceOwner: Tester.Organisation.ID,
},
NextStep: &user.StartIdentityProviderFlowResponse_AuthUrl{
- AuthUrl: "https://example.com/oauth/v2/authorize?client_id=clientID&prompt=select_account&redirect_uri=https%3A%2F%2Flocalhost%3A8080%2Fidps%2Fcallback&response_type=code&scope=openid+profile+email&state=",
+ AuthUrl: "https://example.com/oauth/v2/authorize?client_id=clientID&prompt=select_account&redirect_uri=http%3A%2F%2Flocalhost%3A8080%2Fidps%2Fcallback&response_type=code&scope=openid+profile+email&state=",
},
},
wantErr: false,
diff --git a/internal/api/grpc/user/v2/user_test.go b/internal/api/grpc/user/v2/user_test.go
index 8332c8703..fa3ce0b0d 100644
--- a/internal/api/grpc/user/v2/user_test.go
+++ b/internal/api/grpc/user/v2/user_test.go
@@ -1,7 +1,6 @@
package user
import (
- "errors"
"testing"
"time"
@@ -25,74 +24,6 @@ import (
var ignoreTypes = []protoreflect.FullName{"google.protobuf.Duration", "google.protobuf.Struct"}
-func Test_hashedPasswordToCommand(t *testing.T) {
- type args struct {
- hashed *user.HashedPassword
- }
- type res struct {
- want string
- err func(error) bool
- }
- tests := []struct {
- name string
- args args
- res res
- }{
- {
- "not hashed",
- args{
- hashed: nil,
- },
- res{
- "",
- nil,
- },
- },
- {
- "hashed, not bcrypt",
- args{
- hashed: &user.HashedPassword{
- Hash: "hash",
- Algorithm: "custom",
- },
- },
- res{
- "",
- func(err error) bool {
- return errors.Is(err, caos_errs.ThrowInvalidArgument(nil, "USER-JDk4t", "Errors.InvalidArgument"))
- },
- },
- },
- {
- "hashed, bcrypt",
- args{
- hashed: &user.HashedPassword{
- Hash: "hash",
- Algorithm: "bcrypt",
- },
- },
- res{
- "hash",
- nil,
- },
- },
- }
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- got, err := hashedPasswordToCommand(tt.args.hashed)
- if tt.res.err == nil {
- require.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 Test_intentToIDPInformationPb(t *testing.T) {
decryption := func(err error) crypto.EncryptionAlgorithm {
mCrypto := crypto.NewMockEncryptionAlgorithm(gomock.NewController(t))
diff --git a/internal/api/http/cookie.go b/internal/api/http/cookie.go
index ecc243e8e..18a748dc3 100644
--- a/internal/api/http/cookie.go
+++ b/internal/api/http/cookie.go
@@ -111,7 +111,7 @@ func (c *CookieHandler) SetCookie(w http.ResponseWriter, name, domain, value str
c.httpSet(w, name, domain, value, c.maxAge)
}
-func (c *CookieHandler) SetEncryptedCookie(w http.ResponseWriter, name, domain string, value interface{}) error {
+func (c *CookieHandler) SetEncryptedCookie(w http.ResponseWriter, name, domain string, value interface{}, sameSiteNone bool) error {
if c.securecookie == nil {
return errors.ThrowInternal(nil, "HTTP-s2HUtx", "securecookie not configured")
}
@@ -119,7 +119,11 @@ func (c *CookieHandler) SetEncryptedCookie(w http.ResponseWriter, name, domain s
if err != nil {
return err
}
- c.httpSet(w, name, domain, encoded, c.maxAge)
+ sameSite := c.sameSite
+ if sameSiteNone {
+ sameSite = http.SameSiteNoneMode
+ }
+ c.httpSetWithSameSite(w, name, domain, encoded, c.maxAge, sameSite)
return nil
}
@@ -128,6 +132,10 @@ func (c *CookieHandler) DeleteCookie(w http.ResponseWriter, name string) {
}
func (c *CookieHandler) httpSet(w http.ResponseWriter, name, domain, value string, maxage int) {
+ c.httpSetWithSameSite(w, name, domain, value, maxage, c.sameSite)
+}
+
+func (c *CookieHandler) httpSetWithSameSite(w http.ResponseWriter, name, domain, value string, maxage int, sameSite http.SameSite) {
http.SetCookie(w, &http.Cookie{
Name: SetCookiePrefix(name, domain, c.path, c.secureOnly),
Value: value,
@@ -136,7 +144,7 @@ func (c *CookieHandler) httpSet(w http.ResponseWriter, name, domain, value strin
MaxAge: maxage,
HttpOnly: c.httpOnly,
Secure: c.secureOnly,
- SameSite: c.sameSite,
+ SameSite: sameSite,
})
varyValues := w.Header().Values("vary")
for _, vary := range varyValues {
diff --git a/internal/api/http/domain_check.go b/internal/api/http/domain_check.go
index b4281c778..3d3989799 100644
--- a/internal/api/http/domain_check.go
+++ b/internal/api/http/domain_check.go
@@ -15,7 +15,7 @@ const (
CheckTypeHTTP CheckType = iota
CheckTypeDNS
- HTTPPattern = "https://%s/.well-known/zitadel-challenge/%s"
+ HTTPPattern = "https://%s/.well-known/zitadel-challenge/%s.txt"
DNSPattern = "_zitadel-challenge.%s"
)
diff --git a/internal/api/http/middleware/user_agent_cookie.go b/internal/api/http/middleware/user_agent_cookie.go
index 99aa482bf..36d5155f1 100644
--- a/internal/api/http/middleware/user_agent_cookie.go
+++ b/internal/api/http/middleware/user_agent_cookie.go
@@ -6,6 +6,9 @@ import (
"strings"
"time"
+ "github.com/zitadel/logging"
+
+ "github.com/zitadel/zitadel/internal/api/authz"
http_utils "github.com/zitadel/zitadel/internal/api/http"
"github.com/zitadel/zitadel/internal/errors"
"github.com/zitadel/zitadel/internal/id"
@@ -71,8 +74,11 @@ func (ua *userAgentHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
}
if err == nil {
ctx := context.WithValue(r.Context(), userAgentKey, agent.ID)
+ instance := authz.GetInstance(r.Context())
+ allowedHosts := instance.SecurityPolicyAllowedOrigins()
r = r.WithContext(ctx)
- ua.setUserAgent(w, r.Host, agent)
+ err = ua.setUserAgent(w, r.Host, agent, len(allowedHosts) > 0)
+ logging.WithFields("instanceID", instance.InstanceID()).OnError(err).Error("unable to set user agent cookie")
}
ua.nextHandler.ServeHTTP(w, r)
}
@@ -94,8 +100,8 @@ func (ua *userAgentHandler) getUserAgent(r *http.Request) (*UserAgent, error) {
return userAgent, nil
}
-func (ua *userAgentHandler) setUserAgent(w http.ResponseWriter, host string, agent *UserAgent) error {
- err := ua.cookieHandler.SetEncryptedCookie(w, ua.cookieName, host, agent)
+func (ua *userAgentHandler) setUserAgent(w http.ResponseWriter, host string, agent *UserAgent, iframe bool) error {
+ err := ua.cookieHandler.SetEncryptedCookie(w, ua.cookieName, host, agent, iframe)
if err != nil {
return errors.ThrowPermissionDenied(err, "HTTP-AqgqdA", "cannot set user agent cookie")
}
diff --git a/internal/api/oidc/amr.go b/internal/api/oidc/amr.go
new file mode 100644
index 000000000..727e0f888
--- /dev/null
+++ b/internal/api/oidc/amr.go
@@ -0,0 +1,47 @@
+package oidc
+
+import "github.com/zitadel/zitadel/internal/domain"
+
+const (
+ // Password states that the users password has been verified
+ // Deprecated: use `PWD` instead
+ Password = "password"
+ // PWD states that the users password has been verified
+ PWD = "pwd"
+ // MFA states that multiple factors have been verified (e.g. pwd and otp or passkey)
+ MFA = "mfa"
+ // OTP states that a one time password has been verified (e.g. TOTP)
+ OTP = "otp"
+ // UserPresence states that the end users presence has been verified (e.g. passkey and u2f)
+ UserPresence = "user"
+)
+
+// AuthMethodTypesToAMR maps zitadel auth method types to Authentication Method Reference Values
+// as defined in [RFC 8176, section 2].
+//
+// [RFC 8176, section 2]: https://datatracker.ietf.org/doc/html/rfc8176#section-2
+func AuthMethodTypesToAMR(methodTypes []domain.UserAuthMethodType) []string {
+ amr := make([]string, 0, 4)
+ var mfa bool
+ for _, methodType := range methodTypes {
+ switch methodType {
+ case domain.UserAuthMethodTypePassword:
+ amr = append(amr, PWD)
+ case domain.UserAuthMethodTypePasswordless:
+ mfa = true
+ amr = append(amr, UserPresence)
+ case domain.UserAuthMethodTypeU2F:
+ amr = append(amr, UserPresence)
+ case domain.UserAuthMethodTypeOTP:
+ amr = append(amr, OTP)
+ case domain.UserAuthMethodTypeIDP:
+ // no AMR value according to specification
+ case domain.UserAuthMethodTypeUnspecified:
+ // ignore
+ }
+ }
+ if mfa || len(amr) >= 2 {
+ amr = append(amr, MFA)
+ }
+ return amr
+}
diff --git a/internal/api/oidc/amr_test.go b/internal/api/oidc/amr_test.go
new file mode 100644
index 000000000..1861085bc
--- /dev/null
+++ b/internal/api/oidc/amr_test.go
@@ -0,0 +1,69 @@
+package oidc
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+
+ "github.com/zitadel/zitadel/internal/domain"
+)
+
+func TestAMR(t *testing.T) {
+ type args struct {
+ methodTypes []domain.UserAuthMethodType
+ }
+ tests := []struct {
+ name string
+ args args
+ want []string
+ }{
+ {
+ "no checks, empty",
+ args{
+ nil,
+ },
+ []string{},
+ },
+ {
+ "pw checked",
+ args{
+ []domain.UserAuthMethodType{domain.UserAuthMethodTypePassword},
+ },
+ []string{PWD},
+ },
+ {
+ "passkey checked",
+ args{
+ []domain.UserAuthMethodType{domain.UserAuthMethodTypePasswordless},
+ },
+ []string{UserPresence, MFA},
+ },
+ {
+ "u2f checked",
+ args{
+ []domain.UserAuthMethodType{domain.UserAuthMethodTypeU2F},
+ },
+ []string{UserPresence},
+ },
+ {
+ "otp checked",
+ args{
+ []domain.UserAuthMethodType{domain.UserAuthMethodTypeOTP},
+ },
+ []string{OTP},
+ },
+ {
+ "multiple checked",
+ args{
+ []domain.UserAuthMethodType{domain.UserAuthMethodTypePassword, domain.UserAuthMethodTypeU2F},
+ },
+ []string{PWD, UserPresence, MFA},
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ got := AuthMethodTypesToAMR(tt.args.methodTypes)
+ assert.Equal(t, tt.want, got)
+ })
+ }
+}
diff --git a/internal/api/oidc/auth_request.go b/internal/api/oidc/auth_request.go
index 0c943bfd6..4ba077d04 100644
--- a/internal/api/oidc/auth_request.go
+++ b/internal/api/oidc/auth_request.go
@@ -2,6 +2,7 @@ package oidc
import (
"context"
+ "encoding/base64"
"strings"
"time"
@@ -10,16 +11,75 @@ import (
"github.com/zitadel/oidc/v2/pkg/op"
"github.com/zitadel/zitadel/internal/api/authz"
+ http_utils "github.com/zitadel/zitadel/internal/api/http"
"github.com/zitadel/zitadel/internal/api/http/middleware"
+ "github.com/zitadel/zitadel/internal/command"
+ "github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/errors"
"github.com/zitadel/zitadel/internal/query"
"github.com/zitadel/zitadel/internal/telemetry/tracing"
"github.com/zitadel/zitadel/internal/user/model"
)
+const (
+ LoginClientHeader = "x-zitadel-login-client"
+)
+
func (o *OPStorage) CreateAuthRequest(ctx context.Context, req *oidc.AuthRequest, userID string) (_ op.AuthRequest, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
+
+ headers, _ := http_utils.HeadersFromCtx(ctx)
+ if loginClient := headers.Get(LoginClientHeader); loginClient != "" {
+ return o.createAuthRequestLoginClient(ctx, req, userID, loginClient)
+ }
+
+ return o.createAuthRequest(ctx, req, userID)
+}
+
+func (o *OPStorage) createAuthRequestLoginClient(ctx context.Context, req *oidc.AuthRequest, hintUserID, loginClient string) (op.AuthRequest, error) {
+ project, err := o.query.ProjectByClientID(ctx, req.ClientID, false)
+ if err != nil {
+ return nil, err
+ }
+ scope, err := o.assertProjectRoleScopesByProject(ctx, project, req.Scopes)
+ if err != nil {
+ return nil, err
+ }
+ audience, err := o.audienceFromProjectID(ctx, project.ID)
+ if err != nil {
+ return nil, err
+ }
+ audience = domain.AddAudScopeToAudience(ctx, audience, scope)
+ authRequest := &command.AuthRequest{
+ LoginClient: loginClient,
+ ClientID: req.ClientID,
+ RedirectURI: req.RedirectURI,
+ State: req.State,
+ Nonce: req.Nonce,
+ Scope: scope,
+ Audience: audience,
+ ResponseType: ResponseTypeToBusiness(req.ResponseType),
+ CodeChallenge: CodeChallengeToBusiness(req.CodeChallenge, req.CodeChallengeMethod),
+ Prompt: PromptToBusiness(req.Prompt),
+ UILocales: UILocalesToBusiness(req.UILocales),
+ MaxAge: MaxAgeToBusiness(req.MaxAge),
+ }
+ if req.LoginHint != "" {
+ authRequest.LoginHint = &req.LoginHint
+ }
+ if hintUserID != "" {
+ authRequest.HintUserID = &hintUserID
+ }
+
+ aar, err := o.command.AddAuthRequest(ctx, authRequest)
+ if err != nil {
+ return nil, err
+ }
+ return &AuthRequestV2{aar}, nil
+}
+
+func (o *OPStorage) createAuthRequest(ctx context.Context, req *oidc.AuthRequest, userID string) (_ op.AuthRequest, err error) {
userAgentID, ok := middleware.UserAgentIDFromCtx(ctx)
if !ok {
return nil, errors.ThrowPreconditionFailed(nil, "OIDC-sd436", "no user agent id")
@@ -36,9 +96,31 @@ func (o *OPStorage) CreateAuthRequest(ctx context.Context, req *oidc.AuthRequest
return AuthRequestFromBusiness(resp)
}
+func (o *OPStorage) audienceFromProjectID(ctx context.Context, projectID string) ([]string, error) {
+ projectIDQuery, err := query.NewAppProjectIDSearchQuery(projectID)
+ if err != nil {
+ return nil, err
+ }
+ appIDs, err := o.query.SearchClientIDs(ctx, &query.AppSearchQueries{Queries: []query.SearchQuery{projectIDQuery}}, false)
+ if err != nil {
+ return nil, err
+ }
+
+ return append(appIDs, projectID), nil
+}
+
func (o *OPStorage) AuthRequestByID(ctx context.Context, id string) (_ op.AuthRequest, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
+
+ if strings.HasPrefix(id, command.IDPrefixV2) {
+ req, err := o.command.GetCurrentAuthRequest(ctx, id)
+ if err != nil {
+ return nil, err
+ }
+ return &AuthRequestV2{req}, nil
+ }
+
userAgentID, ok := middleware.UserAgentIDFromCtx(ctx)
if !ok {
return nil, errors.ThrowPreconditionFailed(nil, "OIDC-D3g21", "no user agent id")
@@ -54,6 +136,17 @@ func (o *OPStorage) AuthRequestByCode(ctx context.Context, code string) (_ op.Au
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
+ plainCode, err := o.decryptGrant(code)
+ if err != nil {
+ return nil, err
+ }
+ if strings.HasPrefix(plainCode, command.IDPrefixV2) {
+ authReq, err := o.command.ExchangeAuthCode(ctx, plainCode)
+ if err != nil {
+ return nil, err
+ }
+ return &AuthRequestV2{authReq}, nil
+ }
resp, err := o.repo.AuthRequestByCode(ctx, code)
if err != nil {
return nil, err
@@ -61,9 +154,23 @@ func (o *OPStorage) AuthRequestByCode(ctx context.Context, code string) (_ op.Au
return AuthRequestFromBusiness(resp)
}
+// decryptGrant decrypts a code or refresh_token
+func (o *OPStorage) decryptGrant(grant string) (string, error) {
+ decodedGrant, err := base64.RawURLEncoding.DecodeString(grant)
+ if err != nil {
+ return "", err
+ }
+ return o.encAlg.DecryptString(decodedGrant, o.encAlg.EncryptionKeyID())
+}
+
func (o *OPStorage) SaveAuthCode(ctx context.Context, id, code string) (err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
+
+ if strings.HasPrefix(id, command.IDPrefixV2) {
+ return o.command.AddAuthRequestCode(ctx, id, code)
+ }
+
userAgentID, ok := middleware.UserAgentIDFromCtx(ctx)
if !ok {
return errors.ThrowPreconditionFailed(nil, "OIDC-Dgus2", "no user agent id")
@@ -81,12 +188,15 @@ func (o *OPStorage) DeleteAuthRequest(ctx context.Context, id string) (err error
func (o *OPStorage) CreateAccessToken(ctx context.Context, req op.TokenRequest) (_ string, _ time.Time, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
+
var userAgentID, applicationID, userOrgID string
- authReq, ok := req.(*AuthRequest)
- if ok {
+ switch authReq := req.(type) {
+ case *AuthRequest:
userAgentID = authReq.AgentID
applicationID = authReq.ApplicationID
userOrgID = authReq.UserOrgID
+ case *AuthRequestV2:
+ return o.command.AddOIDCSessionAccessToken(setContextUserSystem(ctx), authReq.GetID())
}
accessTokenLifetime, _, _, _, err := o.getOIDCSettings(ctx)
@@ -104,6 +214,15 @@ func (o *OPStorage) CreateAccessToken(ctx context.Context, req op.TokenRequest)
func (o *OPStorage) CreateAccessAndRefreshTokens(ctx context.Context, req op.TokenRequest, refreshToken string) (_, _ string, _ time.Time, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
+
+ // handle V2 request directly
+ switch tokenReq := req.(type) {
+ case *AuthRequestV2:
+ return o.command.AddOIDCSessionRefreshAndAccessToken(setContextUserSystem(ctx), tokenReq.GetID())
+ case *RefreshTokenRequestV2:
+ return o.command.ExchangeOIDCSessionRefreshAndAccessToken(setContextUserSystem(ctx), tokenReq.OIDCSessionWriteModel.AggregateID, refreshToken, tokenReq.RequestedScopes)
+ }
+
userAgentID, applicationID, userOrgID, authTime, authMethodsReferences := getInfoFromRequest(req)
scopes, err := o.assertProjectRoleScopes(ctx, applicationID, req.GetScopes())
if err != nil {
@@ -142,7 +261,22 @@ func getInfoFromRequest(req op.TokenRequest) (string, string, string, time.Time,
return "", "", "", time.Time{}, nil
}
-func (o *OPStorage) TokenRequestByRefreshToken(ctx context.Context, refreshToken string) (op.RefreshTokenRequest, error) {
+func (o *OPStorage) TokenRequestByRefreshToken(ctx context.Context, refreshToken string) (_ op.RefreshTokenRequest, err error) {
+ ctx, span := tracing.NewSpan(ctx)
+ defer func() { span.EndWithError(err) }()
+
+ plainToken, err := o.decryptGrant(refreshToken)
+ if err != nil {
+ return nil, err
+ }
+ if strings.HasPrefix(plainToken, command.IDPrefixV2) {
+ oidcSession, err := o.command.OIDCSessionByRefreshToken(ctx, plainToken)
+ if err != nil {
+ return nil, err
+ }
+ return &RefreshTokenRequestV2{OIDCSessionWriteModel: oidcSession}, nil
+ }
+
tokenView, err := o.repo.RefreshTokenByToken(ctx, refreshToken)
if err != nil {
return nil, err
@@ -174,7 +308,56 @@ func (o *OPStorage) TerminateSession(ctx context.Context, userID, clientID strin
return err
}
-func (o *OPStorage) RevokeToken(ctx context.Context, token, userID, clientID string) *oidc.Error {
+func (o *OPStorage) TerminateSessionFromRequest(ctx context.Context, endSessionRequest *op.EndSessionRequest) (redirectURI string, err error) {
+ ctx, span := tracing.NewSpan(ctx)
+ defer func() { span.EndWithError(err) }()
+
+ // check for the login client header
+ // and if not provided, terminate the session using the V1 method
+ headers, _ := http_utils.HeadersFromCtx(ctx)
+ if loginClient := headers.Get(LoginClientHeader); loginClient == "" {
+ return endSessionRequest.RedirectURI, o.TerminateSession(ctx, endSessionRequest.UserID, endSessionRequest.ClientID)
+ }
+
+ // in case there are not id_token_hint, redirect to the UI and let it decide which session to terminate
+ if endSessionRequest.IDTokenHintClaims == nil {
+ return o.defaultLogoutURLV2 + endSessionRequest.RedirectURI, nil
+ }
+
+ // terminate the session of the id_token_hint
+ _, err = o.command.TerminateSessionWithoutTokenCheck(ctx, endSessionRequest.IDTokenHintClaims.SessionID)
+ if err != nil {
+ return "", err
+ }
+ return endSessionRequest.RedirectURI, nil
+}
+
+func (o *OPStorage) RevokeToken(ctx context.Context, token, userID, clientID string) (err *oidc.Error) {
+ ctx, span := tracing.NewSpan(ctx)
+ defer func() {
+ // check for nil, because `err` is not an error and EndWithError would panic
+ if err == nil {
+ span.End()
+ return
+ }
+ span.EndWithError(err)
+ }()
+
+ if strings.HasPrefix(token, command.IDPrefixV2) {
+ err := o.command.RevokeOIDCSessionToken(ctx, token, clientID)
+ if err == nil {
+ return nil
+ }
+ if errors.IsPreconditionFailed(err) {
+ return oidc.ErrInvalidClient().WithDescription("token was not issued for this client")
+ }
+ return oidc.ErrServerError().WithParent(err)
+ }
+
+ return o.revokeTokenV1(ctx, token, userID, clientID)
+}
+
+func (o *OPStorage) revokeTokenV1(ctx context.Context, token, userID, clientID string) *oidc.Error {
refreshToken, err := o.repo.RefreshTokenByID(ctx, token, userID)
if err == nil {
if refreshToken.ClientID != clientID {
@@ -204,6 +387,17 @@ func (o *OPStorage) RevokeToken(ctx context.Context, token, userID, clientID str
}
func (o *OPStorage) GetRefreshTokenInfo(ctx context.Context, clientID string, token string) (userID string, tokenID string, err error) {
+ plainToken, err := o.decryptGrant(token)
+ if err != nil {
+ return "", "", op.ErrInvalidRefreshToken
+ }
+ if strings.HasPrefix(plainToken, command.IDPrefixV2) {
+ oidcSession, err := o.command.OIDCSessionByRefreshToken(ctx, plainToken)
+ if err != nil {
+ return "", "", op.ErrInvalidRefreshToken
+ }
+ return oidcSession.UserID, oidcSession.OIDCRefreshTokenID(oidcSession.RefreshTokenID), nil
+ }
refreshToken, err := o.repo.RefreshTokenByToken(ctx, token)
if err != nil {
return "", "", op.ErrInvalidRefreshToken
@@ -245,6 +439,29 @@ func (o *OPStorage) assertProjectRoleScopes(ctx context.Context, clientID string
return scopes, nil
}
+func (o *OPStorage) assertProjectRoleScopesByProject(ctx context.Context, project *query.Project, scopes []string) ([]string, error) {
+ for _, scope := range scopes {
+ if strings.HasPrefix(scope, ScopeProjectRolePrefix) {
+ return scopes, nil
+ }
+ }
+ if !project.ProjectRoleAssertion {
+ return scopes, nil
+ }
+ projectIDQuery, err := query.NewProjectRoleProjectIDSearchQuery(project.ID)
+ if err != nil {
+ return nil, errors.ThrowInternal(err, "OIDC-Cyc78", "Errors.Internal")
+ }
+ roles, err := o.query.SearchProjectRoles(ctx, true, &query.ProjectRoleSearchQueries{Queries: []query.SearchQuery{projectIDQuery}}, false)
+ if err != nil {
+ return nil, err
+ }
+ for _, role := range roles.ProjectRoles {
+ scopes = append(scopes, ScopeProjectRolePrefix+role.Key)
+ }
+ return scopes, nil
+}
+
func (o *OPStorage) assertClientScopesForPAT(ctx context.Context, token *model.TokenView, clientID, projectID string) error {
token.Audience = append(token.Audience, clientID)
projectIDQuery, err := query.NewProjectRoleProjectIDSearchQuery(projectID)
@@ -279,3 +496,58 @@ func (o *OPStorage) getOIDCSettings(ctx context.Context) (accessTokenLifetime, i
}
return o.defaultAccessTokenLifetime, o.defaultIdTokenLifetime, o.defaultRefreshTokenIdleExpiration, o.defaultRefreshTokenExpiration, nil
}
+
+func CreateErrorCallbackURL(authReq op.AuthRequest, reason, description, uri string, authorizer op.Authorizer) (string, error) {
+ e := struct {
+ Error string `schema:"error"`
+ Description string `schema:"error_description,omitempty"`
+ URI string `schema:"error_uri,omitempty"`
+ State string `schema:"state,omitempty"`
+ }{
+ Error: reason,
+ Description: description,
+ URI: uri,
+ State: authReq.GetState(),
+ }
+ callback, err := op.AuthResponseURL(authReq.GetRedirectURI(), authReq.GetResponseType(), authReq.GetResponseMode(), e, authorizer.Encoder())
+ if err != nil {
+ return "", err
+ }
+ return callback, nil
+}
+
+func CreateCodeCallbackURL(ctx context.Context, authReq op.AuthRequest, authorizer op.Authorizer) (string, error) {
+ code, err := op.CreateAuthRequestCode(ctx, authReq, authorizer.Storage(), authorizer.Crypto())
+ if err != nil {
+ return "", err
+ }
+ codeResponse := struct {
+ code string
+ state string
+ }{
+ code: code,
+ state: authReq.GetState(),
+ }
+ callback, err := op.AuthResponseURL(authReq.GetRedirectURI(), authReq.GetResponseType(), authReq.GetResponseMode(), &codeResponse, authorizer.Encoder())
+ if err != nil {
+ return "", err
+ }
+ return callback, err
+}
+
+func CreateTokenCallbackURL(ctx context.Context, req op.AuthRequest, authorizer op.Authorizer) (string, error) {
+ client, err := authorizer.Storage().GetClientByClientID(ctx, req.GetClientID())
+ if err != nil {
+ return "", err
+ }
+ createAccessToken := req.GetResponseType() != oidc.ResponseTypeIDTokenOnly
+ resp, err := op.CreateTokenResponse(ctx, req, client, authorizer, createAccessToken, "", "")
+ if err != nil {
+ return "", err
+ }
+ callback, err := op.AuthResponseURL(req.GetRedirectURI(), req.GetResponseType(), req.GetResponseMode(), resp, authorizer.Encoder())
+ if err != nil {
+ return "", err
+ }
+ return callback, err
+}
diff --git a/internal/api/oidc/auth_request_converter.go b/internal/api/oidc/auth_request_converter.go
index 647346084..02cbf8cba 100644
--- a/internal/api/oidc/auth_request_converter.go
+++ b/internal/api/oidc/auth_request_converter.go
@@ -17,15 +17,6 @@ import (
"github.com/zitadel/zitadel/internal/user/model"
)
-const (
- // DEPRECATED: use `amrPWD` instead
- amrPassword = "password"
- amrPWD = "pwd"
- amrMFA = "mfa"
- amrOTP = "otp"
- amrUserPresence = "user"
-)
-
type AuthRequest struct {
*domain.AuthRequest
}
@@ -40,19 +31,19 @@ func (a *AuthRequest) GetACR() string {
}
func (a *AuthRequest) GetAMR() []string {
- amr := make([]string, 0)
+ list := make([]string, 0)
if a.PasswordVerified {
- amr = append(amr, amrPassword, amrPWD)
+ list = append(list, Password, PWD)
}
if len(a.MFAsVerified) > 0 {
- amr = append(amr, amrMFA)
+ list = append(list, MFA)
for _, mfa := range a.MFAsVerified {
if amrMFA := AMRFromMFAType(mfa); amrMFA != "" {
- amr = append(amr, amrMFA)
+ list = append(list, amrMFA)
}
}
}
- return amr
+ return list
}
func (a *AuthRequest) GetAudience() []string {
@@ -271,10 +262,10 @@ func CodeChallengeToOIDC(challenge *domain.OIDCCodeChallenge) *oidc.CodeChalleng
func AMRFromMFAType(mfaType domain.MFAType) string {
switch mfaType {
case domain.MFATypeOTP:
- return amrOTP
+ return OTP
case domain.MFATypeU2F,
domain.MFATypeU2FUserVerification:
- return amrUserPresence
+ return UserPresence
default:
return ""
}
diff --git a/internal/api/oidc/auth_request_converter_v2.go b/internal/api/oidc/auth_request_converter_v2.go
new file mode 100644
index 000000000..9f4c0d0a1
--- /dev/null
+++ b/internal/api/oidc/auth_request_converter_v2.go
@@ -0,0 +1,106 @@
+package oidc
+
+import (
+ "time"
+
+ "github.com/zitadel/oidc/v2/pkg/oidc"
+
+ "github.com/zitadel/zitadel/internal/command"
+)
+
+type AuthRequestV2 struct {
+ *command.CurrentAuthRequest
+}
+
+func (a *AuthRequestV2) GetID() string {
+ return a.ID
+}
+
+func (a *AuthRequestV2) GetACR() string {
+ return "" //PLANNED: impl
+}
+
+func (a *AuthRequestV2) GetAMR() []string {
+ return AuthMethodTypesToAMR(a.AuthMethods)
+}
+
+func (a *AuthRequestV2) GetAudience() []string {
+ return a.Audience
+}
+
+func (a *AuthRequestV2) GetAuthTime() time.Time {
+ return a.AuthTime
+}
+
+func (a *AuthRequestV2) GetClientID() string {
+ return a.ClientID
+}
+
+func (a *AuthRequestV2) GetCodeChallenge() *oidc.CodeChallenge {
+ return CodeChallengeToOIDC(a.CodeChallenge)
+}
+
+func (a *AuthRequestV2) GetNonce() string {
+ return a.Nonce
+}
+
+func (a *AuthRequestV2) GetRedirectURI() string {
+ return a.RedirectURI
+}
+
+func (a *AuthRequestV2) GetResponseType() oidc.ResponseType {
+ return ResponseTypeToOIDC(a.ResponseType)
+}
+
+func (a *AuthRequestV2) GetResponseMode() oidc.ResponseMode {
+ return ""
+}
+
+func (a *AuthRequestV2) GetScopes() []string {
+ return a.Scope
+}
+
+func (a *AuthRequestV2) GetState() string {
+ return a.State
+}
+
+func (a *AuthRequestV2) GetSubject() string {
+ return a.UserID
+}
+
+func (a *AuthRequestV2) Done() bool {
+ return a.UserID != "" && a.SessionID != ""
+}
+
+type RefreshTokenRequestV2 struct {
+ *command.OIDCSessionWriteModel
+ RequestedScopes []string
+}
+
+func (r *RefreshTokenRequestV2) GetAMR() []string {
+ return AuthMethodTypesToAMR(r.AuthMethods)
+}
+
+func (r *RefreshTokenRequestV2) GetAudience() []string {
+ return r.Audience
+}
+
+func (r *RefreshTokenRequestV2) GetAuthTime() time.Time {
+ return r.AuthTime
+}
+
+func (r *RefreshTokenRequestV2) GetClientID() string {
+ return r.ClientID
+}
+
+func (r *RefreshTokenRequestV2) GetScopes() []string {
+ return r.Scope
+}
+
+func (r *RefreshTokenRequestV2) GetSubject() string {
+ return r.UserID
+}
+
+func (r *RefreshTokenRequestV2) SetCurrentScopes(scopes []string) {
+ r.RequestedScopes = scopes
+}
diff --git a/internal/api/oidc/auth_request_integration_test.go b/internal/api/oidc/auth_request_integration_test.go
new file mode 100644
index 000000000..7651b32c8
--- /dev/null
+++ b/internal/api/oidc/auth_request_integration_test.go
@@ -0,0 +1,550 @@
+//go:build integration
+
+package oidc_test
+
+import (
+ "context"
+ "net/url"
+ "testing"
+ "time"
+
+ "github.com/muhlemmer/gu"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+ "github.com/zitadel/oidc/v2/pkg/client/rp"
+ "github.com/zitadel/oidc/v2/pkg/oidc"
+
+ http_utils "github.com/zitadel/zitadel/internal/api/http"
+ oidc_api "github.com/zitadel/zitadel/internal/api/oidc"
+ "github.com/zitadel/zitadel/internal/command"
+ oidc_pb "github.com/zitadel/zitadel/pkg/grpc/oidc/v2alpha"
+ session "github.com/zitadel/zitadel/pkg/grpc/session/v2alpha"
+)
+
+var (
+ armPasskey = []string{oidc_api.UserPresence, oidc_api.MFA}
+ armPassword = []string{oidc_api.PWD}
+)
+
+func TestOPStorage_CreateAuthRequest(t *testing.T) {
+ clientID := createClient(t)
+
+ id := createAuthRequest(t, clientID, redirectURI)
+ require.Contains(t, id, command.IDPrefixV2)
+}
+
+func TestOPStorage_CreateAccessToken_code(t *testing.T) {
+ clientID := createClient(t)
+ authRequestID := createAuthRequest(t, clientID, redirectURI)
+ sessionID, sessionToken, startTime, changeTime := Tester.CreatePasskeySession(t, CTXLOGIN, User.GetUserId())
+ linkResp, err := Tester.Client.OIDCv2.CreateCallback(CTXLOGIN, &oidc_pb.CreateCallbackRequest{
+ AuthRequestId: authRequestID,
+ CallbackKind: &oidc_pb.CreateCallbackRequest_Session{
+ Session: &oidc_pb.Session{
+ SessionId: sessionID,
+ SessionToken: sessionToken,
+ },
+ },
+ })
+ require.NoError(t, err)
+
+ // test code exchange
+ code := assertCodeResponse(t, linkResp.GetCallbackUrl())
+ tokens, err := exchangeTokens(t, clientID, code)
+ require.NoError(t, err)
+ assertTokens(t, tokens, false)
+ assertIDTokenClaims(t, tokens.IDTokenClaims, armPasskey, startTime, changeTime)
+
+ // callback on a succeeded request must fail
+ linkResp, err = Tester.Client.OIDCv2.CreateCallback(CTXLOGIN, &oidc_pb.CreateCallbackRequest{
+ AuthRequestId: authRequestID,
+ CallbackKind: &oidc_pb.CreateCallbackRequest_Session{
+ Session: &oidc_pb.Session{
+ SessionId: sessionID,
+ SessionToken: sessionToken,
+ },
+ },
+ })
+ require.Error(t, err)
+
+ // exchange with a used code must fail
+ _, err = exchangeTokens(t, clientID, code)
+ require.Error(t, err)
+}
+
+func TestOPStorage_CreateAccessToken_implicit(t *testing.T) {
+ clientID := createImplicitClient(t)
+ authRequestID := createAuthRequestImplicit(t, clientID, redirectURIImplicit)
+ sessionID, sessionToken, startTime, changeTime := Tester.CreatePasskeySession(t, CTXLOGIN, User.GetUserId())
+ linkResp, err := Tester.Client.OIDCv2.CreateCallback(CTXLOGIN, &oidc_pb.CreateCallbackRequest{
+ AuthRequestId: authRequestID,
+ CallbackKind: &oidc_pb.CreateCallbackRequest_Session{
+ Session: &oidc_pb.Session{
+ SessionId: sessionID,
+ SessionToken: sessionToken,
+ },
+ },
+ })
+ require.NoError(t, err)
+
+ // test implicit callback
+ callback, err := url.Parse(linkResp.GetCallbackUrl())
+ require.NoError(t, err)
+ values, err := url.ParseQuery(callback.Fragment)
+ require.NoError(t, err)
+ accessToken := values.Get("access_token")
+ idToken := values.Get("id_token")
+ refreshToken := values.Get("refresh_token")
+ assert.NotEmpty(t, accessToken)
+ assert.NotEmpty(t, idToken)
+ assert.Empty(t, refreshToken)
+ assert.NotEmpty(t, values.Get("expires_in"))
+ assert.Equal(t, oidc.BearerToken, values.Get("token_type"))
+ assert.Equal(t, "state", values.Get("state"))
+
+ // check id_token / claims
+ provider, err := Tester.CreateRelyingParty(clientID, redirectURIImplicit)
+ require.NoError(t, err)
+ claims, err := rp.VerifyTokens[*oidc.IDTokenClaims](context.Background(), accessToken, idToken, provider.IDTokenVerifier())
+ require.NoError(t, err)
+ assertIDTokenClaims(t, claims, armPasskey, startTime, changeTime)
+
+ // callback on a succeeded request must fail
+ linkResp, err = Tester.Client.OIDCv2.CreateCallback(CTXLOGIN, &oidc_pb.CreateCallbackRequest{
+ AuthRequestId: authRequestID,
+ CallbackKind: &oidc_pb.CreateCallbackRequest_Session{
+ Session: &oidc_pb.Session{
+ SessionId: sessionID,
+ SessionToken: sessionToken,
+ },
+ },
+ })
+ require.Error(t, err)
+}
+
+func TestOPStorage_CreateAccessAndRefreshTokens_code(t *testing.T) {
+ clientID := createClient(t)
+ authRequestID := createAuthRequest(t, clientID, redirectURI, oidc.ScopeOpenID, oidc.ScopeOfflineAccess)
+ sessionID, sessionToken, startTime, changeTime := Tester.CreatePasskeySession(t, CTXLOGIN, User.GetUserId())
+ linkResp, err := Tester.Client.OIDCv2.CreateCallback(CTXLOGIN, &oidc_pb.CreateCallbackRequest{
+ AuthRequestId: authRequestID,
+ CallbackKind: &oidc_pb.CreateCallbackRequest_Session{
+ Session: &oidc_pb.Session{
+ SessionId: sessionID,
+ SessionToken: sessionToken,
+ },
+ },
+ })
+ require.NoError(t, err)
+
+ // test code exchange (expect refresh token to be returned)
+ code := assertCodeResponse(t, linkResp.GetCallbackUrl())
+ tokens, err := exchangeTokens(t, clientID, code)
+ require.NoError(t, err)
+ assertTokens(t, tokens, true)
+ assertIDTokenClaims(t, tokens.IDTokenClaims, armPasskey, startTime, changeTime)
+}
+
+func TestOPStorage_CreateAccessAndRefreshTokens_refresh(t *testing.T) {
+ clientID := createClient(t)
+ provider, err := Tester.CreateRelyingParty(clientID, redirectURI)
+ require.NoError(t, err)
+ authRequestID := createAuthRequest(t, clientID, redirectURI, oidc.ScopeOpenID, oidc.ScopeOfflineAccess)
+ sessionID, sessionToken, startTime, changeTime := Tester.CreatePasskeySession(t, CTXLOGIN, User.GetUserId())
+ linkResp, err := Tester.Client.OIDCv2.CreateCallback(CTXLOGIN, &oidc_pb.CreateCallbackRequest{
+ AuthRequestId: authRequestID,
+ CallbackKind: &oidc_pb.CreateCallbackRequest_Session{
+ Session: &oidc_pb.Session{
+ SessionId: sessionID,
+ SessionToken: sessionToken,
+ },
+ },
+ })
+ require.NoError(t, err)
+
+ // code exchange
+ code := assertCodeResponse(t, linkResp.GetCallbackUrl())
+ tokens, err := exchangeTokens(t, clientID, code)
+ require.NoError(t, err)
+ assertTokens(t, tokens, true)
+ assertIDTokenClaims(t, tokens.IDTokenClaims, armPasskey, startTime, changeTime)
+
+ // test actual refresh grant
+ newTokens, err := refreshTokens(t, clientID, tokens.RefreshToken)
+ require.NoError(t, err)
+ assertTokens(t, newTokens, true)
+ // auth time must still be the initial
+ assertIDTokenClaims(t, newTokens.IDTokenClaims, armPasskey, startTime, changeTime)
+
+ // refresh with an old refresh_token must fail
+ _, err = rp.RefreshAccessToken(provider, tokens.RefreshToken, "", "")
+ require.Error(t, err)
+}
+
+func TestOPStorage_RevokeToken_access_token(t *testing.T) {
+ clientID := createClient(t)
+ provider, err := Tester.CreateRelyingParty(clientID, redirectURI)
+ require.NoError(t, err)
+ authRequestID := createAuthRequest(t, clientID, redirectURI, oidc.ScopeOpenID, oidc.ScopeOfflineAccess)
+ sessionID, sessionToken, startTime, changeTime := Tester.CreatePasskeySession(t, CTXLOGIN, User.GetUserId())
+ linkResp, err := Tester.Client.OIDCv2.CreateCallback(CTXLOGIN, &oidc_pb.CreateCallbackRequest{
+ AuthRequestId: authRequestID,
+ CallbackKind: &oidc_pb.CreateCallbackRequest_Session{
+ Session: &oidc_pb.Session{
+ SessionId: sessionID,
+ SessionToken: sessionToken,
+ },
+ },
+ })
+ require.NoError(t, err)
+
+ // code exchange
+ code := assertCodeResponse(t, linkResp.GetCallbackUrl())
+ tokens, err := exchangeTokens(t, clientID, code)
+ require.NoError(t, err)
+ assertTokens(t, tokens, true)
+ assertIDTokenClaims(t, tokens.IDTokenClaims, armPasskey, startTime, changeTime)
+
+ // revoke access token
+ err = rp.RevokeToken(provider, tokens.AccessToken, "access_token")
+ require.NoError(t, err)
+
+ // userinfo must fail
+ _, err = rp.Userinfo(tokens.AccessToken, tokens.TokenType, tokens.IDTokenClaims.Subject, provider)
+ require.Error(t, err)
+
+ // refresh grant must still work
+ _, err = refreshTokens(t, clientID, tokens.RefreshToken)
+ require.NoError(t, err)
+
+ // revocation with the same access token must not fail (with or without hint)
+ err = rp.RevokeToken(provider, tokens.AccessToken, "access_token")
+ require.NoError(t, err)
+ err = rp.RevokeToken(provider, tokens.AccessToken, "")
+ require.NoError(t, err)
+}
+
+func TestOPStorage_RevokeToken_access_token_invalid_token_hint_type(t *testing.T) {
+ clientID := createClient(t)
+ provider, err := Tester.CreateRelyingParty(clientID, redirectURI)
+ require.NoError(t, err)
+ authRequestID := createAuthRequest(t, clientID, redirectURI, oidc.ScopeOpenID, oidc.ScopeOfflineAccess)
+ sessionID, sessionToken, startTime, changeTime := Tester.CreatePasskeySession(t, CTXLOGIN, User.GetUserId())
+ linkResp, err := Tester.Client.OIDCv2.CreateCallback(CTXLOGIN, &oidc_pb.CreateCallbackRequest{
+ AuthRequestId: authRequestID,
+ CallbackKind: &oidc_pb.CreateCallbackRequest_Session{
+ Session: &oidc_pb.Session{
+ SessionId: sessionID,
+ SessionToken: sessionToken,
+ },
+ },
+ })
+ require.NoError(t, err)
+
+ // code exchange
+ code := assertCodeResponse(t, linkResp.GetCallbackUrl())
+ tokens, err := exchangeTokens(t, clientID, code)
+ require.NoError(t, err)
+ assertTokens(t, tokens, true)
+ assertIDTokenClaims(t, tokens.IDTokenClaims, armPasskey, startTime, changeTime)
+
+ // revoke access token
+ err = rp.RevokeToken(provider, tokens.AccessToken, "refresh_token")
+ require.NoError(t, err)
+
+ // userinfo must fail
+ _, err = rp.Userinfo(tokens.AccessToken, tokens.TokenType, tokens.IDTokenClaims.Subject, provider)
+ require.Error(t, err)
+
+ // refresh grant must still work
+ _, err = refreshTokens(t, clientID, tokens.RefreshToken)
+ require.NoError(t, err)
+}
+
+func TestOPStorage_RevokeToken_refresh_token(t *testing.T) {
+ clientID := createClient(t)
+ provider, err := Tester.CreateRelyingParty(clientID, redirectURI)
+ require.NoError(t, err)
+ authRequestID := createAuthRequest(t, clientID, redirectURI, oidc.ScopeOpenID, oidc.ScopeOfflineAccess)
+ sessionID, sessionToken, startTime, changeTime := Tester.CreatePasskeySession(t, CTXLOGIN, User.GetUserId())
+ linkResp, err := Tester.Client.OIDCv2.CreateCallback(CTXLOGIN, &oidc_pb.CreateCallbackRequest{
+ AuthRequestId: authRequestID,
+ CallbackKind: &oidc_pb.CreateCallbackRequest_Session{
+ Session: &oidc_pb.Session{
+ SessionId: sessionID,
+ SessionToken: sessionToken,
+ },
+ },
+ })
+ require.NoError(t, err)
+
+ // code exchange
+ code := assertCodeResponse(t, linkResp.GetCallbackUrl())
+ tokens, err := exchangeTokens(t, clientID, code)
+ require.NoError(t, err)
+ assertTokens(t, tokens, true)
+ assertIDTokenClaims(t, tokens.IDTokenClaims, armPasskey, startTime, changeTime)
+
+ // revoke refresh token -> invalidates also access token
+ err = rp.RevokeToken(provider, tokens.RefreshToken, "refresh_token")
+ require.NoError(t, err)
+
+ // userinfo must fail
+ _, err = rp.Userinfo(tokens.AccessToken, tokens.TokenType, tokens.IDTokenClaims.Subject, provider)
+ require.Error(t, err)
+
+ // refresh must fail
+ _, err = refreshTokens(t, clientID, tokens.RefreshToken)
+ require.Error(t, err)
+
+ // revocation with the same refresh token must not fail (with or without hint)
+ err = rp.RevokeToken(provider, tokens.RefreshToken, "refresh_token")
+ require.NoError(t, err)
+ err = rp.RevokeToken(provider, tokens.RefreshToken, "")
+ require.NoError(t, err)
+}
+
+func TestOPStorage_RevokeToken_refresh_token_invalid_token_type_hint(t *testing.T) {
+ clientID := createClient(t)
+ provider, err := Tester.CreateRelyingParty(clientID, redirectURI)
+ require.NoError(t, err)
+ authRequestID := createAuthRequest(t, clientID, redirectURI, oidc.ScopeOpenID, oidc.ScopeOfflineAccess)
+ sessionID, sessionToken, startTime, changeTime := Tester.CreatePasskeySession(t, CTXLOGIN, User.GetUserId())
+ linkResp, err := Tester.Client.OIDCv2.CreateCallback(CTXLOGIN, &oidc_pb.CreateCallbackRequest{
+ AuthRequestId: authRequestID,
+ CallbackKind: &oidc_pb.CreateCallbackRequest_Session{
+ Session: &oidc_pb.Session{
+ SessionId: sessionID,
+ SessionToken: sessionToken,
+ },
+ },
+ })
+ require.NoError(t, err)
+
+ // code exchange
+ code := assertCodeResponse(t, linkResp.GetCallbackUrl())
+ tokens, err := exchangeTokens(t, clientID, code)
+ require.NoError(t, err)
+ assertTokens(t, tokens, true)
+ assertIDTokenClaims(t, tokens.IDTokenClaims, armPasskey, startTime, changeTime)
+
+ // revoke refresh token even with a wrong hint
+ err = rp.RevokeToken(provider, tokens.RefreshToken, "access_token")
+ require.NoError(t, err)
+
+ // userinfo must fail
+ _, err = rp.Userinfo(tokens.AccessToken, tokens.TokenType, tokens.IDTokenClaims.Subject, provider)
+ require.Error(t, err)
+
+ // refresh must fail
+ _, err = refreshTokens(t, clientID, tokens.RefreshToken)
+ require.Error(t, err)
+}
+
+func TestOPStorage_RevokeToken_invalid_client(t *testing.T) {
+ clientID := createClient(t)
+ authRequestID := createAuthRequest(t, clientID, redirectURI, oidc.ScopeOpenID, oidc.ScopeOfflineAccess)
+ sessionID, sessionToken, startTime, changeTime := Tester.CreatePasskeySession(t, CTXLOGIN, User.GetUserId())
+ linkResp, err := Tester.Client.OIDCv2.CreateCallback(CTXLOGIN, &oidc_pb.CreateCallbackRequest{
+ AuthRequestId: authRequestID,
+ CallbackKind: &oidc_pb.CreateCallbackRequest_Session{
+ Session: &oidc_pb.Session{
+ SessionId: sessionID,
+ SessionToken: sessionToken,
+ },
+ },
+ })
+ require.NoError(t, err)
+
+ // code exchange
+ code := assertCodeResponse(t, linkResp.GetCallbackUrl())
+ tokens, err := exchangeTokens(t, clientID, code)
+ require.NoError(t, err)
+ assertTokens(t, tokens, true)
+ assertIDTokenClaims(t, tokens.IDTokenClaims, armPasskey, startTime, changeTime)
+
+ // simulate second client (not part of the audience) trying to revoke the token
+ otherClientID := createClient(t)
+ provider, err := Tester.CreateRelyingParty(otherClientID, redirectURI)
+ require.NoError(t, err)
+ err = rp.RevokeToken(provider, tokens.AccessToken, "")
+ require.Error(t, err)
+}
+
+func TestOPStorage_TerminateSession(t *testing.T) {
+ clientID := createClient(t)
+ provider, err := Tester.CreateRelyingParty(clientID, redirectURI)
+ require.NoError(t, err)
+ authRequestID := createAuthRequest(t, clientID, redirectURI)
+ sessionID, sessionToken, startTime, changeTime := Tester.CreatePasskeySession(t, CTXLOGIN, User.GetUserId())
+ linkResp, err := Tester.Client.OIDCv2.CreateCallback(CTXLOGIN, &oidc_pb.CreateCallbackRequest{
+ AuthRequestId: authRequestID,
+ CallbackKind: &oidc_pb.CreateCallbackRequest_Session{
+ Session: &oidc_pb.Session{
+ SessionId: sessionID,
+ SessionToken: sessionToken,
+ },
+ },
+ })
+ require.NoError(t, err)
+
+ // test code exchange
+ code := assertCodeResponse(t, linkResp.GetCallbackUrl())
+ tokens, err := exchangeTokens(t, clientID, code)
+ require.NoError(t, err)
+ assertTokens(t, tokens, false)
+ assertIDTokenClaims(t, tokens.IDTokenClaims, armPasskey, startTime, changeTime)
+
+ // userinfo must not fail
+ _, err = rp.Userinfo(tokens.AccessToken, tokens.TokenType, tokens.IDTokenClaims.Subject, provider)
+ require.NoError(t, err)
+
+ postLogoutRedirect, err := rp.EndSession(provider, tokens.IDToken, logoutRedirectURI, "state")
+ require.NoError(t, err)
+ assert.Equal(t, logoutRedirectURI+"?state=state", postLogoutRedirect.String())
+
+ // userinfo must fail
+ _, err = rp.Userinfo(tokens.AccessToken, tokens.TokenType, tokens.IDTokenClaims.Subject, provider)
+ require.Error(t, err)
+}
+
+func TestOPStorage_TerminateSession_refresh_grant(t *testing.T) {
+ clientID := createClient(t)
+ provider, err := Tester.CreateRelyingParty(clientID, redirectURI)
+ require.NoError(t, err)
+ authRequestID := createAuthRequest(t, clientID, redirectURI, oidc.ScopeOpenID, oidc.ScopeOfflineAccess)
+ sessionID, sessionToken, startTime, changeTime := Tester.CreatePasskeySession(t, CTXLOGIN, User.GetUserId())
+ linkResp, err := Tester.Client.OIDCv2.CreateCallback(CTXLOGIN, &oidc_pb.CreateCallbackRequest{
+ AuthRequestId: authRequestID,
+ CallbackKind: &oidc_pb.CreateCallbackRequest_Session{
+ Session: &oidc_pb.Session{
+ SessionId: sessionID,
+ SessionToken: sessionToken,
+ },
+ },
+ })
+ require.NoError(t, err)
+
+ // test code exchange
+ code := assertCodeResponse(t, linkResp.GetCallbackUrl())
+ tokens, err := exchangeTokens(t, clientID, code)
+ require.NoError(t, err)
+ assertTokens(t, tokens, true)
+ assertIDTokenClaims(t, tokens.IDTokenClaims, armPasskey, startTime, changeTime)
+
+ // userinfo must not fail
+ _, err = rp.Userinfo(tokens.AccessToken, tokens.TokenType, tokens.IDTokenClaims.Subject, provider)
+ require.NoError(t, err)
+
+ postLogoutRedirect, err := rp.EndSession(provider, tokens.IDToken, logoutRedirectURI, "state")
+ require.NoError(t, err)
+ assert.Equal(t, logoutRedirectURI+"?state=state", postLogoutRedirect.String())
+
+ // userinfo must fail
+ _, err = rp.Userinfo(tokens.AccessToken, tokens.TokenType, tokens.IDTokenClaims.Subject, provider)
+ require.Error(t, err)
+
+ refreshedTokens, err := refreshTokens(t, clientID, tokens.RefreshToken)
+ require.NoError(t, err)
+
+ // userinfo must not fail
+ _, err = rp.Userinfo(refreshedTokens.AccessToken, refreshedTokens.TokenType, refreshedTokens.IDTokenClaims.Subject, provider)
+ require.NoError(t, err)
+}
+
+func TestOPStorage_TerminateSession_empty_id_token_hint(t *testing.T) {
+ clientID := createClient(t)
+ provider, err := Tester.CreateRelyingParty(clientID, redirectURI)
+ require.NoError(t, err)
+ authRequestID := createAuthRequest(t, clientID, redirectURI)
+ sessionID, sessionToken, startTime, changeTime := Tester.CreatePasskeySession(t, CTXLOGIN, User.GetUserId())
+ linkResp, err := Tester.Client.OIDCv2.CreateCallback(CTXLOGIN, &oidc_pb.CreateCallbackRequest{
+ AuthRequestId: authRequestID,
+ CallbackKind: &oidc_pb.CreateCallbackRequest_Session{
+ Session: &oidc_pb.Session{
+ SessionId: sessionID,
+ SessionToken: sessionToken,
+ },
+ },
+ })
+ require.NoError(t, err)
+
+ // test code exchange
+ code := assertCodeResponse(t, linkResp.GetCallbackUrl())
+ tokens, err := exchangeTokens(t, clientID, code)
+ require.NoError(t, err)
+ assertTokens(t, tokens, false)
+ assertIDTokenClaims(t, tokens.IDTokenClaims, armPasskey, startTime, changeTime)
+
+ postLogoutRedirect, err := rp.EndSession(provider, "", logoutRedirectURI, "state")
+ require.NoError(t, err)
+ assert.Equal(t, http_utils.BuildOrigin(Tester.Host(), Tester.Config.ExternalSecure)+Tester.Config.OIDC.DefaultLogoutURLV2+logoutRedirectURI+"?state=state", postLogoutRedirect.String())
+
+ // userinfo must not fail until login UI terminated session
+ _, err = rp.Userinfo(tokens.AccessToken, tokens.TokenType, tokens.IDTokenClaims.Subject, provider)
+ require.NoError(t, err)
+
+ // simulate termination by login UI
+ _, err = Tester.Client.SessionV2.DeleteSession(CTXLOGIN, &session.DeleteSessionRequest{
+ SessionId: sessionID,
+ SessionToken: gu.Ptr(sessionToken),
+ })
+ require.NoError(t, err)
+
+ // userinfo must fail
+ _, err = rp.Userinfo(tokens.AccessToken, tokens.TokenType, tokens.IDTokenClaims.Subject, provider)
+ require.Error(t, err)
+}
+
+func exchangeTokens(t testing.TB, clientID, code string) (*oidc.Tokens[*oidc.IDTokenClaims], error) {
+ provider, err := Tester.CreateRelyingParty(clientID, redirectURI)
+ require.NoError(t, err)
+
+ codeVerifier := "codeVerifier"
+ return rp.CodeExchange[*oidc.IDTokenClaims](context.Background(), code, provider, rp.WithCodeVerifier(codeVerifier))
+}
+
+func refreshTokens(t testing.TB, clientID, refreshToken string) (*oidc.Tokens[*oidc.IDTokenClaims], error) {
+ provider, err := Tester.CreateRelyingParty(clientID, redirectURI)
+ require.NoError(t, err)
+
+ tokens, err := rp.RefreshAccessToken(provider, refreshToken, "", "")
+ if err != nil {
+ return nil, err
+ }
+ idToken, _ := tokens.Extra("id_token").(string)
+ claims, err := rp.VerifyTokens[*oidc.IDTokenClaims](context.Background(), tokens.AccessToken, idToken, provider.IDTokenVerifier())
+ if err != nil {
+ return nil, err
+ }
+ return &oidc.Tokens[*oidc.IDTokenClaims]{
+ Token: tokens,
+ IDToken: idToken,
+ IDTokenClaims: claims,
+ }, nil
+}
+
+func assertCodeResponse(t *testing.T, callback string) string {
+ callbackURL, err := url.Parse(callback)
+ require.NoError(t, err)
+ code := callbackURL.Query().Get("code")
+ require.NotEmpty(t, code)
+ assert.Equal(t, "state", callbackURL.Query().Get("state"))
+ return code
+}
+
+func assertTokens(t *testing.T, tokens *oidc.Tokens[*oidc.IDTokenClaims], requireRefreshToken bool) {
+ assert.NotEmpty(t, tokens.AccessToken)
+ assert.NotEmpty(t, tokens.IDToken)
+ if requireRefreshToken {
+ assert.NotEmpty(t, tokens.RefreshToken)
+ } else {
+ assert.Empty(t, tokens.RefreshToken)
+ }
+}
+
+func assertIDTokenClaims(t *testing.T, claims *oidc.IDTokenClaims, arm []string, sessionStart, sessionChange time.Time) {
+ assert.Equal(t, User.GetUserId(), claims.Subject)
+ assert.Equal(t, arm, claims.AuthenticationMethodsReferences)
+ assertOIDCTimeRange(t, claims.AuthTime, sessionStart, sessionChange)
+}
diff --git a/internal/api/oidc/client.go b/internal/api/oidc/client.go
index e1a26f866..7a418df9a 100644
--- a/internal/api/oidc/client.go
+++ b/internal/api/oidc/client.go
@@ -6,6 +6,7 @@ import (
"encoding/json"
"fmt"
"strings"
+ "time"
"github.com/dop251/goja"
"github.com/zitadel/logging"
@@ -17,6 +18,7 @@ import (
"github.com/zitadel/zitadel/internal/actions/object"
"github.com/zitadel/zitadel/internal/api/authz"
api_http "github.com/zitadel/zitadel/internal/api/http"
+ "github.com/zitadel/zitadel/internal/command"
"github.com/zitadel/zitadel/internal/crypto"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/errors"
@@ -66,7 +68,7 @@ func (o *OPStorage) GetClientByClientID(ctx context.Context, id string) (_ op.Cl
return nil, err
}
- return ClientFromBusiness(client, o.defaultLoginURL, accessTokenLifetime, idTokenLifetime, allowedScopes)
+ return ClientFromBusiness(client, o.defaultLoginURL, o.defaultLoginURLV2, accessTokenLifetime, idTokenLifetime, allowedScopes)
}
func (o *OPStorage) GetKeyByIDAndClientID(ctx context.Context, keyID, userID string) (_ *jose.JSONWebKey, err error) {
@@ -119,18 +121,26 @@ func (o *OPStorage) AuthorizeClientIDSecret(ctx context.Context, id string, secr
func (o *OPStorage) SetUserinfoFromToken(ctx context.Context, userInfo *oidc.UserInfo, tokenID, subject, origin string) (err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
+
+ if strings.HasPrefix(tokenID, command.IDPrefixV2) {
+ token, err := o.query.ActiveAccessTokenByToken(ctx, tokenID)
+ if err != nil {
+ return err
+ }
+ if err = o.isOriginAllowed(ctx, token.ClientID, origin); err != nil {
+ return err
+ }
+ return o.setUserinfo(ctx, userInfo, token.UserID, token.ClientID, token.Scope, nil)
+ }
+
token, err := o.repo.TokenByIDs(ctx, subject, tokenID)
if err != nil {
return errors.ThrowPermissionDenied(nil, "OIDC-Dsfb2", "token is not valid or has expired")
}
if token.ApplicationID != "" {
- app, err := o.query.AppByOIDCClientID(ctx, token.ApplicationID, false)
- if err != nil {
+ if err = o.isOriginAllowed(ctx, token.ApplicationID, origin); err != nil {
return err
}
- if origin != "" && !api_http.IsOriginAllowed(app.OIDCConfig.AllowedOrigins, origin) {
- return errors.ThrowPermissionDenied(nil, "OIDC-da1f3", "origin is not allowed")
- }
}
return o.setUserinfo(ctx, userInfo, token.UserID, token.ApplicationID, token.Scopes, nil)
}
@@ -153,7 +163,37 @@ func (o *OPStorage) SetUserinfoFromScopes(ctx context.Context, userInfo *oidc.Us
return o.setUserinfo(ctx, userInfo, userID, applicationID, scopes, nil)
}
-func (o *OPStorage) SetIntrospectionFromToken(ctx context.Context, introspection *oidc.IntrospectionResponse, tokenID, subject, clientID string) error {
+// SetUserinfoFromRequest extends the SetUserinfoFromScopes during the id_token generation.
+// This is required for V2 tokens to be able to set the sessionID (`sid`) claim.
+func (o *OPStorage) SetUserinfoFromRequest(ctx context.Context, userinfo *oidc.UserInfo, request op.IDTokenRequest, _ []string) error {
+ switch t := request.(type) {
+ case *AuthRequestV2:
+ userinfo.AppendClaims("sid", t.SessionID)
+ case *RefreshTokenRequestV2:
+ userinfo.AppendClaims("sid", t.SessionID)
+ }
+ return nil
+}
+
+func (o *OPStorage) SetIntrospectionFromToken(ctx context.Context, introspection *oidc.IntrospectionResponse, tokenID, subject, clientID string) (err error) {
+ ctx, span := tracing.NewSpan(ctx)
+ defer func() { span.EndWithError(err) }()
+
+ if strings.HasPrefix(tokenID, command.IDPrefixV2) {
+ token, err := o.query.ActiveAccessTokenByToken(ctx, tokenID)
+ if err != nil {
+ return err
+ }
+ projectID, err := o.query.ProjectIDFromClientID(ctx, clientID, false)
+ if err != nil {
+ return errors.ThrowPermissionDenied(nil, "OIDC-Adfg5", "client not found")
+ }
+ return o.introspect(ctx, introspection,
+ tokenID, token.UserID, token.ClientID, projectID,
+ token.Audience, token.Scope,
+ token.AccessTokenCreation, token.AccessTokenExpiration)
+ }
+
token, err := o.repo.TokenByIDs(ctx, subject, tokenID)
if err != nil {
return errors.ThrowPermissionDenied(nil, "OIDC-Dsfb2", "token is not valid or has expired")
@@ -168,27 +208,10 @@ func (o *OPStorage) SetIntrospectionFromToken(ctx context.Context, introspection
return errors.ThrowPreconditionFailed(err, "OIDC-AGefw", "Errors.Internal")
}
}
- for _, aud := range token.Audience {
- if aud == clientID || aud == projectID {
- userInfo := new(oidc.UserInfo)
- err := o.setUserinfo(ctx, userInfo, subject, clientID, token.Scopes, []string{projectID}) // always
- if err != nil {
- return err
- }
- introspection.SetUserInfo(userInfo)
- introspection.Scope = token.Scopes
- introspection.ClientID = token.ApplicationID
- introspection.TokenType = oidc.BearerToken
- introspection.Expiration = oidc.FromTime(token.Expiration)
- introspection.IssuedAt = oidc.FromTime(token.CreationDate)
- introspection.NotBefore = oidc.FromTime(token.CreationDate)
- introspection.Audience = token.Audience
- introspection.Issuer = op.IssuerFromContext(ctx)
- introspection.JWTID = token.ID
- return nil
- }
- }
- return errors.ThrowPermissionDenied(nil, "OIDC-sdg3G", "token is not valid for this client")
+ return o.introspect(ctx, introspection,
+ token.ID, token.UserID, token.ApplicationID, projectID,
+ token.Audience, token.Scopes,
+ token.CreationDate, token.Expiration)
}
func (o *OPStorage) ClientCredentialsTokenRequest(ctx context.Context, clientID string, scope []string) (op.TokenRequest, error) {
@@ -230,6 +253,55 @@ func (o *OPStorage) ClientCredentials(ctx context.Context, clientID, clientSecre
}, nil
}
+// isOriginAllowed checks whether a call by the client to the endpoint is allowed from the provided origin
+// if no origin is provided, no error will be returned
+func (o *OPStorage) isOriginAllowed(ctx context.Context, clientID, origin string) error {
+ if origin == "" {
+ return nil
+ }
+ app, err := o.query.AppByOIDCClientID(ctx, clientID, false)
+ if err != nil {
+ return err
+ }
+ if api_http.IsOriginAllowed(app.OIDCConfig.AllowedOrigins, origin) {
+ return nil
+ }
+ return errors.ThrowPermissionDenied(nil, "OIDC-da1f3", "origin is not allowed")
+}
+
+func (o *OPStorage) introspect(
+ ctx context.Context,
+ introspection *oidc.IntrospectionResponse,
+ tokenID, subject, clientID, projectID string,
+ audience, scope []string,
+ tokenCreation, tokenExpiration time.Time,
+) (err error) {
+ ctx, span := tracing.NewSpan(ctx)
+ defer func() { span.EndWithError(err) }()
+
+ for _, aud := range audience {
+ if aud == clientID || aud == projectID {
+ userInfo := new(oidc.UserInfo)
+ err = o.setUserinfo(ctx, userInfo, subject, clientID, scope, []string{projectID}) // always
+ if err != nil {
+ return err
+ }
+ introspection.SetUserInfo(userInfo)
+ introspection.Scope = scope
+ introspection.ClientID = clientID
+ introspection.TokenType = oidc.BearerToken
+ introspection.Expiration = oidc.FromTime(tokenExpiration)
+ introspection.IssuedAt = oidc.FromTime(tokenCreation)
+ introspection.NotBefore = oidc.FromTime(tokenCreation)
+ introspection.Audience = audience
+ introspection.Issuer = op.IssuerFromContext(ctx)
+ introspection.JWTID = tokenID
+ return nil
+ }
+ }
+ return errors.ThrowPermissionDenied(nil, "OIDC-sdg3G", "token is not valid for this client")
+}
+
func (o *OPStorage) checkOrgScopes(ctx context.Context, user *query.User, scopes []string) ([]string, error) {
for i := len(scopes) - 1; i >= 0; i-- {
scope := scopes[i]
diff --git a/internal/api/oidc/client_converter.go b/internal/api/oidc/client_converter.go
index 6b32f3892..5f3f25f75 100644
--- a/internal/api/oidc/client_converter.go
+++ b/internal/api/oidc/client_converter.go
@@ -7,6 +7,7 @@ import (
"github.com/zitadel/oidc/v2/pkg/oidc"
"github.com/zitadel/oidc/v2/pkg/op"
+ "github.com/zitadel/zitadel/internal/command"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/errors"
"github.com/zitadel/zitadel/internal/query"
@@ -15,18 +16,20 @@ import (
type Client struct {
app *query.App
defaultLoginURL string
+ defaultLoginURLV2 string
defaultAccessTokenLifetime time.Duration
defaultIdTokenLifetime time.Duration
allowedScopes []string
}
-func ClientFromBusiness(app *query.App, defaultLoginURL string, defaultAccessTokenLifetime, defaultIdTokenLifetime time.Duration, allowedScopes []string) (op.Client, error) {
+func ClientFromBusiness(app *query.App, defaultLoginURL, defaultLoginURLV2 string, defaultAccessTokenLifetime, defaultIdTokenLifetime time.Duration, allowedScopes []string) (op.Client, error) {
if app.OIDCConfig == nil {
return nil, errors.ThrowInvalidArgument(nil, "OIDC-d5bhD", "client is not a proper oidc application")
}
return &Client{
app: app,
defaultLoginURL: defaultLoginURL,
+ defaultLoginURLV2: defaultLoginURLV2,
defaultAccessTokenLifetime: defaultAccessTokenLifetime,
defaultIdTokenLifetime: defaultIdTokenLifetime,
allowedScopes: allowedScopes},
@@ -46,6 +49,9 @@ func (c *Client) GetID() string {
}
func (c *Client) LoginURL(id string) string {
+ if strings.HasPrefix(id, command.IDPrefixV2) {
+ return c.defaultLoginURLV2 + id
+ }
return c.defaultLoginURL + id
}
diff --git a/internal/api/oidc/client_integration_test.go b/internal/api/oidc/client_integration_test.go
new file mode 100644
index 000000000..513f25a75
--- /dev/null
+++ b/internal/api/oidc/client_integration_test.go
@@ -0,0 +1,135 @@
+//go:build integration
+
+package oidc_test
+
+import (
+ "context"
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+ "github.com/zitadel/oidc/v2/pkg/client/rp"
+ "github.com/zitadel/oidc/v2/pkg/client/rs"
+ "github.com/zitadel/oidc/v2/pkg/oidc"
+
+ "github.com/zitadel/zitadel/pkg/grpc/authn"
+ "github.com/zitadel/zitadel/pkg/grpc/management"
+ oidc_pb "github.com/zitadel/zitadel/pkg/grpc/oidc/v2alpha"
+)
+
+func TestOPStorage_SetUserinfoFromToken(t *testing.T) {
+ clientID := createClient(t)
+ authRequestID := createAuthRequest(t, clientID, redirectURI, oidc.ScopeOpenID, oidc.ScopeProfile, oidc.ScopeEmail, oidc.ScopeOfflineAccess)
+ sessionID, sessionToken, startTime, changeTime := Tester.CreatePasskeySession(t, CTXLOGIN, User.GetUserId())
+ linkResp, err := Tester.Client.OIDCv2.CreateCallback(CTXLOGIN, &oidc_pb.CreateCallbackRequest{
+ AuthRequestId: authRequestID,
+ CallbackKind: &oidc_pb.CreateCallbackRequest_Session{
+ Session: &oidc_pb.Session{
+ SessionId: sessionID,
+ SessionToken: sessionToken,
+ },
+ },
+ })
+ require.NoError(t, err)
+
+ // code exchange
+ code := assertCodeResponse(t, linkResp.GetCallbackUrl())
+ tokens, err := exchangeTokens(t, clientID, code)
+ require.NoError(t, err)
+ assertTokens(t, tokens, true)
+ assertIDTokenClaims(t, tokens.IDTokenClaims, armPasskey, startTime, changeTime)
+
+ // test actual userinfo
+ provider, err := Tester.CreateRelyingParty(clientID, redirectURI)
+ require.NoError(t, err)
+ userinfo, err := rp.Userinfo(tokens.AccessToken, tokens.TokenType, tokens.IDTokenClaims.Subject, provider)
+ require.NoError(t, err)
+ assertUserinfo(t, userinfo)
+}
+
+func TestOPStorage_SetIntrospectionFromToken(t *testing.T) {
+ project, err := Tester.CreateProject(CTX)
+ require.NoError(t, err)
+ app, err := Tester.CreateOIDCNativeClient(CTX, redirectURI, logoutRedirectURI, project.GetId())
+ require.NoError(t, err)
+ api, err := Tester.CreateAPIClient(CTX, project.GetId())
+ require.NoError(t, err)
+ keyResp, err := Tester.Client.Mgmt.AddAppKey(CTX, &management.AddAppKeyRequest{
+ ProjectId: project.GetId(),
+ AppId: api.GetAppId(),
+ Type: authn.KeyType_KEY_TYPE_JSON,
+ ExpirationDate: nil,
+ })
+ require.NoError(t, err)
+ resourceServer, err := Tester.CreateResourceServer(keyResp.GetKeyDetails())
+ require.NoError(t, err)
+
+ scope := []string{oidc.ScopeOpenID, oidc.ScopeProfile, oidc.ScopeEmail, oidc.ScopeOfflineAccess}
+ authRequestID := createAuthRequest(t, app.GetClientId(), redirectURI, scope...)
+ sessionID, sessionToken, startTime, changeTime := Tester.CreatePasskeySession(t, CTXLOGIN, User.GetUserId())
+ linkResp, err := Tester.Client.OIDCv2.CreateCallback(CTXLOGIN, &oidc_pb.CreateCallbackRequest{
+ AuthRequestId: authRequestID,
+ CallbackKind: &oidc_pb.CreateCallbackRequest_Session{
+ Session: &oidc_pb.Session{
+ SessionId: sessionID,
+ SessionToken: sessionToken,
+ },
+ },
+ })
+ require.NoError(t, err)
+
+ // code exchange
+ code := assertCodeResponse(t, linkResp.GetCallbackUrl())
+ tokens, err := exchangeTokens(t, app.GetClientId(), code)
+ require.NoError(t, err)
+ assertTokens(t, tokens, true)
+ assertIDTokenClaims(t, tokens.IDTokenClaims, armPasskey, startTime, changeTime)
+
+ // test actual introspection
+ introspection, err := rs.Introspect(context.Background(), resourceServer, tokens.AccessToken)
+ require.NoError(t, err)
+ assertIntrospection(t, introspection,
+ Tester.OIDCIssuer(), app.GetClientId(),
+ scope, []string{app.GetClientId(), api.GetClientId(), project.GetId()},
+ tokens.Expiry, tokens.Expiry.Add(-12*time.Hour))
+}
+
+func assertUserinfo(t *testing.T, userinfo *oidc.UserInfo) {
+ assert.Equal(t, User.GetUserId(), userinfo.Subject)
+ assert.Equal(t, "Mickey", userinfo.GivenName)
+ assert.Equal(t, "Mouse", userinfo.FamilyName)
+ assert.Equal(t, "Mickey Mouse", userinfo.Name)
+ assert.NotEmpty(t, userinfo.PreferredUsername)
+ assert.Equal(t, userinfo.PreferredUsername, userinfo.Email)
+ assert.False(t, bool(userinfo.EmailVerified))
+ assertOIDCTime(t, userinfo.UpdatedAt, User.GetDetails().GetChangeDate().AsTime())
+}
+
+func assertIntrospection(
+ t *testing.T,
+ introspection *oidc.IntrospectionResponse,
+ issuer, clientID string,
+ scope, audience []string,
+ expiration, creation time.Time,
+) {
+ assert.True(t, introspection.Active)
+ assert.Equal(t, scope, []string(introspection.Scope))
+ assert.Equal(t, clientID, introspection.ClientID)
+ assert.Equal(t, oidc.BearerToken, introspection.TokenType)
+ assertOIDCTime(t, introspection.Expiration, expiration)
+ assertOIDCTime(t, introspection.IssuedAt, creation)
+ assertOIDCTime(t, introspection.NotBefore, creation)
+ assert.Equal(t, User.GetUserId(), introspection.Subject)
+ assert.ElementsMatch(t, audience, introspection.Audience)
+ assert.Equal(t, issuer, introspection.Issuer)
+ assert.NotEmpty(t, introspection.JWTID)
+ assert.NotEmpty(t, introspection.Username)
+ assert.Equal(t, introspection.Username, introspection.PreferredUsername)
+ assert.Equal(t, "Mickey", introspection.GivenName)
+ assert.Equal(t, "Mouse", introspection.FamilyName)
+ assert.Equal(t, "Mickey Mouse", introspection.Name)
+ assert.Equal(t, introspection.Username, introspection.Email)
+ assert.False(t, bool(introspection.EmailVerified))
+ assertOIDCTime(t, introspection.UpdatedAt, User.GetDetails().GetChangeDate().AsTime())
+}
diff --git a/internal/api/oidc/oidc_integration_test.go b/internal/api/oidc/oidc_integration_test.go
new file mode 100644
index 000000000..da34b30d7
--- /dev/null
+++ b/internal/api/oidc/oidc_integration_test.go
@@ -0,0 +1,292 @@
+//go:build integration
+
+package oidc_test
+
+import (
+ "context"
+ "fmt"
+ "os"
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+ "github.com/zitadel/oidc/v2/pkg/client/rp"
+ "github.com/zitadel/oidc/v2/pkg/oidc"
+ "google.golang.org/grpc/metadata"
+
+ "github.com/zitadel/zitadel/internal/domain"
+ "github.com/zitadel/zitadel/internal/integration"
+ "github.com/zitadel/zitadel/pkg/grpc/auth"
+ oidc_pb "github.com/zitadel/zitadel/pkg/grpc/oidc/v2alpha"
+ session "github.com/zitadel/zitadel/pkg/grpc/session/v2alpha"
+ user "github.com/zitadel/zitadel/pkg/grpc/user/v2alpha"
+)
+
+var (
+ CTX context.Context
+ CTXLOGIN context.Context
+ Tester *integration.Tester
+ User *user.AddHumanUserResponse
+)
+
+const (
+ redirectURI = "oidcintegrationtest://callback"
+ redirectURIImplicit = "http://localhost:9999/callback"
+ logoutRedirectURI = "oidcintegrationtest://logged-out"
+ zitadelAudienceScope = domain.ProjectIDScope + domain.ProjectIDScopeZITADEL + domain.AudSuffix
+)
+
+func TestMain(m *testing.M) {
+ os.Exit(func() int {
+ ctx, errCtx, cancel := integration.Contexts(5 * time.Minute)
+ defer cancel()
+
+ Tester = integration.NewTester(ctx)
+ defer Tester.Done()
+
+ CTX, _ = Tester.WithAuthorization(ctx, integration.OrgOwner), errCtx
+ User = Tester.CreateHumanUser(CTX)
+ Tester.SetUserPassword(CTX, User.GetUserId(), integration.UserPassword)
+ Tester.RegisterUserPasskey(CTX, User.GetUserId())
+ CTXLOGIN, _ = Tester.WithAuthorization(ctx, integration.Login), errCtx
+ return m.Run()
+ }())
+}
+
+func Test_ZITADEL_API_missing_audience_scope(t *testing.T) {
+ clientID := createClient(t)
+ authRequestID := createAuthRequest(t, clientID, redirectURI, oidc.ScopeOpenID)
+ sessionID, sessionToken, startTime, changeTime := Tester.CreatePasskeySession(t, CTXLOGIN, User.GetUserId())
+ linkResp, err := Tester.Client.OIDCv2.CreateCallback(CTXLOGIN, &oidc_pb.CreateCallbackRequest{
+ AuthRequestId: authRequestID,
+ CallbackKind: &oidc_pb.CreateCallbackRequest_Session{
+ Session: &oidc_pb.Session{
+ SessionId: sessionID,
+ SessionToken: sessionToken,
+ },
+ },
+ })
+ require.NoError(t, err)
+
+ // code exchange
+ code := assertCodeResponse(t, linkResp.GetCallbackUrl())
+ tokens, err := exchangeTokens(t, clientID, code)
+ require.NoError(t, err)
+ assertTokens(t, tokens, false)
+ assertIDTokenClaims(t, tokens.IDTokenClaims, armPasskey, startTime, changeTime)
+
+ ctx := metadata.AppendToOutgoingContext(context.Background(), "Authorization", fmt.Sprintf("%s %s", tokens.TokenType, tokens.AccessToken))
+
+ myUserResp, err := Tester.Client.Auth.GetMyUser(ctx, &auth.GetMyUserRequest{})
+ require.Error(t, err)
+ require.Nil(t, myUserResp)
+}
+
+func Test_ZITADEL_API_missing_authentication(t *testing.T) {
+ clientID := createClient(t)
+ authRequestID := createAuthRequest(t, clientID, redirectURI, oidc.ScopeOpenID, zitadelAudienceScope)
+ createResp, err := Tester.Client.SessionV2.CreateSession(CTX, &session.CreateSessionRequest{
+ Checks: &session.Checks{
+ User: &session.CheckUser{
+ Search: &session.CheckUser_UserId{UserId: User.GetUserId()},
+ },
+ },
+ Domain: Tester.Config.ExternalDomain,
+ })
+ require.NoError(t, err)
+ linkResp, err := Tester.Client.OIDCv2.CreateCallback(CTXLOGIN, &oidc_pb.CreateCallbackRequest{
+ AuthRequestId: authRequestID,
+ CallbackKind: &oidc_pb.CreateCallbackRequest_Session{
+ Session: &oidc_pb.Session{
+ SessionId: createResp.GetSessionId(),
+ SessionToken: createResp.GetSessionToken(),
+ },
+ },
+ })
+ require.NoError(t, err)
+
+ // code exchange
+ code := assertCodeResponse(t, linkResp.GetCallbackUrl())
+ tokens, err := exchangeTokens(t, clientID, code)
+ require.NoError(t, err)
+
+ ctx := metadata.AppendToOutgoingContext(context.Background(), "Authorization", fmt.Sprintf("%s %s", tokens.TokenType, tokens.AccessToken))
+
+ myUserResp, err := Tester.Client.Auth.GetMyUser(ctx, &auth.GetMyUserRequest{})
+ require.Error(t, err)
+ require.Nil(t, myUserResp)
+}
+
+func Test_ZITADEL_API_missing_mfa(t *testing.T) {
+ clientID := createClient(t)
+ authRequestID := createAuthRequest(t, clientID, redirectURI, oidc.ScopeOpenID, zitadelAudienceScope)
+ sessionID, sessionToken, startTime, changeTime := Tester.CreatePasswordSession(t, CTX, User.GetUserId(), integration.UserPassword)
+ linkResp, err := Tester.Client.OIDCv2.CreateCallback(CTXLOGIN, &oidc_pb.CreateCallbackRequest{
+ AuthRequestId: authRequestID,
+ CallbackKind: &oidc_pb.CreateCallbackRequest_Session{
+ Session: &oidc_pb.Session{
+ SessionId: sessionID,
+ SessionToken: sessionToken,
+ },
+ },
+ })
+ require.NoError(t, err)
+
+ // code exchange
+ code := assertCodeResponse(t, linkResp.GetCallbackUrl())
+ tokens, err := exchangeTokens(t, clientID, code)
+ require.NoError(t, err)
+ assertIDTokenClaims(t, tokens.IDTokenClaims, armPassword, startTime, changeTime)
+
+ ctx := metadata.AppendToOutgoingContext(context.Background(), "Authorization", fmt.Sprintf("%s %s", tokens.TokenType, tokens.AccessToken))
+
+ myUserResp, err := Tester.Client.Auth.GetMyUser(ctx, &auth.GetMyUserRequest{})
+ require.Error(t, err)
+ require.Nil(t, myUserResp)
+}
+
+func Test_ZITADEL_API_success(t *testing.T) {
+ clientID := createClient(t)
+ authRequestID := createAuthRequest(t, clientID, redirectURI, oidc.ScopeOpenID, zitadelAudienceScope)
+ sessionID, sessionToken, startTime, changeTime := Tester.CreatePasskeySession(t, CTXLOGIN, User.GetUserId())
+ linkResp, err := Tester.Client.OIDCv2.CreateCallback(CTXLOGIN, &oidc_pb.CreateCallbackRequest{
+ AuthRequestId: authRequestID,
+ CallbackKind: &oidc_pb.CreateCallbackRequest_Session{
+ Session: &oidc_pb.Session{
+ SessionId: sessionID,
+ SessionToken: sessionToken,
+ },
+ },
+ })
+ require.NoError(t, err)
+
+ // code exchange
+ code := assertCodeResponse(t, linkResp.GetCallbackUrl())
+ tokens, err := exchangeTokens(t, clientID, code)
+ require.NoError(t, err)
+ assertTokens(t, tokens, false)
+ assertIDTokenClaims(t, tokens.IDTokenClaims, armPasskey, startTime, changeTime)
+
+ ctx := metadata.AppendToOutgoingContext(context.Background(), "Authorization", fmt.Sprintf("%s %s", tokens.TokenType, tokens.AccessToken))
+
+ myUserResp, err := Tester.Client.Auth.GetMyUser(ctx, &auth.GetMyUserRequest{})
+ require.NoError(t, err)
+ require.Equal(t, User.GetUserId(), myUserResp.GetUser().GetId())
+}
+
+func Test_ZITADEL_API_inactive_access_token(t *testing.T) {
+ clientID := createClient(t)
+ authRequestID := createAuthRequest(t, clientID, redirectURI, oidc.ScopeOpenID, oidc.ScopeOfflineAccess, zitadelAudienceScope)
+ sessionID, sessionToken, startTime, changeTime := Tester.CreatePasskeySession(t, CTXLOGIN, User.GetUserId())
+ linkResp, err := Tester.Client.OIDCv2.CreateCallback(CTXLOGIN, &oidc_pb.CreateCallbackRequest{
+ AuthRequestId: authRequestID,
+ CallbackKind: &oidc_pb.CreateCallbackRequest_Session{
+ Session: &oidc_pb.Session{
+ SessionId: sessionID,
+ SessionToken: sessionToken,
+ },
+ },
+ })
+ require.NoError(t, err)
+
+ // code exchange
+ code := assertCodeResponse(t, linkResp.GetCallbackUrl())
+ tokens, err := exchangeTokens(t, clientID, code)
+ require.NoError(t, err)
+ assertTokens(t, tokens, true)
+ assertIDTokenClaims(t, tokens.IDTokenClaims, armPasskey, startTime, changeTime)
+
+ // make sure token works
+ ctx := metadata.AppendToOutgoingContext(context.Background(), "Authorization", fmt.Sprintf("%s %s", tokens.TokenType, tokens.AccessToken))
+ myUserResp, err := Tester.Client.Auth.GetMyUser(ctx, &auth.GetMyUserRequest{})
+ require.NoError(t, err)
+ require.Equal(t, User.GetUserId(), myUserResp.GetUser().GetId())
+
+ // refresh token
+ newTokens, err := refreshTokens(t, clientID, tokens.RefreshToken)
+ require.NoError(t, err)
+ assert.NotEmpty(t, newTokens.AccessToken)
+
+ // use invalidated token
+ ctx = metadata.AppendToOutgoingContext(context.Background(), "Authorization", fmt.Sprintf("%s %s", tokens.TokenType, tokens.AccessToken))
+ myUserResp, err = Tester.Client.Auth.GetMyUser(ctx, &auth.GetMyUserRequest{})
+ require.Error(t, err)
+ require.Nil(t, myUserResp)
+}
+
+func Test_ZITADEL_API_terminated_session(t *testing.T) {
+ clientID := createClient(t)
+ provider, err := Tester.CreateRelyingParty(clientID, redirectURI)
+ require.NoError(t, err)
+ authRequestID := createAuthRequest(t, clientID, redirectURI, oidc.ScopeOpenID, oidc.ScopeOfflineAccess, zitadelAudienceScope)
+ sessionID, sessionToken, startTime, changeTime := Tester.CreatePasskeySession(t, CTXLOGIN, User.GetUserId())
+ linkResp, err := Tester.Client.OIDCv2.CreateCallback(CTXLOGIN, &oidc_pb.CreateCallbackRequest{
+ AuthRequestId: authRequestID,
+ CallbackKind: &oidc_pb.CreateCallbackRequest_Session{
+ Session: &oidc_pb.Session{
+ SessionId: sessionID,
+ SessionToken: sessionToken,
+ },
+ },
+ })
+ require.NoError(t, err)
+
+ // code exchange
+ code := assertCodeResponse(t, linkResp.GetCallbackUrl())
+ tokens, err := exchangeTokens(t, clientID, code)
+ require.NoError(t, err)
+ assertTokens(t, tokens, true)
+ assertIDTokenClaims(t, tokens.IDTokenClaims, armPasskey, startTime, changeTime)
+
+ // make sure token works
+ ctx := metadata.AppendToOutgoingContext(context.Background(), "Authorization", fmt.Sprintf("%s %s", tokens.TokenType, tokens.AccessToken))
+ myUserResp, err := Tester.Client.Auth.GetMyUser(ctx, &auth.GetMyUserRequest{})
+ require.NoError(t, err)
+ require.Equal(t, User.GetUserId(), myUserResp.GetUser().GetId())
+
+ // refresh token
+ postLogoutRedirect, err := rp.EndSession(provider, tokens.IDToken, logoutRedirectURI, "state")
+ require.NoError(t, err)
+ assert.Equal(t, logoutRedirectURI+"?state=state", postLogoutRedirect.String())
+
+ // use token from terminated session
+ ctx = metadata.AppendToOutgoingContext(context.Background(), "Authorization", fmt.Sprintf("%s %s", tokens.TokenType, tokens.AccessToken))
+ myUserResp, err = Tester.Client.Auth.GetMyUser(ctx, &auth.GetMyUserRequest{})
+ require.Error(t, err)
+ require.Nil(t, myUserResp)
+}
+
+func createClient(t testing.TB) string {
+ project, err := Tester.CreateProject(CTX)
+ require.NoError(t, err)
+ app, err := Tester.CreateOIDCNativeClient(CTX, redirectURI, logoutRedirectURI, project.GetId())
+ require.NoError(t, err)
+ return app.GetClientId()
+}
+
+func createImplicitClient(t testing.TB) string {
+ app, err := Tester.CreateOIDCImplicitFlowClient(CTX, redirectURIImplicit)
+ require.NoError(t, err)
+ return app.GetClientId()
+}
+
+func createAuthRequest(t testing.TB, clientID, redirectURI string, scope ...string) string {
+ redURL, err := Tester.CreateOIDCAuthRequest(clientID, Tester.Users[integration.FirstInstanceUsersKey][integration.Login].ID, redirectURI, scope...)
+ require.NoError(t, err)
+ return redURL
+}
+
+func createAuthRequestImplicit(t testing.TB, clientID, redirectURI string, scope ...string) string {
+ redURL, err := Tester.CreateOIDCAuthRequestImplicit(clientID, Tester.Users[integration.FirstInstanceUsersKey][integration.Login].ID, redirectURI, scope...)
+ require.NoError(t, err)
+ return redURL
+}
+
+func assertOIDCTime(t *testing.T, actual oidc.Time, expected time.Time) {
+ assertOIDCTimeRange(t, actual, expected, expected)
+}
+
+func assertOIDCTimeRange(t *testing.T, actual oidc.Time, expectedStart, expectedEnd time.Time) {
+ assert.WithinRange(t, actual.AsTime(), expectedStart.Add(-1*time.Second), expectedEnd.Add(1*time.Second))
+}
diff --git a/internal/api/oidc/op.go b/internal/api/oidc/op.go
index 9574f561b..62dad091e 100644
--- a/internal/api/oidc/op.go
+++ b/internal/api/oidc/op.go
@@ -41,6 +41,8 @@ type Config struct {
Cache *middleware.CacheConfig
CustomEndpoints *EndpointConfig
DeviceAuth *DeviceAuthorizationConfig
+ DefaultLoginURLV2 string
+ DefaultLogoutURLV2 string
}
type EndpointConfig struct {
@@ -65,6 +67,8 @@ type OPStorage struct {
query *query.Queries
eventstore *eventstore.Eventstore
defaultLoginURL string
+ defaultLoginURLV2 string
+ defaultLogoutURLV2 string
defaultAccessTokenLifetime time.Duration
defaultIdTokenLifetime time.Duration
signingKeyAlgorithm string
@@ -181,6 +185,8 @@ func newStorage(config Config, command *command.Commands, query *query.Queries,
query: query,
eventstore: es,
defaultLoginURL: fmt.Sprintf("%s%s?%s=", login.HandlerPrefix, login.EndpointLogin, login.QueryAuthRequestID),
+ defaultLoginURLV2: config.DefaultLoginURLV2,
+ defaultLogoutURLV2: config.DefaultLogoutURLV2,
signingKeyAlgorithm: config.SigningKeyAlgorithm,
defaultAccessTokenLifetime: config.DefaultAccessTokenLifetime,
defaultIdTokenLifetime: config.DefaultIdTokenLifetime,
diff --git a/internal/api/ui/console/console.go b/internal/api/ui/console/console.go
index 320d753b4..7e00605b7 100644
--- a/internal/api/ui/console/console.go
+++ b/internal/api/ui/console/console.go
@@ -34,7 +34,7 @@ type spaHandler struct {
}
var (
- //go:embed static/*
+ //go:embed static
static embed.FS
)
diff --git a/internal/api/ui/console/static/.gitkeep b/internal/api/ui/console/static/.gitkeep
deleted file mode 100644
index e69de29bb..000000000
diff --git a/.artifacts/zitadel/.gitkeep b/internal/api/ui/console/static/gitkeep
similarity index 100%
rename from .artifacts/zitadel/.gitkeep
rename to internal/api/ui/console/static/gitkeep
diff --git a/internal/api/ui/login/external_provider_handler.go b/internal/api/ui/login/external_provider_handler.go
index 38029ad05..ee7696cc4 100644
--- a/internal/api/ui/login/external_provider_handler.go
+++ b/internal/api/ui/login/external_provider_handler.go
@@ -897,6 +897,10 @@ func (l *Login) appendUserGrants(ctx context.Context, userGrants []*domain.UserG
}
func (l *Login) externalAuthFailed(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, tokens *oidc.Tokens[*oidc.IDTokenClaims], user idp.User, err error) {
+ if authReq == nil {
+ l.renderLogin(w, r, authReq, err)
+ return
+ }
if _, _, actionErr := l.runPostExternalAuthenticationActions(&domain.ExternalUser{}, tokens, authReq, r, user, err); actionErr != nil {
logging.WithError(err).Error("both external user authentication and action post authentication failed")
}
diff --git a/internal/api/ui/login/static/i18n/bg.yaml b/internal/api/ui/login/static/i18n/bg.yaml
index 2c4ca6666..20b1c1084 100644
--- a/internal/api/ui/login/static/i18n/bg.yaml
+++ b/internal/api/ui/login/static/i18n/bg.yaml
@@ -221,6 +221,8 @@ RegistrationUser:
Japanese: 日本語
Spanish: Español
Bulgarian: Български
+ Portuguese: Português
+ Macedonian: Македонски
GenderLabel: Пол
Female: Женски пол
Male: Мъжки
@@ -253,6 +255,7 @@ ExternalRegistrationUserOverview:
Japanese: 日本語
Polish: Полски
Spanish: Español
+ Macedonian: Македонски
TosAndPrivacyLabel: Правила и условия
TosConfirm: Приемам
TosLinkText: TOS
@@ -309,6 +312,8 @@ ExternalNotFound:
Polish: Полски
Japanese: 日本語
Spanish: Español
+ Macedonian: Македонски
+
DeviceAuth:
Title: Упълномощаване на устройството
UserCode:
diff --git a/internal/api/ui/login/static/i18n/de.yaml b/internal/api/ui/login/static/i18n/de.yaml
index ca8c6cf58..984580359 100644
--- a/internal/api/ui/login/static/i18n/de.yaml
+++ b/internal/api/ui/login/static/i18n/de.yaml
@@ -223,6 +223,8 @@ RegistrationUser:
Japanese: 日本語
Spanish: Español
Bulgarian: Български
+ Portuguese: Português
+ Macedonian: Македонски
GenderLabel: Geschlecht
Female: weiblich
Male: männlich
@@ -256,6 +258,7 @@ ExternalRegistrationUserOverview:
Polish: Polski
Japanese: 日本語
Spanish: Español
+ Macedonian: Македонски
TosAndPrivacyLabel: Allgemeine Geschäftsbedingungen und Datenschutz
TosConfirm: Ich akzeptiere die
TosLinkText: AGBs
@@ -317,7 +320,8 @@ ExternalNotFound:
Polish: Polski
Japanese: 日本語
Spanish: Español
-
+ Macedonian: Македонски
+
DeviceAuth:
Title: Geräteautorisierung
UserCode:
diff --git a/internal/api/ui/login/static/i18n/en.yaml b/internal/api/ui/login/static/i18n/en.yaml
index 83862ee5f..ceb60654e 100644
--- a/internal/api/ui/login/static/i18n/en.yaml
+++ b/internal/api/ui/login/static/i18n/en.yaml
@@ -211,8 +211,8 @@ RegistrationUser:
DescriptionOrgRegister: Enter your Userdata.
EmailLabel: E-Mail
UsernameLabel: Username
- FirstnameLabel: First name
- LastnameLabel: Surname
+ FirstnameLabel: Given name
+ LastnameLabel: Family name
LanguageLabel: Language
German: Deutsch
English: English
@@ -222,6 +222,8 @@ RegistrationUser:
Polish: Polski
Japanese: 日本語
Spanish: Español
+ Portuguese: Português
+ Macedonian: Македонски
GenderLabel: Gender
Female: Female
Male: Male
@@ -242,8 +244,8 @@ ExternalRegistrationUserOverview:
Description: We have taken your user details from the selected provider. You can now change or complete them.
EmailLabel: E-Mail
UsernameLabel: Username
- FirstnameLabel: First name
- LastnameLabel: Surname
+ FirstnameLabel: Given name
+ LastnameLabel: Family name
NicknameLabel: Nickname
PhoneLabel: Phonenumber
LanguageLabel: Language
@@ -255,6 +257,7 @@ ExternalRegistrationUserOverview:
Japanese: 日本語
Polish: Polski
Spanish: Español
+ Macedonian: Македонски
TosAndPrivacyLabel: Terms and conditions
TosConfirm: I accept the
TosLinkText: TOS
@@ -270,8 +273,8 @@ RegistrationOrg:
OrgNameLabel: Organization name
EmailLabel: E-Mail
UsernameLabel: Username
- FirstnameLabel: First name
- LastnameLabel: Surname
+ FirstnameLabel: Given name
+ LastnameLabel: Family name
PasswordLabel: Password
PasswordConfirmLabel: Password confirmation
TosAndPrivacyLabel: Terms and conditions
@@ -317,6 +320,8 @@ ExternalNotFound:
Japanese: 日本語
Spanish: Español
Bulgarian: Български
+ Macedonian: Македонски
+
DeviceAuth:
Title: Device Authorization
UserCode:
@@ -366,8 +371,8 @@ Errors:
NotFound: Profile not found
NotChanged: Profile not changed
Empty: Profile is empty
- FirstNameEmpty: First name in profile is empty
- LastNameEmpty: Last name in profile is empty
+ FirstNameEmpty: Given name in profile is empty
+ LastNameEmpty: Family name in profile is empty
IDMissing: Profile ID is missing
Email:
NotFound: Email not found
diff --git a/internal/api/ui/login/static/i18n/es.yaml b/internal/api/ui/login/static/i18n/es.yaml
index e92cda9f1..7adf48d53 100644
--- a/internal/api/ui/login/static/i18n/es.yaml
+++ b/internal/api/ui/login/static/i18n/es.yaml
@@ -223,6 +223,8 @@ RegistrationUser:
Japanese: 日本語
Spanish: Español
Bulgarian: Български
+ Portuguese: Português
+ Macedonian: Македонски
GenderLabel: Género
Female: Mujer
Male: Hombre
@@ -256,6 +258,7 @@ ExternalRegistrationUserOverview:
Japanese: 日本語
Polish: Polski
Spanish: Español
+ Macedonian: Македонски
TosAndPrivacyLabel: Términos y condiciones
TosConfirm: Acepto los
TosLinkText: TDS
@@ -317,7 +320,8 @@ ExternalNotFound:
Polish: Polski
Japanese: 日本語
Spanish: Español
-
+ Macedonian: Македонски
+
Footer:
PoweredBy: Powered By
Tos: TDS
diff --git a/internal/api/ui/login/static/i18n/fr.yaml b/internal/api/ui/login/static/i18n/fr.yaml
index b963c5837..7cd2b0ad8 100644
--- a/internal/api/ui/login/static/i18n/fr.yaml
+++ b/internal/api/ui/login/static/i18n/fr.yaml
@@ -223,6 +223,8 @@ RegistrationUser:
Japanese: 日本語
Spanish: Español
Bulgarian: Български
+ Portuguese: Português
+ Macedonian: Македонски
GenderLabel: Genre
Female: Femme
Male: Homme
@@ -256,6 +258,7 @@ ExternalRegistrationUserOverview:
Polish: Polski
Japanese: 日本語
Spanish: Español
+ Macedonian: Македонски
TosAndPrivacyLabel: Termes et conditions
TosConfirm: J'accepte les
TosLinkText: TOS
@@ -317,6 +320,7 @@ ExternalNotFound:
Polish: Polski
Japanese: 日本語
Spanish: Español
+ Macedonian: Македонски
DeviceAuth:
Title: Autorisation de l'appareil
diff --git a/internal/api/ui/login/static/i18n/it.yaml b/internal/api/ui/login/static/i18n/it.yaml
index 6073342e2..8a30e9f97 100644
--- a/internal/api/ui/login/static/i18n/it.yaml
+++ b/internal/api/ui/login/static/i18n/it.yaml
@@ -223,6 +223,8 @@ RegistrationUser:
Japanese: 日本語
Spanish: Español
Bulgarian: Български
+ Portuguese: Português
+ Macedonian: Македонски
GenderLabel: Genere
Female: Femminile
Male: Maschile
@@ -256,6 +258,7 @@ ExternalRegistrationUserOverview:
Polish: Polski
Japanese: 日本語
Spanish: Español
+ Macedonian: Македонски
TosAndPrivacyLabel: Termini di servizio
TosConfirm: Accetto i
TosLinkText: Termini di servizio
@@ -317,6 +320,7 @@ ExternalNotFound:
Polish: Polski
Japanese: 日本語
Spanish: Español
+ Macedonian: Македонски
DeviceAuth:
Title: Autorizzazione del dispositivo
diff --git a/internal/api/ui/login/static/i18n/ja.yaml b/internal/api/ui/login/static/i18n/ja.yaml
index a5028e9e5..599710131 100644
--- a/internal/api/ui/login/static/i18n/ja.yaml
+++ b/internal/api/ui/login/static/i18n/ja.yaml
@@ -215,6 +215,8 @@ RegistrationUser:
Japanese: 日本語
Spanish: Español
Bulgarian: Български
+ Portuguese: Português
+ Macedonian: Македонски
GenderLabel: 性別
Female: 女性
Male: 男性
@@ -248,6 +250,7 @@ ExternalRegistrationUserOverview:
Polish: Polski
Japanese: 日本語
Spanish: Español
+ Macedonian: Македонски
TosAndPrivacyLabel: 利用規約
TosConfirm: 私は利用規約を承諾します。
TosLinkText: TOS
@@ -309,6 +312,7 @@ ExternalNotFound:
Polish: Polski
Japanese: 日本語
Spanish: Español
+ Macedonian: Македонски
DeviceAuth:
Title: デバイス認証
diff --git a/internal/api/ui/login/static/i18n/mk.yaml b/internal/api/ui/login/static/i18n/mk.yaml
new file mode 100644
index 000000000..d17f2fdbb
--- /dev/null
+++ b/internal/api/ui/login/static/i18n/mk.yaml
@@ -0,0 +1,456 @@
+Login:
+ Title: Добредојдовте назад!
+ Description: Внесете ги вашите податоци за најава.
+ TitleLinking: Најава за поврзување на корисник
+ DescriptionLinking: Внесете ги вашите податоци за најава за да го поврзете вашиот надворешен корисник со корисник на ZITADEL.
+ LoginNameLabel: Корисничко име за најава
+ UsernamePlaceHolder: корисничко име
+ LoginnamePlaceHolder: username@domain
+ ExternalUserDescription: Најава со надворешен корисник.
+ MustBeMemberOfOrg: Корисникот мора да биде член на организацијата {{.OrgName}}.
+ RegisterButtonText: регистрирај се
+ NextButtonText: следно
+
+LDAP:
+ Title: Најава
+ Description: Внесете ги вашите податоци за најава.
+ LoginNameLabel: Корисничко име за најава
+ PasswordLabel: Лозинка
+ NextButtonText: следно
+
+SelectAccount:
+ Title: Изберете корисничка сметка
+ Description: Користете ја вашата ZITADEL корисничка сметка
+ TitleLinking: Изберете корисничка сметка за поврзување на корисник
+ DescriptionLinking: Изберете ја вашата корисничка сметка за да ја поврзете со вашиот надворешен корисник.
+ OtherUser: Друг корисник
+ SessionState0: активна
+ SessionState1: неактивна
+ MustBeMemberOfOrg: Корисникот мора да биде член на организацијата {{.OrgName}}.
+
+Password:
+ Title: Лозинка
+ Description: Внесете ги вашите податоци за најава.
+ PasswordLabel: Лозинка
+ MinLength: Минимална должина
+ HasUppercase: Голема буква
+ HasLowercase: Мала буква
+ HasNumber: Број
+ HasSymbol: Симбол
+ Confirmation: Потврда на лозинка
+ ResetLinkText: ресетирај лозинка
+ BackButtonText: назад
+ NextButtonText: следно
+
+UsernameChange:
+ Title: Промена на корисничко име
+ Description: Поставете го вашето ново корисничко име
+ UsernameLabel: Корисничко име
+ CancelButtonText: откажи
+ NextButtonText: следно
+
+UsernameChangeDone:
+ Title: Корисничкото име е променето
+ Description: Вашето корисничко име е успешно променето.
+ NextButtonText: следно
+
+InitPassword:
+ Title: Поставете лозинка
+ Description: Добивте код кој треба да го внесете во формата подолу за да поставите нова лозинка.
+ CodeLabel: Код
+ NewPasswordLabel: Нова лозинка
+ NewPasswordConfirmLabel: Потврда на лозинка
+ ResendButtonText: повторно испрати код
+ NextButtonText: следно
+
+InitPasswordDone:
+ Title: Лозинката е поставена
+ Description: Лозинката е успешно поставена.
+ NextButtonText: следно
+ CancelButtonText: откажи
+
+InitUser:
+ Title: Активирајте корисник
+ Description: Проверете ја вашата е-пошта со кодот подолу и поставете ја вашата лозинка.
+ CodeLabel: Код
+ NewPasswordLabel: Нова лозинка
+ NewPasswordConfirm: Потврда на лозинка
+ NextButtonText: следно
+ ResendButtonText: повторно испрати код
+
+InitUserDone:
+ Title: Корисникот е активиран
+ Description: Е-поштата е верифицирана и лозинката е успешно поставена.
+ NextButtonText: следно
+ CancelButtonText: откажи
+
+InitMFAPrompt:
+ Title: Подесување на 2-факторска автентикација
+ Description: 2-факторската автентикација ви дава дополнителна безбедност за вашата корисничка сметка. Ова обезбедува само вие да имате пристап до вашата сметка.
+ Provider0: Апликација за автентикација (на пример Google/Microsoft Authenticator, Authy)
+ Provider1: Во зависност од вашиот уред (на пример FaceID, Windows Hello, отпечаток од прст)
+ NextButtonText: следно
+ SkipButtonText: прескокни
+
+InitMFAOTP:
+ Title: Потврда на 2-факторска автентикација
+ Description: Креирајте ја вашата 2-факторска автентикација. Превземете апликација за автентикација доколку сѐ уште немате една.
+ OTPDescription: Скенирајте го кодот со вашата апликација за автентикација (на пример Google/Microsoft Authenticator, Authy) или копирајте го тајниот клуч и вметнете го генерираниот код подолу.
+ SecretLabel: Таен клуч
+ CodeLabel: Код
+ NextButtonText: следно
+ CancelButtonText: откажи
+
+InitMFAU2F:
+ Title: Додајте безбедносен клуч
+ Description: Безбедносниот клуч е метод на верификација кој може да се интегрира во вашиот телефон, да користи Bluetooth или директно да се поврзе во USB приклучокот на вашиот компјутер.
+ TokenNameLabel: Име на безбедносниот клуч / уред
+ NotSupported: WebAuthN не е поддржан од вашиот прелистувач. Ве молиме проверете дали е ажуриран со најновата верзија или пак користете друг прелистувач (на пример Chrome, Safari, Firefox)
+ RegisterTokenButtonText: Додајте безбедносен клуч
+ ErrorRetry: Обидете се повторно, креирајте нов предизвик или изберете друг метод.
+
+InitMFADone:
+ Title: Потврден безбедносен клуч
+ Description: Одлично! Успешно ја подесивте вашата 2-факторска автентикација и ја зголемивте безбедноста на вашата корисничка сметка. Факторот мора да се користи при секоја најава.
+ NextButtonText: следно
+ CancelButtonText: откажи
+
+MFAProvider:
+ Provider0: Апликација за автентикација (на пример Google/Microsoft Authenticator, Authy)
+ Provider1: Во зависност од вашиот уред (на пример FaceID, Windows Hello, отпечаток од прст)
+ ChooseOther: или изберете друга опција
+
+VerifyMFAOTP:
+ Title: Потврда на 2-факторска автентикација
+ Description: Потврдете ја 2-факторска автентикација
+ CodeLabel: Код
+ NextButtonText: следно
+
+VerifyMFAU2F:
+ Title: Потврда на 2-факторска автентикација
+ Description: Потврдете ја вашата 2-факторска автентикација со регистрираниот уред (на пример FaceID, Windows Hello, отпечаток од прст)
+ NotSupported: WebAuthN не е поддржан од вашиот прелистувач. Ве молиме проверете дали е ажуриран со најновата верзија или пак користете друг прелистувач (Chrome, Safari, Firefox)
+ ErrorRetry: Обидете се повторно, креирајте нов барање или изберете друг метод.
+ ValidateTokenButtonText: Потврди 2-факторска автентикација
+
+Passwordless:
+ Title: Најава без лозинка
+ Description: Најавете се со методи за автентикација кои ги нуди вашиот уред, како FaceID, Windows Hello или отпечаток од прст.
+ NotSupported: WebAuthN не е поддржан од вашиот прелистувач. Ве молиме проверете дали е ажуриран со најновата верзија или пак користете друг прелистувач (на пример Chrome, Safari, Firefox)
+ ErrorRetry: Обидете се повторно, креирајте нов предизвик или изберете друг метод.
+ LoginWithPwButtonText: Најава со лозинка
+ ValidateTokenButtonText: Најава без лозинка
+
+PasswordlessPrompt:
+ Title: Подесување на најава без лозинка
+ Description: Дали сакате да подесите најава без лозинка? (Методи за автентикација на вашиот уред, како FaceID, Windows Hello или отпечаток од прст)
+ DescriptionInit: Мора да подесите најава без лозинка. Користете го линкот што ви беше даден за регистрација на вашиот уред.
+ PasswordlessButtonText: Отиди на најава без лозинка
+ NextButtonText: следно
+ SkipButtonText: прескокни
+
+PasswordlessRegistration:
+ Title: Подесување на најава без лозинка
+ Description: Додадете ја вашата автентикација со давање на име (на пример, МојМобиленТелефон, MacBook итн) и кликнете на копчето 'Регистрирај најава без лозинка' подолу.
+ TokenNameLabel: Име на уредот
+ NotSupported: WebAuthN не е поддржан од вашиот прелистувач. Ве молиме проверете дали е ажуриран со најновата верзија или пак користете друг прелистувач (на пример, Chrome, Safari, Firefox)
+ RegisterTokenButtonText: Регистрирај најава без лозинка
+ ErrorRetry: Обидете се повторно, креирајте нов предизвик или изберете друг метод.
+
+PasswordlessRegistrationDone:
+ Title: Подесена најава без лозинка
+ Description: Уредот за најава без лозинка е успешно додаден.
+ DescriptionClose: Сега можете да го затворите овој прозорец.
+ NextButtonText: следно
+ CancelButtonText: откажи
+
+PasswordChange:
+ Title: Промена на лозинка
+ Description: Променете ја вашата лозинка. Внесете ја старата и новата лозинка.
+ OldPasswordLabel: Стара лозинка
+ NewPasswordLabel: Нова лозинка
+ NewPasswordConfirmLabel: Потврда на лозинка
+ CancelButtonText: откажи
+ NextButtonText: следно
+ Footer: Футер
+
+PasswordChangeDone:
+ Title: Промена на лозинка
+ Description: Вашата лозинка беше успешно променета.
+ NextButtonText: следно
+
+PasswordResetDone:
+ Title: Пратен линк за ресетирање на лозинка
+ Description: Проверете ја вашата е-пошта за ресетирање на лозинката.
+ NextButtonText: следно
+
+EmailVerification:
+ Title: Верификација на е-пошта
+ Description: Ви пративме е-пошта за да ја верификувате вашата адреса за е-пошта. Ве молиме внесете го кодот во формата подолу.
+ CodeLabel: Код
+ NextButtonText: следно
+ ResendButtonText: испрати повторно
+
+EmailVerificationDone:
+ Title: Верификација на е-пошта
+ Description: Вашата е-пошта беше успешно верифицирана.
+ NextButtonText: следно
+ CancelButtonText: откажи
+ LoginButtonText: најава
+
+RegisterOption:
+ Title: Опции за регистрација
+ Description: Изберете како сакате да се регистрирате
+ RegisterUsernamePasswordButtonText: Со корисничко име и лозинка
+ ExternalLoginDescription: или регистрирајте се со надворешен корисник
+ LoginButtonText: најава
+
+RegistrationUser:
+ Title: Регистрација
+ Description: Внесете ги вашите кориснички податоци. Вашата е-пошта ќе се користи како корисничко име.
+ DescriptionOrgRegister: Внесете ги вашите кориснички податоци.
+ EmailLabel: Е-пошта
+ UsernameLabel: Корисничко име
+ FirstnameLabel: Име
+ LastnameLabel: Презиме
+ LanguageLabel: Јазик
+ German: Deutsch
+ English: English
+ Italian: Italiano
+ French: Français
+ Chinese: 简体中文
+ Polish: Polski
+ Japanese: 日本語
+ Spanish: Español
+ Portuguese: Português
+ Macedonian: Македонски
+ GenderLabel: Пол
+ Female: Женски
+ Male: Машки
+ Diverse: различен / X
+ PasswordLabel: Лозинка
+ PasswordConfirmLabel: Потврда на лозинка
+ TosAndPrivacyLabel: Правила и услови
+ TosConfirm: Се согласувам со
+ TosLinkText: правилата за користење
+ PrivacyConfirm: Се согласувам со
+ PrivacyLinkText: политиката за приватност
+ ExternalLogin: или регистрирајте се со надворешен корисник
+ BackButtonText: најава
+ NextButtonText: следно
+
+ExternalRegistrationUserOverview:
+ Title: Регистрација на надворешен корисник
+ Description: Ги преземаме вашите податоци за корисник од избраниот провајдер. Сега можете да ги промените или дополните.
+ EmailLabel: Е-пошта
+ UsernameLabel: Корисничко име
+ FirstnameLabel: Име
+ LastnameLabel: Презиме
+ NicknameLabel: Прекар
+ PhoneLabel: Телефонски број
+ LanguageLabel: Јазик
+ German: Deutsch
+ English: English
+ Italian: Italiano
+ French: Français
+ Chinese: 简体中文
+ Japanese: 日本語
+ Polish: Polski
+ Spanish: Español
+ Portuguese: Português
+ Macedonian: Македонски
+ TosAndPrivacyLabel: Правила и услови
+ TosConfirm: Се согласувам со
+ TosLinkText: правилата за користење
+ PrivacyConfirm: Се согласувам со
+ PrivacyLinkText: политиката за приватност
+ ExternalLogin: или регистрирајте се со надворешен корисник
+ BackButtonText: назад
+ NextButtonText: зачувај
+
+RegistrationOrg:
+ Title: Регистрација на организација
+ Description: Внесете го името и податоците на вашата организација.
+ OrgNameLabel: Име на организација
+ EmailLabel: Е-пошта
+ UsernameLabel: Корисничко име
+ FirstnameLabel: Име
+ LastnameLabel: Презиме
+ PasswordLabel: Лозинка
+ PasswordConfirmLabel: Потврда на лозинка
+ TosAndPrivacyLabel: Правила и услови
+ TosConfirm: Се согласувам со
+ TosLinkText: правилата за користење
+ PrivacyConfirm: Се согласувам со
+ PrivacyLinkText: политиката за приватност
+ SaveButtonText: Креирај организација
+
+LoginSuccess:
+ Title: Успешна најава
+ AutoRedirectDescription: Ќе бидете автоматски пренасочени назад кон вашата апликација. Доколку не сте автоматски пренасочени, кликнете на копчето подолу. Потоа можете да го затворите прозорецот.
+ RedirectedDescription: Сега можете да го затворите овој прозорец.
+ NextButtonText: следно
+
+LogoutDone:
+ Title: Одјавени
+ Description: Успешно сте одјавени.
+ LoginButtonText: најава
+
+LinkingUsersDone:
+ Title: Поврзување на корисници
+ Description: Поврзувањето на корисници е завршено.
+ CancelButtonText: откажи
+ NextButtonText: следно
+
+ExternalNotFound:
+ Title: Не е пронајден надворешен корисник
+ Description: Надворешниот корисник не е пронајден. Дали сакате да го поврзете вашиот корисник или автоматски да регистрирате нов.
+ LinkButtonText: Поврзи
+ AutoRegisterButtonText: регистрирај
+ TosAndPrivacyLabel: Правила и услови
+ TosConfirm: Се согласувам со
+ TosLinkText: правилата за користење
+ PrivacyConfirm: Се согласувам со
+ PrivacyLinkText: политиката за приватност
+ German: Deutsch
+ English: English
+ Italian: Italiano
+ French: Français
+ Chinese: 简体中文
+ Polish: Polski
+ Japanese: 日本語
+ Spanish: Español
+ Bulgarian: Български
+ Portuguese: Português
+ Macedonian: Македонски
+
+DeviceAuth:
+ Title: Овластување преку уред
+ UserCode:
+ Label: Код на корисник
+ Description: Внесете го кодот на корисникот прикажан на уредот.
+ ButtonNext: следно
+ Action:
+ Description: Овластување за пристап за уред.
+ GrantDevice: со ова ќе овозможите уредот да има право за
+ AccessToScopes: пристап до следниве области
+ Button:
+ Allow: овозможи
+ Deny: одбиј
+ Done:
+ Description: Завршено.
+ Approved: Овластувањето на уредот е одобрено. Сега можете да се вратите на уредот.
+ Denied: Овластувањето на уредот е одбиено. Сега можете да се вратите на уредот.
+
+Footer:
+ PoweredBy: Поддржано од
+ Tos: Услови за користење
+ PrivacyPolicy: Политика за приватност
+ Help: Помош
+ SupportEmail: Е-пошта за поддршка
+
+Errors:
+ Internal: Се појави внатрешна грешка
+ AuthRequest:
+ NotFound: Не може да се пронајде барањето за автентикација
+ UserAgentNotCorresponding: User Agent не се совпаѓа
+ UserAgentNotFound: Не е пронајден User Agent ID
+ TokenNotFound: Токенот не е пронајден
+ RequestTypeNotSupported: Типот на барање не е подржан
+ MissingParameters: Недостасуваат задолжителни параметри
+ User:
+ NotFound: Корисникот не е пронајден
+ AlreadyExists: Корисникот веќе постои
+ Inactive: Корисникот е неактивен
+ NotFoundOnOrg: Корисникот не е пронајден во избраната организација
+ NotAllowedOrg: Корисникот не е член на бараната организација
+ NotMatchingUserID: Корисникот и корисникот во барањето за автентикација не се совпаѓаат
+ UserIDMissing: ID на корисник е празно
+ Invalid: Невалидни кориснички податоци
+ DomainNotAllowedAsUsername: Доменот е веќе резервиран и не може да се користи
+ NotAllowedToLink: Корисникот нема дозвола да се поврзе со надворешен провајдер за најава
+ Profile:
+ NotFound: Профилот не е пронајден
+ NotChanged: Профилот не е променет
+ Empty: Профилот е празен
+ FirstNameEmpty: Името во профилот е празно
+ LastNameEmpty: Презимето во профилот е празно
+ IDMissing: ID на профил е празно
+ Email:
+ NotFound: Е-поштата не е пронајдена
+ Invalid: Е-поштата не е валидна
+ AlreadyVerified: Е-поштата веќе е верифицирана
+ NotChanged: Е-поштата не е променета
+ Empty: Е-поштата е празна
+ IDMissing: ID на е-пошта е празно
+ Phone:
+ NotFound: Телефонскиот број не е пронајден
+ Invalid: Телефонскиот број не е валиден
+ AlreadyVerified: Телефонскиот број е веќе верифициран
+ Empty: Телефонскиот број е празен
+ NotChanged: Телефонскиот број не е променет
+ Address:
+ NotFound: Адресата не е пронајдена
+ NotChanged: Адресата не е променета
+ Username:
+ AlreadyExists: Корисничкото име е веќе зафатено
+ Reserved: Корисничкото име е веќе зафатено
+ Empty: Корисничкото име е празно
+ Password:
+ ConfirmationWrong: Потврдата на лозинката не е точна
+ Empty: Лозинката е празна
+ Invalid: Лозинката не е валидна
+ InvalidAndLocked: Лозинката не е валидна и корисникот е заклучен, контактирајте со вашиот администратор.
+ UsernameOrPassword:
+ Invalid: Корисничкото име и/или лозинката не се валидни
+ PasswordComplexityPolicy:
+ NotFound: Политиката за сложеност на лозинката не е пронајдена
+ MinLength: Лозинката е прекратка
+ HasLower: Лозинката мора да содржи мала буква
+ HasUpper: Лозинката мора да содржи голема буква
+ HasNumber: Лозинката мора да содржи број
+ HasSymbol: Лозинката мора да содржи симбол
+ Code:
+ Expired: Кодот е истечен
+ Invalid: Кодот не е валиден
+ Empty: Кодот е празен
+ CryptoCodeNil: Крипто кодот е nil
+ NotFound: Кодот не е пронајден
+ GeneratorAlgNotSupported: Неподдржан алгоритам на генераторот
+ EmailVerify:
+ UserIDEmpty: ID на корисник е празно
+ ExternalData:
+ CouldNotRead: Надворешните податоци не можеа да се прочитаат точно
+ MFA:
+ NoProviders: Нема достапни провајдери за мултифакторна автентикација
+ OTP:
+ AlreadyReady: Мултифактор OTP (Еднократна Лозинка) е веќе подесена
+ NotExisting: Мултифактор OTP (Еднократна Лозинка) не постои
+ InvalidCode: Невалиден код
+ NotReady: Мултифактор OTP (Еднократна Лозинка) не е подготвена
+ Locked: Корисникот е заклучен
+ SomethingWentWrong: Се случи нешто неочекувано
+ NotActive: Корисникот не е активен
+ ExternalIDP:
+ IDPTypeNotImplemented: Типот на идентитетски надворешен провајдер не е имплементиран
+ NotAllowed: Надворешниот идентитетски провајдер за најава не е дозволен
+ IDPConfigIDEmpty: ID на надворешниот идентитетски провајдер е празно
+ ExternalUserIDEmpty: ID на надворешниот корисник е празно
+ UserDisplayNameEmpty: Приказното име на корисникот е празно
+ NoExternalUserData: Нема преземени податоци за надворешен корисник
+ CreationNotAllowed: Креирањето на нов корисник не е дозволено на овој провајдер
+ LinkingNotAllowed: Поврзувањето на корисник не е дозволено на овој провајдер
+ GrantRequired: Не е можно најавување. Корисникот мора да има барем едно овластување за апликацијата. Ве молиме контактирајте го вашиот администратор.
+ ProjectRequired: Не е можно најавување. Организацијата на корисникот мора да биде доделена на проектот. Ве молиме контактирајте го вашиот администратор.
+ IdentityProvider:
+ InvalidConfig: Конфигурацијата на идентитетскиот провајдер не е валидна
+ IAM:
+ LockoutPolicy:
+ NotExisting: Политиката за заклучување не постои
+ Org:
+ LoginPolicy:
+ RegistrationNotAllowed: Не е дозволена регистрација
+ DeviceAuth:
+ NotExisting: Кодот на корисникот не постои
+
+optional: (опционално)
diff --git a/internal/api/ui/login/static/i18n/pl.yaml b/internal/api/ui/login/static/i18n/pl.yaml
index 12b4b2e24..56d4cc683 100644
--- a/internal/api/ui/login/static/i18n/pl.yaml
+++ b/internal/api/ui/login/static/i18n/pl.yaml
@@ -223,6 +223,8 @@ RegistrationUser:
Japanese: 日本語
Spanish: Español
Bulgarian: Български
+ Portuguese: Português
+ Macedonian: Македонски
GenderLabel: Płeć
Female: Kobieta
Male: Mężczyzna
@@ -256,6 +258,7 @@ ExternalRegistrationUserOverview:
Polish: Polski
Japanese: 日本語
Spanish: Español
+ Macedonian: Македонски
TosAndPrivacyLabel: Warunki i zasady
TosConfirm: Akceptuję
TosLinkText: Warunki korzystania
@@ -317,6 +320,7 @@ ExternalNotFound:
Polish: Polski
Japanese: 日本語
Spanish: Español
+ Macedonian: Македонски
DeviceAuth:
Title: Autoryzacja urządzenia
diff --git a/internal/api/ui/login/static/i18n/pt.yaml b/internal/api/ui/login/static/i18n/pt.yaml
new file mode 100644
index 000000000..e887ff6bc
--- /dev/null
+++ b/internal/api/ui/login/static/i18n/pt.yaml
@@ -0,0 +1,452 @@
+Login:
+ Title: Bem-vindo de volta!
+ Description: Insira seus dados de login.
+ TitleLinking: Login para vinculação de usuário
+ DescriptionLinking: Insira seus dados de login para vincular seu usuário externo a um usuário do ZITADEL.
+ LoginNameLabel: Nome de login
+ UsernamePlaceHolder: nome de usuário
+ LoginnamePlaceHolder: nome de usuário@domínio
+ ExternalUserDescription: Faça login com um usuário externo.
+ MustBeMemberOfOrg: O usuário deve ser membro da organização {{.OrgName}}.
+ RegisterButtonText: registrar
+ NextButtonText: próximo
+
+LDAP:
+ Title: Login
+ Description: Insira seus dados de login.
+ LoginNameLabel: Nome de login
+ PasswordLabel: Senha
+ NextButtonText: próximo
+
+SelectAccount:
+ Title: Selecionar conta
+ Description: Use sua conta ZITADEL
+ TitleLinking: Selecionar conta para vinculação de usuário
+ DescriptionLinking: Selecione sua conta para vincular com seu usuário externo.
+ OtherUser: Outro usuário
+ SessionState0: ativo
+ SessionState1: inativo
+ MustBeMemberOfOrg: O usuário deve ser membro da organização {{.OrgName}}.
+
+Password:
+ Title: Senha
+ Description: Insira seus dados de login.
+ PasswordLabel: Senha
+ MinLength: Comprimento mínimo
+ HasUppercase: Letra maiúscula
+ HasLowercase: Letra minúscula
+ HasNumber: Número
+ HasSymbol: Símbolo
+ Confirmation: Confirmação corresponde
+ ResetLinkText: redefinir senha
+ BackButtonText: voltar
+ NextButtonText: próximo
+
+UsernameChange:
+ Title: Alterar nome de usuário
+ Description: Defina seu novo nome de usuário
+ UsernameLabel: Nome de usuário
+ CancelButtonText: cancelar
+ NextButtonText: próximo
+
+UsernameChangeDone:
+ Title: Nome de usuário alterado
+ Description: Seu nome de usuário foi alterado com sucesso.
+ NextButtonText: próximo
+
+InitPassword:
+ Title: Definir senha
+ Description: Você recebeu um código, que deve inserir no formulário abaixo para definir sua nova senha.
+ CodeLabel: Código
+ NewPasswordLabel: Nova senha
+ NewPasswordConfirmLabel: Confirmar senha
+ ResendButtonText: reenviar código
+ NextButtonText: próximo
+
+InitPasswordDone:
+ Title: Senha definida
+ Description: Senha definida com sucesso
+ NextButtonText: próximo
+ CancelButtonText: cancelar
+
+InitUser:
+ Title: Ativar usuário
+ Description: Verifique seu e-mail com o código abaixo e defina sua senha.
+ CodeLabel: Código
+ NewPasswordLabel: Nova senha
+ NewPasswordConfirm: Confirmar senha
+ NextButtonText: próximo
+ ResendButtonText: reenviar código
+
+InitUserDone:
+ Title: Usuário ativado
+ Description: E-mail verificado e senha definida com sucesso
+ NextButtonText: próximo
+ CancelButtonText: cancelar
+
+InitMFAPrompt:
+ Title: Configuração de 2 fatores
+ Description: A autenticação de 2 fatores fornece uma segurança adicional para sua conta de usuário. Isso garante que apenas você tenha acesso à sua conta.
+ Provider0: Aplicativo de autenticação (por exemplo, Google/Microsoft Authenticator, Authy)
+ Provider1: Dependente do dispositivo (por exemplo, FaceID, Windows Hello, Impressão digital)
+ NextButtonText: próximo
+ SkipButtonText: pular
+
+InitMFAOTP:
+ Title: Verificação de 2 fatores
+ Description: Crie sua verificação de 2 fatores. Faça o download de um aplicativo de autenticação se ainda não tiver um.
+ OTPDescription: Digitalize o código com seu aplicativo de autenticação (por exemplo, Google/Microsoft Authenticator, Authy) ou copie o segredo e insira o código gerado abaixo.
+ SecretLabel: Segredo
+ CodeLabel: Código
+ NextButtonText: próximo
+ CancelButtonText: cancelar
+
+InitMFAU2F:
+ Title: Adicionar chave de segurança
+ Description: Uma chave de segurança é um método de verificação que pode ser incorporado ao seu telefone, usar Bluetooth ou conectar diretamente à porta USB do seu computador.
+ TokenNameLabel: Nome da chave de segurança / dispositivo
+ NotSupported: WebAuthN não é suportado pelo seu navegador. Verifique se ele está atualizado ou use outro navegador (por exemplo, Chrome, Safari, Firefox)
+ RegisterTokenButtonText: Adicionar chave de segurança
+ ErrorRetry: Tentar novamente, criar um novo desafio ou escolher um método diferente.
+
+InitMFADone:
+ Title: Chave de segurança verificada
+ Description: Incrível! Você configurou com sucesso a autenticação de 2 fatores e tornou sua conta muito mais segura. O fator deve ser inserido em cada login.
+ NextButtonText: próximo
+ CancelButtonText: cancelar
+
+MFAProvider:
+ Provider0: Aplicativo de autenticação (por exemplo, Google/Microsoft Authenticator, Authy)
+ Provider1: Dependente do dispositivo (por exemplo, FaceID, Windows Hello, Impressão digital)
+ ChooseOther: ou escolha outra opção
+
+VerifyMFAOTP:
+ Title: Verificar 2 fatores
+ Description: Verifique seu segundo fator
+ CodeLabel: Código
+ NextButtonText: próximo
+
+VerifyMFAU2F:
+ Title: Verificação de 2 fatores
+ Description: Verifique seu 2 fatores com o dispositivo registrado (por exemplo, FaceID, Windows Hello, Impressão digital)
+ NotSupported: WebAuthN não é suportado pelo seu navegador. Verifique se está usando a versão mais recente ou altere para um navegador compatível (Chrome, Safari, Firefox)
+ ErrorRetry: Tentar novamente, criar uma nova solicitação ou escolher outro método.
+ ValidateTokenButtonText: Verificar 2 fatores
+
+Passwordless:
+ Title: Login sem senha
+ Description: Faça login com métodos de autenticação fornecidos pelo seu dispositivo, como FaceID, Windows Hello ou Impressão digital.
+ NotSupported: WebAuthN não é suportado pelo seu navegador. Verifique se ele está atualizado ou use outro navegador (por exemplo, Chrome, Safari, Firefox)
+ ErrorRetry: Tentar novamente, criar um novo desafio ou escolher um método diferente.
+ LoginWithPwButtonText: Fazer login com senha
+ ValidateTokenButtonText: Fazer login sem senha
+
+PasswordlessPrompt:
+ Title: Configuração de login sem senha
+ Description: Você gostaria de configurar o login sem senha? (Métodos de autenticação do seu dispositivo, como FaceID, Windows Hello ou Impressão digital)
+
+PasswordlessRegistration:
+ Title: Configuração de login sem senha
+ Description: Adicione sua autenticação fornecendo um nome (por exemplo, MeuCelular, MacBook, etc.) e clique no botão 'Registrar login sem senha' abaixo.
+ TokenNameLabel: Nome do dispositivo
+ NotSupported: WebAuthN não é suportado pelo seu navegador. Verifique se ele está atualizado ou use outro navegador (por exemplo, Chrome, Safari, Firefox)
+ RegisterTokenButtonText: Registrar login sem senha
+ ErrorRetry: Tentar novamente, criar um novo desafio ou escolher um método diferente.
+
+PasswordlessRegistrationDone:
+ Title: Configuração de login sem senha concluída
+ Description: Dispositivo para login sem senha adicionado com sucesso.
+ DescriptionClose: Agora você pode fechar esta janela.
+ NextButtonText: próximo
+ CancelButtonText: cancelar
+
+PasswordChange:
+ Title: Alterar senha
+ Description: Altere sua senha. Insira sua senha antiga e nova.
+ OldPasswordLabel: Senha antiga
+ NewPasswordLabel: Nova senha
+ NewPasswordConfirmLabel: Confirmação de senha
+ CancelButtonText: cancelar
+ NextButtonText: próximo
+ Footer: Rodapé
+
+PasswordChangeDone:
+ Title: Alteração de senha
+ Description: Sua senha foi alterada com sucesso.
+ NextButtonText: próximo
+
+PasswordResetDone:
+ Title: Link de redefinição de senha enviado
+ Description: Verifique seu e-mail para redefinir sua senha.
+ NextButtonText: próximo
+
+EmailVerification:
+ Title: Verificação de e-mail
+ Description: Enviamos um e-mail para verificar seu endereço. Insira o código no formulário abaixo.
+ CodeLabel: Código
+ NextButtonText: próximo
+ ResendButtonText: reenviar código
+
+EmailVerificationDone:
+ Title: Verificação de e-mail
+ Description: Seu endereço de e-mail foi verificado com sucesso.
+ NextButtonText: próximo
+ CancelButtonText: cancelar
+ LoginButtonText: login
+
+RegisterOption:
+ Title: Opções de registro
+ Description: Escolha como deseja se registrar
+ RegisterUsernamePasswordButtonText: Com nome de usuário e senha
+ ExternalLoginDescription: ou registre-se com um usuário externo
+ LoginButtonText: login
+
+RegistrationUser:
+ Title: Registro
+ Description: Insira seus dados de usuário. Seu endereço de e-mail será usado como nome de login.
+ DescriptionOrgRegister: Insira seus dados de usuário.
+ EmailLabel: E-mail
+ UsernameLabel: Nome de usuário
+ FirstnameLabel: Nome próprio
+ LastnameLabel: Sobrenome
+ LanguageLabel: Idioma
+ German: Alemão
+ English: Inglês
+ Italian: Italiano
+ French: Francês
+ Chinese: Chinês simplificado
+ Polish: Polonês
+ Japanese: Japonês
+ Spanish: Espanhol
+ Bulgarian: Búlgaro
+ Portuguese: Português
+ Macedonian: Македонски
+ GenderLabel: Gênero
+ Female: Feminino
+ Male: Masculino
+ Diverse: Diverso / X
+ PasswordLabel: Senha
+ PasswordConfirmLabel: Confirmação de senha
+ TosAndPrivacyLabel: Termos e condições
+ TosConfirm: Eu aceito os
+ TosLinkText: termos de serviço
+ PrivacyConfirm: Eu aceito a
+ PrivacyLinkText: política de privacidade
+ ExternalLogin: ou registre-se com um usuário externo
+ BackButtonText: login
+ NextButtonText: próximo
+
+ExternalRegistrationUserOverview:
+ Title: Registro de usuário externo
+ Description: Coletamos seus detalhes de usuário do provedor selecionado. Agora você pode alterá-los ou completá-los.
+ EmailLabel: E-mail
+ UsernameLabel: Nome de usuário
+ FirstnameLabel: Nome próprio
+ LastnameLabel: Sobrenome
+ NicknameLabel: Apelido
+ PhoneLabel: Número de telefone
+ LanguageLabel: Idioma
+ German: Alemão
+ English: Inglês
+ Italian: Italiano
+ French: Francês
+ Chinese: Chinês simplificado
+ Japanese: Japonês
+ Polish: Polonês
+ Spanish: Espanhol
+ Portuguese: Português
+ Macedonian: Македонски
+ TosAndPrivacyLabel: Termos e condições
+ TosConfirm: Eu aceito os
+ TosLinkText: termos de serviço
+ PrivacyConfirm: Eu aceito a
+ PrivacyLinkText: política de privacidade
+ ExternalLogin: ou registre-se com um usuário externo
+ BackButtonText: voltar
+ NextButtonText: salvar
+
+RegistrationOrg:
+ Title: Registro de organização
+ Description: Insira o nome de sua organização e seus dados de usuário.
+ OrgNameLabel: Nome da organização
+ EmailLabel: E-mail
+ UsernameLabel: Nome de usuário
+ FirstnameLabel: Nome próprio
+ LastnameLabel: Sobrenome
+ PasswordLabel: Senha
+ PasswordConfirmLabel: Confirmação de senha
+ TosAndPrivacyLabel: Termos e condições
+ TosConfirm: Eu aceito os
+ TosLinkText: termos de serviço
+ PrivacyConfirm: Eu aceito a
+ PrivacyLinkText: política de privacidade
+ SaveButtonText: Criar organização
+
+LoginSuccess:
+ Title: Login bem-sucedido
+ AutoRedirectDescription: Você será redirecionado de volta para o seu aplicativo automaticamente. Se isso não acontecer, clique no botão abaixo. Você pode fechar a janela em seguida.
+ RedirectedDescription: Agora você pode fechar esta janela.
+ NextButtonText: próximo
+
+LogoutDone:
+ Title: Logout concluído
+ Description: Você fez logout com sucesso.
+ LoginButtonText: login
+
+LinkingUsersDone:
+ Title: Vinculação de usuários
+ Description: Vinculação de usuários concluída.
+ CancelButtonText: cancelar
+ NextButtonText: próximo
+
+ExternalNotFound:
+ Title: Usuário externo não encontrado
+ Description: Usuário externo não encontrado. Deseja vincular seu usuário ou registrar um novo.
+ LinkButtonText: Vincular
+ AutoRegisterButtonText: registrar
+ TosAndPrivacyLabel: Termos e condições
+ TosConfirm: Eu aceito os
+ TosLinkText: termos de serviço
+ PrivacyConfirm: Eu aceito a
+ PrivacyLinkText: política de privacidade
+ German: Alemão
+ English: Inglês
+ Italian: Italiano
+ French: Francês
+ Chinese: Chinês simplificado
+ Polish: Polonês
+ Japanese: Japonês
+ Spanish: Espanhol
+ Bulgarian: Búlgaro
+ Portuguese: Português
+ Macedonian: Македонски
+DeviceAuth:
+ Title: Autorização de dispositivo
+ UserCode:
+ Label: Código do usuário
+ Description: Insira o código do usuário apresentado no dispositivo.
+ ButtonNext: próximo
+ Action:
+ Description: Conceder acesso ao dispositivo.
+ GrantDevice: você está prestes a conceder acesso aodispositivo
+ AccessToScopes: acesso às seguintes permissões
+ Button:
+ Allow: permitir
+ Deny: negar
+ Done:
+ Description: Concluído.
+ Approved: Autorização de dispositivo aprovada. Agora você pode voltar ao dispositivo.
+ Denied: Autorização de dispositivo negada. Agora você pode voltar ao dispositivo.
+
+Footer:
+ PoweredBy: Desenvolvido por
+ Tos: Termos de serviço
+ PrivacyPolicy: Política de privacidade
+ Help: Ajuda
+ SupportEmail: E-mail de suporte
+
+Errors:
+ Internal: Ocorreu um erro interno
+ AuthRequest:
+ NotFound: Não foi possível encontrar a solicitação de autenticação
+ UserAgentNotCorresponding: O agente do usuário não corresponde
+ UserAgentNotFound: ID do agente do usuário não encontrado
+ TokenNotFound: Token não encontrado
+ RequestTypeNotSupported: Tipo de solicitação não suportado
+ MissingParameters: Parâmetros obrigatórios faltando
+ User:
+ NotFound: O usuário não pôde ser encontrado
+ AlreadyExists: O usuário já existe
+ Inactive: O usuário está inativo
+ NotFoundOnOrg: O usuário não pôde ser encontrado na organização escolhida
+ NotAllowedOrg: O usuário não é membro da organização requerida
+ NotMatchingUserID: O usuário e o usuário na solicitação de autenticação não correspondem
+ UserIDMissing: O ID do usuário está vazio
+ Invalid: Dados de usuário inválidos
+ DomainNotAllowedAsUsername: O domínio já está reservado e não pode ser usado
+ NotAllowedToLink: O usuário não está autorizado a vincular com provedor de login externo
+ Profile:
+ NotFound: Perfil não encontrado
+ NotChanged: Perfil não foi alterado
+ Empty: O perfil está vazio
+ FirstNameEmpty: O nome próprio no perfil está vazio
+ LastNameEmpty: O sobrenome no perfil está vazio
+ IDMissing: ID do perfil está faltando
+ Email:
+ NotFound: E-mail não encontrado
+ Invalid: E-mail inválido
+ AlreadyVerified: O e-mail já está verificado
+ NotChanged: E-mail não foi alterado
+ Empty: O e-mail está vazio
+ IDMissing: ID do e-mail está faltando
+ Phone:
+ NotFound: Número de telefone não encontrado
+ Invalid: Número de telefone inválido
+ AlreadyVerified: O número de telefone já está verificado
+ Empty: O número de telefone está vazio
+ NotChanged: O número de telefone não foi alterado
+ Address:
+ NotFound: Endereço não encontrado
+ NotChanged: Endereço não foi alterado
+ Username:
+ AlreadyExists: Nome de usuário já está em uso
+ Reserved: O nome de usuário já está em uso
+ Empty: O nome de usuário está vazio
+ Password:
+ ConfirmationWrong: A confirmação da senha está incorreta
+ Empty: A senha está vazia
+ Invalid: A senha é inválida
+ InvalidAndLocked: A senha é inválida e o usuário está bloqueado, entre em contato com o administrador.
+ UsernameOrPassword:
+ Invalid: Nome de usuário ou senha inválidos
+ PasswordComplexityPolicy:
+ NotFound: Política de complexidade de senha não encontrada
+ MinLength: A senha é muito curta
+ HasLower: A senha deve conter letra minúscula
+ HasUpper: A senha deve conter letra maiúscula
+ HasNumber: A senha deve conter número
+ HasSymbol: A senha deve conter símbolo
+ Code:
+ Expired: O código expirou
+ Invalid: O código é inválido
+ Empty: O código está vazio
+ CryptoCodeNil: O código criptografado está nulo
+ NotFound: Não foi possível encontrar o código
+ GeneratorAlgNotSupported: Algoritmo do gerador não suportado
+ EmailVerify:
+ UserIDEmpty: O ID do usuário está vazio
+ ExternalData:
+ CouldNotRead: Não foi possível ler os dados externos corretamente
+ MFA:
+ NoProviders: Nenhum provedor de autenticação de vários fatores disponível
+ OTP:
+ AlreadyReady: A autenticação de vários fatores por OTP (senha única) já está configurada
+ NotExisting: A autenticação de vários fatores por OTP (senha única) não existe
+ InvalidCode: Código inválido
+ NotReady: A autenticação de vários fatores por OTP (senha única) não está pronta
+ Locked: O usuário está bloqueado
+ SomethingWentWrong: Algo deu errado
+ NotActive: O usuário não está ativo
+ ExternalIDP:
+ IDPTypeNotImplemented: Tipo de provedor de login externo não implementado
+ NotAllowed: Provedor de login externo não permitido
+ IDPConfigIDEmpty: ID do Provedor de Identidade está vazio
+ ExternalUserIDEmpty: ID do usuário externo está vazio
+ UserDisplayNameEmpty: O nome de exibição do usuário está vazio
+ NoExternalUserData: Nenhum dado de usuário externo recebido
+ CreationNotAllowed: A criação de um novo usuário não é permitida neste provedor
+ LinkingNotAllowed: A vinculação de um usuário não é permitida neste provedor
+ GrantRequired: Login não é possível. O usuário precisa ter pelo menos uma permissão no aplicativo. Entre em contato com o administrador.
+ ProjectRequired: Login não é possível. A organização do usuário precisa ser concedida ao projeto. Entre em contato com o administrador.
+ IdentityProvider:
+ InvalidConfig: Configuração do provedor de identidade inválida
+ IAM:
+ LockoutPolicy:
+ NotExisting: Política de bloqueio não existe
+ Org:
+ LoginPolicy:
+ RegistrationNotAllowed: O registro não é permitido
+ DeviceAuth:
+ NotExisting: Código do usuário não existe
+
+optional: (opcional)
diff --git a/internal/api/ui/login/static/i18n/zh.yaml b/internal/api/ui/login/static/i18n/zh.yaml
index f44b39bc6..0680fa267 100644
--- a/internal/api/ui/login/static/i18n/zh.yaml
+++ b/internal/api/ui/login/static/i18n/zh.yaml
@@ -223,6 +223,8 @@ RegistrationUser:
Japanese: 日本語
Spanish: Español
Bulgarian: Български
+ Portuguese: Português
+ Macedonian: Македонски
GenderLabel: 性别
Female: 女性
Male: 男性
@@ -256,6 +258,7 @@ ExternalRegistrationUserOverview:
Polish: Polski
Japanese: 日本語
Spanish: Español
+ Macedonian: Македонски
TosAndPrivacyLabel: 条款和条款
TosConfirm: 我接受
TosLinkText: 服务条款
@@ -317,6 +320,7 @@ ExternalNotFound:
Polish: Polski
Japanese: 日本語
Spanish: Español
+ Macedonian: Македонски
DeviceAuth:
Title: 设备授权
diff --git a/internal/api/ui/login/static/resources/images/idp/ms.svg b/internal/api/ui/login/static/resources/images/idp/ms.svg
new file mode 100644
index 000000000..1f7397648
--- /dev/null
+++ b/internal/api/ui/login/static/resources/images/idp/ms.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/internal/api/ui/login/static/resources/themes/scss/styles/identity_provider/identity_provider_base.scss b/internal/api/ui/login/static/resources/themes/scss/styles/identity_provider/identity_provider_base.scss
index 074ebd346..a558e1435 100644
--- a/internal/api/ui/login/static/resources/themes/scss/styles/identity_provider/identity_provider_base.scss
+++ b/internal/api/ui/login/static/resources/themes/scss/styles/identity_provider/identity_provider_base.scss
@@ -1,68 +1,78 @@
-$lgn-idp-margin: .5rem 0;
+$lgn-idp-margin: 0.5rem 0;
$lgn-idp-padding: 0 1px;
$lgn-idp-provider-name-line-height: 36px;
-$lgn-idp-border-radius: .5rem;
-$googlelogosource: '../../../images/idp/google';
-$githublogosource: '../../../images/idp/github';
-$gitlablogosource: '../../../images/idp/gitlab';
+$lgn-idp-border-radius: 0.5rem;
@mixin lgn-idp-base {
- display: block;
- margin: $lgn-idp-margin;
- box-sizing: border-box;
- display: flex;
- align-items: center;
- position: relative;
- padding: $lgn-idp-padding;
- border-radius: $lgn-idp-border-radius;
- border-width: 1px;
- border-style: solid;
-
+ display: block;
+ margin: $lgn-idp-margin;
+ box-sizing: border-box;
+ display: flex;
+ align-items: center;
+ position: relative;
+ padding: $lgn-idp-padding;
+ border-radius: $lgn-idp-border-radius;
+ border-width: 1px;
+ border-style: solid;
+ transition: border-color 0.2s ease-in-out;
+
+ span.logo {
+ height: 46px;
+ width: 46px;
+ }
+
+ span.provider-name {
+ line-height: $lgn-idp-provider-name-line-height;
+ position: absolute;
+ left: 50%;
+ transform: translateX(-50%);
+ }
+
+ &.google {
span.logo {
- height: 46px;
- width: 46px;
+ height: 46px;
+ width: 46px;
+ background-image: var(--google-image-src);
+ background-size: 25px;
+ background-position: center;
+ background-repeat: no-repeat;
+ border-radius: 5px;
}
+ }
- span.provider-name {
- line-height: $lgn-idp-provider-name-line-height;
- position: absolute;
- left: 50%;
- transform: translateX(-50%);
- }
-
- &.google {
- span.logo {
- height: 46px;
- width: 46px;
- background-image: url($googlelogosource + '.png');
- background-size: 25px;
- background-position: center;
- background-repeat: no-repeat;
- border-radius: 5px;
- }
+ &.github {
+ span.logo {
+ height: 46px;
+ width: 46px;
+ background-image: var(--github-image-src);
+ background-size: 25px;
+ background-position: center;
+ background-repeat: no-repeat;
+ border-radius: 5px;
}
+ }
- &.github {
- span.logo {
- height: 46px;
- width: 46px;
- background-image: url($githublogosource + '.png');
- background-size: 25px;
- background-position: center;
- background-repeat: no-repeat;
- border-radius: 5px;
- }
+ &.gitlab {
+ span.logo {
+ height: 46px;
+ width: 46px;
+ background-image: var(--gitlab-image-src);
+ background-size: 100%;
+ background-position: center;
+ background-repeat: no-repeat;
+ border-radius: 5px;
}
+ }
- &.gitlab {
- span.logo {
- height: 46px;
- width: 46px;
- background-image: url($gitlablogosource + '.png');
- background-size: 100%;
- background-position: center;
- background-repeat: no-repeat;
- border-radius: 5px;
- }
+ &.azure {
+ span.logo {
+ height: 46px;
+ width: 46px;
+ background-image: var(--azure-image-src);
+ background-size: 25px;
+ background-position: center;
+ background-repeat: no-repeat;
+ border-radius: 5px;
}
+ }
}
diff --git a/internal/api/ui/login/static/resources/themes/scss/styles/identity_provider/identity_provider_theme.scss b/internal/api/ui/login/static/resources/themes/scss/styles/identity_provider/identity_provider_theme.scss
index 0d29a038c..f05846faf 100644
--- a/internal/api/ui/login/static/resources/themes/scss/styles/identity_provider/identity_provider_theme.scss
+++ b/internal/api/ui/login/static/resources/themes/scss/styles/identity_provider/identity_provider_theme.scss
@@ -10,26 +10,11 @@
color: var(--zitadel-color-text-500);
&:hover {
- @include lgn-idp-elevation(1);
+ border-color: var(--zitadel-color-input-border-hover);
}
&:not([disabled]):active {
- @include lgn-idp-elevation(2);
- }
-
- &.google {
- color: var(--zitadel-color-google-text);
- background-color: var(--zitadel-color-google-background);
- }
-
- &.github {
- color: var(--zitadel-color-github-text);
- background-color: var(--zitadel-color-github-background);
- }
-
- &.gitlab {
- color: var(--zitadel-color-gitlab-text);
- background-color: var(--zitadel-color-gitlab-background);
+ border-color: var(--zitadel-color-input-border-active);
}
}
diff --git a/internal/api/ui/login/static/resources/themes/scss/styles/vars.scss b/internal/api/ui/login/static/resources/themes/scss/styles/vars.scss
index 304ed8a4f..3242e5f45 100644
--- a/internal/api/ui/login/static/resources/themes/scss/styles/vars.scss
+++ b/internal/api/ui/login/static/resources/themes/scss/styles/vars.scss
@@ -108,8 +108,8 @@
--zitadel-color-grey-800: #424242;
--zitadel-color-grey-900: #212121;
- --zitadel-icon-select: url("../select_arrow_light.svg");
- --zitadel-logo-powered-by: url("../logo-dark.svg");
+ --zitadel-icon-select: url(../../zitadel/select_arrow_light.svg);
+ --zitadel-logo-powered-by: url(../../zitadel/logo-dark.svg);
--zitadel-color-google-text: #8b8d8d;
--zitadel-color-google-background: #ffffff;
@@ -120,6 +120,11 @@
--zitadel-color-qr: var(--zitadel-color-black);
--zitadel-color-qr-background: var(--zitadel-color-white);
+
+ --google-image-src: url(../../../images/idp/google.png);
+ --github-image-src: url(../../../images/idp/github.png);
+ --gitlab-image-src: url(../../../images/idp/gitlab.png);
+ --azure-image-src: url(../../../images/idp/ms.svg);
}
.lgn-dark-theme {
@@ -213,8 +218,8 @@
);
/*--zitadel-color-label: var(--zitadel-color-grey-600); same */
- --zitadel-icon-select: url("../select_arrow_dark.svg");
- --zitadel-logo-powered-by: url("../logo-light.svg");
+ --zitadel-icon-select: url(../../zitadel/select_arrow_dark.svg);
+ --zitadel-logo-powered-by: url(../../zitadel/logo-light.svg);
--zitadel-color-google-text: #8b8d8d;
--zitadel-color-google-background: #ffffff;
@@ -222,4 +227,9 @@
--zitadel-color-github-background: #ffffff;
--zitadel-color-gitlab-text: #8b8d8d;
--zitadel-color-gitlab-background: #ffffff;
+
+ --google-image-src: url(../../../images/idp/google.png);
+ --github-image-src: url(../../../images/idp/github-white.png);
+ --gitlab-image-src: url(../../../images/idp/gitlab.png);
+ --azure-image-src: url(../../../images/idp/ms.svg);
}
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
deleted file mode 100644
index 6f7a3e972..000000000
--- a/internal/api/ui/login/static/resources/themes/zitadel/css/zitadel.css
+++ /dev/null
@@ -1,3125 +0,0 @@
-@charset "UTF-8";
-:root {
- --zitadel-color-primary: var(--zitadel-color-primary-500);
- --zitadel-color-background: var(--zitadel-color-background-500);
- --zitadel-color-warn: var(--zitadel-color-warn-500);
- --zitadel-color-text: var(--zitadel-color-text-500);
- --zitadel-color-primary-50: #eaedfa;
- --zitadel-color-primary-100: #ccd2f2;
- --zitadel-color-primary-200: #aab4ea;
- --zitadel-color-primary-300: #8796e1;
- --zitadel-color-primary-400: #6e80da;
- --zitadel-color-primary-500: #5469d4;
- --zitadel-color-primary-600: #4d61cf;
- --zitadel-color-primary-700: #4356c9;
- --zitadel-color-primary-800: #3a4cc3;
- --zitadel-color-primary-900: #293bb9;
- --zitadel-color-primary-rgb: rgb(84, 105, 212);
- --zitadel-color-primary-contrast: var(--zitadel-color-white);
- --zitadel-color-warn-50: #f9e7eb;
- --zitadel-color-warn-100: #f0c4cc;
- --zitadel-color-warn-200: #e69dab;
- --zitadel-color-warn-300: #dc7689;
- --zitadel-color-warn-400: #d5586f;
- --zitadel-color-warn-500: #cd3b56;
- --zitadel-color-warn-600: #c8354f;
- --zitadel-color-warn-700: #c12d45;
- --zitadel-color-warn-800: #ba263c;
- --zitadel-color-warn-900: #ae192b;
- --zitadel-font-family: "Lato";
- --zitadel-color-background-50: rgb(255, 255, 255);
- --zitadel-color-background-100: rgb(255, 255, 255);
- --zitadel-color-background-200: rgb(255, 255, 255);
- --zitadel-color-background-300: rgb(255, 255, 255);
- --zitadel-color-background-400: rgb(255, 255, 255);
- --zitadel-color-background-500: rgb(250, 250, 250);
- --zitadel-color-background-600: rgb(222, 222, 222);
- --zitadel-color-background-700: rgb(195, 195, 194);
- --zitadel-color-background-800: rgb(168, 168, 168);
- --zitadel-color-background-900: rgb(142, 142, 142);
- --zitadel-color-background-contrast: rgb(0, 0, 0);
- --zitadel-color-footer-line: rgba(0, 0, 0, 0.12);
- --zitadel-color-card-hover: #ffffff;
- --zitadel-color-input-background: #00000003;
- --zitadel-color-input-border: #1a191938;
- --zitadel-color-input-border-hover: #1a1b1b;
- --zitadel-color-input-border-active: var(--zitadel-color-primary-500);
- --zitadel-color-input-placeholder: var(--zitadel-color-grey-600);
- --zitadel-color-text-50: rgb(0, 0, 0);
- --zitadel-color-text-100: rgb(0, 0, 0);
- --zitadel-color-text-200: rgb(0, 0, 0);
- --zitadel-color-text-300: rgb(0, 0, 0);
- --zitadel-color-text-400: rgb(0, 0, 0);
- --zitadel-color-text-500: rgb(0, 0, 0);
- --zitadel-color-text-600: rgb(0, 0, 0);
- --zitadel-color-text-700: rgb(0, 0, 0);
- --zitadel-color-text-800: rgb(0, 0, 0);
- --zitadel-color-text-900: rgb(0, 0, 0);
- --zitadel-color-text-contrast: rgb(255, 255, 255);
- --zitadel-color-label: #0000008a;
- --zitadel-color-account-selector-hover: rgba(0, 0, 0, 0.02);
- --zitadel-color-account-selector-active: rgba(0, 0, 0, 0.05);
- --zitadel-color-success: #0e6245;
- --zitadel-color-success-background: #cbf4c9;
- --zitadel-color-state-success-font: #0e6245;
- --zitadel-color-state-success-background: #cbf4c9;
- --zitadel-color-state-warn-font: #620e0e;
- --zitadel-color-state-warn-background: #ffc1c1;
- --zitadel-color-footer-background: #00000002;
- --zitadel-color-footer-background-mozilla: var(
- --zitadel-color-background-600
- );
- --zitadel-color-divider: rgba(0, 0, 0, 0.12);
- --zitadel-color-button-disabled: rgba(0, 0, 0, 0.26);
- --zitadel-color-button-disabled-background: rgba(0, 0, 0, 0.12);
- --zitadel-color-button-selected-background: var(--zitadel-color-grey-900);
- --zitadel-color-button-disabled-selected-background: var(
- --zitadel-color-grey-800
- );
- --zitadel-color-raised-button-background: var(--zitadel-color-white);
- --zitadel-color-stroked-button-hover-background: #00000010;
- --zitadel-color-stroked-button-active-background: #00000020;
- --zitadel-color-raised-button-active-background: var(
- --zitadel-color-primary-700
- );
- --zitadel-color-white: #ffffff;
- --zitadel-color-black: #000000;
- --zitadel-color-grey-50: #fafafa;
- --zitadel-color-grey-100: #f5f5f5;
- --zitadel-color-grey-200: #eeeeee;
- --zitadel-color-grey-300: #e0e0e0;
- --zitadel-color-grey-400: #bdbdbd;
- --zitadel-color-grey-500: #9e9e9e;
- --zitadel-color-grey-600: #757575;
- --zitadel-color-grey-700: #616161;
- --zitadel-color-grey-800: #424242;
- --zitadel-color-grey-900: #212121;
- --zitadel-icon-select: url("../select_arrow_light.svg");
- --zitadel-logo-powered-by: url("../logo-dark.svg");
- --zitadel-color-google-text: #8b8d8d;
- --zitadel-color-google-background: #ffffff;
- --zitadel-color-github-text: #8b8d8d;
- --zitadel-color-github-background: #ffffff;
- --zitadel-color-gitlab-text: #8b8d8d;
- --zitadel-color-gitlab-background: #ffffff;
- --zitadel-color-qr: var(--zitadel-color-black);
- --zitadel-color-qr-background: var(--zitadel-color-white);
-}
-
-.lgn-dark-theme {
- --zitadel-color-primary-50: #f1f7fd;
- --zitadel-color-primary-100: #afd1f2;
- --zitadel-color-primary-200: #7fb5ea;
- --zitadel-color-primary-300: #4192e0;
- --zitadel-color-primary-400: #2782dc;
- --zitadel-color-primary-500: #2073c4;
- --zitadel-color-primary-600: #1c64aa;
- --zitadel-color-primary-700: #17548f;
- --zitadel-color-primary-800: #134575;
- --zitadel-color-primary-900: #0f355b;
- --zitadel-color-warn-50: #ffe7eb;
- --zitadel-color-warn-100: #ffc4ce;
- --zitadel-color-warn-200: #ff9dad;
- --zitadel-color-warn-300: #ff768c;
- --zitadel-color-warn-400: #ff5874;
- --zitadel-color-warn-500: #ff3b5b;
- --zitadel-color-warn-600: #ff3553;
- --zitadel-color-warn-700: #ff2d49;
- --zitadel-color-warn-800: #ff2640;
- --zitadel-color-warn-900: #ff192f;
- --zitadel-font-family: "Lato";
- --zitadel-color-background-50: rgb(60, 60, 60);
- --zitadel-color-background-100: rgb(55, 55, 55);
- --zitadel-color-background-200: rgb(49, 49, 49);
- --zitadel-color-background-300: rgb(44, 44, 44);
- --zitadel-color-background-400: rgb(36, 36, 36);
- --zitadel-color-background-500: rgb(33, 33, 33);
- --zitadel-color-background-600: rgb(30, 30, 30);
- --zitadel-color-background-700: rgb(28, 28, 28);
- --zitadel-color-background-800: rgb(25, 25, 25);
- --zitadel-color-background-900: rgb(23, 23, 23);
- --zitadel-color-background-contrast: rgb(255, 255, 255);
- --zitadel-color-footer-line: rgba(255, 255, 255, 0.12);
- --zitadel-color-card-hover: #ffffff20;
- --zitadel-color-input-background: rgba(0, 0, 0, 0.2);
- --zitadel-color-input-border: #f9f7f725;
- --zitadel-color-input-border-hover: #ffffff;
- --zitadel-color-input-border-active: var(--zitadel-color-primary-500);
- --zitadel-color-input-placeholder: var(--zitadel-color-grey-600);
- --zitadel-color-text-50: rgb(255, 255, 255);
- --zitadel-color-text-100: rgb(255, 255, 255);
- --zitadel-color-text-200: rgb(255, 255, 255);
- --zitadel-color-text-300: rgb(255, 255, 255);
- --zitadel-color-text-400: rgb(255, 255, 255);
- --zitadel-color-text-500: rgb(255, 255, 255);
- --zitadel-color-text-600: rgb(221, 222, 223);
- --zitadel-color-text-700: rgb(194, 195, 195);
- --zitadel-color-text-800: rgb(167, 168, 169);
- --zitadel-color-text-900: rgb(141, 142, 143);
- --zitadel-color-text-contrast: rgb(0, 0, 0);
- --zitadel-color-label: #ffffffb3;
- --zitadel-color-account-selector-hover: rgba(255, 255, 255, 0.02);
- --zitadel-color-account-selector-active: rgba(255, 255, 255, 0.05);
- --zitadel-color-success: #cbf4c9;
- --zitadel-color-success-background: #4f566b;
- --zitadel-color-state-success-font: #cbf4c9;
- --zitadel-color-state-success-background: #68cf8340;
- --zitadel-color-state-warn-font: #ffc1c1;
- --zitadel-color-state-warn-background: #af455359;
- --zitadel-color-footer-background: #00000020;
- --zitadel-color-footer-background-mozilla: var(
- --zitadel-color-background-600
- );
- --zitadel-color-divider: rgba(255, 255, 255, 0.12);
- --zitadel-color-button-disabled: rgba(0, 0, 0, 0.26);
- --zitadel-color-button-disabled-background: rgba(255, 255, 255, 0.12);
- --zitadel-color-button-selected-background: var(--zitadel-color-grey-900);
- --zitadel-color-button-disabled-selected-background: var(
- --zitadel-color-grey-800
- );
- --zitadel-color-raised-button-background: var(--zitadel-color-grey-800);
- --zitadel-color-stroked-button-hover-background: #ffffff10;
- --zitadel-color-stroked-button-active-background: #ffffff20;
- --zitadel-color-raised-button-active-background: var(
- --zitadel-color-primary-300
- );
- /*--zitadel-color-label: var(--zitadel-color-grey-600); same */
- --zitadel-icon-select: url("../select_arrow_dark.svg");
- --zitadel-logo-powered-by: url("../logo-light.svg");
- --zitadel-color-google-text: #8b8d8d;
- --zitadel-color-google-background: #ffffff;
- --zitadel-color-github-text: #8b8d8d;
- --zitadel-color-github-background: #ffffff;
- --zitadel-color-gitlab-text: #8b8d8d;
- --zitadel-color-gitlab-background: #ffffff;
-}
-
-body {
- margin: 0 auto;
- display: flex;
- flex-direction: column;
- height: 100vh;
-}
-body .lgn-grow {
- flex: 1;
-}
-body.waiting * {
- cursor: wait !important;
-}
-
-.hidden {
- display: none !important;
-}
-
-.text-align-center {
- text-align: center;
-}
-
-footer {
- width: 100%;
- box-sizing: border-box;
- background: #00000020;
- min-height: 50px;
- display: flex;
- align-items: center;
- padding: 0 1rem;
-}
-footer .fill-space {
- flex: 1;
-}
-footer a {
- font-size: 12px;
- margin-left: 1rem;
- display: block;
-}
-@media only screen and (min-width: 600px) {
- footer {
- position: fixed;
- bottom: 0;
- left: 0;
- right: 0;
- }
-}
-footer .watermark {
- display: flex;
-}
-footer .watermark .powered {
- font-size: 12px;
- margin: auto 2px;
- display: none;
-}
-@media only screen and (min-width: 600px) {
- footer .watermark .powered {
- display: inline-block;
- }
-}
-footer .watermark .lgn-logo-watermark {
- height: 40px;
- min-width: 125px;
- margin: auto 2px;
-}
-
-.lgn-header {
- display: flex;
- position: relative;
- margin: auto;
- padding: 0;
- max-width: 250px;
- min-height: 150px;
- align-items: center;
- justify-content: center;
-}
-.lgn-header .lgn-logo {
- display: block;
- height: 100%;
- margin: 0 auto;
- max-width: 250px;
- max-height: 150px;
- object-fit: contain;
-}
-
-.lgn-button, .lgn-stroked-button, .lgn-icon-button {
- box-sizing: border-box;
- position: relative;
- cursor: pointer;
- outline: none;
- border: none;
- -webkit-tap-highlight-color: transparent;
- display: inline-block;
- white-space: nowrap;
- text-decoration: none;
- vertical-align: baseline;
- text-align: center;
- margin: 0;
- min-width: 64px;
- line-height: 36px;
- padding: 0 16px;
- border-radius: 6px;
- overflow: visible;
- transition: all 0.2s cubic-bezier(0.645, 0.045, 0.355, 1);
-}
-.lgn-button[disabled], .lgn-stroked-button[disabled], .lgn-icon-button[disabled] {
- cursor: default;
-}
-.lgn-button::-moz-focus-inner, .lgn-stroked-button::-moz-focus-inner, .lgn-icon-button::-moz-focus-inner {
- border: 0;
-}
-
-.lgn-raised-button {
- box-sizing: border-box;
- position: relative;
- cursor: pointer;
- outline: none;
- border: none;
- -webkit-tap-highlight-color: transparent;
- display: inline-block;
- white-space: nowrap;
- text-decoration: none;
- vertical-align: baseline;
- text-align: center;
- margin: 0;
- min-width: 64px;
- line-height: 36px;
- padding: 0 16px;
- border-radius: 6px;
- overflow: visible;
- transition: all 0.2s cubic-bezier(0.645, 0.045, 0.355, 1);
- transform: translate3d(0, 0, 0);
-}
-.lgn-raised-button[disabled] {
- cursor: default;
-}
-.lgn-raised-button::-moz-focus-inner {
- border: 0;
-}
-
-.lgn-icon-button {
- padding: 0;
- min-width: 0;
- width: 40px;
- height: 40px;
- flex-shrink: 0;
- line-height: 40px;
- border-radius: 50%;
-}
-.lgn-icon-button i, .lgn-icon-button .mat-icon {
- line-height: 40px;
-}
-
-.lgn-stroked-button {
- border: 1px solid currentColor;
- padding: 0 15px;
- line-height: 34px;
-}
-
-.lgn-button:focus {
- pointer-events: none;
- transition: opacity 200ms cubic-bezier(0.35, 0, 0.25, 1), background-color 200ms cubic-bezier(0.35, 0, 0.25, 1);
-}
-
-.lgn-button *, .lgn-stroked-button *, .lgn-raised-button * {
- vertical-align: middle;
-}
-
-input:not([type=radio]):not([type=checkbox]),
-.lgn-input {
- display: block;
- box-sizing: border-box;
- padding-inline-start: 10px;
- outline: none;
- display: inline-block;
- text-align: start;
- cursor: text;
- border-radius: 4px;
- transform: all 0.2 linear;
- font-size: 1rem;
- border-style: solid;
- border-width: 1px;
- height: 40px;
- padding: 10px;
- transition: border-color 0.2s ease-in-out;
- width: 100%;
- margin: 0 0 2px 0;
-}
-input:not([type=radio]):not([type=checkbox])::placeholder,
-.lgn-input::placeholder {
- font-size: 14px;
- font-style: italic;
-}
-
-select,
-.lgn-select {
- display: block;
- box-sizing: border-box;
- padding-inline-start: 10px;
- outline: none;
- display: inline-block;
- text-align: start;
- cursor: text;
- border-radius: 4px;
- transform: all 0.2 linear;
- font-size: 1rem;
- border-style: solid;
- border-width: 1px;
- height: 40px;
- padding: 10px;
- transition: border-color 0.2s ease-in-out;
- width: 100%;
- margin: 0 0 2px 0;
-}
-select::placeholder,
-.lgn-select::placeholder {
- font-size: 14px;
- font-style: italic;
-}
-
-.lgn-suffix-wrapper {
- position: relative;
-}
-.lgn-suffix-wrapper [lgnSuffix] {
- position: absolute;
- right: 0.5rem;
- top: 9px;
- height: inherit;
- vertical-align: middle;
- max-width: 150px;
- overflow-x: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
-}
-
-.lgn-label {
- display: block;
- font-size: 12px;
- transition: color 0.2s ease;
- margin: 0 0 2px 0;
- font-weight: 400;
-}
-.lgn-label * {
- font-size: 12px;
-}
-
-@keyframes ripple {
- 0% {
- box-shadow: 0 0 0 1px rgba(0, 0, 0, 0);
- }
- 50% {
- box-shadow: 0 0 0 15px rgba(0, 0, 0, 0.1);
- }
- 100% {
- box-shadow: 0 0 0 15px rgba(0, 0, 0, 0);
- }
-}
-.lgn-radio {
- outline: none;
- margin: 16px 0;
- display: inline-block;
-}
-.lgn-radio.block {
- display: block;
-}
-.lgn-radio label {
- display: inline-block;
- height: 20px;
- position: relative;
- padding: 0 0 0 30px;
- margin-bottom: 0;
- cursor: pointer;
- vertical-align: middle;
- padding-top: 2px;
- outline: none;
- font-size: 14px;
- line-height: 16px;
-}
-.lgn-radio label::before, .lgn-radio label::after {
- position: absolute;
- content: "";
- border-radius: 50%;
- transition: all 0.3s ease;
- transition-property: transform, border-color;
-}
-.lgn-radio label::before {
- left: 0;
- top: 0;
- width: 20px;
- height: 20px;
- border-width: 2px;
- border-style: solid;
-}
-.lgn-radio label::after {
- top: 5px;
- left: 5px;
- width: 10px;
- height: 10px;
- transform: scale(0);
-}
-.lgn-radio input[type=radio] {
- display: block;
- opacity: 0;
- margin: 0;
- width: 0;
- height: 0;
- outline: none;
-}
-.lgn-radio input[type=radio]:checked + label::before {
- animation: ripple 0.2s linear forwards;
-}
-.lgn-radio input[type=radio]:checked + label::after {
- transform: scale(1);
-}
-.lgn-radio input[type=radio]:focus + label {
- opacity: 1;
-}
-.lgn-radio input[type=radio]:disabled + label {
- cursor: not-allowed;
- font-style: italic;
-}
-.lgn-radio input[type=radio]:disabled:focus {
- opacity: 0;
-}
-.lgn-radio *,
-.lgn-radio *::before,
-.lgn-radio *::after {
- box-sizing: border-box;
-}
-
-a {
- text-decoration: none;
- cursor: pointer;
- margin: 2px 0;
-}
-a.block {
- display: block;
-}
-a.sub-formfield-link {
- margin: 10px 0;
- text-decoration: underline;
-}
-
-.lgn-idp {
- display: block;
- margin: 0.5rem 0;
- box-sizing: border-box;
- display: flex;
- align-items: center;
- position: relative;
- padding: 0 1px;
- border-radius: 0.5rem;
- border-width: 1px;
- border-style: solid;
-}
-.lgn-idp span.logo {
- height: 46px;
- width: 46px;
-}
-.lgn-idp span.provider-name {
- line-height: 36px;
- position: absolute;
- left: 50%;
- transform: translateX(-50%);
-}
-.lgn-idp.google span.logo {
- height: 46px;
- width: 46px;
- background-image: url("../../../images/idp/google.png");
- background-size: 25px;
- background-position: center;
- background-repeat: no-repeat;
- border-radius: 5px;
-}
-.lgn-idp.github span.logo {
- height: 46px;
- width: 46px;
- background-image: url("../../../images/idp/github.png");
- background-size: 25px;
- background-position: center;
- background-repeat: no-repeat;
- border-radius: 5px;
-}
-.lgn-idp.gitlab span.logo {
- height: 46px;
- width: 46px;
- background-image: url("../../../images/idp/gitlab.png");
- background-size: 100%;
- background-position: center;
- background-repeat: no-repeat;
- border-radius: 5px;
-}
-
-.lgn-error {
- display: flex;
- align-items: center;
- outline: none;
-}
-.lgn-error i {
- margin-right: 0.5rem;
- font-size: 1.5rem;
-}
-
-#wa-error {
- flex-direction: column;
-}
-
-.lgn-qrcode {
- display: block;
- margin: auto;
- margin: 1rem 0;
- margin-bottom: 1.5rem;
-}
-.lgn-qrcode svg {
- margin: auto;
- display: block;
- border-radius: 0.5rem;
-}
-
-.lgn-max-width-wrapper {
- max-width: 440px;
- margin: auto;
- margin-top: 50px;
- margin-bottom: 50px;
- display: block;
-}
-@media only screen and (min-width: 600px) {
- .lgn-max-width-wrapper {
- margin-top: 100px;
- }
-}
-
-.content-container {
- position: relative;
- display: block;
- margin: 0 auto 0 auto;
- padding: 20px;
- width: 100%;
- box-sizing: border-box;
-}
-.content-container .lgn-head h1 {
- text-align: center;
-}
-.content-container .lgn-head p {
- text-align: center;
-}
-.content-container .lgn-actions {
- display: flex;
- align-items: center;
- padding: 0.5rem 0;
- margin-top: 1rem;
-}
-.content-container .lgn-actions.lgn-reverse-order {
- flex-direction: row-reverse;
-}
-.content-container .lgn-actions .fill-space {
- flex: 1;
-}
-.content-container .lgn-row {
- display: flex;
- align-items: center;
- padding: 0;
-}
-.content-container .lgn-row.lgn-reverse-order {
- flex-direction: row-reverse;
-}
-.content-container .lgn-row .fill-space {
- flex: 1;
-}
-.content-container .lgn-login-profile {
- margin-bottom: 0.5rem;
-}
-.content-container .lgn-login-profile .lgn-profile-image {
- display: block;
- margin: 0.5rem;
-}
-.content-container .lgn-login-profile .lgn-names {
- border-width: 1px;
- border-style: solid;
- border-radius: 50vw;
- display: flex;
- align-items: center;
-}
-.content-container .lgn-login-profile .lgn-names .inline-block {
- display: inline-block;
- margin: 0 2px;
-}
-.content-container .lgn-login-profile .lgn-names .lgn-displayname {
- margin: 0.5rem 1rem;
-}
-.content-container .lgn-login-profile .lgn-names .lgn-loginname {
- min-width: 0;
-}
-.content-container .lgn-login-profile .lgn-names .lgn-loginname p {
- margin: 0.5rem 0.5rem;
- text-overflow: ellipsis;
- overflow: hidden;
- white-space: nowrap;
-}
-.content-container .lgn-left-action {
- position: absolute;
- left: 1rem;
- top: -75px;
- transform: translateY(-50%);
-}
-.content-container .lgn-register-options {
- display: flex;
- flex-direction: column;
-}
-.content-container .lgn-mfa-other {
- display: flex;
- flex-direction: column;
-}
-.content-container .lgn-mfa-other button {
- width: 100%;
- margin: 0.5rem 0;
-}
-
-i {
- font-size: 1.2rem;
-}
-
-.lgn-account-selection {
- display: flex;
- flex-direction: column;
- width: 100%;
-}
-.lgn-account-selection .lgn-account {
- display: flex;
- align-items: center;
- border: none;
- outline: none;
- cursor: pointer;
-}
-.lgn-account-selection .lgn-account:disabled {
- cursor: not-allowed;
-}
-.lgn-account-selection .lgn-account .left {
- padding: 0.5rem 1rem;
-}
-.lgn-account-selection .lgn-account .lgn-names {
- display: block;
- margin: 0.5rem;
- text-align: start;
- overflow: hidden;
-}
-.lgn-account-selection .lgn-account .lgn-names .lgn-displayname {
- font-size: 16px;
- margin: 0.5rem 0;
- margin-bottom: 0;
- text-overflow: ellipsis;
- overflow: hidden;
-}
-.lgn-account-selection .lgn-account .lgn-names .lgn-loginname {
- font-size: 14px;
- margin: 0;
- text-overflow: ellipsis;
- overflow: hidden;
-}
-.lgn-account-selection .lgn-account .lgn-names .lgn-session-state {
- margin: 0.25rem 0;
- border-radius: 50vw;
- padding: 2px 0.5rem;
- letter-spacing: 0.05em;
- font-size: 12px;
- white-space: nowrap;
- box-shadow: 0 0 3px #0000001a;
- width: fit-content;
- line-height: 1rem;
-}
-.lgn-account-selection .lgn-account .fill-space {
- flex: 1;
-}
-.lgn-account-selection .lgn-account i {
- font-size: 1.2rem;
-}
-.lgn-account-selection .lgn-account i.account-add {
- font-size: 1.4rem;
- margin-left: 3px;
-}
-
-.lgn-avatar {
- height: 32px;
- width: 32px;
- border-radius: 50%;
- display: flex;
- align-items: center;
- justify-content: center;
- text-transform: uppercase;
- box-sizing: border-box;
- letter-spacing: 0.05em;
- font-size: 14px;
- outline: none;
- font-weight: bold;
- overflow: hidden;
-}
-
-.lgn-avatar .avatar-img {
- height: 100%;
- width: 100%;
- object-fit: cover;
-}
-
-.lgn-checkbox {
- display: inline-block;
- position: relative;
- margin: 0 0 10px;
- font-size: 14px;
- line-height: 24px;
- box-sizing: border-box;
-}
-.lgn-checkbox input[type=checkbox] {
- position: absolute;
- top: 4px;
- left: 0;
- width: 16px;
- height: 16px;
- opacity: 0;
- z-index: 0;
- box-sizing: inherit;
- margin: 2px;
-}
-.lgn-checkbox label {
- display: block;
- padding: 0 0 0 30px;
- cursor: pointer;
- box-sizing: inherit;
- font-size: 14px;
- line-height: 28px;
-}
-.lgn-checkbox label a {
- text-decoration: underline;
-}
-.lgn-checkbox label:before {
- content: "";
- position: absolute;
- top: 6px;
- left: 0;
- width: 16px;
- height: 16px;
- box-sizing: border-box;
- border-radius: 2px;
- transition: all 0.28s cubic-bezier(0.4, 0, 0.2, 1);
- transition-property: background-color, border-color;
-}
-.lgn-checkbox label:after {
- content: "";
- position: absolute;
- top: 7px;
- left: 5px;
- width: 4px;
- height: 10px;
- transform: rotate(45deg);
- transition: border-color 0.28s cubic-bezier(0.4, 0, 0.2, 1);
-}
-
-.lgn-select, select {
- background-position: right 10px center;
- background-repeat: no-repeat;
- background-size: auto 50%;
- color: white;
- -moz-appearance: none;
- -webkit-appearance: none;
- appearance: none;
-}
-.lgn-select:not([disabled]), select:not([disabled]) {
- cursor: pointer;
-}
-.lgn-select::-ms-expand, select::-ms-expand {
- display: none;
-}
-
-.lgn-list.lgn-policy, ul.lgn-policy {
- display: flex;
- flex-wrap: wrap;
-}
-.lgn-list.lgn-policy li, ul.lgn-policy li {
- flex: 1 0 50%;
-}
-.lgn-list li, ul li {
- display: flex;
- align-items: center;
- list-style: none;
- margin: 4px 0;
- font-style: italic;
-}
-.lgn-list li i, ul li i {
- margin: 0 0.5rem;
- font-size: 20px;
-}
-.lgn-list:not(.lgn-no-dots) li::before, ul:not(.lgn-no-dots) li::before {
- content: "•";
- font-weight: bold;
- display: inline-block;
- width: 20px;
- margin-left: -20px;
- font-style: normal;
-}
-.lgn-list.lgn-no-dots, ul.lgn-no-dots {
- list-style-type: none;
- padding-inline-start: 0;
-}
-
-@font-face {
- font-family: Aileron;
- src: url(../../../fonts/ailerons/ailerons.otf) format("opentype");
-}
-@font-face {
- font-family: Lato;
- src: url(../../../fonts/lato/Lato-Thin.ttf) format("truetype");
- font-style: normal;
- font-weight: 100;
-}
-@font-face {
- font-family: Lato;
- src: url(../../../fonts/lato/Lato-ThinItalic.ttf) format("truetype");
- font-style: italic;
- font-weight: 100;
-}
-@font-face {
- font-family: Lato;
- src: url(../../../fonts/lato/Lato-Light.ttf) format("truetype");
- font-style: normal;
- font-weight: 200;
-}
-@font-face {
- font-family: Lato;
- src: url(../../../fonts/lato/Lato-LightItalic.ttf) format("truetype");
- font-style: italic;
- font-weight: 200;
-}
-@font-face {
- font-family: Lato;
- src: url(../../../fonts/lato/Lato-Regular.ttf) format("truetype");
- font-style: normal;
- font-weight: 400;
-}
-@font-face {
- font-family: Lato;
- src: url(../../../fonts/lato/Lato-Italic.ttf) format("truetype");
- font-style: italic;
- font-weight: 400;
-}
-@font-face {
- font-family: Lato;
- src: url(../../../fonts/lato/Lato-Bold.ttf) format("truetype");
- font-style: normal;
- font-weight: 700;
-}
-@font-face {
- font-family: Lato;
- src: url(../../../fonts/lato/Lato-BoldItalic.ttf) format("truetype");
- font-style: italic;
- font-weight: 700;
-}
-@font-face {
- font-family: Lato;
- src: url(../../../fonts/lato/Lato-Black.ttf) format("truetype");
- font-style: normal;
- font-weight: 800;
-}
-@font-face {
- font-family: Lato;
- src: url(../../../fonts/lato/Lato-BlackItalic.ttf) format("truetype");
- font-style: italic;
- font-weight: 800;
-}
-@font-face {
- font-family: Roboto;
- src: url(../../../fonts/Roboto/Roboto-Black.ttf) format("truetype");
- font-style: normal;
- font-weight: 900;
-}
-@font-face {
- font-family: Roboto;
- src: url(../../../fonts/Roboto/Roboto-BlackItalic.ttf) format("truetype");
- font-style: italic;
- font-weight: 900;
-}
-@font-face {
- font-family: Roboto;
- src: url(../../../fonts/Roboto/Roboto-Bold.ttf) format("truetype");
- font-style: normal;
- font-weight: 700;
-}
-@font-face {
- font-family: Roboto;
- src: url(../../../fonts/Roboto/Roboto-BoldItalic.ttf) format("truetype");
- font-style: italic;
- font-weight: 700;
-}
-@font-face {
- font-family: Roboto;
- src: url(../../../fonts/Roboto/Roboto-Regular.ttf) format("truetype");
- font-style: normal;
- font-weight: 400;
-}
-@font-face {
- font-family: Roboto;
- src: url(../../../fonts/Roboto/Roboto-Italic.ttf) format("truetype");
- font-style: italic;
- font-weight: 400;
-}
-@font-face {
- font-family: Roboto;
- src: url(../../../fonts/Roboto/Roboto-Light.ttf) format("truetype");
- font-style: normal;
- font-weight: 300;
-}
-@font-face {
- font-family: Roboto;
- src: url(../../../fonts/Roboto/Roboto-LightItalic.ttf) format("truetype");
- font-style: italic;
- font-weight: 300;
-}
-@font-face {
- font-family: Roboto;
- src: url(../../../fonts/Roboto/Roboto-Medium.ttf) format("truetype");
- font-style: normal;
- font-weight: 500;
-}
-@font-face {
- font-family: Roboto;
- src: url(../../../fonts/Roboto/Roboto-MediumItalic.ttf) format("truetype");
- font-style: italic;
- font-weight: 500;
-}
-@font-face {
- font-family: Roboto;
- src: url(../../../fonts/Roboto/Roboto-Thin.ttf) format("truetype");
- font-style: normal;
- font-weight: 100;
-}
-@font-face {
- font-family: Roboto;
- src: url(../../../fonts/Roboto/Roboto-ThinItalic.ttf) format("truetype");
- font-style: italic;
- font-weight: 100;
-}
-@font-face {
- font-family: Raleway;
- src: url(../../../fonts/Raleway/Raleway-Black.ttf) format("truetype");
- font-style: normal;
- font-weight: 900;
-}
-@font-face {
- font-family: Raleway;
- src: url(../../../fonts/Raleway/Raleway-BlackItalic.ttf) format("truetype");
- font-style: italic;
- font-weight: 900;
-}
-@font-face {
- font-family: Raleway;
- src: url(../../../fonts/Raleway/Raleway-Bold.ttf) format("truetype");
- font-style: normal;
- font-weight: 700;
-}
-@font-face {
- font-family: Raleway;
- src: url(../../../fonts/Raleway/Raleway-BoldItalic.ttf) format("truetype");
- font-style: italic;
- font-weight: 700;
-}
-@font-face {
- font-family: Raleway;
- src: url(../../../fonts/Raleway/Raleway-ExtraBold.ttf) format("truetype");
- font-style: normal;
- font-weight: 800;
-}
-@font-face {
- font-family: Raleway;
- src: url(../../../fonts/Raleway/Raleway-ExtraBoldItalic.ttf) format("truetype");
- font-style: italic;
- font-weight: 800;
-}
-@font-face {
- font-family: Raleway;
- src: url(../../../fonts/Raleway/Raleway-ExtraLight.ttf) format("truetype");
- font-style: normal;
- font-weight: 200;
-}
-@font-face {
- font-family: Raleway;
- src: url(../../../fonts/Raleway/Raleway-ExtraLightItalic.ttf) format("truetype");
- font-style: italic;
- font-weight: 200;
-}
-@font-face {
- font-family: Raleway;
- src: url(../../../fonts/Raleway/Raleway-Light.ttf) format("truetype");
- font-style: normal;
- font-weight: 300;
-}
-@font-face {
- font-family: Raleway;
- src: url(../../../fonts/Raleway/Raleway-LightItalic.ttf) format("truetype");
- font-style: italic;
- font-weight: 300;
-}
-@font-face {
- font-family: Raleway;
- src: url(../../../fonts/Raleway/Raleway-Medium.ttf) format("truetype");
- font-style: normal;
- font-weight: 500;
-}
-@font-face {
- font-family: Raleway;
- src: url(../../../fonts/Raleway/Raleway-MediumItalic.ttf) format("truetype");
- font-style: italic;
- font-weight: 500;
-}
-@font-face {
- font-family: Raleway;
- src: url(../../../fonts/Raleway/Raleway-Regular.ttf) format("truetype");
- font-style: normal;
- font-weight: 400;
-}
-@font-face {
- font-family: Raleway;
- src: url(../../../fonts/Raleway/Raleway-Italic.ttf) format("truetype");
- font-style: italic;
- font-weight: 400;
-}
-@font-face {
- font-family: Raleway;
- src: url(../../../fonts/Raleway/Raleway-SemiBold.ttf) format("truetype");
- font-style: normal;
- font-weight: 600;
-}
-@font-face {
- font-family: Raleway;
- src: url(../../../fonts/Raleway/Raleway-SemiBoldItalic.ttf) format("truetype");
- font-style: italic;
- font-weight: 600;
-}
-@font-face {
- font-family: Raleway;
- src: url(../../../fonts/Raleway/Raleway-Thin.ttf) format("truetype");
- font-style: normal;
- font-weight: 100;
-}
-@font-face {
- font-family: Raleway;
- src: url(../../../fonts/Raleway/Raleway-ThinItalic.ttf) format("truetype");
- font-style: italic;
- font-weight: 100;
-}
-@font-face {
- font-family: PTSans;
- src: url(../../../fonts/PT_Sans/PTSans-Bold.ttf) format("truetype");
- font-style: normal;
- font-weight: 700;
-}
-@font-face {
- font-family: PTSans;
- src: url(../../../fonts/PT_Sans/PTSans-BoldItalic.ttf) format("truetype");
- font-style: italic;
- font-weight: 700;
-}
-@font-face {
- font-family: PTSans;
- src: url(../../../fonts/PT_Sans/PTSans-Italic.ttf) format("truetype");
- font-style: italic;
- font-weight: 400;
-}
-@font-face {
- font-family: PTSans;
- src: url(../../../fonts/PT_Sans/PTSans-Regular.ttf) format("truetype");
- font-style: normal;
- font-weight: 400;
-}
-.lgn-success-label {
- border-radius: 0.5rem;
- padding: 0.5rem;
-}
-
-.lgn-mfa-options {
- display: flex;
- flex-direction: row;
- justify-content: space-evenly;
- margin: 2rem 0;
-}
-.lgn-mfa-options .mfa {
- display: flex;
- flex-direction: column;
- align-items: center;
- flex: 1;
- padding: 0 0.5rem;
-}
-.lgn-mfa-options .mfa label {
- display: flex;
- flex-direction: column;
- align-items: center;
- position: relative;
-}
-.lgn-mfa-options .mfa label input[type=radio] {
- position: absolute;
- top: 0;
- left: 0;
- right: 0;
- bottom: 0;
-}
-.lgn-mfa-options .mfa label span {
- text-align: center;
-}
-.lgn-mfa-options .mfa label .mfa-img {
- border-width: 1px;
- border-style: solid;
- border-radius: 0.5rem;
- padding: 1rem;
- display: flex;
- margin-bottom: 0.5rem;
- transition: all 0.2s ease;
-}
-
-footer {
- width: 100%;
- box-sizing: border-box;
- background: #00000020;
- min-height: 50px;
- display: flex;
- align-items: center;
- padding: 0 1rem;
-}
-footer .fill-space {
- flex: 1;
-}
-footer a {
- font-size: 12px;
- margin-left: 1rem;
- display: block;
-}
-@media only screen and (min-width: 600px) {
- footer {
- position: fixed;
- bottom: 0;
- left: 0;
- right: 0;
- }
-}
-footer .watermark {
- display: flex;
-}
-footer .watermark .powered {
- font-size: 12px;
- margin: auto 2px;
- display: none;
-}
-@media only screen and (min-width: 600px) {
- footer .watermark .powered {
- display: inline-block;
- }
-}
-footer .watermark .lgn-logo-watermark {
- height: 40px;
- min-width: 125px;
- margin: auto 2px;
-}
-
-.lgn-header {
- display: flex;
- position: relative;
- margin: auto;
- padding: 0;
- max-width: 250px;
- min-height: 150px;
- align-items: center;
- justify-content: center;
-}
-.lgn-header .lgn-logo {
- display: block;
- height: 100%;
- margin: 0 auto;
- max-width: 250px;
- max-height: 150px;
- object-fit: contain;
-}
-
-.lgn-button, .lgn-stroked-button, .lgn-icon-button {
- box-sizing: border-box;
- position: relative;
- cursor: pointer;
- outline: none;
- border: none;
- -webkit-tap-highlight-color: transparent;
- display: inline-block;
- white-space: nowrap;
- text-decoration: none;
- vertical-align: baseline;
- text-align: center;
- margin: 0;
- min-width: 64px;
- line-height: 36px;
- padding: 0 16px;
- border-radius: 6px;
- overflow: visible;
- transition: all 0.2s cubic-bezier(0.645, 0.045, 0.355, 1);
-}
-.lgn-button[disabled], .lgn-stroked-button[disabled], .lgn-icon-button[disabled] {
- cursor: default;
-}
-.lgn-button::-moz-focus-inner, .lgn-stroked-button::-moz-focus-inner, .lgn-icon-button::-moz-focus-inner {
- border: 0;
-}
-
-.lgn-raised-button {
- box-sizing: border-box;
- position: relative;
- cursor: pointer;
- outline: none;
- border: none;
- -webkit-tap-highlight-color: transparent;
- display: inline-block;
- white-space: nowrap;
- text-decoration: none;
- vertical-align: baseline;
- text-align: center;
- margin: 0;
- min-width: 64px;
- line-height: 36px;
- padding: 0 16px;
- border-radius: 6px;
- overflow: visible;
- transition: all 0.2s cubic-bezier(0.645, 0.045, 0.355, 1);
- transform: translate3d(0, 0, 0);
-}
-.lgn-raised-button[disabled] {
- cursor: default;
-}
-.lgn-raised-button::-moz-focus-inner {
- border: 0;
-}
-
-.lgn-icon-button {
- padding: 0;
- min-width: 0;
- width: 40px;
- height: 40px;
- flex-shrink: 0;
- line-height: 40px;
- border-radius: 50%;
-}
-.lgn-icon-button i, .lgn-icon-button .mat-icon {
- line-height: 40px;
-}
-
-.lgn-stroked-button {
- border: 1px solid currentColor;
- padding: 0 15px;
- line-height: 34px;
-}
-
-.lgn-button:focus {
- pointer-events: none;
- transition: opacity 200ms cubic-bezier(0.35, 0, 0.25, 1), background-color 200ms cubic-bezier(0.35, 0, 0.25, 1);
-}
-
-.lgn-button *, .lgn-stroked-button *, .lgn-raised-button * {
- vertical-align: middle;
-}
-
-input:not([type=radio]):not([type=checkbox]),
-.lgn-input {
- display: block;
- box-sizing: border-box;
- padding-inline-start: 10px;
- outline: none;
- display: inline-block;
- text-align: start;
- cursor: text;
- border-radius: 4px;
- transform: all 0.2 linear;
- font-size: 1rem;
- border-style: solid;
- border-width: 1px;
- height: 40px;
- padding: 10px;
- transition: border-color 0.2s ease-in-out;
- width: 100%;
- margin: 0 0 2px 0;
-}
-input:not([type=radio]):not([type=checkbox])::placeholder,
-.lgn-input::placeholder {
- font-size: 14px;
- font-style: italic;
-}
-
-select,
-.lgn-select {
- display: block;
- box-sizing: border-box;
- padding-inline-start: 10px;
- outline: none;
- display: inline-block;
- text-align: start;
- cursor: text;
- border-radius: 4px;
- transform: all 0.2 linear;
- font-size: 1rem;
- border-style: solid;
- border-width: 1px;
- height: 40px;
- padding: 10px;
- transition: border-color 0.2s ease-in-out;
- width: 100%;
- margin: 0 0 2px 0;
-}
-select::placeholder,
-.lgn-select::placeholder {
- font-size: 14px;
- font-style: italic;
-}
-
-.lgn-suffix-wrapper {
- position: relative;
-}
-.lgn-suffix-wrapper [lgnSuffix] {
- position: absolute;
- right: 0.5rem;
- top: 9px;
- height: inherit;
- vertical-align: middle;
- max-width: 150px;
- overflow-x: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
-}
-
-.lgn-label {
- display: block;
- font-size: 12px;
- transition: color 0.2s ease;
- margin: 0 0 2px 0;
- font-weight: 400;
-}
-.lgn-label * {
- font-size: 12px;
-}
-
-@keyframes ripple {
- 0% {
- box-shadow: 0 0 0 1px rgba(0, 0, 0, 0);
- }
- 50% {
- box-shadow: 0 0 0 15px rgba(0, 0, 0, 0.1);
- }
- 100% {
- box-shadow: 0 0 0 15px rgba(0, 0, 0, 0);
- }
-}
-.lgn-radio {
- outline: none;
- margin: 16px 0;
- display: inline-block;
-}
-.lgn-radio.block {
- display: block;
-}
-.lgn-radio label {
- display: inline-block;
- height: 20px;
- position: relative;
- padding: 0 0 0 30px;
- margin-bottom: 0;
- cursor: pointer;
- vertical-align: middle;
- padding-top: 2px;
- outline: none;
- font-size: 14px;
- line-height: 16px;
-}
-.lgn-radio label::before, .lgn-radio label::after {
- position: absolute;
- content: "";
- border-radius: 50%;
- transition: all 0.3s ease;
- transition-property: transform, border-color;
-}
-.lgn-radio label::before {
- left: 0;
- top: 0;
- width: 20px;
- height: 20px;
- border-width: 2px;
- border-style: solid;
-}
-.lgn-radio label::after {
- top: 5px;
- left: 5px;
- width: 10px;
- height: 10px;
- transform: scale(0);
-}
-.lgn-radio input[type=radio] {
- display: block;
- opacity: 0;
- margin: 0;
- width: 0;
- height: 0;
- outline: none;
-}
-.lgn-radio input[type=radio]:checked + label::before {
- animation: ripple 0.2s linear forwards;
-}
-.lgn-radio input[type=radio]:checked + label::after {
- transform: scale(1);
-}
-.lgn-radio input[type=radio]:focus + label {
- opacity: 1;
-}
-.lgn-radio input[type=radio]:disabled + label {
- cursor: not-allowed;
- font-style: italic;
-}
-.lgn-radio input[type=radio]:disabled:focus {
- opacity: 0;
-}
-.lgn-radio *,
-.lgn-radio *::before,
-.lgn-radio *::after {
- box-sizing: border-box;
-}
-
-a {
- text-decoration: none;
- cursor: pointer;
- margin: 2px 0;
-}
-a.block {
- display: block;
-}
-a.sub-formfield-link {
- margin: 10px 0;
- text-decoration: underline;
-}
-
-.lgn-idp {
- display: block;
- margin: 0.5rem 0;
- box-sizing: border-box;
- display: flex;
- align-items: center;
- position: relative;
- padding: 0 1px;
- border-radius: 0.5rem;
- border-width: 1px;
- border-style: solid;
-}
-.lgn-idp span.logo {
- height: 46px;
- width: 46px;
-}
-.lgn-idp span.provider-name {
- line-height: 36px;
- position: absolute;
- left: 50%;
- transform: translateX(-50%);
-}
-.lgn-idp.google span.logo {
- height: 46px;
- width: 46px;
- background-image: url("../../../images/idp/google.png");
- background-size: 25px;
- background-position: center;
- background-repeat: no-repeat;
- border-radius: 5px;
-}
-.lgn-idp.github span.logo {
- height: 46px;
- width: 46px;
- background-image: url("../../../images/idp/github.png");
- background-size: 25px;
- background-position: center;
- background-repeat: no-repeat;
- border-radius: 5px;
-}
-.lgn-idp.gitlab span.logo {
- height: 46px;
- width: 46px;
- background-image: url("../../../images/idp/gitlab.png");
- background-size: 100%;
- background-position: center;
- background-repeat: no-repeat;
- border-radius: 5px;
-}
-
-.lgn-error {
- display: flex;
- align-items: center;
- outline: none;
-}
-.lgn-error i {
- margin-right: 0.5rem;
- font-size: 1.5rem;
-}
-
-#wa-error {
- flex-direction: column;
-}
-
-.lgn-qrcode {
- display: block;
- margin: auto;
- margin: 1rem 0;
- margin-bottom: 1.5rem;
-}
-.lgn-qrcode svg {
- margin: auto;
- display: block;
- border-radius: 0.5rem;
-}
-
-.lgn-max-width-wrapper {
- max-width: 440px;
- margin: auto;
- margin-top: 50px;
- margin-bottom: 50px;
- display: block;
-}
-@media only screen and (min-width: 600px) {
- .lgn-max-width-wrapper {
- margin-top: 100px;
- }
-}
-
-.content-container {
- position: relative;
- display: block;
- margin: 0 auto 0 auto;
- padding: 20px;
- width: 100%;
- box-sizing: border-box;
-}
-.content-container .lgn-head h1 {
- text-align: center;
-}
-.content-container .lgn-head p {
- text-align: center;
-}
-.content-container .lgn-actions {
- display: flex;
- align-items: center;
- padding: 0.5rem 0;
- margin-top: 1rem;
-}
-.content-container .lgn-actions.lgn-reverse-order {
- flex-direction: row-reverse;
-}
-.content-container .lgn-actions .fill-space {
- flex: 1;
-}
-.content-container .lgn-row {
- display: flex;
- align-items: center;
- padding: 0;
-}
-.content-container .lgn-row.lgn-reverse-order {
- flex-direction: row-reverse;
-}
-.content-container .lgn-row .fill-space {
- flex: 1;
-}
-.content-container .lgn-login-profile {
- margin-bottom: 0.5rem;
-}
-.content-container .lgn-login-profile .lgn-profile-image {
- display: block;
- margin: 0.5rem;
-}
-.content-container .lgn-login-profile .lgn-names {
- border-width: 1px;
- border-style: solid;
- border-radius: 50vw;
- display: flex;
- align-items: center;
-}
-.content-container .lgn-login-profile .lgn-names .inline-block {
- display: inline-block;
- margin: 0 2px;
-}
-.content-container .lgn-login-profile .lgn-names .lgn-displayname {
- margin: 0.5rem 1rem;
-}
-.content-container .lgn-login-profile .lgn-names .lgn-loginname {
- min-width: 0;
-}
-.content-container .lgn-login-profile .lgn-names .lgn-loginname p {
- margin: 0.5rem 0.5rem;
- text-overflow: ellipsis;
- overflow: hidden;
- white-space: nowrap;
-}
-.content-container .lgn-left-action {
- position: absolute;
- left: 1rem;
- top: -75px;
- transform: translateY(-50%);
-}
-.content-container .lgn-register-options {
- display: flex;
- flex-direction: column;
-}
-.content-container .lgn-mfa-other {
- display: flex;
- flex-direction: column;
-}
-.content-container .lgn-mfa-other button {
- width: 100%;
- margin: 0.5rem 0;
-}
-
-i {
- font-size: 1.2rem;
-}
-
-.lgn-account-selection {
- display: flex;
- flex-direction: column;
- width: 100%;
-}
-.lgn-account-selection .lgn-account {
- display: flex;
- align-items: center;
- border: none;
- outline: none;
- cursor: pointer;
-}
-.lgn-account-selection .lgn-account:disabled {
- cursor: not-allowed;
-}
-.lgn-account-selection .lgn-account .left {
- padding: 0.5rem 1rem;
-}
-.lgn-account-selection .lgn-account .lgn-names {
- display: block;
- margin: 0.5rem;
- text-align: start;
- overflow: hidden;
-}
-.lgn-account-selection .lgn-account .lgn-names .lgn-displayname {
- font-size: 16px;
- margin: 0.5rem 0;
- margin-bottom: 0;
- text-overflow: ellipsis;
- overflow: hidden;
-}
-.lgn-account-selection .lgn-account .lgn-names .lgn-loginname {
- font-size: 14px;
- margin: 0;
- text-overflow: ellipsis;
- overflow: hidden;
-}
-.lgn-account-selection .lgn-account .lgn-names .lgn-session-state {
- margin: 0.25rem 0;
- border-radius: 50vw;
- padding: 2px 0.5rem;
- letter-spacing: 0.05em;
- font-size: 12px;
- white-space: nowrap;
- box-shadow: 0 0 3px #0000001a;
- width: fit-content;
- line-height: 1rem;
-}
-.lgn-account-selection .lgn-account .fill-space {
- flex: 1;
-}
-.lgn-account-selection .lgn-account i {
- font-size: 1.2rem;
-}
-.lgn-account-selection .lgn-account i.account-add {
- font-size: 1.4rem;
- margin-left: 3px;
-}
-
-.lgn-avatar {
- height: 32px;
- width: 32px;
- border-radius: 50%;
- display: flex;
- align-items: center;
- justify-content: center;
- text-transform: uppercase;
- box-sizing: border-box;
- letter-spacing: 0.05em;
- font-size: 14px;
- outline: none;
- font-weight: bold;
- overflow: hidden;
-}
-
-.lgn-avatar .avatar-img {
- height: 100%;
- width: 100%;
- object-fit: cover;
-}
-
-.lgn-checkbox {
- display: inline-block;
- position: relative;
- margin: 0 0 10px;
- font-size: 14px;
- line-height: 24px;
- box-sizing: border-box;
-}
-.lgn-checkbox input[type=checkbox] {
- position: absolute;
- top: 4px;
- left: 0;
- width: 16px;
- height: 16px;
- opacity: 0;
- z-index: 0;
- box-sizing: inherit;
- margin: 2px;
-}
-.lgn-checkbox label {
- display: block;
- padding: 0 0 0 30px;
- cursor: pointer;
- box-sizing: inherit;
- font-size: 14px;
- line-height: 28px;
-}
-.lgn-checkbox label a {
- text-decoration: underline;
-}
-.lgn-checkbox label:before {
- content: "";
- position: absolute;
- top: 6px;
- left: 0;
- width: 16px;
- height: 16px;
- box-sizing: border-box;
- border-radius: 2px;
- transition: all 0.28s cubic-bezier(0.4, 0, 0.2, 1);
- transition-property: background-color, border-color;
-}
-.lgn-checkbox label:after {
- content: "";
- position: absolute;
- top: 7px;
- left: 5px;
- width: 4px;
- height: 10px;
- transform: rotate(45deg);
- transition: border-color 0.28s cubic-bezier(0.4, 0, 0.2, 1);
-}
-
-.lgn-select, select {
- background-position: right 10px center;
- background-repeat: no-repeat;
- background-size: auto 50%;
- color: white;
- -moz-appearance: none;
- -webkit-appearance: none;
- appearance: none;
-}
-.lgn-select:not([disabled]), select:not([disabled]) {
- cursor: pointer;
-}
-.lgn-select::-ms-expand, select::-ms-expand {
- display: none;
-}
-
-.lgn-list.lgn-policy, ul.lgn-policy {
- display: flex;
- flex-wrap: wrap;
-}
-.lgn-list.lgn-policy li, ul.lgn-policy li {
- flex: 1 0 50%;
-}
-.lgn-list li, ul li {
- display: flex;
- align-items: center;
- list-style: none;
- margin: 4px 0;
- font-style: italic;
-}
-.lgn-list li i, ul li i {
- margin: 0 0.5rem;
- font-size: 20px;
-}
-.lgn-list:not(.lgn-no-dots) li::before, ul:not(.lgn-no-dots) li::before {
- content: "•";
- font-weight: bold;
- display: inline-block;
- width: 20px;
- margin-left: -20px;
- font-style: normal;
-}
-.lgn-list.lgn-no-dots, ul.lgn-no-dots {
- list-style-type: none;
- padding-inline-start: 0;
-}
-
-@font-face {
- font-family: Aileron;
- src: url(../../../fonts/ailerons/ailerons.otf) format("opentype");
-}
-@font-face {
- font-family: Lato;
- src: url(../../../fonts/lato/Lato-Thin.ttf) format("truetype");
- font-style: normal;
- font-weight: 100;
-}
-@font-face {
- font-family: Lato;
- src: url(../../../fonts/lato/Lato-ThinItalic.ttf) format("truetype");
- font-style: italic;
- font-weight: 100;
-}
-@font-face {
- font-family: Lato;
- src: url(../../../fonts/lato/Lato-Light.ttf) format("truetype");
- font-style: normal;
- font-weight: 200;
-}
-@font-face {
- font-family: Lato;
- src: url(../../../fonts/lato/Lato-LightItalic.ttf) format("truetype");
- font-style: italic;
- font-weight: 200;
-}
-@font-face {
- font-family: Lato;
- src: url(../../../fonts/lato/Lato-Regular.ttf) format("truetype");
- font-style: normal;
- font-weight: 400;
-}
-@font-face {
- font-family: Lato;
- src: url(../../../fonts/lato/Lato-Italic.ttf) format("truetype");
- font-style: italic;
- font-weight: 400;
-}
-@font-face {
- font-family: Lato;
- src: url(../../../fonts/lato/Lato-Bold.ttf) format("truetype");
- font-style: normal;
- font-weight: 700;
-}
-@font-face {
- font-family: Lato;
- src: url(../../../fonts/lato/Lato-BoldItalic.ttf) format("truetype");
- font-style: italic;
- font-weight: 700;
-}
-@font-face {
- font-family: Lato;
- src: url(../../../fonts/lato/Lato-Black.ttf) format("truetype");
- font-style: normal;
- font-weight: 800;
-}
-@font-face {
- font-family: Lato;
- src: url(../../../fonts/lato/Lato-BlackItalic.ttf) format("truetype");
- font-style: italic;
- font-weight: 800;
-}
-@font-face {
- font-family: Roboto;
- src: url(../../../fonts/Roboto/Roboto-Black.ttf) format("truetype");
- font-style: normal;
- font-weight: 900;
-}
-@font-face {
- font-family: Roboto;
- src: url(../../../fonts/Roboto/Roboto-BlackItalic.ttf) format("truetype");
- font-style: italic;
- font-weight: 900;
-}
-@font-face {
- font-family: Roboto;
- src: url(../../../fonts/Roboto/Roboto-Bold.ttf) format("truetype");
- font-style: normal;
- font-weight: 700;
-}
-@font-face {
- font-family: Roboto;
- src: url(../../../fonts/Roboto/Roboto-BoldItalic.ttf) format("truetype");
- font-style: italic;
- font-weight: 700;
-}
-@font-face {
- font-family: Roboto;
- src: url(../../../fonts/Roboto/Roboto-Regular.ttf) format("truetype");
- font-style: normal;
- font-weight: 400;
-}
-@font-face {
- font-family: Roboto;
- src: url(../../../fonts/Roboto/Roboto-Italic.ttf) format("truetype");
- font-style: italic;
- font-weight: 400;
-}
-@font-face {
- font-family: Roboto;
- src: url(../../../fonts/Roboto/Roboto-Light.ttf) format("truetype");
- font-style: normal;
- font-weight: 300;
-}
-@font-face {
- font-family: Roboto;
- src: url(../../../fonts/Roboto/Roboto-LightItalic.ttf) format("truetype");
- font-style: italic;
- font-weight: 300;
-}
-@font-face {
- font-family: Roboto;
- src: url(../../../fonts/Roboto/Roboto-Medium.ttf) format("truetype");
- font-style: normal;
- font-weight: 500;
-}
-@font-face {
- font-family: Roboto;
- src: url(../../../fonts/Roboto/Roboto-MediumItalic.ttf) format("truetype");
- font-style: italic;
- font-weight: 500;
-}
-@font-face {
- font-family: Roboto;
- src: url(../../../fonts/Roboto/Roboto-Thin.ttf) format("truetype");
- font-style: normal;
- font-weight: 100;
-}
-@font-face {
- font-family: Roboto;
- src: url(../../../fonts/Roboto/Roboto-ThinItalic.ttf) format("truetype");
- font-style: italic;
- font-weight: 100;
-}
-@font-face {
- font-family: Raleway;
- src: url(../../../fonts/Raleway/Raleway-Black.ttf) format("truetype");
- font-style: normal;
- font-weight: 900;
-}
-@font-face {
- font-family: Raleway;
- src: url(../../../fonts/Raleway/Raleway-BlackItalic.ttf) format("truetype");
- font-style: italic;
- font-weight: 900;
-}
-@font-face {
- font-family: Raleway;
- src: url(../../../fonts/Raleway/Raleway-Bold.ttf) format("truetype");
- font-style: normal;
- font-weight: 700;
-}
-@font-face {
- font-family: Raleway;
- src: url(../../../fonts/Raleway/Raleway-BoldItalic.ttf) format("truetype");
- font-style: italic;
- font-weight: 700;
-}
-@font-face {
- font-family: Raleway;
- src: url(../../../fonts/Raleway/Raleway-ExtraBold.ttf) format("truetype");
- font-style: normal;
- font-weight: 800;
-}
-@font-face {
- font-family: Raleway;
- src: url(../../../fonts/Raleway/Raleway-ExtraBoldItalic.ttf) format("truetype");
- font-style: italic;
- font-weight: 800;
-}
-@font-face {
- font-family: Raleway;
- src: url(../../../fonts/Raleway/Raleway-ExtraLight.ttf) format("truetype");
- font-style: normal;
- font-weight: 200;
-}
-@font-face {
- font-family: Raleway;
- src: url(../../../fonts/Raleway/Raleway-ExtraLightItalic.ttf) format("truetype");
- font-style: italic;
- font-weight: 200;
-}
-@font-face {
- font-family: Raleway;
- src: url(../../../fonts/Raleway/Raleway-Light.ttf) format("truetype");
- font-style: normal;
- font-weight: 300;
-}
-@font-face {
- font-family: Raleway;
- src: url(../../../fonts/Raleway/Raleway-LightItalic.ttf) format("truetype");
- font-style: italic;
- font-weight: 300;
-}
-@font-face {
- font-family: Raleway;
- src: url(../../../fonts/Raleway/Raleway-Medium.ttf) format("truetype");
- font-style: normal;
- font-weight: 500;
-}
-@font-face {
- font-family: Raleway;
- src: url(../../../fonts/Raleway/Raleway-MediumItalic.ttf) format("truetype");
- font-style: italic;
- font-weight: 500;
-}
-@font-face {
- font-family: Raleway;
- src: url(../../../fonts/Raleway/Raleway-Regular.ttf) format("truetype");
- font-style: normal;
- font-weight: 400;
-}
-@font-face {
- font-family: Raleway;
- src: url(../../../fonts/Raleway/Raleway-Italic.ttf) format("truetype");
- font-style: italic;
- font-weight: 400;
-}
-@font-face {
- font-family: Raleway;
- src: url(../../../fonts/Raleway/Raleway-SemiBold.ttf) format("truetype");
- font-style: normal;
- font-weight: 600;
-}
-@font-face {
- font-family: Raleway;
- src: url(../../../fonts/Raleway/Raleway-SemiBoldItalic.ttf) format("truetype");
- font-style: italic;
- font-weight: 600;
-}
-@font-face {
- font-family: Raleway;
- src: url(../../../fonts/Raleway/Raleway-Thin.ttf) format("truetype");
- font-style: normal;
- font-weight: 100;
-}
-@font-face {
- font-family: Raleway;
- src: url(../../../fonts/Raleway/Raleway-ThinItalic.ttf) format("truetype");
- font-style: italic;
- font-weight: 100;
-}
-@font-face {
- font-family: PTSans;
- src: url(../../../fonts/PT_Sans/PTSans-Bold.ttf) format("truetype");
- font-style: normal;
- font-weight: 700;
-}
-@font-face {
- font-family: PTSans;
- src: url(../../../fonts/PT_Sans/PTSans-BoldItalic.ttf) format("truetype");
- font-style: italic;
- font-weight: 700;
-}
-@font-face {
- font-family: PTSans;
- src: url(../../../fonts/PT_Sans/PTSans-Italic.ttf) format("truetype");
- font-style: italic;
- font-weight: 400;
-}
-@font-face {
- font-family: PTSans;
- src: url(../../../fonts/PT_Sans/PTSans-Regular.ttf) format("truetype");
- font-style: normal;
- font-weight: 400;
-}
-.lgn-success-label {
- border-radius: 0.5rem;
- padding: 0.5rem;
-}
-
-.lgn-mfa-options {
- display: flex;
- flex-direction: row;
- justify-content: space-evenly;
- margin: 2rem 0;
-}
-.lgn-mfa-options .mfa {
- display: flex;
- flex-direction: column;
- align-items: center;
- flex: 1;
- padding: 0 0.5rem;
-}
-.lgn-mfa-options .mfa label {
- display: flex;
- flex-direction: column;
- align-items: center;
- position: relative;
-}
-.lgn-mfa-options .mfa label input[type=radio] {
- position: absolute;
- top: 0;
- left: 0;
- right: 0;
- bottom: 0;
-}
-.lgn-mfa-options .mfa label span {
- text-align: center;
-}
-.lgn-mfa-options .mfa label .mfa-img {
- border-width: 1px;
- border-style: solid;
- border-radius: 0.5rem;
- padding: 1rem;
- display: flex;
- margin-bottom: 0.5rem;
- transition: all 0.2s ease;
-}
-
-@font-face {
- font-family: Aileron;
- src: url(../../../fonts/ailerons/ailerons.otf) format("opentype");
-}
-@font-face {
- font-family: Lato;
- src: url(../../../fonts/lato/Lato-Thin.ttf) format("truetype");
- font-style: normal;
- font-weight: 100;
-}
-@font-face {
- font-family: Lato;
- src: url(../../../fonts/lato/Lato-ThinItalic.ttf) format("truetype");
- font-style: italic;
- font-weight: 100;
-}
-@font-face {
- font-family: Lato;
- src: url(../../../fonts/lato/Lato-Light.ttf) format("truetype");
- font-style: normal;
- font-weight: 200;
-}
-@font-face {
- font-family: Lato;
- src: url(../../../fonts/lato/Lato-LightItalic.ttf) format("truetype");
- font-style: italic;
- font-weight: 200;
-}
-@font-face {
- font-family: Lato;
- src: url(../../../fonts/lato/Lato-Regular.ttf) format("truetype");
- font-style: normal;
- font-weight: 400;
-}
-@font-face {
- font-family: Lato;
- src: url(../../../fonts/lato/Lato-Italic.ttf) format("truetype");
- font-style: italic;
- font-weight: 400;
-}
-@font-face {
- font-family: Lato;
- src: url(../../../fonts/lato/Lato-Bold.ttf) format("truetype");
- font-style: normal;
- font-weight: 700;
-}
-@font-face {
- font-family: Lato;
- src: url(../../../fonts/lato/Lato-BoldItalic.ttf) format("truetype");
- font-style: italic;
- font-weight: 700;
-}
-@font-face {
- font-family: Lato;
- src: url(../../../fonts/lato/Lato-Black.ttf) format("truetype");
- font-style: normal;
- font-weight: 800;
-}
-@font-face {
- font-family: Lato;
- src: url(../../../fonts/lato/Lato-BlackItalic.ttf) format("truetype");
- font-style: italic;
- font-weight: 800;
-}
-@font-face {
- font-family: Roboto;
- src: url(../../../fonts/Roboto/Roboto-Black.ttf) format("truetype");
- font-style: normal;
- font-weight: 900;
-}
-@font-face {
- font-family: Roboto;
- src: url(../../../fonts/Roboto/Roboto-BlackItalic.ttf) format("truetype");
- font-style: italic;
- font-weight: 900;
-}
-@font-face {
- font-family: Roboto;
- src: url(../../../fonts/Roboto/Roboto-Bold.ttf) format("truetype");
- font-style: normal;
- font-weight: 700;
-}
-@font-face {
- font-family: Roboto;
- src: url(../../../fonts/Roboto/Roboto-BoldItalic.ttf) format("truetype");
- font-style: italic;
- font-weight: 700;
-}
-@font-face {
- font-family: Roboto;
- src: url(../../../fonts/Roboto/Roboto-Regular.ttf) format("truetype");
- font-style: normal;
- font-weight: 400;
-}
-@font-face {
- font-family: Roboto;
- src: url(../../../fonts/Roboto/Roboto-Italic.ttf) format("truetype");
- font-style: italic;
- font-weight: 400;
-}
-@font-face {
- font-family: Roboto;
- src: url(../../../fonts/Roboto/Roboto-Light.ttf) format("truetype");
- font-style: normal;
- font-weight: 300;
-}
-@font-face {
- font-family: Roboto;
- src: url(../../../fonts/Roboto/Roboto-LightItalic.ttf) format("truetype");
- font-style: italic;
- font-weight: 300;
-}
-@font-face {
- font-family: Roboto;
- src: url(../../../fonts/Roboto/Roboto-Medium.ttf) format("truetype");
- font-style: normal;
- font-weight: 500;
-}
-@font-face {
- font-family: Roboto;
- src: url(../../../fonts/Roboto/Roboto-MediumItalic.ttf) format("truetype");
- font-style: italic;
- font-weight: 500;
-}
-@font-face {
- font-family: Roboto;
- src: url(../../../fonts/Roboto/Roboto-Thin.ttf) format("truetype");
- font-style: normal;
- font-weight: 100;
-}
-@font-face {
- font-family: Roboto;
- src: url(../../../fonts/Roboto/Roboto-ThinItalic.ttf) format("truetype");
- font-style: italic;
- font-weight: 100;
-}
-@font-face {
- font-family: Raleway;
- src: url(../../../fonts/Raleway/Raleway-Black.ttf) format("truetype");
- font-style: normal;
- font-weight: 900;
-}
-@font-face {
- font-family: Raleway;
- src: url(../../../fonts/Raleway/Raleway-BlackItalic.ttf) format("truetype");
- font-style: italic;
- font-weight: 900;
-}
-@font-face {
- font-family: Raleway;
- src: url(../../../fonts/Raleway/Raleway-Bold.ttf) format("truetype");
- font-style: normal;
- font-weight: 700;
-}
-@font-face {
- font-family: Raleway;
- src: url(../../../fonts/Raleway/Raleway-BoldItalic.ttf) format("truetype");
- font-style: italic;
- font-weight: 700;
-}
-@font-face {
- font-family: Raleway;
- src: url(../../../fonts/Raleway/Raleway-ExtraBold.ttf) format("truetype");
- font-style: normal;
- font-weight: 800;
-}
-@font-face {
- font-family: Raleway;
- src: url(../../../fonts/Raleway/Raleway-ExtraBoldItalic.ttf) format("truetype");
- font-style: italic;
- font-weight: 800;
-}
-@font-face {
- font-family: Raleway;
- src: url(../../../fonts/Raleway/Raleway-ExtraLight.ttf) format("truetype");
- font-style: normal;
- font-weight: 200;
-}
-@font-face {
- font-family: Raleway;
- src: url(../../../fonts/Raleway/Raleway-ExtraLightItalic.ttf) format("truetype");
- font-style: italic;
- font-weight: 200;
-}
-@font-face {
- font-family: Raleway;
- src: url(../../../fonts/Raleway/Raleway-Light.ttf) format("truetype");
- font-style: normal;
- font-weight: 300;
-}
-@font-face {
- font-family: Raleway;
- src: url(../../../fonts/Raleway/Raleway-LightItalic.ttf) format("truetype");
- font-style: italic;
- font-weight: 300;
-}
-@font-face {
- font-family: Raleway;
- src: url(../../../fonts/Raleway/Raleway-Medium.ttf) format("truetype");
- font-style: normal;
- font-weight: 500;
-}
-@font-face {
- font-family: Raleway;
- src: url(../../../fonts/Raleway/Raleway-MediumItalic.ttf) format("truetype");
- font-style: italic;
- font-weight: 500;
-}
-@font-face {
- font-family: Raleway;
- src: url(../../../fonts/Raleway/Raleway-Regular.ttf) format("truetype");
- font-style: normal;
- font-weight: 400;
-}
-@font-face {
- font-family: Raleway;
- src: url(../../../fonts/Raleway/Raleway-Italic.ttf) format("truetype");
- font-style: italic;
- font-weight: 400;
-}
-@font-face {
- font-family: Raleway;
- src: url(../../../fonts/Raleway/Raleway-SemiBold.ttf) format("truetype");
- font-style: normal;
- font-weight: 600;
-}
-@font-face {
- font-family: Raleway;
- src: url(../../../fonts/Raleway/Raleway-SemiBoldItalic.ttf) format("truetype");
- font-style: italic;
- font-weight: 600;
-}
-@font-face {
- font-family: Raleway;
- src: url(../../../fonts/Raleway/Raleway-Thin.ttf) format("truetype");
- font-style: normal;
- font-weight: 100;
-}
-@font-face {
- font-family: Raleway;
- src: url(../../../fonts/Raleway/Raleway-ThinItalic.ttf) format("truetype");
- font-style: italic;
- font-weight: 100;
-}
-@font-face {
- font-family: PTSans;
- src: url(../../../fonts/PT_Sans/PTSans-Bold.ttf) format("truetype");
- font-style: normal;
- font-weight: 700;
-}
-@font-face {
- font-family: PTSans;
- src: url(../../../fonts/PT_Sans/PTSans-BoldItalic.ttf) format("truetype");
- font-style: italic;
- font-weight: 700;
-}
-@font-face {
- font-family: PTSans;
- src: url(../../../fonts/PT_Sans/PTSans-Italic.ttf) format("truetype");
- font-style: italic;
- font-weight: 400;
-}
-@font-face {
- font-family: PTSans;
- src: url(../../../fonts/PT_Sans/PTSans-Regular.ttf) format("truetype");
- font-style: normal;
- font-weight: 400;
-}
-.lgn-max-width-wrapper {
- max-width: 440px;
- margin: auto;
- margin-top: 50px;
- margin-bottom: 50px;
- display: block;
-}
-@media only screen and (min-width: 600px) {
- .lgn-max-width-wrapper {
- margin-top: 100px;
- }
-}
-
-.content-container {
- position: relative;
- display: block;
- margin: 0 auto 0 auto;
- padding: 20px;
- width: 100%;
- box-sizing: border-box;
-}
-.content-container .lgn-head h1 {
- text-align: center;
-}
-.content-container .lgn-head p {
- text-align: center;
-}
-.content-container .lgn-actions {
- display: flex;
- align-items: center;
- padding: 0.5rem 0;
- margin-top: 1rem;
-}
-.content-container .lgn-actions.lgn-reverse-order {
- flex-direction: row-reverse;
-}
-.content-container .lgn-actions .fill-space {
- flex: 1;
-}
-.content-container .lgn-row {
- display: flex;
- align-items: center;
- padding: 0;
-}
-.content-container .lgn-row.lgn-reverse-order {
- flex-direction: row-reverse;
-}
-.content-container .lgn-row .fill-space {
- flex: 1;
-}
-.content-container .lgn-login-profile {
- margin-bottom: 0.5rem;
-}
-.content-container .lgn-login-profile .lgn-profile-image {
- display: block;
- margin: 0.5rem;
-}
-.content-container .lgn-login-profile .lgn-names {
- border-width: 1px;
- border-style: solid;
- border-radius: 50vw;
- display: flex;
- align-items: center;
-}
-.content-container .lgn-login-profile .lgn-names .inline-block {
- display: inline-block;
- margin: 0 2px;
-}
-.content-container .lgn-login-profile .lgn-names .lgn-displayname {
- margin: 0.5rem 1rem;
-}
-.content-container .lgn-login-profile .lgn-names .lgn-loginname {
- min-width: 0;
-}
-.content-container .lgn-login-profile .lgn-names .lgn-loginname p {
- margin: 0.5rem 0.5rem;
- text-overflow: ellipsis;
- overflow: hidden;
- white-space: nowrap;
-}
-.content-container .lgn-left-action {
- position: absolute;
- left: 1rem;
- top: -75px;
- transform: translateY(-50%);
-}
-.content-container .lgn-register-options {
- display: flex;
- flex-direction: column;
-}
-.content-container .lgn-mfa-other {
- display: flex;
- flex-direction: column;
-}
-.content-container .lgn-mfa-other button {
- width: 100%;
- margin: 0.5rem 0;
-}
-
-i {
- font-size: 1.2rem;
-}
-
-.lgn-register .double-col {
- display: flex;
- flex-wrap: wrap;
- margin: 0 -0.5rem;
-}
-.lgn-register .double-col .lgn-field {
- margin: 0 0.5rem;
- flex: 1;
- box-sizing: border-box;
- margin-bottom: 0.5rem;
-}
-.lgn-register .lgn-field {
- margin-bottom: 0.5rem;
-}
-
-.lgn-header {
- display: flex;
- position: relative;
- margin: auto;
- padding: 0;
- max-width: 250px;
- min-height: 150px;
- align-items: center;
- justify-content: center;
-}
-.lgn-header .lgn-logo {
- display: block;
- height: 100%;
- margin: 0 auto;
- max-width: 250px;
- max-height: 150px;
- object-fit: contain;
-}
-
-@keyframes shake {
- 10%, 90% {
- transform: translate3d(-1px, 0, 0);
- }
- 20%, 80% {
- transform: translate3d(2px, 0, 0);
- }
- 30%, 50%, 70% {
- transform: translate3d(-4px, 0, 0);
- }
- 40%, 60% {
- transform: translate3d(4px, 0, 0);
- }
-}
-[shake] {
- animation: shake 0.8s cubic-bezier(0.36, 0.07, 0.19, 0.97) both;
- transform: translate3d(0, 0, 0);
-}
-
-h1, .lgn-headline, .lgn-typography h1 {
- font: 400 24px/32px serif;
- font-family: var(--zitadel-font-family), -apple-system, BlinkMacSystemFont, sans-serif;
- letter-spacing: normal;
- margin: 0 0 16px;
-}
-
-h2, .lgn-title, .lgn-typography h2 {
- font: 500 20px/32px serif;
- font-family: var(--zitadel-font-family), -apple-system, BlinkMacSystemFont, sans-serif;
- letter-spacing: normal;
- margin: 0 0 16px;
-}
-
-h3, .lgn-subheading-2, .lgn-typography h3 {
- font: 400 16px/28px serif;
- font-family: var(--zitadel-font-family), -apple-system, BlinkMacSystemFont, sans-serif;
- letter-spacing: normal;
- margin: 0 0 16px;
-}
-
-h4, .lgn-subheading-1, .lgn-typography h4 {
- font: 400 15px/24px serif;
- font-family: var(--zitadel-font-family), -apple-system, BlinkMacSystemFont, sans-serif;
- letter-spacing: normal;
- margin: 0 0 16px;
-}
-
-.lgn-body-strong, .lgn-body-2 {
- font: 500 14px/24px serif;
- font-family: var(--zitadel-font-family), -apple-system, BlinkMacSystemFont, sans-serif;
- letter-spacing: normal;
-}
-
-p, .lgn-body, .lgn-body-1, .lgn-typography {
- font: 400 14px/20px serif;
- font-family: var(--zitadel-font-family), -apple-system, BlinkMacSystemFont, sans-serif;
- letter-spacing: normal;
-}
-p p, .lgn-body p, .lgn-body-1 p, .lgn-typography p {
- margin: 0 0 12px;
-}
-
-button, .lgn-button, .lgn-raised-button, .lgn-stroked-button {
- font: 500 14px/36px serif;
- font-family: var(--zitadel-font-family), -apple-system, BlinkMacSystemFont, sans-serif;
- letter-spacing: normal;
-}
-
-span {
- font: 400 14px/20px serif;
- font-family: var(--zitadel-font-family), -apple-system, BlinkMacSystemFont, sans-serif;
- letter-spacing: normal;
-}
-
-a, .lgn-a {
- font: 400 14px/20px serif;
- font-family: var(--zitadel-font-family), -apple-system, BlinkMacSystemFont, sans-serif;
- letter-spacing: normal;
-}
-
-small .lgn-small, .lgn-caption {
- font: 400 12px/20px serif;
- font-family: var(--zitadel-font-family), -apple-system, BlinkMacSystemFont, sans-serif;
- letter-spacing: normal;
-}
-
-label, .lgn-label, .lgn-typography .lgn-label, .lgn-typography label {
- font: 400 12px/20px serif;
- font-family: var(--zitadel-font-family), -apple-system, BlinkMacSystemFont, sans-serif;
- letter-spacing: normal;
- margin: 0 0 2px 0;
-}
-
-.lgn-display-4, .lgn-typography .lgn-display-4 {
- font: 300 112px/112px serif;
- font-family: var(--zitadel-font-family), -apple-system, BlinkMacSystemFont, sans-serif;
- letter-spacing: -0.05em;
- margin: 0 0 56px;
-}
-
-.lgn-display-3, .lgn-typography .lgn-display-3 {
- font: 400 56px/56px serif;
- font-family: var(--zitadel-font-family), -apple-system, BlinkMacSystemFont, sans-serif;
- letter-spacing: -0.02em;
- margin: 0 0 64px;
-}
-
-.lgn-display-2, .lgn-typography .lgn-display-2 {
- font: 400 45px/48px serif;
- font-family: var(--zitadel-font-family), -apple-system, BlinkMacSystemFont, sans-serif;
- letter-spacing: -0.005em;
- margin: 0 0 64px;
-}
-
-.lgn-display-1, .lgn-typography .lgn-display-1 {
- font: 400 34px/40px serif;
- font-family: var(--zitadel-font-family), -apple-system, BlinkMacSystemFont, sans-serif;
- letter-spacing: normal;
- margin: 0 0 64px;
-}
-
-.lgn-app-background {
- background-color: var(--zitadel-color-background);
- color: var(--zitadel-color-text);
-}
-
-.lgn-theme-loaded-marker {
- display: none;
-}
-
-.lgn-logo {
- background-position: auto;
- background-size: contain;
-}
-
-.lgn-button,
-.lgn-stroked-button,
-.lgn-icon-button {
- color: inherit;
- background: transparent;
-}
-.lgn-button.lgn-primary,
-.lgn-stroked-button.lgn-primary,
-.lgn-icon-button.lgn-primary {
- color: var(--zitadel-color-primary);
-}
-.lgn-button.lgn-accent,
-.lgn-stroked-button.lgn-accent,
-.lgn-icon-button.lgn-accent {
- color: var(--zitadel-color-primary);
-}
-.lgn-button.lgn-warn,
-.lgn-stroked-button.lgn-warn,
-.lgn-icon-button.lgn-warn {
- color: var(--zitadel-color-primary);
-}
-.lgn-button.lgn-primary[disabled], .lgn-button.lgn-accent[disabled], .lgn-button.lgn-warn[disabled], .lgn-button[disabled][disabled],
-.lgn-stroked-button.lgn-primary[disabled],
-.lgn-stroked-button.lgn-accent[disabled],
-.lgn-stroked-button.lgn-warn[disabled],
-.lgn-stroked-button[disabled][disabled],
-.lgn-icon-button.lgn-primary[disabled],
-.lgn-icon-button.lgn-accent[disabled],
-.lgn-icon-button.lgn-warn[disabled],
-.lgn-icon-button[disabled][disabled] {
- color: var(--zitadel-color-button-disabled);
-}
-
-.lgn-stroked-button:hover,
-.lgn-stroked-button:focus {
- background: var(--zitadel-color-stroked-button-hover-background);
-}
-
-.lgn-stroked-button:active {
- background: var(--zitadel-color-stroked-button-active-background);
-}
-
-.lgn-button:focus,
-.lgn-raised-button:focus {
- box-shadow: 0px 3px 5px -1px rgba(0, 0, 0, 0.2), 0px 5px 8px 0px rgba(0, 0, 0, 0.14), 0px 1px 14px 0px rgba(0, 0, 0, 0.12);
- background: var(--zitadel-color-background);
-}
-
-.lgn-stroked-button:not([disabled]) {
- border-color: var(--zitadel-color-divider);
-}
-
-.lgn-button.lgn-primary {
- color: var(--zitadel-color-primary);
-}
-.lgn-button.lgn-accent {
- color: var(--zitadel-color-primary);
-}
-.lgn-button.lgn-warn {
- color: var(--zitadel-color-primary);
-}
-.lgn-button.lgn-primary[disabled], .lgn-button.lgn-accent[disabled], .lgn-button.lgn-warn[disabled], .lgn-button[disabled][disabled] {
- color: var(--zitadel-color-button-disabled);
-}
-.lgn-button:hover {
- background-color: rgba(var(--zitadel-color-primary-rgb), 0.1);
-}
-.lgn-button:focus {
- background-color: rgba(var(--zitadel-color-primary-rgb), 0.2);
-}
-.lgn-button:active {
- background-color: rgba(var(--zitadel-color-primary-rgb), 0.25);
-}
-
-.lgn-raised-button {
- color: var(--zitadel-color-text);
- background-color: var(--zitadel-color-raised-button-background);
-}
-.lgn-raised-button.lgn-primary {
- color: var(--zitadel-color-primary-contrast);
-}
-.lgn-raised-button.lgn-accent {
- color: var(--zitadel-color-primary-contrast);
-}
-.lgn-raised-button.lgn-warn {
- color: var(--zitadel-color-primary-contrast);
-}
-.lgn-raised-button.lgn-primary[disabled], .lgn-raised-button.lgn-accent[disabled], .lgn-raised-button.lgn-warn[disabled], .lgn-raised-button[disabled][disabled] {
- color: var(--zitadel-color-button-disabled);
-}
-.lgn-raised-button.lgn-primary {
- background-color: var(--zitadel-color-primary);
-}
-.lgn-raised-button.lgn-accent {
- background-color: var(--zitadel-color-primary);
-}
-.lgn-raised-button.lgn-warn {
- background-color: var(--zitadel-color-primary);
-}
-.lgn-raised-button.lgn-primary[disabled], .lgn-raised-button.lgn-accent[disabled], .lgn-raised-button.lgn-warn[disabled], .lgn-raised-button[disabled][disabled] {
- background-color: var(--zitadel-color-button-disabled-background);
-}
-.lgn-raised-button:active {
- background-color: var(--zitadel-color-raised-button-active-background);
-}
-
-.lgn-button,
-.lgn-stroked-button {
- box-shadow: 0px 0px 0px 0px rgba(0, 0, 0, 0.2), 0px 0px 0px 0px rgba(0, 0, 0, 0.14), 0px 0px 0px 0px rgba(0, 0, 0, 0.12);
-}
-
-.lgn-raised-button {
- box-shadow: 0px 3px 1px -2px rgba(0, 0, 0, 0.2), 0px 2px 2px 0px rgba(0, 0, 0, 0.14), 0px 1px 5px 0px rgba(0, 0, 0, 0.12);
-}
-.lgn-raised-button:not([disabled]):active {
- box-shadow: 0px 3px 5px -1px rgba(0, 0, 0, 0.2), 0px 5px 8px 0px rgba(0, 0, 0, 0.14), 0px 1px 14px 0px rgba(0, 0, 0, 0.12);
-}
-.lgn-raised-button[disabled] {
- box-shadow: 0px 0px 0px 0px rgba(0, 0, 0, 0.2), 0px 0px 0px 0px rgba(0, 0, 0, 0.14), 0px 0px 0px 0px rgba(0, 0, 0, 0.12);
-}
-
-.lgn-input:not([disabled]),
-select:not([disabled]),
-.lgn-select:not([disabled]) {
- border-color: var(--zitadel-color-input-border);
-}
-
-.lgn-input,
-select,
-.lgn-select {
- color: var(--zitadel-color-text);
- background-color: var(--zitadel-color-input-background);
-}
-.lgn-input:hover,
-select:hover,
-.lgn-select:hover {
- border-color: var(--zitadel-color-input-border-hover);
-}
-.lgn-input:active,
-select:active,
-.lgn-select:active {
- border-color: var(--zitadel-color-input-border-active);
-}
-.lgn-input:focus,
-select:focus,
-.lgn-select:focus {
- border-color: var(--zitadel-color-input-border-active);
-}
-.lgn-input.lgn-touched[color=warn],
-select.lgn-touched[color=warn],
-.lgn-select.lgn-touched[color=warn] {
- border-color: var(--zitadel-color-warn);
-}
-
-.lgn-input::placeholder,
-select::placeholder,
-.lgn-select::placeholder {
- color: var(--zitadel-color-input-placeholder);
-}
-
-label::before {
- border-color: var(--zitadel-color-input-border);
-}
-label::after {
- background: var(--zitadel-color-primary);
-}
-
-input[type=radio] {
- opacity: 0;
-}
-input[type=radio]:checked + label::before {
- border-color: var(--zitadel-color-primary);
-}
-input[type=radio]:focus + label {
- opacity: 1;
- outline: none;
-}
-input[type=radio]:disabled + label::before {
- border-color: var(--zitadel-color-button-disabled-background);
-}
-input[type=radio]:disabled:checked + label::before {
- color: var(--zitadel-color-button-selected-background);
-}
-input[type=radio]:disabled:checked + label::after {
- background: var(--zitadel-color-button-disabled-selected-background);
-}
-input[type=radio]:disabled:focus {
- opacity: 0;
-}
-
-input[type=checkbox] {
- position: absolute;
- top: 4px;
- left: 0;
- width: 16px;
- height: 16px;
- opacity: 0;
- z-index: 0;
-}
-
-.lgn-checkbox label:before {
- background-color: transparent;
- border: 2px solid var(--zitadel-color-input-border);
-}
-.lgn-checkbox label:after {
- border-bottom: 2px solid transparent;
- border-right: 2px solid transparent;
- background: transparent;
-}
-
-input[type=checkbox]:checked + label:before {
- background-color: var(--zitadel-color-primary);
- border-color: var(--zitadel-color-primary);
-}
-input[type=checkbox]:checked + label:after {
- border-color: var(--zitadel-color-primary-contrast);
-}
-
-.lgn-label {
- color: var(--zitadel-color-label);
-}
-
-footer {
- background-color: var(--zitadel-color-footer-background);
- border-top: 1px solid var(--zitadel-color-footer-line);
- backdrop-filter: blur(10px);
-}
-@-moz-document url-prefix() {
- footer {
- background-color: var(--zitadel-color-footer-background-mozilla);
- backdrop-filter: none;
- }
-}
-footer a {
- color: var(--zitadel-color-text-500);
-}
-footer .lgn-logo-watermark {
- background: var(--zitadel-logo-powered-by) no-repeat;
- background-position: center;
- background-size: contain;
-}
-
-a:not(.lgn-stroked-button):not(.lgn-button):not(.lgn-raised-button):not(.lgn-idp) {
- color: var(--zitadel-color-text-500);
-}
-a:not(.lgn-stroked-button):not(.lgn-button):not(.lgn-raised-button):not(.lgn-idp):hover, a:not(.lgn-stroked-button):not(.lgn-button):not(.lgn-raised-button):not(.lgn-idp):active {
- color: var(--zitadel-color-primary-500);
-}
-
-.lgn-error {
- color: var(--zitadel-color-warn);
-}
-
-.lgn-qrcode svg {
- background: var(--zitadel-color-qr-background);
-}
-.lgn-qrcode svg rect.color {
- fill: var(--zitadel-color-qr);
-}
-.lgn-qrcode svg rect.bg-color {
- fill: var(--zitadel-color-qr-background);
-}
-
-.lgn-error .lgn-error-message {
- color: var(--zitadel-color-warn);
-}
-
-.lgn-login-profile .lgn-names {
- border-color: var(--zitadel-color-divider);
-}
-
-.lgn-account-selection {
- display: flex;
- flex-direction: column;
- width: 100%;
-}
-.lgn-account-selection .lgn-account {
- color: inherit;
- background: transparent;
- box-shadow: inset 0 -1px var(--zitadel-color-footer-line);
-}
-.lgn-account-selection .lgn-account:hover {
- background-color: var(--zitadel-color-account-selector-hover);
-}
-.lgn-account-selection .lgn-account:focus {
- background-color: var(--zitadel-color-account-selector-active);
-}
-.lgn-account-selection .lgn-account:active {
- background-color: var(--zitadel-color-account-selector-active);
-}
-.lgn-account-selection .lgn-account .lgn-loginname {
- font-size: 14px;
- color: var(--zitadel-color-label);
-}
-.lgn-account-selection .lgn-account .lgn-session-state {
- color: var(--zitadel-color-label);
-}
-.lgn-account-selection .lgn-account .lgn-session-state.i0 {
- color: var(--zitadel-color-state-success-font);
- background: var(--zitadel-color-state-success-background);
-}
-.lgn-account-selection .lgn-account .lgn-session-state.i1 {
- color: var(--zitadel-color-state-warn-font);
- background: var(--zitadel-color-state-warn-background);
-}
-
-.lgn-avatar:not(.transparent).lgn-primary {
- background-color: var(--zitadel-color-primary);
-}
-.lgn-avatar:not(.transparent).lgn-accent {
- background-color: var(--zitadel-color-primary);
-}
-.lgn-avatar:not(.transparent).lgn-warn {
- background-color: var(--zitadel-color-primary);
-}
-.lgn-avatar:not(.transparent).lgn-primary[disabled], .lgn-avatar:not(.transparent).lgn-accent[disabled], .lgn-avatar:not(.transparent).lgn-warn[disabled], .lgn-avatar:not(.transparent)[disabled][disabled] {
- background-color: var(--itadel-color-button-disabled-background);
-}
-
-.lgn-select, select {
- background-image: var(--zitadel-icon-select);
-}
-
-.lgn-list li::before,
-ul li::before {
- color: var(--zitadel-color-primary);
-}
-.lgn-list li i.lgn-warn,
-ul li i.lgn-warn {
- color: var(--zitadel-color-warn);
-}
-.lgn-list li i.lgn-valid,
-ul li i.lgn-valid {
- color: var(--zitadel-color-success);
-}
-
-.lgn-idp {
- border-color: var(--zitadel-color-divider);
- color: var(--zitadel-color-text-500);
-}
-.lgn-idp:hover {
- box-shadow: 0px 2px 1px -1px rgba(0, 0, 0, 0.2), 0px 1px 1px 0px rgba(0, 0, 0, 0.14), 0px 1px 3px 0px rgba(0, 0, 0, 0.12);
-}
-.lgn-idp:not([disabled]):active {
- box-shadow: 0px 3px 1px -2px rgba(0, 0, 0, 0.2), 0px 2px 2px 0px rgba(0, 0, 0, 0.14), 0px 1px 5px 0px rgba(0, 0, 0, 0.12);
-}
-.lgn-idp.google {
- color: var(--zitadel-color-google-text);
- background-color: var(--zitadel-color-google-background);
-}
-.lgn-idp.github {
- color: var(--zitadel-color-github-text);
- background-color: var(--zitadel-color-github-background);
-}
-.lgn-idp.gitlab {
- color: var(--zitadel-color-gitlab-text);
- background-color: var(--zitadel-color-gitlab-background);
-}
-
-.lgn-idp-providers .lgn-idp-desc {
- color: var(--zitadel-color-label);
-}
-
-.lgn-success-label {
- color: var(--zitadel-color-success);
- background-color: var(--zitadel-color-success-background);
-}
-
-.lgn-mfa-options .mfa label input[type=radio]:hover + .mfa-img, .lgn-mfa-options .mfa label input[type=radio]:focus + .mfa-img {
- background-color: var(--zitadel-color-card-hover);
- border-color: var(--zitadel-color-input-border-hover);
-}
-.lgn-mfa-options .mfa label .mfa-img {
- border: 1px solid var(--zitadel-color-input-border);
-}
-.lgn-mfa-options .mfa label input[type=radio]:checked + .mfa-img {
- background-color: var(--zitadel-color-card-hover);
- border: 1px solid var(--zitadel-color-input-border-active);
- box-shadow: 0 0 0 2px var(--zitadel-color-input-border-active);
-}
-
-/*# sourceMappingURL=zitadel.css.map */
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
deleted file mode 100644
index 0fa129cf1..000000000
--- a/internal/api/ui/login/static/resources/themes/zitadel/css/zitadel.css.map
+++ /dev/null
@@ -1 +0,0 @@
-{"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/change_password.html b/internal/api/ui/login/static/templates/change_password.html
index 456b9d444..45eacb3ab 100644
--- a/internal/api/ui/login/static/templates/change_password.html
+++ b/internal/api/ui/login/static/templates/change_password.html
@@ -12,7 +12,7 @@
{{ .CSRF }}
-
+
diff --git a/internal/api/ui/login/static/templates/external_not_found_option.html b/internal/api/ui/login/static/templates/external_not_found_option.html
index 2edd1cc95..086662a8e 100644
--- a/internal/api/ui/login/static/templates/external_not_found_option.html
+++ b/internal/api/ui/login/static/templates/external_not_found_option.html
@@ -78,6 +78,10 @@
+
+
diff --git a/internal/api/ui/login/static/templates/init_user.html b/internal/api/ui/login/static/templates/init_user.html
index 1cc1073c8..cf0ad7104 100644
--- a/internal/api/ui/login/static/templates/init_user.html
+++ b/internal/api/ui/login/static/templates/init_user.html
@@ -16,7 +16,7 @@
-
+
diff --git a/internal/api/ui/login/static/templates/password.html b/internal/api/ui/login/static/templates/password.html
index f689188bb..15e9d7c09 100644
--- a/internal/api/ui/login/static/templates/password.html
+++ b/internal/api/ui/login/static/templates/password.html
@@ -12,7 +12,7 @@
{{ .CSRF }}
-
+
diff --git a/internal/auth/repository/eventsourcing/eventstore/auth_request.go b/internal/auth/repository/eventsourcing/eventstore/auth_request.go
index 4de5b0237..09c64ba27 100644
--- a/internal/auth/repository/eventsourcing/eventstore/auth_request.go
+++ b/internal/auth/repository/eventsourcing/eventstore/auth_request.go
@@ -719,6 +719,7 @@ func (repo *AuthRequestRepo) checkLoginName(ctx context.Context, request *domain
func (repo *AuthRequestRepo) checkDomainDiscovery(ctx context.Context, request *domain.AuthRequest, loginName string) bool {
// check if there's a suffix in the loginname
+ loginName = strings.TrimSpace(strings.ToLower(loginName))
index := strings.LastIndex(loginName, "@")
if index < 0 {
return false
@@ -841,6 +842,7 @@ func queryLoginPolicyToDomain(policy *query.LoginPolicy) *domain.LoginPolicy {
AllowRegister: policy.AllowRegister,
AllowExternalIDP: policy.AllowExternalIDPs,
ForceMFA: policy.ForceMFA,
+ ForceMFALocalOnly: policy.ForceMFALocalOnly,
SecondFactors: policy.SecondFactors,
MultiFactors: policy.MultiFactors,
PasswordlessType: policy.PasswordlessType,
@@ -974,7 +976,7 @@ func (repo *AuthRequestRepo) nextSteps(ctx context.Context, request *domain.Auth
}
}
- step, ok, err := repo.mfaChecked(userSession, request, user)
+ step, ok, err := repo.mfaChecked(userSession, request, user, isInternalLogin && len(request.LinkingUsers) == 0)
if err != nil {
return nil, err
}
@@ -1093,9 +1095,9 @@ func (repo *AuthRequestRepo) firstFactorChecked(request *domain.AuthRequest, use
return &domain.PasswordStep{}
}
-func (repo *AuthRequestRepo) mfaChecked(userSession *user_model.UserSessionView, request *domain.AuthRequest, user *user_model.UserView) (domain.NextStep, bool, error) {
+func (repo *AuthRequestRepo) mfaChecked(userSession *user_model.UserSessionView, request *domain.AuthRequest, user *user_model.UserView, isInternalAuthentication bool) (domain.NextStep, bool, error) {
mfaLevel := request.MFALevel()
- allowedProviders, required := user.MFATypesAllowed(mfaLevel, request.LoginPolicy)
+ allowedProviders, required := user.MFATypesAllowed(mfaLevel, request.LoginPolicy, isInternalAuthentication)
promptRequired := (user.MFAMaxSetUp < mfaLevel) || (len(allowedProviders) == 0 && required)
if promptRequired || !repo.mfaSkippedOrSetUp(user, request) {
types := user.MFATypesSetupPossible(mfaLevel, request.LoginPolicy)
diff --git a/internal/auth/repository/eventsourcing/eventstore/auth_request_test.go b/internal/auth/repository/eventsourcing/eventstore/auth_request_test.go
index c5dd8f411..bdcc01213 100644
--- a/internal/auth/repository/eventsourcing/eventstore/auth_request_test.go
+++ b/internal/auth/repository/eventsourcing/eventstore/auth_request_test.go
@@ -1439,6 +1439,7 @@ func TestAuthRequestRepo_mfaChecked(t *testing.T) {
userSession *user_model.UserSessionView
request *domain.AuthRequest
user *user_model.UserView
+ isInternal bool
}
tests := []struct {
name string
@@ -1472,6 +1473,7 @@ func TestAuthRequestRepo_mfaChecked(t *testing.T) {
MFAMaxSetUp: domain.MFALevelNotSetUp,
},
},
+ isInternal: true,
},
nil,
false,
@@ -1490,6 +1492,7 @@ func TestAuthRequestRepo_mfaChecked(t *testing.T) {
MFAMaxSetUp: domain.MFALevelNotSetUp,
},
},
+ isInternal: true,
},
nil,
true,
@@ -1509,6 +1512,7 @@ func TestAuthRequestRepo_mfaChecked(t *testing.T) {
MFAMaxSetUp: domain.MFALevelNotSetUp,
},
},
+ isInternal: true,
},
&domain.MFAPromptStep{
MFAProviders: []domain.MFAType{
@@ -1533,6 +1537,7 @@ func TestAuthRequestRepo_mfaChecked(t *testing.T) {
MFAMaxSetUp: domain.MFALevelNotSetUp,
},
},
+ isInternal: true,
},
&domain.MFAPromptStep{
Required: true,
@@ -1557,6 +1562,7 @@ func TestAuthRequestRepo_mfaChecked(t *testing.T) {
MFAInitSkipped: testNow,
},
},
+ isInternal: true,
},
nil,
true,
@@ -1578,6 +1584,7 @@ func TestAuthRequestRepo_mfaChecked(t *testing.T) {
},
},
userSession: &user_model.UserSessionView{SecondFactorVerification: testNow.Add(-5 * time.Hour)},
+ isInternal: true,
},
nil,
true,
@@ -1599,6 +1606,7 @@ func TestAuthRequestRepo_mfaChecked(t *testing.T) {
},
},
userSession: &user_model.UserSessionView{},
+ isInternal: true,
},
&domain.MFAVerificationStep{
@@ -1607,11 +1615,107 @@ func TestAuthRequestRepo_mfaChecked(t *testing.T) {
false,
nil,
},
+ {
+ "external not checked or forced but set up, want step",
+ args{
+ request: &domain.AuthRequest{
+ LoginPolicy: &domain.LoginPolicy{
+ SecondFactors: []domain.SecondFactorType{domain.SecondFactorTypeOTP},
+ SecondFactorCheckLifetime: 18 * time.Hour,
+ },
+ },
+ user: &user_model.UserView{
+ HumanView: &user_model.HumanView{
+ MFAMaxSetUp: domain.MFALevelSecondFactor,
+ OTPState: user_model.MFAStateReady,
+ },
+ },
+ userSession: &user_model.UserSessionView{},
+ isInternal: false,
+ },
+ &domain.MFAVerificationStep{
+ MFAProviders: []domain.MFAType{domain.MFATypeOTP},
+ },
+ false,
+ nil,
+ },
+ {
+ "external not forced but checked",
+ args{
+ request: &domain.AuthRequest{
+ LoginPolicy: &domain.LoginPolicy{
+ SecondFactors: []domain.SecondFactorType{domain.SecondFactorTypeOTP},
+ SecondFactorCheckLifetime: 18 * time.Hour,
+ },
+ },
+ user: &user_model.UserView{
+ HumanView: &user_model.HumanView{
+ MFAMaxSetUp: domain.MFALevelSecondFactor,
+ OTPState: user_model.MFAStateReady,
+ },
+ },
+ userSession: &user_model.UserSessionView{SecondFactorVerification: testNow.Add(-5 * time.Hour)},
+ isInternal: false,
+ },
+ nil,
+ true,
+ nil,
+ },
+ {
+ "external not checked but required, want step",
+ args{
+ request: &domain.AuthRequest{
+ LoginPolicy: &domain.LoginPolicy{
+ SecondFactors: []domain.SecondFactorType{domain.SecondFactorTypeOTP},
+ SecondFactorCheckLifetime: 18 * time.Hour,
+ ForceMFA: true,
+ },
+ },
+ user: &user_model.UserView{
+ HumanView: &user_model.HumanView{
+ MFAMaxSetUp: domain.MFALevelNotSetUp,
+ },
+ },
+ userSession: &user_model.UserSessionView{},
+ isInternal: false,
+ },
+ &domain.MFAPromptStep{
+ Required: true,
+ MFAProviders: []domain.MFAType{
+ domain.MFATypeOTP,
+ },
+ },
+ false,
+ nil,
+ },
+ {
+ "external not checked but local required",
+ args{
+ request: &domain.AuthRequest{
+ LoginPolicy: &domain.LoginPolicy{
+ SecondFactors: []domain.SecondFactorType{domain.SecondFactorTypeOTP},
+ SecondFactorCheckLifetime: 18 * time.Hour,
+ ForceMFA: true,
+ ForceMFALocalOnly: true,
+ },
+ },
+ user: &user_model.UserView{
+ HumanView: &user_model.HumanView{
+ MFAMaxSetUp: domain.MFALevelNotSetUp,
+ },
+ },
+ userSession: &user_model.UserSessionView{},
+ isInternal: false,
+ },
+ nil,
+ true,
+ nil,
+ },
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
repo := &AuthRequestRepo{}
- got, ok, err := repo.mfaChecked(tt.args.userSession, tt.args.request, tt.args.user)
+ got, ok, err := repo.mfaChecked(tt.args.userSession, tt.args.request, tt.args.user, tt.args.isInternal)
if (tt.errFunc != nil && !tt.errFunc(err)) || (err != nil && tt.errFunc == nil) {
t.Errorf("got wrong err: %v ", err)
return
diff --git a/internal/authz/repository/eventsourcing/eventstore/token_verifier.go b/internal/authz/repository/eventsourcing/eventstore/token_verifier.go
index b2bff6155..3fc61c854 100644
--- a/internal/authz/repository/eventsourcing/eventstore/token_verifier.go
+++ b/internal/authz/repository/eventsourcing/eventstore/token_verifier.go
@@ -15,7 +15,9 @@ import (
"github.com/zitadel/zitadel/internal/api/authz"
http_util "github.com/zitadel/zitadel/internal/api/http"
"github.com/zitadel/zitadel/internal/authz/repository/eventsourcing/view"
+ "github.com/zitadel/zitadel/internal/command"
"github.com/zitadel/zitadel/internal/crypto"
+ "github.com/zitadel/zitadel/internal/domain"
caos_errs "github.com/zitadel/zitadel/internal/errors"
v1 "github.com/zitadel/zitadel/internal/eventstore/v1"
"github.com/zitadel/zitadel/internal/eventstore/v1/models"
@@ -28,7 +30,6 @@ import (
type TokenVerifierRepo struct {
TokenVerificationKey crypto.EncryptionAlgorithm
- IAMID string
Eventstore v1.Eventstore
View *view.View
Query *query.Queries
@@ -92,6 +93,21 @@ func (repo *TokenVerifierRepo) VerifyAccessToken(ctx context.Context, tokenStrin
if !ok {
return "", "", "", "", "", caos_errs.ThrowUnauthenticated(nil, "APP-Reb32", "invalid token")
}
+ if strings.HasPrefix(tokenID, command.IDPrefixV2) {
+ userID, clientID, resourceOwner, err = repo.verifyAccessTokenV2(ctx, tokenID, verifierClientID, projectID)
+ return
+ }
+ if sessionID, ok := strings.CutPrefix(tokenID, authz.SessionTokenPrefix); ok {
+ userID, clientID, resourceOwner, err = repo.verifySessionToken(ctx, sessionID, tokenString)
+ return
+ }
+ return repo.verifyAccessTokenV1(ctx, tokenID, subject, verifierClientID, projectID)
+}
+
+func (repo *TokenVerifierRepo) verifyAccessTokenV1(ctx context.Context, tokenID, subject, verifierClientID, projectID string) (userID string, agentID string, clientID, prefLang, resourceOwner string, err error) {
+ ctx, span := tracing.NewSpan(ctx)
+ defer func() { span.EndWithError(err) }()
+
_, tokenSpan := tracing.NewNamedSpan(ctx, "token")
token, err := repo.tokenByID(ctx, tokenID, subject)
tokenSpan.EndWithError(err)
@@ -104,12 +120,98 @@ func (repo *TokenVerifierRepo) VerifyAccessToken(ctx context.Context, tokenStrin
if token.IsPAT {
return token.UserID, "", "", "", token.ResourceOwner, nil
}
- for _, aud := range token.Audience {
- if verifierClientID == aud || projectID == aud {
- return token.UserID, token.UserAgentID, token.ApplicationID, token.PreferredLanguage, token.ResourceOwner, nil
+ if err = verifyAudience(token.Audience, verifierClientID, projectID); err != nil {
+ return "", "", "", "", "", err
+ }
+ return token.UserID, token.UserAgentID, token.ApplicationID, token.PreferredLanguage, token.ResourceOwner, nil
+}
+
+func (repo *TokenVerifierRepo) verifyAccessTokenV2(ctx context.Context, token, verifierClientID, projectID string) (userID, clientID, resourceOwner string, err error) {
+ ctx, span := tracing.NewSpan(ctx)
+ defer func() { span.EndWithError(err) }()
+
+ activeToken, err := repo.Query.ActiveAccessTokenByToken(ctx, token)
+ if err != nil {
+ return "", "", "", err
+ }
+ if err = verifyAudience(activeToken.Audience, verifierClientID, projectID); err != nil {
+ return "", "", "", err
+ }
+ if err = repo.checkAuthentication(ctx, activeToken.AuthMethods, activeToken.UserID); err != nil {
+ return "", "", "", err
+ }
+ return activeToken.UserID, activeToken.ClientID, activeToken.ResourceOwner, nil
+}
+
+func (repo *TokenVerifierRepo) verifySessionToken(ctx context.Context, sessionID, token string) (userID, clientID, resourceOwner string, err error) {
+ ctx, span := tracing.NewSpan(ctx)
+ defer func() { span.EndWithError(err) }()
+
+ session, err := repo.Query.SessionByID(ctx, false, sessionID, token)
+ if err != nil {
+ return "", "", "", err
+ }
+ if err = repo.checkAuthentication(ctx, authMethodsFromSession(session), session.UserFactor.UserID); err != nil {
+ return "", "", "", err
+ }
+ return session.UserFactor.UserID, "", session.UserFactor.ResourceOwner, nil
+}
+
+// checkAuthentication ensures the session or token was authenticated (at least a single [domain.UserAuthMethodType]).
+// It will also check if there was a multi factor authentication, if either MFA is forced by the login policy or if the user has set up any
+func (repo *TokenVerifierRepo) checkAuthentication(ctx context.Context, authMethods []domain.UserAuthMethodType, userID string) error {
+ if len(authMethods) == 0 {
+ return caos_errs.ThrowPermissionDenied(nil, "AUTHZ-Kl3p0", "authentication required")
+ }
+ if domain.HasMFA(authMethods) {
+ return nil
+ }
+ availableAuthMethods, forceMFA, forceMFALocalOnly, err := repo.Query.ListUserAuthMethodTypesRequired(setCallerCtx(ctx, userID), userID, false)
+ if err != nil {
+ return err
+ }
+ if domain.RequiresMFA(forceMFA, forceMFALocalOnly, hasIDPAuthentication(authMethods)) || domain.HasMFA(availableAuthMethods) {
+ return caos_errs.ThrowPermissionDenied(nil, "AUTHZ-Kl3p0", "mfa required")
+ }
+ return nil
+}
+
+func hasIDPAuthentication(authMethods []domain.UserAuthMethodType) bool {
+ for _, method := range authMethods {
+ if method == domain.UserAuthMethodTypeIDP {
+ return true
}
}
- return "", "", "", "", "", caos_errs.ThrowUnauthenticated(nil, "APP-Zxfako", "invalid audience")
+ return false
+}
+
+func authMethodsFromSession(session *query.Session) []domain.UserAuthMethodType {
+ types := make([]domain.UserAuthMethodType, 0, domain.UserAuthMethodTypeIDP)
+ if !session.PasswordFactor.PasswordCheckedAt.IsZero() {
+ types = append(types, domain.UserAuthMethodTypePassword)
+ }
+ if !session.PasskeyFactor.PasskeyCheckedAt.IsZero() {
+ types = append(types, domain.UserAuthMethodTypePasswordless)
+ }
+ if !session.IntentFactor.IntentCheckedAt.IsZero() {
+ types = append(types, domain.UserAuthMethodTypeIDP)
+ }
+ // TODO: add checks with https://github.com/zitadel/zitadel/issues/5477
+ /*
+ if !session.TOTPFactor.TOTPCheckedAt.IsZero() {
+ types = append(types, domain.UserAuthMethodTypeOTP)
+ }
+ if !session.U2FFactor.U2FCheckedAt.IsZero() {
+ types = append(types, domain.UserAuthMethodTypeU2F)
+ }
+ */
+ return types
+}
+
+func setCallerCtx(ctx context.Context, userID string) context.Context {
+ ctxData := authz.GetCtxData(ctx)
+ ctxData.UserID = userID
+ return authz.SetCtxData(ctx, ctxData)
}
func (repo *TokenVerifierRepo) ProjectIDAndOriginsByClientID(ctx context.Context, clientID string) (projectID string, origins []string, err error) {
@@ -184,6 +286,15 @@ func (repo *TokenVerifierRepo) decryptAccessToken(token string) (string, error)
return tokenIDSubject, nil
}
+func verifyAudience(audience []string, verifierClientID, projectID string) error {
+ for _, aud := range audience {
+ if verifierClientID == aud || projectID == aud {
+ return nil
+ }
+ }
+ return caos_errs.ThrowUnauthenticated(nil, "APP-Zxfako", "invalid audience")
+}
+
type openIDKeySet struct {
*query.Queries
}
diff --git a/internal/command/auth_request.go b/internal/command/auth_request.go
new file mode 100644
index 000000000..bbcee503c
--- /dev/null
+++ b/internal/command/auth_request.go
@@ -0,0 +1,214 @@
+package command
+
+import (
+ "context"
+ "time"
+
+ "github.com/zitadel/zitadel/internal/api/authz"
+ "github.com/zitadel/zitadel/internal/domain"
+ "github.com/zitadel/zitadel/internal/errors"
+ "github.com/zitadel/zitadel/internal/repository/authrequest"
+ "github.com/zitadel/zitadel/internal/telemetry/tracing"
+)
+
+type AuthRequest struct {
+ ID string
+ LoginClient string
+ ClientID string
+ RedirectURI string
+ State string
+ Nonce string
+ Scope []string
+ Audience []string
+ ResponseType domain.OIDCResponseType
+ CodeChallenge *domain.OIDCCodeChallenge
+ Prompt []domain.Prompt
+ UILocales []string
+ MaxAge *time.Duration
+ LoginHint *string
+ HintUserID *string
+}
+
+type CurrentAuthRequest struct {
+ *AuthRequest
+ SessionID string
+ UserID string
+ AuthMethods []domain.UserAuthMethodType
+ AuthTime time.Time
+}
+
+const IDPrefixV2 = "V2_"
+
+func (c *Commands) AddAuthRequest(ctx context.Context, authRequest *AuthRequest) (_ *CurrentAuthRequest, err error) {
+ authRequestID, err := c.idGenerator.Next()
+ if err != nil {
+ return nil, err
+ }
+ authRequest.ID = IDPrefixV2 + authRequestID
+ writeModel, err := c.getAuthRequestWriteModel(ctx, authRequest.ID)
+ if err != nil {
+ return nil, err
+ }
+ if writeModel.AuthRequestState != domain.AuthRequestStateUnspecified {
+ return nil, errors.ThrowPreconditionFailed(nil, "COMMAND-Sf3gt", "Errors.AuthRequest.AlreadyExisting")
+ }
+ err = c.pushAppendAndReduce(ctx, writeModel, authrequest.NewAddedEvent(
+ ctx,
+ &authrequest.NewAggregate(authRequest.ID, authz.GetInstance(ctx).InstanceID()).Aggregate,
+ authRequest.LoginClient,
+ authRequest.ClientID,
+ authRequest.RedirectURI,
+ authRequest.State,
+ authRequest.Nonce,
+ authRequest.Scope,
+ authRequest.Audience,
+ authRequest.ResponseType,
+ authRequest.CodeChallenge,
+ authRequest.Prompt,
+ authRequest.UILocales,
+ authRequest.MaxAge,
+ authRequest.LoginHint,
+ authRequest.HintUserID,
+ ))
+ if err != nil {
+ return nil, err
+ }
+ return authRequestWriteModelToCurrentAuthRequest(writeModel), nil
+}
+
+func (c *Commands) LinkSessionToAuthRequest(ctx context.Context, id, sessionID, sessionToken string, checkLoginClient bool) (*domain.ObjectDetails, *CurrentAuthRequest, error) {
+ writeModel, err := c.getAuthRequestWriteModel(ctx, id)
+ if err != nil {
+ return nil, nil, err
+ }
+ if writeModel.AuthRequestState == domain.AuthRequestStateUnspecified {
+ return nil, nil, errors.ThrowNotFound(nil, "COMMAND-jae5P", "Errors.AuthRequest.NotExisting")
+ }
+ if writeModel.AuthRequestState != domain.AuthRequestStateAdded {
+ return nil, nil, errors.ThrowPreconditionFailed(nil, "COMMAND-Sx208nt", "Errors.AuthRequest.AlreadyHandled")
+ }
+ if checkLoginClient && authz.GetCtxData(ctx).UserID != writeModel.LoginClient {
+ return nil, nil, errors.ThrowPermissionDenied(nil, "COMMAND-rai9Y", "Errors.AuthRequest.WrongLoginClient")
+ }
+ sessionWriteModel := NewSessionWriteModel(sessionID, authz.GetCtxData(ctx).OrgID)
+ err = c.eventstore.FilterToQueryReducer(ctx, sessionWriteModel)
+ if err != nil {
+ return nil, nil, err
+ }
+ if sessionWriteModel.State == domain.SessionStateUnspecified {
+ return nil, nil, errors.ThrowNotFound(nil, "COMMAND-x0099887", "Errors.Session.NotExisting")
+ }
+ if err := c.sessionPermission(ctx, sessionWriteModel, sessionToken, domain.PermissionSessionWrite); err != nil {
+ return nil, nil, err
+ }
+
+ if err := c.pushAppendAndReduce(ctx, writeModel, authrequest.NewSessionLinkedEvent(
+ ctx, &authrequest.NewAggregate(id, authz.GetInstance(ctx).InstanceID()).Aggregate,
+ sessionID,
+ sessionWriteModel.UserID,
+ sessionWriteModel.AuthenticationTime(),
+ sessionWriteModel.AuthMethodTypes(),
+ )); err != nil {
+ return nil, nil, err
+ }
+ return writeModelToObjectDetails(&writeModel.WriteModel), authRequestWriteModelToCurrentAuthRequest(writeModel), nil
+}
+
+func (c *Commands) FailAuthRequest(ctx context.Context, id string, reason domain.OIDCErrorReason) (*domain.ObjectDetails, *CurrentAuthRequest, error) {
+ writeModel, err := c.getAuthRequestWriteModel(ctx, id)
+ if err != nil {
+ return nil, nil, err
+ }
+ if writeModel.AuthRequestState != domain.AuthRequestStateAdded {
+ return nil, nil, errors.ThrowPreconditionFailed(nil, "COMMAND-Sx202nt", "Errors.AuthRequest.AlreadyHandled")
+ }
+ err = c.pushAppendAndReduce(ctx, writeModel, authrequest.NewFailedEvent(
+ ctx,
+ &authrequest.NewAggregate(id, authz.GetInstance(ctx).InstanceID()).Aggregate,
+ reason,
+ ))
+ if err != nil {
+ return nil, nil, err
+ }
+ return writeModelToObjectDetails(&writeModel.WriteModel), authRequestWriteModelToCurrentAuthRequest(writeModel), nil
+}
+
+func (c *Commands) AddAuthRequestCode(ctx context.Context, authRequestID, code string) (err error) {
+ if code == "" {
+ return errors.ThrowPreconditionFailed(nil, "COMMAND-Ht52d", "Errors.AuthRequest.InvalidCode")
+ }
+ writeModel, err := c.getAuthRequestWriteModel(ctx, authRequestID)
+ if err != nil {
+ return err
+ }
+ if writeModel.AuthRequestState != domain.AuthRequestStateAdded || writeModel.SessionID == "" {
+ return errors.ThrowPreconditionFailed(nil, "COMMAND-SFwd2", "Errors.AuthRequest.AlreadyHandled")
+ }
+ return c.pushAppendAndReduce(ctx, writeModel, authrequest.NewCodeAddedEvent(ctx,
+ &authrequest.NewAggregate(writeModel.AggregateID, authz.GetInstance(ctx).InstanceID()).Aggregate))
+}
+
+func (c *Commands) ExchangeAuthCode(ctx context.Context, code string) (authRequest *CurrentAuthRequest, err error) {
+ if code == "" {
+ return nil, errors.ThrowPreconditionFailed(nil, "COMMAND-Sf3g2", "Errors.AuthRequest.InvalidCode")
+ }
+ writeModel, err := c.getAuthRequestWriteModel(ctx, code)
+ if err != nil {
+ return nil, err
+ }
+ if writeModel.AuthRequestState != domain.AuthRequestStateCodeAdded {
+ return nil, errors.ThrowPreconditionFailed(nil, "COMMAND-SFwd2", "Errors.AuthRequest.NoCode")
+ }
+ err = c.pushAppendAndReduce(ctx, writeModel, authrequest.NewCodeExchangedEvent(ctx,
+ &authrequest.NewAggregate(writeModel.AggregateID, authz.GetInstance(ctx).InstanceID()).Aggregate))
+ if err != nil {
+ return nil, err
+ }
+ return authRequestWriteModelToCurrentAuthRequest(writeModel), nil
+}
+
+func authRequestWriteModelToCurrentAuthRequest(writeModel *AuthRequestWriteModel) (_ *CurrentAuthRequest) {
+ return &CurrentAuthRequest{
+ AuthRequest: &AuthRequest{
+ ID: writeModel.AggregateID,
+ LoginClient: writeModel.LoginClient,
+ ClientID: writeModel.ClientID,
+ RedirectURI: writeModel.RedirectURI,
+ State: writeModel.State,
+ Nonce: writeModel.Nonce,
+ Scope: writeModel.Scope,
+ Audience: writeModel.Audience,
+ ResponseType: writeModel.ResponseType,
+ CodeChallenge: writeModel.CodeChallenge,
+ Prompt: writeModel.Prompt,
+ UILocales: writeModel.UILocales,
+ MaxAge: writeModel.MaxAge,
+ LoginHint: writeModel.LoginHint,
+ HintUserID: writeModel.HintUserID,
+ },
+ SessionID: writeModel.SessionID,
+ UserID: writeModel.UserID,
+ AuthMethods: writeModel.AuthMethods,
+ AuthTime: writeModel.AuthTime,
+ }
+}
+
+func (c *Commands) GetCurrentAuthRequest(ctx context.Context, id string) (_ *CurrentAuthRequest, err error) {
+ wm, err := c.getAuthRequestWriteModel(ctx, id)
+ if err != nil {
+ return nil, err
+ }
+ return authRequestWriteModelToCurrentAuthRequest(wm), nil
+}
+
+func (c *Commands) getAuthRequestWriteModel(ctx context.Context, id string) (writeModel *AuthRequestWriteModel, err error) {
+ ctx, span := tracing.NewSpan(ctx)
+ defer func() { span.EndWithError(err) }()
+
+ writeModel = NewAuthRequestWriteModel(ctx, id)
+ err = c.eventstore.FilterToQueryReducer(ctx, writeModel)
+ if err != nil {
+ return nil, err
+ }
+ return writeModel, nil
+}
diff --git a/internal/command/auth_request_model.go b/internal/command/auth_request_model.go
new file mode 100644
index 000000000..d27caf876
--- /dev/null
+++ b/internal/command/auth_request_model.go
@@ -0,0 +1,110 @@
+package command
+
+import (
+ "context"
+ "time"
+
+ "github.com/zitadel/zitadel/internal/api/authz"
+ "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/authrequest"
+)
+
+type AuthRequestWriteModel struct {
+ eventstore.WriteModel
+ aggregate *eventstore.Aggregate
+
+ LoginClient string
+ ClientID string
+ RedirectURI string
+ State string
+ Nonce string
+ Scope []string
+ Audience []string
+ ResponseType domain.OIDCResponseType
+ CodeChallenge *domain.OIDCCodeChallenge
+ Prompt []domain.Prompt
+ UILocales []string
+ MaxAge *time.Duration
+ LoginHint *string
+ HintUserID *string
+ SessionID string
+ UserID string
+ AuthTime time.Time
+ AuthMethods []domain.UserAuthMethodType
+ AuthRequestState domain.AuthRequestState
+}
+
+func NewAuthRequestWriteModel(ctx context.Context, id string) *AuthRequestWriteModel {
+ return &AuthRequestWriteModel{
+ WriteModel: eventstore.WriteModel{
+ AggregateID: id,
+ },
+ aggregate: &authrequest.NewAggregate(id, authz.GetInstance(ctx).InstanceID()).Aggregate,
+ }
+}
+
+func (m *AuthRequestWriteModel) Reduce() error {
+ for _, event := range m.Events {
+ switch e := event.(type) {
+ case *authrequest.AddedEvent:
+ m.LoginClient = e.LoginClient
+ m.ClientID = e.ClientID
+ m.RedirectURI = e.RedirectURI
+ m.State = e.State
+ m.Nonce = e.Nonce
+ m.Scope = e.Scope
+ m.Audience = e.Audience
+ m.ResponseType = e.ResponseType
+ m.CodeChallenge = e.CodeChallenge
+ m.Prompt = e.Prompt
+ m.UILocales = e.UILocales
+ m.MaxAge = e.MaxAge
+ m.LoginHint = e.LoginHint
+ m.HintUserID = e.HintUserID
+ m.AuthRequestState = domain.AuthRequestStateAdded
+ case *authrequest.SessionLinkedEvent:
+ m.SessionID = e.SessionID
+ m.UserID = e.UserID
+ m.AuthTime = e.AuthTime
+ m.AuthMethods = e.AuthMethods
+ case *authrequest.CodeAddedEvent:
+ m.AuthRequestState = domain.AuthRequestStateCodeAdded
+ case *authrequest.FailedEvent:
+ m.AuthRequestState = domain.AuthRequestStateFailed
+ case *authrequest.CodeExchangedEvent:
+ m.AuthRequestState = domain.AuthRequestStateCodeExchanged
+ case *authrequest.SucceededEvent:
+ m.AuthRequestState = domain.AuthRequestStateSucceeded
+ }
+ }
+
+ return m.WriteModel.Reduce()
+}
+
+func (m *AuthRequestWriteModel) Query() *eventstore.SearchQueryBuilder {
+ return eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent).
+ AddQuery().
+ AggregateTypes(authrequest.AggregateType).
+ AggregateIDs(m.AggregateID).
+ Builder()
+}
+
+// CheckAuthenticated checks that the auth request exists, a session must have been linked
+// and in case of a Code Flow the code must have been exchanged
+func (m *AuthRequestWriteModel) CheckAuthenticated() error {
+ if m.SessionID == "" {
+ return caos_errs.ThrowPreconditionFailed(nil, "AUTHR-SF2r2", "Errors.AuthRequest.NotAuthenticated")
+ }
+ // in case of OIDC Code Flow, the code must have been exchanged
+ if m.ResponseType == domain.OIDCResponseTypeCode && m.AuthRequestState == domain.AuthRequestStateCodeExchanged {
+ return nil
+ }
+ // in case of OIDC Implicit Flow, check that the requests exists, but has not succeeded yet
+ if (m.ResponseType == domain.OIDCResponseTypeIDToken || m.ResponseType == domain.OIDCResponseTypeIDTokenToken) &&
+ m.AuthRequestState == domain.AuthRequestStateAdded {
+ return nil
+ }
+ return caos_errs.ThrowPreconditionFailed(nil, "AUTHR-sajk3", "Errors.AuthRequest.NotAuthenticated")
+}
diff --git a/internal/command/auth_request_test.go b/internal/command/auth_request_test.go
new file mode 100644
index 000000000..f54043b36
--- /dev/null
+++ b/internal/command/auth_request_test.go
@@ -0,0 +1,997 @@
+package command
+
+import (
+ "context"
+ "testing"
+ "time"
+
+ "github.com/muhlemmer/gu"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+
+ "github.com/zitadel/zitadel/internal/api/authz"
+ "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/repository"
+ "github.com/zitadel/zitadel/internal/id"
+ "github.com/zitadel/zitadel/internal/id/mock"
+ "github.com/zitadel/zitadel/internal/repository/authrequest"
+ "github.com/zitadel/zitadel/internal/repository/session"
+)
+
+func TestCommands_AddAuthRequest(t *testing.T) {
+ mockCtx := authz.NewMockContext("instanceID", "orgID", "loginClient")
+ type fields struct {
+ eventstore *eventstore.Eventstore
+ idGenerator id.Generator
+ }
+ type args struct {
+ ctx context.Context
+ request *AuthRequest
+ }
+ tests := []struct {
+ name string
+ fields fields
+ args args
+ want *CurrentAuthRequest
+ wantErr error
+ }{
+ {
+ "already exists error",
+ fields{
+ eventstore: eventstoreExpect(t,
+ expectFilter(
+ eventFromEventPusher(
+ authrequest.NewAddedEvent(mockCtx, &authrequest.NewAggregate("V2_id", "instanceID").Aggregate,
+ "loginClient",
+ "clientID",
+ "redirectURI",
+ "state",
+ "nonce",
+ []string{"openid"},
+ []string{"audience"},
+ domain.OIDCResponseTypeCode,
+ nil,
+ nil,
+ nil,
+ nil,
+ nil,
+ nil,
+ ),
+ ),
+ ),
+ ),
+ idGenerator: mock.NewIDGeneratorExpectIDs(t, "id"),
+ },
+ args{
+ ctx: mockCtx,
+ request: &AuthRequest{},
+ },
+ nil,
+ caos_errs.ThrowPreconditionFailed(nil, "COMMAND-Sf3gt", "Errors.AuthRequest.AlreadyExisting"),
+ },
+ {
+ "added",
+ fields{
+ eventstore: eventstoreExpect(t,
+ expectFilter(),
+ expectPush(
+ []*repository.Event{
+ eventFromEventPusherWithInstanceID("instanceID",
+ authrequest.NewAddedEvent(mockCtx, &authrequest.NewAggregate("V2_id", "instanceID").Aggregate,
+ "loginClient",
+ "clientID",
+ "redirectURI",
+ "state",
+ "nonce",
+ []string{"openid"},
+ []string{"audience"},
+ domain.OIDCResponseTypeCode,
+ &domain.OIDCCodeChallenge{
+ Challenge: "challenge",
+ Method: domain.CodeChallengeMethodS256,
+ },
+ []domain.Prompt{domain.PromptNone},
+ []string{"en", "de"},
+ gu.Ptr(time.Duration(0)),
+ gu.Ptr("loginHint"),
+ gu.Ptr("hintUserID"),
+ ),
+ ),
+ }),
+ ),
+ idGenerator: mock.NewIDGeneratorExpectIDs(t, "id"),
+ },
+ args{
+ ctx: mockCtx,
+ request: &AuthRequest{
+ LoginClient: "loginClient",
+ ClientID: "clientID",
+ RedirectURI: "redirectURI",
+ State: "state",
+ Nonce: "nonce",
+ Scope: []string{"openid"},
+ Audience: []string{"audience"},
+ ResponseType: domain.OIDCResponseTypeCode,
+ CodeChallenge: &domain.OIDCCodeChallenge{
+ Challenge: "challenge",
+ Method: domain.CodeChallengeMethodS256,
+ },
+ Prompt: []domain.Prompt{domain.PromptNone},
+ UILocales: []string{"en", "de"},
+ MaxAge: gu.Ptr(time.Duration(0)),
+ LoginHint: gu.Ptr("loginHint"),
+ HintUserID: gu.Ptr("hintUserID"),
+ },
+ },
+ &CurrentAuthRequest{
+ AuthRequest: &AuthRequest{
+ ID: "V2_id",
+ LoginClient: "loginClient",
+ ClientID: "clientID",
+ RedirectURI: "redirectURI",
+ State: "state",
+ Nonce: "nonce",
+ Scope: []string{"openid"},
+ Audience: []string{"audience"},
+ ResponseType: domain.OIDCResponseTypeCode,
+ CodeChallenge: &domain.OIDCCodeChallenge{
+ Challenge: "challenge",
+ Method: domain.CodeChallengeMethodS256,
+ },
+ Prompt: []domain.Prompt{domain.PromptNone},
+ UILocales: []string{"en", "de"},
+ MaxAge: gu.Ptr(time.Duration(0)),
+ LoginHint: gu.Ptr("loginHint"),
+ HintUserID: gu.Ptr("hintUserID"),
+ },
+ },
+ nil,
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ c := &Commands{
+ eventstore: tt.fields.eventstore,
+ idGenerator: tt.fields.idGenerator,
+ }
+ got, err := c.AddAuthRequest(tt.args.ctx, tt.args.request)
+ require.ErrorIs(t, tt.wantErr, err)
+ assert.Equal(t, tt.want, got)
+ })
+ }
+}
+
+func TestCommands_LinkSessionToAuthRequest(t *testing.T) {
+ mockCtx := authz.NewMockContext("instanceID", "orgID", "loginClient")
+ type fields struct {
+ eventstore *eventstore.Eventstore
+ tokenVerifier func(ctx context.Context, sessionToken, sessionID, tokenID string) (err error)
+ checkPermission domain.PermissionCheck
+ }
+ type args struct {
+ ctx context.Context
+ id string
+ sessionID string
+ sessionToken string
+ checkLoginClient bool
+ }
+ type res struct {
+ details *domain.ObjectDetails
+ authReq *CurrentAuthRequest
+ wantErr error
+ }
+ tests := []struct {
+ name string
+ fields fields
+ args args
+ res res
+ }{
+ {
+ "authRequest not found",
+ fields{
+ eventstore: eventstoreExpect(t,
+ expectFilter(),
+ ),
+ tokenVerifier: func(ctx context.Context, sessionToken, sessionID, tokenID string) (err error) {
+ return nil
+ },
+ checkPermission: newMockPermissionCheckNotAllowed(),
+ },
+ args{
+ ctx: mockCtx,
+ id: "id",
+ sessionID: "sessionID",
+ },
+ res{
+ wantErr: caos_errs.ThrowNotFound(nil, "COMMAND-jae5P", "Errors.AuthRequest.NotExisting"),
+ },
+ },
+ {
+ "authRequest not existing",
+ fields{
+ eventstore: eventstoreExpect(t,
+ expectFilter(
+ eventFromEventPusher(
+ authrequest.NewAddedEvent(mockCtx, &authrequest.NewAggregate("id", "instanceID").Aggregate,
+ "loginClient",
+ "clientID",
+ "redirectURI",
+ "state",
+ "nonce",
+ []string{"openid"},
+ []string{"audience"},
+ domain.OIDCResponseTypeCode,
+ nil,
+ nil,
+ nil,
+ nil,
+ nil,
+ nil,
+ ),
+ ),
+ eventFromEventPusher(
+ authrequest.NewFailedEvent(mockCtx, &authrequest.NewAggregate("id", "instanceID").Aggregate,
+ domain.OIDCErrorReasonUnspecified),
+ ),
+ ),
+ ),
+ tokenVerifier: func(ctx context.Context, sessionToken, sessionID, tokenID string) (err error) {
+ return nil
+ },
+ checkPermission: newMockPermissionCheckAllowed(),
+ },
+ args{
+ ctx: mockCtx,
+ id: "id",
+ sessionID: "sessionID",
+ },
+ res{
+ wantErr: caos_errs.ThrowPreconditionFailed(nil, "COMMAND-Sx208nt", "Errors.AuthRequest.AlreadyHandled"),
+ },
+ },
+ {
+ "wrong login client",
+ fields{
+ eventstore: eventstoreExpect(t,
+ expectFilter(
+ eventFromEventPusher(
+ authrequest.NewAddedEvent(mockCtx, &authrequest.NewAggregate("id", "instanceID").Aggregate,
+ "loginClient",
+ "clientID",
+ "redirectURI",
+ "state",
+ "nonce",
+ []string{"openid"},
+ []string{"audience"},
+ domain.OIDCResponseTypeCode,
+ nil,
+ nil,
+ nil,
+ nil,
+ nil,
+ nil,
+ ),
+ ),
+ ),
+ ),
+ tokenVerifier: func(ctx context.Context, sessionToken, sessionID, tokenID string) (err error) {
+ return nil
+ },
+ checkPermission: newMockPermissionCheckAllowed(),
+ },
+ args{
+ ctx: authz.NewMockContext("instanceID", "orgID", "wrongLoginClient"),
+ id: "id",
+ sessionID: "sessionID",
+ sessionToken: "token",
+ checkLoginClient: true,
+ },
+ res{
+ wantErr: caos_errs.ThrowPermissionDenied(nil, "COMMAND-rai9Y", "Errors.AuthRequest.WrongLoginClient"),
+ },
+ },
+ {
+ "session not existing",
+ fields{
+ eventstore: eventstoreExpect(t,
+ expectFilter(
+ eventFromEventPusher(
+ authrequest.NewAddedEvent(mockCtx, &authrequest.NewAggregate("V2_id", "instanceID").Aggregate,
+ "loginClient",
+ "clientID",
+ "redirectURI",
+ "state",
+ "nonce",
+ []string{"openid"},
+ []string{"audience"},
+ domain.OIDCResponseTypeCode,
+ nil,
+ nil,
+ nil,
+ nil,
+ nil,
+ nil,
+ ),
+ ),
+ ),
+ expectFilter(),
+ ),
+ tokenVerifier: func(ctx context.Context, sessionToken, sessionID, tokenID string) (err error) {
+ return nil
+ },
+ checkPermission: newMockPermissionCheckNotAllowed(),
+ },
+ args{
+ ctx: mockCtx,
+ id: "V2_id",
+ sessionID: "sessionID",
+ },
+ res{
+ wantErr: caos_errs.ThrowNotFound(nil, "COMMAND-x0099887", "Errors.Session.NotExisting"),
+ },
+ },
+ {
+ "missing permission",
+ fields{
+ eventstore: eventstoreExpect(t,
+ expectFilter(
+ eventFromEventPusher(
+ authrequest.NewAddedEvent(mockCtx, &authrequest.NewAggregate("V2_id", "instanceID").Aggregate,
+ "loginClient",
+ "clientID",
+ "redirectURI",
+ "state",
+ "nonce",
+ []string{"openid"},
+ []string{"audience"},
+ domain.OIDCResponseTypeCode,
+ nil,
+ nil,
+ nil,
+ nil,
+ nil,
+ nil,
+ ),
+ ),
+ ),
+ expectFilter(
+ eventFromEventPusher(
+ session.NewAddedEvent(mockCtx, &session.NewAggregate("sessionID", "org1").Aggregate, "domain.tld")),
+ ),
+ ),
+ tokenVerifier: func(ctx context.Context, sessionToken, sessionID, tokenID string) (err error) {
+ return nil
+ },
+ checkPermission: newMockPermissionCheckNotAllowed(),
+ },
+ args{
+ ctx: mockCtx,
+ id: "V2_id",
+ sessionID: "sessionID",
+ },
+ res{
+ wantErr: caos_errs.ThrowPermissionDenied(nil, "AUTHZ-HKJD33", "Errors.PermissionDenied"),
+ },
+ },
+ {
+ "invalid session token",
+ fields{
+ eventstore: eventstoreExpect(t,
+ expectFilter(
+ eventFromEventPusher(
+ authrequest.NewAddedEvent(mockCtx, &authrequest.NewAggregate("V2_id", "instanceID").Aggregate,
+ "loginClient",
+ "clientID",
+ "redirectURI",
+ "state",
+ "nonce",
+ []string{"openid"},
+ []string{"audience"},
+ domain.OIDCResponseTypeCode,
+ nil,
+ nil,
+ nil,
+ nil,
+ nil,
+ nil,
+ ),
+ ),
+ ),
+ expectFilter(
+ eventFromEventPusher(
+ session.NewAddedEvent(mockCtx, &session.NewAggregate("sessionID", "org1").Aggregate, "domain.tld")),
+ ),
+ ),
+ tokenVerifier: func(ctx context.Context, sessionToken, sessionID, tokenID string) (err error) {
+ return caos_errs.ThrowPermissionDenied(nil, "COMMAND-sGr42", "Errors.Session.Token.Invalid")
+ },
+ },
+ args{
+ ctx: mockCtx,
+ id: "V2_id",
+ sessionID: "sessionID",
+ sessionToken: "invalid",
+ },
+ res{
+ wantErr: caos_errs.ThrowPermissionDenied(nil, "COMMAND-sGr42", "Errors.Session.Token.Invalid"),
+ },
+ },
+ {
+ "linked",
+ fields{
+ eventstore: eventstoreExpect(t,
+ expectFilter(
+ eventFromEventPusher(
+ authrequest.NewAddedEvent(mockCtx, &authrequest.NewAggregate("V2_id", "instanceID").Aggregate,
+ "loginClient",
+ "clientID",
+ "redirectURI",
+ "state",
+ "nonce",
+ []string{"openid"},
+ []string{"audience"},
+ domain.OIDCResponseTypeCode,
+ nil,
+ nil,
+ nil,
+ nil,
+ nil,
+ nil,
+ ),
+ ),
+ ),
+ expectFilter(
+ eventFromEventPusher(
+ session.NewAddedEvent(mockCtx, &session.NewAggregate("sessionID", "org1").Aggregate, "domain.tld"),
+ ),
+ eventFromEventPusher(
+ session.NewUserCheckedEvent(mockCtx, &session.NewAggregate("sessionID", "org1").Aggregate,
+ "userID", testNow),
+ ),
+ eventFromEventPusher(
+ session.NewPasswordCheckedEvent(mockCtx, &session.NewAggregate("sessionID", "org1").Aggregate,
+ testNow),
+ ),
+ ),
+ expectPush(
+ []*repository.Event{eventFromEventPusherWithInstanceID(
+ "instanceID",
+ authrequest.NewSessionLinkedEvent(mockCtx, &authrequest.NewAggregate("V2_id", "instanceID").Aggregate,
+ "sessionID",
+ "userID",
+ testNow,
+ []domain.UserAuthMethodType{domain.UserAuthMethodTypePassword},
+ ),
+ )}),
+ ),
+ tokenVerifier: func(ctx context.Context, sessionToken, sessionID, tokenID string) (err error) {
+ return nil
+ },
+ checkPermission: newMockPermissionCheckAllowed(),
+ },
+ args{
+ ctx: mockCtx,
+ id: "V2_id",
+ sessionID: "sessionID",
+ sessionToken: "token",
+ },
+ res{
+ details: &domain.ObjectDetails{ResourceOwner: "instanceID"},
+ authReq: &CurrentAuthRequest{
+ AuthRequest: &AuthRequest{
+ ID: "V2_id",
+ LoginClient: "loginClient",
+ ClientID: "clientID",
+ RedirectURI: "redirectURI",
+ State: "state",
+ Nonce: "nonce",
+ Scope: []string{"openid"},
+ Audience: []string{"audience"},
+ ResponseType: domain.OIDCResponseTypeCode,
+ },
+ SessionID: "sessionID",
+ UserID: "userID",
+ AuthMethods: []domain.UserAuthMethodType{domain.UserAuthMethodTypePassword},
+ },
+ },
+ },
+ {
+ "linked with login client check",
+ fields{
+ eventstore: eventstoreExpect(t,
+ expectFilter(
+ eventFromEventPusher(
+ authrequest.NewAddedEvent(mockCtx, &authrequest.NewAggregate("V2_id", "instanceID").Aggregate,
+ "loginClient",
+ "clientID",
+ "redirectURI",
+ "state",
+ "nonce",
+ []string{"openid"},
+ []string{"audience"},
+ domain.OIDCResponseTypeCode,
+ nil,
+ nil,
+ nil,
+ nil,
+ nil,
+ nil,
+ ),
+ ),
+ ),
+ expectFilter(
+ eventFromEventPusher(
+ session.NewAddedEvent(mockCtx, &session.NewAggregate("sessionID", "org1").Aggregate, "domain.tld"),
+ ),
+ eventFromEventPusher(
+ session.NewUserCheckedEvent(mockCtx, &session.NewAggregate("sessionID", "org1").Aggregate,
+ "userID", testNow),
+ ),
+ eventFromEventPusher(
+ session.NewPasswordCheckedEvent(mockCtx, &session.NewAggregate("sessionID", "org1").Aggregate,
+ testNow),
+ ),
+ ),
+ expectPush(
+ []*repository.Event{eventFromEventPusherWithInstanceID(
+ "instanceID",
+ authrequest.NewSessionLinkedEvent(mockCtx, &authrequest.NewAggregate("V2_id", "instanceID").Aggregate,
+ "sessionID",
+ "userID",
+ testNow,
+ []domain.UserAuthMethodType{domain.UserAuthMethodTypePassword},
+ ),
+ )}),
+ ),
+ tokenVerifier: func(ctx context.Context, sessionToken, sessionID, tokenID string) (err error) {
+ return nil
+ },
+ checkPermission: newMockPermissionCheckAllowed(),
+ },
+ args{
+ ctx: authz.NewMockContext("instanceID", "orgID", "loginClient"),
+ id: "V2_id",
+ sessionID: "sessionID",
+ sessionToken: "token",
+ checkLoginClient: true,
+ },
+ res{
+ details: &domain.ObjectDetails{ResourceOwner: "instanceID"},
+ authReq: &CurrentAuthRequest{
+ AuthRequest: &AuthRequest{
+ ID: "V2_id",
+ LoginClient: "loginClient",
+ ClientID: "clientID",
+ RedirectURI: "redirectURI",
+ State: "state",
+ Nonce: "nonce",
+ Scope: []string{"openid"},
+ Audience: []string{"audience"},
+ ResponseType: domain.OIDCResponseTypeCode,
+ },
+ SessionID: "sessionID",
+ UserID: "userID",
+ AuthMethods: []domain.UserAuthMethodType{domain.UserAuthMethodTypePassword},
+ },
+ },
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ c := &Commands{
+ eventstore: tt.fields.eventstore,
+ sessionTokenVerifier: tt.fields.tokenVerifier,
+ checkPermission: tt.fields.checkPermission,
+ }
+ details, got, err := c.LinkSessionToAuthRequest(tt.args.ctx, tt.args.id, tt.args.sessionID, tt.args.sessionToken, tt.args.checkLoginClient)
+ require.ErrorIs(t, err, tt.res.wantErr)
+ assert.Equal(t, tt.res.details, details)
+ if err == nil {
+ assert.WithinRange(t, got.AuthTime, testNow, testNow)
+ got.AuthTime = time.Time{}
+ }
+ assert.Equal(t, tt.res.authReq, got)
+ })
+ }
+}
+
+func TestCommands_FailAuthRequest(t *testing.T) {
+ mockCtx := authz.NewMockContext("instanceID", "orgID", "loginClient")
+ type fields struct {
+ eventstore *eventstore.Eventstore
+ }
+ type args struct {
+ ctx context.Context
+ id string
+ reason domain.OIDCErrorReason
+ }
+ type res struct {
+ details *domain.ObjectDetails
+ authReq *CurrentAuthRequest
+ wantErr error
+ }
+ tests := []struct {
+ name string
+ fields fields
+ args args
+ res res
+ }{
+ {
+ "authRequest not existing",
+ fields{
+ eventstore: eventstoreExpect(t,
+ expectFilter(),
+ ),
+ },
+ args{
+ ctx: mockCtx,
+ id: "foo",
+ reason: domain.OIDCErrorReasonLoginRequired,
+ },
+ res{
+ wantErr: caos_errs.ThrowPreconditionFailed(nil, "COMMAND-Sx202nt", "Errors.AuthRequest.AlreadyHandled"),
+ },
+ },
+ {
+ "failed",
+ fields{
+ eventstore: eventstoreExpect(t,
+ expectFilter(
+ eventFromEventPusher(
+ authrequest.NewAddedEvent(mockCtx, &authrequest.NewAggregate("V2_id", "instanceID").Aggregate,
+ "loginClient",
+ "clientID",
+ "redirectURI",
+ "state",
+ "nonce",
+ []string{"openid"},
+ []string{"audience"},
+ domain.OIDCResponseTypeCode,
+ nil,
+ nil,
+ nil,
+ nil,
+ nil,
+ nil,
+ ),
+ ),
+ ),
+ expectPush(
+ []*repository.Event{eventFromEventPusherWithInstanceID(
+ "instanceID",
+ authrequest.NewFailedEvent(mockCtx, &authrequest.NewAggregate("V2_id", "instanceID").Aggregate,
+ domain.OIDCErrorReasonLoginRequired),
+ )}),
+ ),
+ },
+ args{
+ ctx: mockCtx,
+ id: "V2_id",
+ reason: domain.OIDCErrorReasonLoginRequired,
+ },
+ res{
+ details: &domain.ObjectDetails{ResourceOwner: "instanceID"},
+ authReq: &CurrentAuthRequest{
+ AuthRequest: &AuthRequest{
+ ID: "V2_id",
+ LoginClient: "loginClient",
+ ClientID: "clientID",
+ RedirectURI: "redirectURI",
+ State: "state",
+ Nonce: "nonce",
+ Scope: []string{"openid"},
+ Audience: []string{"audience"},
+ ResponseType: domain.OIDCResponseTypeCode,
+ },
+ },
+ },
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ c := &Commands{
+ eventstore: tt.fields.eventstore,
+ }
+ details, got, err := c.FailAuthRequest(tt.args.ctx, tt.args.id, tt.args.reason)
+ require.ErrorIs(t, err, tt.res.wantErr)
+ assert.Equal(t, tt.res.details, details)
+ assert.Equal(t, tt.res.authReq, got)
+ })
+ }
+}
+
+func TestCommands_AddAuthRequestCode(t *testing.T) {
+ mockCtx := authz.NewMockContext("instanceID", "orgID", "loginClient")
+ type fields struct {
+ eventstore *eventstore.Eventstore
+ }
+ type args struct {
+ ctx context.Context
+ id string
+ code string
+ }
+ tests := []struct {
+ name string
+ fields fields
+ args args
+ wantErr error
+ }{
+ {
+ "empty code error",
+ fields{
+ eventstore: eventstoreExpect(t),
+ },
+ args{
+ ctx: mockCtx,
+ id: "V2_authRequestID",
+ code: "",
+ },
+ caos_errs.ThrowPreconditionFailed(nil, "COMMAND-Ht52d", "Errors.AuthRequest.InvalidCode"),
+ },
+ {
+ "no session linked error",
+ fields{
+ eventstore: eventstoreExpect(t,
+ expectFilter(
+ eventFromEventPusher(
+ authrequest.NewAddedEvent(mockCtx, &authrequest.NewAggregate("V2_authRequestID", "instanceID").Aggregate,
+ "loginClient",
+ "clientID",
+ "redirectURI",
+ "state",
+ "nonce",
+ []string{"openid"},
+ []string{"audience"},
+ domain.OIDCResponseTypeCode,
+ &domain.OIDCCodeChallenge{
+ Challenge: "challenge",
+ Method: domain.CodeChallengeMethodS256,
+ },
+ []domain.Prompt{domain.PromptNone},
+ []string{"en", "de"},
+ gu.Ptr(time.Duration(0)),
+ gu.Ptr("loginHint"),
+ gu.Ptr("hintUserID"),
+ ),
+ ),
+ ),
+ ),
+ },
+ args{
+ ctx: mockCtx,
+ id: "V2_authRequestID",
+ code: "V2_authRequestID",
+ },
+ caos_errs.ThrowPreconditionFailed(nil, "COMMAND-SFwd2", "Errors.AuthRequest.AlreadyHandled"),
+ },
+ {
+ "success",
+ fields{
+ eventstore: eventstoreExpect(t,
+ expectFilter(
+ eventFromEventPusher(
+ authrequest.NewAddedEvent(mockCtx, &authrequest.NewAggregate("V2_authRequestID", "instanceID").Aggregate,
+ "loginClient",
+ "clientID",
+ "redirectURI",
+ "state",
+ "nonce",
+ []string{"openid"},
+ []string{"audience"},
+ domain.OIDCResponseTypeCode,
+ &domain.OIDCCodeChallenge{
+ Challenge: "challenge",
+ Method: domain.CodeChallengeMethodS256,
+ },
+ []domain.Prompt{domain.PromptNone},
+ []string{"en", "de"},
+ gu.Ptr(time.Duration(0)),
+ gu.Ptr("loginHint"),
+ gu.Ptr("hintUserID"),
+ ),
+ ),
+ eventFromEventPusher(
+ authrequest.NewSessionLinkedEvent(mockCtx, &authrequest.NewAggregate("V2_authRequestID", "instanceID").Aggregate,
+ "sessionID",
+ "userID",
+ testNow,
+ []domain.UserAuthMethodType{domain.UserAuthMethodTypePassword},
+ ),
+ ),
+ ),
+ expectPush(
+ []*repository.Event{
+ eventFromEventPusherWithInstanceID("instanceID",
+ authrequest.NewCodeAddedEvent(mockCtx, &authrequest.NewAggregate("V2_authRequestID", "instanceID").Aggregate),
+ ),
+ },
+ ),
+ ),
+ },
+ args{
+ ctx: mockCtx,
+ id: "V2_authRequestID",
+ code: "V2_authRequestID",
+ },
+ nil,
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ c := &Commands{
+ eventstore: tt.fields.eventstore,
+ }
+ err := c.AddAuthRequestCode(tt.args.ctx, tt.args.id, tt.args.code)
+ assert.ErrorIs(t, tt.wantErr, err)
+ })
+ }
+}
+
+func TestCommands_ExchangeAuthCode(t *testing.T) {
+ mockCtx := authz.NewMockContext("instanceID", "orgID", "loginClient")
+ type fields struct {
+ eventstore *eventstore.Eventstore
+ }
+ type args struct {
+ ctx context.Context
+ code string
+ }
+ type res struct {
+ authRequest *CurrentAuthRequest
+ err error
+ }
+ tests := []struct {
+ name string
+ fields fields
+ args args
+ res res
+ }{
+ {
+ "empty code error",
+ fields{
+ eventstore: eventstoreExpect(t),
+ },
+ args{
+ ctx: mockCtx,
+ code: "",
+ },
+ res{
+ err: caos_errs.ThrowPreconditionFailed(nil, "COMMAND-Sf3g2", "Errors.AuthRequest.InvalidCode"),
+ },
+ },
+ {
+ "no code added error",
+ fields{
+ eventstore: eventstoreExpect(t,
+ expectFilter(
+ eventFromEventPusher(
+ authrequest.NewAddedEvent(mockCtx, &authrequest.NewAggregate("V2_authRequestID", "instanceID").Aggregate,
+ "loginClient",
+ "clientID",
+ "redirectURI",
+ "state",
+ "nonce",
+ []string{"openid"},
+ []string{"audience"},
+ domain.OIDCResponseTypeCode,
+ &domain.OIDCCodeChallenge{
+ Challenge: "challenge",
+ Method: domain.CodeChallengeMethodS256,
+ },
+ []domain.Prompt{domain.PromptNone},
+ []string{"en", "de"},
+ gu.Ptr(time.Duration(0)),
+ gu.Ptr("loginHint"),
+ gu.Ptr("hintUserID"),
+ ),
+ ),
+ ),
+ ),
+ },
+ args{
+ ctx: mockCtx,
+ code: "V2_authRequestID",
+ },
+ res{
+ err: caos_errs.ThrowPreconditionFailed(nil, "COMMAND-SFwd2", "Errors.AuthRequest.NoCode"),
+ },
+ },
+ {
+ "code exchanged",
+ fields{
+ eventstore: eventstoreExpect(t,
+ expectFilter(
+ eventFromEventPusher(
+ authrequest.NewAddedEvent(mockCtx, &authrequest.NewAggregate("V2_authRequestID", "instanceID").Aggregate,
+ "loginClient",
+ "clientID",
+ "redirectURI",
+ "state",
+ "nonce",
+ []string{"openid"},
+ []string{"audience"},
+ domain.OIDCResponseTypeCode,
+ &domain.OIDCCodeChallenge{
+ Challenge: "challenge",
+ Method: domain.CodeChallengeMethodS256,
+ },
+ []domain.Prompt{domain.PromptNone},
+ []string{"en", "de"},
+ gu.Ptr(time.Duration(0)),
+ gu.Ptr("loginHint"),
+ gu.Ptr("hintUserID"),
+ ),
+ ),
+ eventFromEventPusher(
+ authrequest.NewSessionLinkedEvent(mockCtx, &authrequest.NewAggregate("V2_authRequestID", "instanceID").Aggregate,
+ "sessionID",
+ "userID",
+ testNow,
+ []domain.UserAuthMethodType{domain.UserAuthMethodTypePassword},
+ ),
+ ),
+ eventFromEventPusher(
+ authrequest.NewCodeAddedEvent(mockCtx, &authrequest.NewAggregate("V2_authRequestID", "instanceID").Aggregate),
+ ),
+ ),
+ expectPush(
+ []*repository.Event{
+ eventFromEventPusherWithInstanceID("instanceID",
+ authrequest.NewCodeExchangedEvent(mockCtx, &authrequest.NewAggregate("V2_authRequestID", "instanceID").Aggregate),
+ ),
+ },
+ ),
+ ),
+ },
+ args{
+ ctx: mockCtx,
+ code: "V2_authRequestID",
+ },
+ res{
+ authRequest: &CurrentAuthRequest{
+ AuthRequest: &AuthRequest{
+ ID: "V2_authRequestID",
+ LoginClient: "loginClient",
+ ClientID: "clientID",
+ RedirectURI: "redirectURI",
+ State: "state",
+ Nonce: "nonce",
+ Scope: []string{"openid"},
+ Audience: []string{"audience"},
+ ResponseType: domain.OIDCResponseTypeCode,
+ CodeChallenge: &domain.OIDCCodeChallenge{
+ Challenge: "challenge",
+ Method: domain.CodeChallengeMethodS256,
+ },
+ Prompt: []domain.Prompt{domain.PromptNone},
+ UILocales: []string{"en", "de"},
+ MaxAge: gu.Ptr(time.Duration(0)),
+ LoginHint: gu.Ptr("loginHint"),
+ HintUserID: gu.Ptr("hintUserID"),
+ },
+ SessionID: "sessionID",
+ UserID: "userID",
+ AuthMethods: []domain.UserAuthMethodType{domain.UserAuthMethodTypePassword},
+ },
+ },
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ c := &Commands{
+ eventstore: tt.fields.eventstore,
+ }
+ got, err := c.ExchangeAuthCode(tt.args.ctx, tt.args.code)
+ assert.ErrorIs(t, tt.res.err, err)
+
+ if err == nil {
+ // equal on time won't work -> test separately and clear it before comparing the rest
+ assert.WithinRange(t, got.AuthTime, testNow, testNow)
+ got.AuthTime = time.Time{}
+ }
+ assert.Equal(t, tt.res.authRequest, got)
+ })
+ }
+}
diff --git a/internal/command/command.go b/internal/command/command.go
index 3c804576b..9a6a87cb7 100644
--- a/internal/command/command.go
+++ b/internal/command/command.go
@@ -15,9 +15,12 @@ import (
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/id"
"github.com/zitadel/zitadel/internal/repository/action"
+ "github.com/zitadel/zitadel/internal/repository/authrequest"
"github.com/zitadel/zitadel/internal/repository/idpintent"
instance_repo "github.com/zitadel/zitadel/internal/repository/instance"
"github.com/zitadel/zitadel/internal/repository/keypair"
+ "github.com/zitadel/zitadel/internal/repository/milestone"
+ "github.com/zitadel/zitadel/internal/repository/oidcsession"
"github.com/zitadel/zitadel/internal/repository/org"
proj_repo "github.com/zitadel/zitadel/internal/repository/project"
"github.com/zitadel/zitadel/internal/repository/quota"
@@ -42,18 +45,22 @@ type Commands struct {
externalSecure bool
externalPort uint16
- idpConfigEncryption crypto.EncryptionAlgorithm
- smtpEncryption crypto.EncryptionAlgorithm
- smsEncryption crypto.EncryptionAlgorithm
- userEncryption crypto.EncryptionAlgorithm
- userPasswordAlg crypto.HashAlgorithm
- machineKeySize int
- applicationKeySize int
- domainVerificationAlg crypto.EncryptionAlgorithm
- domainVerificationGenerator crypto.Generator
- domainVerificationValidator func(domain, token, verifier string, checkType api_http.CheckType) error
- sessionTokenCreator func(sessionID string) (id string, token string, err error)
- sessionTokenVerifier func(ctx context.Context, sessionToken, sessionID, tokenID string) (err error)
+ idpConfigEncryption crypto.EncryptionAlgorithm
+ smtpEncryption crypto.EncryptionAlgorithm
+ smsEncryption crypto.EncryptionAlgorithm
+ userEncryption crypto.EncryptionAlgorithm
+ userPasswordHasher *crypto.PasswordHasher
+ codeAlg crypto.HashAlgorithm
+ machineKeySize int
+ applicationKeySize int
+ domainVerificationAlg crypto.EncryptionAlgorithm
+ domainVerificationGenerator crypto.Generator
+ domainVerificationValidator func(domain, token, verifier string, checkType api_http.CheckType) error
+ sessionTokenCreator func(sessionID string) (id string, token string, err error)
+ sessionTokenVerifier func(ctx context.Context, sessionToken, sessionID, tokenID string) (err error)
+ defaultAccessTokenLifetime time.Duration
+ defaultRefreshTokenLifetime time.Duration
+ defaultRefreshTokenIdleLifetime time.Duration
multifactors domain.MultifactorConfigs
webauthnConfig *webauthn_helper.Config
@@ -79,6 +86,9 @@ func StartCommands(
httpClient *http.Client,
permissionCheck domain.PermissionCheck,
sessionTokenVerifier func(ctx context.Context, sessionToken string, sessionID string, tokenID string) (err error),
+ defaultAccessTokenLifetime,
+ defaultRefreshTokenLifetime,
+ defaultRefreshTokenIdleLifetime time.Duration,
) (repo *Commands, err error) {
if externalDomain == "" {
return nil, errors.ThrowInvalidArgument(nil, "COMMAND-Df21s", "no external domain specified")
@@ -87,31 +97,34 @@ func StartCommands(
// reuse the oidcEncryption to be able to handle both tokens in the interceptor later on
sessionAlg := oidcEncryption
repo = &Commands{
- eventstore: es,
- static: staticStore,
- idGenerator: idGenerator,
- zitadelRoles: zitadelRoles,
- externalDomain: externalDomain,
- externalSecure: externalSecure,
- externalPort: externalPort,
- keySize: defaults.KeyConfig.Size,
- certKeySize: defaults.KeyConfig.CertificateSize,
- privateKeyLifetime: defaults.KeyConfig.PrivateKeyLifetime,
- publicKeyLifetime: defaults.KeyConfig.PublicKeyLifetime,
- certificateLifetime: defaults.KeyConfig.CertificateLifetime,
- idpConfigEncryption: idpConfigEncryption,
- smtpEncryption: smtpEncryption,
- smsEncryption: smsEncryption,
- userEncryption: userEncryption,
- domainVerificationAlg: domainVerificationEncryption,
- keyAlgorithm: oidcEncryption,
- certificateAlgorithm: samlEncryption,
- webauthnConfig: webAuthN,
- httpClient: httpClient,
- checkPermission: permissionCheck,
- newCode: newCryptoCodeWithExpiry,
- sessionTokenCreator: sessionTokenCreator(idGenerator, sessionAlg),
- sessionTokenVerifier: sessionTokenVerifier,
+ eventstore: es,
+ static: staticStore,
+ idGenerator: idGenerator,
+ zitadelRoles: zitadelRoles,
+ externalDomain: externalDomain,
+ externalSecure: externalSecure,
+ externalPort: externalPort,
+ keySize: defaults.KeyConfig.Size,
+ certKeySize: defaults.KeyConfig.CertificateSize,
+ privateKeyLifetime: defaults.KeyConfig.PrivateKeyLifetime,
+ publicKeyLifetime: defaults.KeyConfig.PublicKeyLifetime,
+ certificateLifetime: defaults.KeyConfig.CertificateLifetime,
+ idpConfigEncryption: idpConfigEncryption,
+ smtpEncryption: smtpEncryption,
+ smsEncryption: smsEncryption,
+ userEncryption: userEncryption,
+ domainVerificationAlg: domainVerificationEncryption,
+ keyAlgorithm: oidcEncryption,
+ certificateAlgorithm: samlEncryption,
+ webauthnConfig: webAuthN,
+ httpClient: httpClient,
+ checkPermission: permissionCheck,
+ newCode: newCryptoCode,
+ sessionTokenCreator: sessionTokenCreator(idGenerator, sessionAlg),
+ sessionTokenVerifier: sessionTokenVerifier,
+ defaultAccessTokenLifetime: defaultAccessTokenLifetime,
+ defaultRefreshTokenLifetime: defaultRefreshTokenLifetime,
+ defaultRefreshTokenIdleLifetime: defaultRefreshTokenIdleLifetime,
}
instance_repo.RegisterEventMappers(repo.eventstore)
@@ -124,8 +137,15 @@ func StartCommands(
quota.RegisterEventMappers(repo.eventstore)
session.RegisterEventMappers(repo.eventstore)
idpintent.RegisterEventMappers(repo.eventstore)
+ authrequest.RegisterEventMappers(repo.eventstore)
+ oidcsession.RegisterEventMappers(repo.eventstore)
+ milestone.RegisterEventMappers(repo.eventstore)
- repo.userPasswordAlg = crypto.NewBCrypt(defaults.SecretGenerators.PasswordSaltCost)
+ repo.codeAlg = crypto.NewBCrypt(defaults.SecretGenerators.PasswordSaltCost)
+ repo.userPasswordHasher, err = defaults.PasswordHasher.PasswordHasher()
+ if err != nil {
+ return nil, err
+ }
repo.machineKeySize = int(defaults.SecretGenerators.MachineKeySize)
repo.applicationKeySize = int(defaults.SecretGenerators.ApplicationKeySize)
diff --git a/internal/command/crypto.go b/internal/command/crypto.go
index e94f66463..8d145f2cc 100644
--- a/internal/command/crypto.go
+++ b/internal/command/crypto.go
@@ -10,15 +10,15 @@ import (
"github.com/zitadel/zitadel/internal/errors"
)
-type cryptoCodeFunc func(ctx context.Context, filter preparation.FilterToQueryReducer, typ domain.SecretGeneratorType, alg crypto.Crypto) (*CryptoCodeWithExpiry, error)
+type cryptoCodeFunc func(ctx context.Context, filter preparation.FilterToQueryReducer, typ domain.SecretGeneratorType, alg crypto.Crypto) (*CryptoCode, error)
-type CryptoCodeWithExpiry struct {
+type CryptoCode struct {
Crypted *crypto.CryptoValue
Plain string
Expiry time.Duration
}
-func newCryptoCodeWithExpiry(ctx context.Context, filter preparation.FilterToQueryReducer, typ domain.SecretGeneratorType, alg crypto.Crypto) (*CryptoCodeWithExpiry, error) {
+func newCryptoCode(ctx context.Context, filter preparation.FilterToQueryReducer, typ domain.SecretGeneratorType, alg crypto.Crypto) (*CryptoCode, error) {
gen, config, err := secretGenerator(ctx, filter, typ, alg)
if err != nil {
return nil, err
@@ -27,7 +27,7 @@ func newCryptoCodeWithExpiry(ctx context.Context, filter preparation.FilterToQue
if err != nil {
return nil, err
}
- return &CryptoCodeWithExpiry{
+ return &CryptoCode{
Crypted: crypted,
Plain: plain,
Expiry: config.Expiry,
@@ -42,14 +42,6 @@ func verifyCryptoCode(ctx context.Context, filter preparation.FilterToQueryReduc
return crypto.VerifyCode(creation, expiry, crypted, plain, gen)
}
-func newCryptoCodeWithPlain(ctx context.Context, filter preparation.FilterToQueryReducer, typ domain.SecretGeneratorType, alg crypto.Crypto) (value *crypto.CryptoValue, plain string, err error) {
- gen, _, err := secretGenerator(ctx, filter, typ, alg)
- if err != nil {
- return nil, "", err
- }
- return crypto.NewCode(gen)
-}
-
func secretGenerator(ctx context.Context, filter preparation.FilterToQueryReducer, typ domain.SecretGeneratorType, alg crypto.Crypto) (crypto.Generator, *crypto.GeneratorConfig, error) {
config, err := secretGeneratorConfig(ctx, filter, typ)
if err != nil {
diff --git a/internal/command/crypto_test.go b/internal/command/crypto_test.go
index a846ae43a..66c2c63c5 100644
--- a/internal/command/crypto_test.go
+++ b/internal/command/crypto_test.go
@@ -19,8 +19,8 @@ import (
)
func mockCode(code string, exp time.Duration) cryptoCodeFunc {
- return func(ctx context.Context, filter preparation.FilterToQueryReducer, _ domain.SecretGeneratorType, alg crypto.Crypto) (*CryptoCodeWithExpiry, error) {
- return &CryptoCodeWithExpiry{
+ return func(ctx context.Context, filter preparation.FilterToQueryReducer, _ domain.SecretGeneratorType, alg crypto.Crypto) (*CryptoCode, error) {
+ return &CryptoCode{
Crypted: &crypto.CryptoValue{
CryptoType: crypto.TypeEncryption,
Algorithm: "enc",
@@ -89,7 +89,7 @@ func Test_newCryptoCode(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
- got, err := newCryptoCodeWithExpiry(context.Background(), tt.eventstore.Filter, tt.args.typ, tt.args.alg)
+ got, err := newCryptoCode(context.Background(), tt.eventstore.Filter, tt.args.typ, tt.args.alg)
require.ErrorIs(t, err, tt.wantErr)
if tt.wantErr == nil {
require.NotNil(t, got)
@@ -105,7 +105,7 @@ func Test_verifyCryptoCode(t *testing.T) {
es := eventstoreExpect(t, expectFilter(
eventFromEventPusher(testSecretGeneratorAddedEvent(domain.SecretGeneratorTypeVerifyEmailCode)),
))
- code, err := newCryptoCodeWithExpiry(context.Background(), es.Filter, domain.SecretGeneratorTypeVerifyEmailCode, crypto.CreateMockHashAlg(gomock.NewController(t)))
+ code, err := newCryptoCode(context.Background(), es.Filter, domain.SecretGeneratorTypeVerifyEmailCode, crypto.CreateMockHashAlg(gomock.NewController(t)))
require.NoError(t, err)
type args struct {
diff --git a/internal/command/email.go b/internal/command/email.go
index f54b3de8e..519887af6 100644
--- a/internal/command/email.go
+++ b/internal/command/email.go
@@ -23,6 +23,6 @@ func (e *Email) Validate() error {
return e.Address.Validate()
}
-func (c *Commands) newEmailCode(ctx context.Context, filter preparation.FilterToQueryReducer, alg crypto.EncryptionAlgorithm) (*CryptoCodeWithExpiry, error) {
+func (c *Commands) newEmailCode(ctx context.Context, filter preparation.FilterToQueryReducer, alg crypto.EncryptionAlgorithm) (*CryptoCode, error) {
return c.newCode(ctx, filter, domain.SecretGeneratorTypeVerifyEmailCode, alg)
}
diff --git a/internal/command/instance.go b/internal/command/instance.go
index 415fae3a2..ac52c351d 100644
--- a/internal/command/instance.go
+++ b/internal/command/instance.go
@@ -70,6 +70,7 @@ type InstanceSetup struct {
AllowRegister bool
AllowExternalIDP bool
ForceMFA bool
+ ForceMFALocalOnly bool
HidePasswordReset bool
IgnoreUnknownUsername bool
AllowDomainDiscovery bool
@@ -226,6 +227,7 @@ func (c *Commands) SetUpInstance(ctx context.Context, setup *InstanceSetup) (str
setup.LoginPolicy.AllowRegister,
setup.LoginPolicy.AllowExternalIDP,
setup.LoginPolicy.ForceMFA,
+ setup.LoginPolicy.ForceMFALocalOnly,
setup.LoginPolicy.HidePasswordReset,
setup.LoginPolicy.IgnoreUnknownUsername,
setup.LoginPolicy.AllowDomainDiscovery,
@@ -335,7 +337,7 @@ func (c *Commands) SetUpInstance(ctx context.Context, setup *InstanceSetup) (str
} else if setup.Org.Human != nil {
setup.Org.Human.ID = userID
validations = append(validations,
- c.AddHumanCommand(setup.Org.Human, orgID, c.userPasswordAlg, c.userEncryption, true),
+ c.AddHumanCommand(setup.Org.Human, orgID, c.userPasswordHasher, c.userEncryption, true),
)
}
@@ -501,6 +503,9 @@ func (c *Commands) ChangeSystemConfig(ctx context.Context, externalDomain string
if err != nil {
return err
}
+ if len(cmds) == 0 {
+ continue
+ }
_, err = c.eventstore.Push(ctx, cmds...)
if err != nil {
return err
diff --git a/internal/command/instance_converter.go b/internal/command/instance_converter.go
index d4e334ef2..6d7f60575 100644
--- a/internal/command/instance_converter.go
+++ b/internal/command/instance_converter.go
@@ -34,6 +34,7 @@ func writeModelToLoginPolicy(wm *LoginPolicyWriteModel) *domain.LoginPolicy {
IgnoreUnknownUsernames: wm.IgnoreUnknownUsernames,
AllowDomainDiscovery: wm.AllowDomainDiscovery,
ForceMFA: wm.ForceMFA,
+ ForceMFALocalOnly: wm.ForceMFALocalOnly,
PasswordlessType: wm.PasswordlessType,
DefaultRedirectURI: wm.DefaultRedirectURI,
PasswordCheckLifetime: wm.PasswordCheckLifetime,
@@ -123,10 +124,10 @@ func writeModelToLockoutPolicy(wm *LockoutPolicyWriteModel) *domain.LockoutPolic
func writeModelToPrivacyPolicy(wm *PrivacyPolicyWriteModel) *domain.PrivacyPolicy {
return &domain.PrivacyPolicy{
- ObjectRoot: writeModelToObjectRoot(wm.WriteModel),
- TOSLink: wm.TOSLink,
- PrivacyLink: wm.PrivacyLink,
- HelpLink: wm.HelpLink,
+ ObjectRoot: writeModelToObjectRoot(wm.WriteModel),
+ TOSLink: wm.TOSLink,
+ PrivacyLink: wm.PrivacyLink,
+ HelpLink: wm.HelpLink,
SupportEmail: wm.SupportEmail,
}
}
diff --git a/internal/command/instance_domain.go b/internal/command/instance_domain.go
index 54e4121bc..cbf505bc7 100644
--- a/internal/command/instance_domain.go
+++ b/internal/command/instance_domain.go
@@ -107,6 +107,9 @@ func (c *Commands) addInstanceDomain(a *instance.Aggregate, instanceDomain strin
if err != nil {
return nil, err
}
+ if consoleChangeEvent == nil {
+ return events, nil
+ }
return append(events, consoleChangeEvent), nil
}, nil
}
@@ -122,6 +125,9 @@ func (c *Commands) prepareUpdateConsoleRedirectURIs(instanceDomain string) prepa
if err != nil {
return nil, err
}
+ if consoleChangeEvent == nil {
+ return nil, nil
+ }
return []eventstore.Command{
consoleChangeEvent,
}, nil
@@ -146,6 +152,9 @@ func (c *Commands) updateConsoleRedirectURIs(ctx context.Context, filter prepara
if !containsURI(appWriteModel.PostLogoutRedirectUris, postLogoutRedirectURI) {
changes = append(changes, project.ChangePostLogoutRedirectURIs(append(appWriteModel.PostLogoutRedirectUris, postLogoutRedirectURI)))
}
+ if len(changes) == 0 {
+ return nil, nil
+ }
return project.NewOIDCConfigChangedEvent(
ctx,
ProjectAggregateFromWriteModel(&appWriteModel.WriteModel),
@@ -154,17 +163,6 @@ func (c *Commands) updateConsoleRedirectURIs(ctx context.Context, filter prepara
)
}
-// checkUpdateConsoleRedirectURIs validates if the required console uri is present in the redirect_uris and post_logout_redirect_uris
-// it will return true only if present in both list, otherwise false
-func (c *Commands) checkUpdateConsoleRedirectURIs(instanceDomain string, redirectURIs, postLogoutRedirectURIs []string) bool {
- redirectURI := http.BuildHTTP(instanceDomain, c.externalPort, c.externalSecure) + consoleRedirectPath
- if !containsURI(redirectURIs, redirectURI) {
- return false
- }
- postLogoutRedirectURI := http.BuildHTTP(instanceDomain, c.externalPort, c.externalSecure) + consolePostLogoutPath
- return containsURI(postLogoutRedirectURIs, postLogoutRedirectURI)
-}
-
func setPrimaryInstanceDomain(a *instance.Aggregate, instanceDomain string) preparation.Validation {
return func() (preparation.CreateCommands, error) {
if instanceDomain = strings.TrimSpace(instanceDomain); instanceDomain == "" {
diff --git a/internal/command/instance_policy_login.go b/internal/command/instance_policy_login.go
index d2fa5cb2d..d31c95f4c 100644
--- a/internal/command/instance_policy_login.go
+++ b/internal/command/instance_policy_login.go
@@ -234,6 +234,7 @@ func prepareChangeDefaultLoginPolicy(a *instance.Aggregate, policy *ChangeLoginP
policy.AllowRegister,
policy.AllowExternalIDP,
policy.ForceMFA,
+ policy.ForceMFALocalOnly,
policy.HidePasswordReset,
policy.IgnoreUnknownUsernames,
policy.AllowDomainDiscovery,
@@ -260,6 +261,7 @@ func prepareAddDefaultLoginPolicy(
allowRegister bool,
allowExternalIDP bool,
forceMFA bool,
+ forceMFALocalOnly bool,
hidePasswordReset bool,
ignoreUnknownUsernames bool,
allowDomainDiscovery bool,
@@ -293,6 +295,7 @@ func prepareAddDefaultLoginPolicy(
allowRegister,
allowExternalIDP,
forceMFA,
+ forceMFALocalOnly,
hidePasswordReset,
ignoreUnknownUsernames,
allowDomainDiscovery,
diff --git a/internal/command/instance_policy_login_model.go b/internal/command/instance_policy_login_model.go
index 3f04616aa..8d854d2b0 100644
--- a/internal/command/instance_policy_login_model.go
+++ b/internal/command/instance_policy_login_model.go
@@ -65,6 +65,7 @@ func (wm *InstanceLoginPolicyWriteModel) NewChangedEvent(
allowRegister,
allowExternalIDP,
forceMFA,
+ forceMFALocalOnly,
hidePasswordReset,
ignoreUnknownUsernames,
allowDomainDiscovery,
@@ -92,6 +93,9 @@ func (wm *InstanceLoginPolicyWriteModel) NewChangedEvent(
if wm.ForceMFA != forceMFA {
changes = append(changes, policy.ChangeForceMFA(forceMFA))
}
+ if wm.ForceMFALocalOnly != forceMFALocalOnly {
+ changes = append(changes, policy.ChangeForceMFALocalOnly(forceMFALocalOnly))
+ }
if passwordlessType.Valid() && wm.PasswordlessType != passwordlessType {
changes = append(changes, policy.ChangePasswordlessType(passwordlessType))
}
diff --git a/internal/command/instance_policy_login_test.go b/internal/command/instance_policy_login_test.go
index 639fc4a65..881f1c964 100644
--- a/internal/command/instance_policy_login_test.go
+++ b/internal/command/instance_policy_login_test.go
@@ -73,6 +73,7 @@ func TestCommandSide_ChangeDefaultLoginPolicy(t *testing.T) {
true,
true,
true,
+ true,
domain.PasswordlessTypeAllowed,
"https://example.com/redirect",
time.Hour*1,
@@ -92,6 +93,7 @@ func TestCommandSide_ChangeDefaultLoginPolicy(t *testing.T) {
AllowUsernamePassword: true,
AllowExternalIDP: true,
ForceMFA: true,
+ ForceMFALocalOnly: true,
HidePasswordReset: true,
IgnoreUnknownUsernames: true,
AllowDomainDiscovery: true,
@@ -129,6 +131,7 @@ func TestCommandSide_ChangeDefaultLoginPolicy(t *testing.T) {
true,
true,
true,
+ true,
domain.PasswordlessTypeAllowed,
"https://example.com/redirect",
time.Hour*1,
@@ -153,6 +156,7 @@ func TestCommandSide_ChangeDefaultLoginPolicy(t *testing.T) {
false,
false,
false,
+ false,
domain.PasswordlessTypeNotAllowed,
"",
time.Hour*10,
@@ -172,6 +176,7 @@ func TestCommandSide_ChangeDefaultLoginPolicy(t *testing.T) {
AllowUsernamePassword: false,
AllowExternalIDP: false,
ForceMFA: false,
+ ForceMFALocalOnly: false,
HidePasswordReset: false,
IgnoreUnknownUsernames: false,
AllowDomainDiscovery: false,
@@ -281,6 +286,7 @@ func TestCommandSide_AddIDPProviderDefaultLoginPolicy(t *testing.T) {
true,
true,
true,
+ true,
domain.PasswordlessTypeAllowed,
"",
time.Hour*1,
@@ -322,6 +328,7 @@ func TestCommandSide_AddIDPProviderDefaultLoginPolicy(t *testing.T) {
true,
true,
true,
+ true,
domain.PasswordlessTypeAllowed,
"",
time.Hour*1,
@@ -383,6 +390,7 @@ func TestCommandSide_AddIDPProviderDefaultLoginPolicy(t *testing.T) {
true,
true,
true,
+ true,
domain.PasswordlessTypeAllowed,
"",
time.Hour*1,
@@ -526,6 +534,7 @@ func TestCommandSide_RemoveIDPProviderDefaultLoginPolicy(t *testing.T) {
true,
true,
true,
+ true,
domain.PasswordlessTypeAllowed,
"",
time.Hour*1,
@@ -567,6 +576,7 @@ func TestCommandSide_RemoveIDPProviderDefaultLoginPolicy(t *testing.T) {
true,
true,
true,
+ true,
domain.PasswordlessTypeAllowed,
"",
time.Hour*1,
@@ -621,6 +631,7 @@ func TestCommandSide_RemoveIDPProviderDefaultLoginPolicy(t *testing.T) {
true,
true,
true,
+ true,
domain.PasswordlessTypeAllowed,
"",
time.Hour*1,
@@ -680,6 +691,7 @@ func TestCommandSide_RemoveIDPProviderDefaultLoginPolicy(t *testing.T) {
true,
true,
true,
+ true,
domain.PasswordlessTypeAllowed,
"",
time.Hour*1,
@@ -747,6 +759,7 @@ func TestCommandSide_RemoveIDPProviderDefaultLoginPolicy(t *testing.T) {
true,
true,
true,
+ true,
domain.PasswordlessTypeAllowed,
"",
time.Hour*1,
@@ -1301,7 +1314,7 @@ func TestCommandSide_RemoveMultiFactorDefaultLoginPolicy(t *testing.T) {
}
}
-func newDefaultLoginPolicyChangedEvent(ctx context.Context, allowRegister, allowUsernamePassword, allowExternalIDP, forceMFA,
+func newDefaultLoginPolicyChangedEvent(ctx context.Context, allowRegister, allowUsernamePassword, allowExternalIDP, forceMFA, forceMFALocalOnly,
hidePasswordReset, ignoreUnknownUsernames, allowDomainDiscovery, disableLoginWithEmail, disableLoginWithPhone bool,
passwordlessType domain.PasswordlessType,
redirectURI string,
@@ -1312,6 +1325,7 @@ func newDefaultLoginPolicyChangedEvent(ctx context.Context, allowRegister, allow
policy.ChangeAllowRegister(allowRegister),
policy.ChangeAllowExternalIDP(allowExternalIDP),
policy.ChangeForceMFA(forceMFA),
+ policy.ChangeForceMFALocalOnly(forceMFALocalOnly),
policy.ChangeAllowUserNamePassword(allowUsernamePassword),
policy.ChangeHidePasswordReset(hidePasswordReset),
policy.ChangeIgnoreUnknownUsernames(ignoreUnknownUsernames),
diff --git a/internal/command/main_test.go b/internal/command/main_test.go
index 184b0a9b0..d1a2b3de5 100644
--- a/internal/command/main_test.go
+++ b/internal/command/main_test.go
@@ -3,10 +3,13 @@ package command
import (
"context"
"database/sql"
+ "strings"
"testing"
"time"
"github.com/golang/mock/gomock"
+ "github.com/zitadel/passwap"
+ "github.com/zitadel/passwap/verifier"
"golang.org/x/text/language"
"github.com/zitadel/zitadel/internal/crypto"
@@ -16,9 +19,11 @@ import (
"github.com/zitadel/zitadel/internal/eventstore/repository"
"github.com/zitadel/zitadel/internal/eventstore/repository/mock"
action_repo "github.com/zitadel/zitadel/internal/repository/action"
+ "github.com/zitadel/zitadel/internal/repository/authrequest"
"github.com/zitadel/zitadel/internal/repository/idpintent"
iam_repo "github.com/zitadel/zitadel/internal/repository/instance"
key_repo "github.com/zitadel/zitadel/internal/repository/keypair"
+ "github.com/zitadel/zitadel/internal/repository/oidcsession"
"github.com/zitadel/zitadel/internal/repository/org"
proj_repo "github.com/zitadel/zitadel/internal/repository/project"
"github.com/zitadel/zitadel/internal/repository/session"
@@ -43,6 +48,8 @@ func eventstoreExpect(t *testing.T, expects ...expect) *eventstore.Eventstore {
action_repo.RegisterEventMappers(es)
session.RegisterEventMappers(es)
idpintent.RegisterEventMappers(es)
+ authrequest.RegisterEventMappers(es)
+ oidcsession.RegisterEventMappers(es)
return es
}
@@ -289,3 +296,38 @@ func newMockPermissionCheckNotAllowed() domain.PermissionCheck {
return errors.ThrowPermissionDenied(nil, "AUTHZ-HKJD33", "Errors.PermissionDenied")
}
}
+
+type plainHasher struct {
+ x string // arbitrary info that triggers update when different from encoding
+}
+
+func (h plainHasher) Hash(password string) (string, error) {
+ return strings.Join([]string{"", "plain", h.x, password}, "$"), nil
+}
+
+func (h plainHasher) Verify(encoded, password string) (verifier.Result, error) {
+ nodes := strings.Split(encoded, "$")
+ if len(nodes) != 4 || nodes[1] != "plain" {
+ return verifier.Skip, nil
+ }
+ if nodes[3] != password {
+ return verifier.Fail, nil
+ }
+ if nodes[2] != h.x {
+ return verifier.NeedUpdate, nil
+ }
+ return verifier.OK, nil
+}
+
+// mockPasswordHasher creates a swapper for plain (cleartext) password used in tests.
+// x can be set to arbitrary info which triggers updates when different from the
+// setting in the encoded hashes. (normally cost parameters)
+//
+// With `x` set to "foo", the following encoded string would be produced by Hash:
+// $plain$foo$password
+func mockPasswordHasher(x string) *crypto.PasswordHasher {
+ return &crypto.PasswordHasher{
+ Swapper: passwap.NewSwapper(plainHasher{x: x}),
+ Prefixes: []string{"$plain$"},
+ }
+}
diff --git a/internal/command/milestone.go b/internal/command/milestone.go
new file mode 100644
index 000000000..f01ec6d15
--- /dev/null
+++ b/internal/command/milestone.go
@@ -0,0 +1,22 @@
+package command
+
+import (
+ "context"
+
+ "github.com/zitadel/zitadel/internal/repository/milestone"
+)
+
+// MilestonePushed writes a new milestone.PushedEvent with a new milestone.Aggregate to the eventstore
+func (c *Commands) MilestonePushed(
+ ctx context.Context,
+ msType milestone.Type,
+ endpoints []string,
+ primaryDomain string,
+) error {
+ id, err := c.idGenerator.Next()
+ if err != nil {
+ return err
+ }
+ _, err = c.eventstore.Push(ctx, milestone.NewPushedEvent(ctx, milestone.NewAggregate(ctx, id), msType, endpoints, primaryDomain, c.externalDomain))
+ return err
+}
diff --git a/internal/command/oidc_session.go b/internal/command/oidc_session.go
new file mode 100644
index 000000000..d990d0053
--- /dev/null
+++ b/internal/command/oidc_session.go
@@ -0,0 +1,370 @@
+package command
+
+import (
+ "context"
+ "encoding/base64"
+ "fmt"
+ "strings"
+ "time"
+
+ "github.com/zitadel/logging"
+
+ "github.com/zitadel/zitadel/internal/api/authz"
+ "github.com/zitadel/zitadel/internal/crypto"
+ "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/id"
+ "github.com/zitadel/zitadel/internal/repository/authrequest"
+ "github.com/zitadel/zitadel/internal/repository/oidcsession"
+ "github.com/zitadel/zitadel/internal/repository/user"
+)
+
+const (
+ TokenDelimiter = "-"
+ AccessTokenPrefix = "at_"
+ RefreshTokenPrefix = "rt_"
+ oidcTokenSubjectDelimiter = ":"
+ oidcTokenFormat = "%s" + oidcTokenSubjectDelimiter + "%s"
+)
+
+// AddOIDCSessionAccessToken creates a new OIDC Session, creates an access token and returns its id and expiration.
+// If the underlying [AuthRequest] is a OIDC Auth Code Flow, it will set the code as exchanged.
+func (c *Commands) AddOIDCSessionAccessToken(ctx context.Context, authRequestID string) (string, time.Time, error) {
+ cmd, err := c.newOIDCSessionAddEvents(ctx, authRequestID)
+ if err != nil {
+ return "", time.Time{}, err
+ }
+ cmd.AddSession(ctx)
+ if err = cmd.AddAccessToken(ctx, cmd.authRequestWriteModel.Scope); err != nil {
+ return "", time.Time{}, err
+ }
+ cmd.SetAuthRequestSuccessful(ctx)
+ accessTokenID, _, accessTokenExpiration, err := cmd.PushEvents(ctx)
+ return accessTokenID, accessTokenExpiration, err
+}
+
+// AddOIDCSessionRefreshAndAccessToken creates a new OIDC Session, creates an access token and refresh token.
+// It returns the access token id, expiration and the refresh token.
+// If the underlying [AuthRequest] is a OIDC Auth Code Flow, it will set the code as exchanged.
+func (c *Commands) AddOIDCSessionRefreshAndAccessToken(ctx context.Context, authRequestID string) (tokenID, refreshToken string, tokenExpiration time.Time, err error) {
+ cmd, err := c.newOIDCSessionAddEvents(ctx, authRequestID)
+ if err != nil {
+ return "", "", time.Time{}, err
+ }
+ cmd.AddSession(ctx)
+ if err = cmd.AddAccessToken(ctx, cmd.authRequestWriteModel.Scope); err != nil {
+ return "", "", time.Time{}, err
+ }
+ if err = cmd.AddRefreshToken(ctx); err != nil {
+ return "", "", time.Time{}, err
+ }
+ cmd.SetAuthRequestSuccessful(ctx)
+ return cmd.PushEvents(ctx)
+}
+
+// ExchangeOIDCSessionRefreshAndAccessToken updates an existing OIDC Session, creates a new access and refresh token.
+// It returns the access token id and expiration and the new refresh token.
+func (c *Commands) ExchangeOIDCSessionRefreshAndAccessToken(ctx context.Context, oidcSessionID, refreshToken string, scope []string) (tokenID, newRefreshToken string, tokenExpiration time.Time, err error) {
+ cmd, err := c.newOIDCSessionUpdateEvents(ctx, oidcSessionID, refreshToken)
+ if err != nil {
+ return "", "", time.Time{}, err
+ }
+ if err = cmd.AddAccessToken(ctx, scope); err != nil {
+ return "", "", time.Time{}, err
+ }
+ if err = cmd.RenewRefreshToken(ctx); err != nil {
+ return "", "", time.Time{}, err
+ }
+ return cmd.PushEvents(ctx)
+}
+
+// OIDCSessionByRefreshToken computes the current state of an existing OIDCSession by a refresh_token (to start a Refresh Token Grant).
+// If either the session is not active, the token is invalid or expired (incl. idle expiration) an invalid refresh token error will be returned.
+func (c *Commands) OIDCSessionByRefreshToken(ctx context.Context, refreshToken string) (*OIDCSessionWriteModel, error) {
+ oidcSessionID, refreshTokenID, err := parseRefreshToken(refreshToken)
+ if err != nil {
+ return nil, err
+ }
+ writeModel := NewOIDCSessionWriteModel(oidcSessionID, "")
+ err = c.eventstore.FilterToQueryReducer(ctx, writeModel)
+ if err != nil {
+ return nil, caos_errs.ThrowPreconditionFailed(err, "OIDCS-SAF31", "Errors.OIDCSession.RefreshTokenInvalid")
+ }
+ if err = writeModel.CheckRefreshToken(refreshTokenID); err != nil {
+ return nil, err
+ }
+ return writeModel, nil
+}
+
+func oidcSessionTokenIDsFromToken(token string) (oidcSessionID, refreshTokenID, accessTokenID string, err error) {
+ split := strings.Split(token, TokenDelimiter)
+ if len(split) != 2 {
+ return "", "", "", caos_errs.ThrowPreconditionFailed(nil, "OIDCS-S87kl", "Errors.OIDCSession.Token.Invalid")
+ }
+ if strings.HasPrefix(split[1], RefreshTokenPrefix) {
+ return split[0], split[1], "", nil
+ }
+ if strings.HasPrefix(split[1], AccessTokenPrefix) {
+ return split[0], "", split[1], nil
+ }
+ return "", "", "", caos_errs.ThrowPreconditionFailed(nil, "OIDCS-S87kl", "Errors.OIDCSession.Token.Invalid")
+}
+
+// RevokeOIDCSessionToken revokes an access_token or refresh_token
+// if the OIDCSession cannot be retrieved by the provided token, is not active or if the token is already revoked,
+// then no error will be returned.
+// The only possible error (except db connection or other internal errors) occurs if a client tries to revoke a token,
+// which was not part of the audience.
+func (c *Commands) RevokeOIDCSessionToken(ctx context.Context, token, clientID string) (err error) {
+ oidcSessionID, refreshTokenID, accessTokenID, err := oidcSessionTokenIDsFromToken(token)
+ if err != nil {
+ logging.WithError(err).Info("token revocation with invalid token format")
+ return nil
+ }
+ writeModel := NewOIDCSessionWriteModel(oidcSessionID, "")
+ err = c.eventstore.FilterToQueryReducer(ctx, writeModel)
+ if err != nil {
+ return caos_errs.ThrowInternal(err, "OIDCS-NB3t2", "Errors.Internal")
+ }
+ if err = writeModel.CheckClient(clientID); err != nil {
+ return err
+ }
+ if refreshTokenID != "" {
+ if err = writeModel.CheckRefreshToken(refreshTokenID); err != nil {
+ logging.WithFields("oidcSessionID", oidcSessionID, "refreshTokenID", refreshTokenID).WithError(err).
+ Info("refresh token revocation with invalid token")
+ return nil
+ }
+ return c.pushAppendAndReduce(ctx, writeModel, oidcsession.NewRefreshTokenRevokedEvent(ctx, writeModel.aggregate))
+ }
+ if err = writeModel.CheckAccessToken(accessTokenID); err != nil {
+ logging.WithFields("oidcSessionID", oidcSessionID, "accessTokenID", accessTokenID).WithError(err).
+ Info("access token revocation with invalid token")
+ return nil
+ }
+ return c.pushAppendAndReduce(ctx, writeModel, oidcsession.NewAccessTokenRevokedEvent(ctx, writeModel.aggregate))
+}
+
+func (c *Commands) newOIDCSessionAddEvents(ctx context.Context, authRequestID string) (*OIDCSessionEvents, error) {
+ authRequestWriteModel, err := c.getAuthRequestWriteModel(ctx, authRequestID)
+ if err != nil {
+ return nil, err
+ }
+ if err = authRequestWriteModel.CheckAuthenticated(); err != nil {
+ return nil, err
+ }
+ sessionWriteModel := NewSessionWriteModel(authRequestWriteModel.SessionID, authz.GetCtxData(ctx).OrgID)
+ err = c.eventstore.FilterToQueryReducer(ctx, sessionWriteModel)
+ if err != nil {
+ return nil, err
+ }
+ if sessionWriteModel.State != domain.SessionStateActive {
+ return nil, caos_errs.ThrowPreconditionFailed(nil, "OIDCS-sjkl3", "Errors.Session.Terminated")
+ }
+ resourceOwner, err := c.getResourceOwnerOfSessionUser(ctx, sessionWriteModel.UserID, sessionWriteModel.InstanceID)
+ if err != nil {
+ return nil, err
+ }
+ accessTokenLifetime, refreshTokenLifeTime, refreshTokenIdleLifetime, err := c.tokenTokenLifetimes(ctx)
+ if err != nil {
+ return nil, err
+ }
+ sessionID, err := c.idGenerator.Next()
+ if err != nil {
+ return nil, err
+ }
+ sessionID = IDPrefixV2 + sessionID
+ return &OIDCSessionEvents{
+ eventstore: c.eventstore,
+ idGenerator: c.idGenerator,
+ encryptionAlg: c.keyAlgorithm,
+ oidcSessionWriteModel: NewOIDCSessionWriteModel(sessionID, resourceOwner),
+ sessionWriteModel: sessionWriteModel,
+ authRequestWriteModel: authRequestWriteModel,
+ accessTokenLifetime: accessTokenLifetime,
+ refreshTokenLifeTime: refreshTokenLifeTime,
+ refreshTokenIdleLifetime: refreshTokenIdleLifetime,
+ }, nil
+}
+
+func (c *Commands) getResourceOwnerOfSessionUser(ctx context.Context, userID, instanceID string) (string, error) {
+ events, err := c.eventstore.Filter(ctx, eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent).
+ InstanceID(instanceID).
+ AllowTimeTravel().
+ OrderAsc().
+ Limit(1).
+ AddQuery().
+ AggregateTypes(user.AggregateType).
+ AggregateIDs(userID).
+ Builder())
+ if err != nil || len(events) != 1 {
+ return "", caos_errs.ThrowInternal(err, "OIDCS-sferh", "Errors.Internal")
+ }
+ return events[0].Aggregate().ResourceOwner, nil
+}
+
+func (c *Commands) decryptRefreshToken(refreshToken string) (refreshTokenID string, err error) {
+ decoded, err := base64.RawURLEncoding.DecodeString(refreshToken)
+ if err != nil {
+ return "", err
+ }
+ decrypted, err := c.keyAlgorithm.DecryptString(decoded, c.keyAlgorithm.EncryptionKeyID())
+ if err != nil {
+ return "", err
+ }
+ _, refreshTokenID, err = parseRefreshToken(decrypted)
+ return refreshTokenID, err
+}
+
+func parseRefreshToken(refreshToken string) (oidcSessionID, refreshTokenID string, err error) {
+ split := strings.Split(refreshToken, TokenDelimiter)
+ if len(split) < 2 || !strings.HasPrefix(split[1], RefreshTokenPrefix) {
+ return "", "", caos_errs.ThrowPreconditionFailed(nil, "OIDCS-JOI23", "Errors.OIDCSession.RefreshTokenInvalid")
+ }
+ // the oidc library requires that every token has the format of :
+ // the V2 tokens don't use the userID anymore, so let's just remove it
+ return split[0], strings.Split(split[1], oidcTokenSubjectDelimiter)[0], nil
+}
+
+func (c *Commands) newOIDCSessionUpdateEvents(ctx context.Context, oidcSessionID, refreshToken string) (*OIDCSessionEvents, error) {
+ refreshTokenID, err := c.decryptRefreshToken(refreshToken)
+ if err != nil {
+ return nil, err
+ }
+ sessionWriteModel := NewOIDCSessionWriteModel(oidcSessionID, "")
+ if err = c.eventstore.FilterToQueryReducer(ctx, sessionWriteModel); err != nil {
+ return nil, err
+ }
+ if err = sessionWriteModel.CheckRefreshToken(refreshTokenID); err != nil {
+ return nil, err
+ }
+ accessTokenLifetime, refreshTokenLifeTime, refreshTokenIdleLifetime, err := c.tokenTokenLifetimes(ctx)
+ if err != nil {
+ return nil, err
+ }
+ return &OIDCSessionEvents{
+ eventstore: c.eventstore,
+ idGenerator: c.idGenerator,
+ encryptionAlg: c.keyAlgorithm,
+ oidcSessionWriteModel: sessionWriteModel,
+ accessTokenLifetime: accessTokenLifetime,
+ refreshTokenLifeTime: refreshTokenLifeTime,
+ refreshTokenIdleLifetime: refreshTokenIdleLifetime,
+ }, nil
+}
+
+type OIDCSessionEvents struct {
+ eventstore *eventstore.Eventstore
+ idGenerator id.Generator
+ encryptionAlg crypto.EncryptionAlgorithm
+ events []eventstore.Command
+ oidcSessionWriteModel *OIDCSessionWriteModel
+ sessionWriteModel *SessionWriteModel
+ authRequestWriteModel *AuthRequestWriteModel
+ accessTokenLifetime time.Duration
+ refreshTokenLifeTime time.Duration
+ refreshTokenIdleLifetime time.Duration
+
+ // accessTokenID is set by the command
+ accessTokenID string
+
+ // refreshToken is set by the command
+ refreshToken string
+}
+
+func (c *OIDCSessionEvents) AddSession(ctx context.Context) {
+ c.events = append(c.events, oidcsession.NewAddedEvent(
+ ctx,
+ c.oidcSessionWriteModel.aggregate,
+ c.sessionWriteModel.UserID,
+ c.sessionWriteModel.AggregateID,
+ c.authRequestWriteModel.ClientID,
+ c.authRequestWriteModel.Audience,
+ c.authRequestWriteModel.Scope,
+ c.sessionWriteModel.AuthMethodTypes(),
+ c.sessionWriteModel.AuthenticationTime(),
+ ))
+}
+
+func (c *OIDCSessionEvents) SetAuthRequestSuccessful(ctx context.Context) {
+ c.events = append(c.events, authrequest.NewSucceededEvent(ctx, c.authRequestWriteModel.aggregate))
+}
+
+func (c *OIDCSessionEvents) AddAccessToken(ctx context.Context, scope []string) error {
+ accessTokenID, err := c.idGenerator.Next()
+ if err != nil {
+ return err
+ }
+ c.accessTokenID = AccessTokenPrefix + accessTokenID
+ c.events = append(c.events, oidcsession.NewAccessTokenAddedEvent(ctx, c.oidcSessionWriteModel.aggregate, c.accessTokenID, scope, c.accessTokenLifetime))
+ return nil
+}
+
+func (c *OIDCSessionEvents) AddRefreshToken(ctx context.Context) (err error) {
+ var refreshTokenID string
+ refreshTokenID, c.refreshToken, err = c.generateRefreshToken(c.sessionWriteModel.UserID)
+ if err != nil {
+ return err
+ }
+ c.events = append(c.events, oidcsession.NewRefreshTokenAddedEvent(ctx, c.oidcSessionWriteModel.aggregate, refreshTokenID, c.refreshTokenLifeTime, c.refreshTokenIdleLifetime))
+ return nil
+}
+
+func (c *OIDCSessionEvents) RenewRefreshToken(ctx context.Context) (err error) {
+ var refreshTokenID string
+ refreshTokenID, c.refreshToken, err = c.generateRefreshToken(c.oidcSessionWriteModel.UserID)
+ if err != nil {
+ return err
+ }
+ c.events = append(c.events, oidcsession.NewRefreshTokenRenewedEvent(ctx, c.oidcSessionWriteModel.aggregate, refreshTokenID, c.refreshTokenIdleLifetime))
+ return nil
+}
+
+func (c *OIDCSessionEvents) generateRefreshToken(userID string) (refreshTokenID, refreshToken string, err error) {
+ refreshTokenID, err = c.idGenerator.Next()
+ if err != nil {
+ return "", "", err
+ }
+ refreshTokenID = RefreshTokenPrefix + refreshTokenID
+ token, err := c.encryptionAlg.Encrypt([]byte(fmt.Sprintf(oidcTokenFormat, c.oidcSessionWriteModel.OIDCRefreshTokenID(refreshTokenID), userID)))
+ if err != nil {
+ return "", "", err
+ }
+ return refreshTokenID, base64.RawURLEncoding.EncodeToString(token), nil
+}
+
+func (c *OIDCSessionEvents) PushEvents(ctx context.Context) (accessTokenID string, refreshToken string, accessTokenExpiration time.Time, err error) {
+ pushedEvents, err := c.eventstore.Push(ctx, c.events...)
+ if err != nil {
+ return "", "", time.Time{}, err
+ }
+ err = AppendAndReduce(c.oidcSessionWriteModel, pushedEvents...)
+ if err != nil {
+ return "", "", time.Time{}, err
+ }
+ // prefix the returned id with the oidcSessionID so that we can retrieve it later on
+ // we need to use `-` as a delimiter because the OIDC library uses `:` and will check for a length of 2 parts
+ return c.oidcSessionWriteModel.AggregateID + TokenDelimiter + c.accessTokenID, c.refreshToken, c.oidcSessionWriteModel.AccessTokenExpiration, nil
+}
+
+func (c *Commands) tokenTokenLifetimes(ctx context.Context) (accessTokenLifetime time.Duration, refreshTokenLifetime time.Duration, refreshTokenIdleLifetime time.Duration, err error) {
+ oidcSettings := NewInstanceOIDCSettingsWriteModel(ctx)
+ err = c.eventstore.FilterToQueryReducer(ctx, oidcSettings)
+ if err != nil {
+ return 0, 0, 0, err
+ }
+ accessTokenLifetime = c.defaultAccessTokenLifetime
+ refreshTokenLifetime = c.defaultRefreshTokenLifetime
+ refreshTokenIdleLifetime = c.defaultRefreshTokenIdleLifetime
+ if oidcSettings.AccessTokenLifetime > 0 {
+ accessTokenLifetime = oidcSettings.AccessTokenLifetime
+ }
+ if oidcSettings.RefreshTokenExpiration > 0 {
+ refreshTokenLifetime = oidcSettings.RefreshTokenExpiration
+ }
+ if oidcSettings.RefreshTokenIdleExpiration > 0 {
+ refreshTokenIdleLifetime = oidcSettings.RefreshTokenIdleExpiration
+ }
+ return accessTokenLifetime, refreshTokenLifetime, refreshTokenIdleLifetime, nil
+}
diff --git a/internal/command/oidc_session_model.go b/internal/command/oidc_session_model.go
new file mode 100644
index 000000000..94725d933
--- /dev/null
+++ b/internal/command/oidc_session_model.go
@@ -0,0 +1,167 @@
+package command
+
+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/oidcsession"
+)
+
+type OIDCSessionWriteModel struct {
+ eventstore.WriteModel
+
+ UserID string
+ SessionID string
+ ClientID string
+ Audience []string
+ Scope []string
+ AuthMethods []domain.UserAuthMethodType
+ AuthTime time.Time
+ State domain.OIDCSessionState
+ AccessTokenID string
+ AccessTokenCreation time.Time
+ AccessTokenExpiration time.Time
+ RefreshTokenID string
+ RefreshToken string
+ RefreshTokenExpiration time.Time
+ RefreshTokenIdleExpiration time.Time
+
+ aggregate *eventstore.Aggregate
+}
+
+func NewOIDCSessionWriteModel(id string, resourceOwner string) *OIDCSessionWriteModel {
+ return &OIDCSessionWriteModel{
+ WriteModel: eventstore.WriteModel{
+ AggregateID: id,
+ ResourceOwner: resourceOwner,
+ },
+ aggregate: &oidcsession.NewAggregate(id, resourceOwner).Aggregate,
+ }
+}
+
+func (wm *OIDCSessionWriteModel) Reduce() error {
+ for _, event := range wm.Events {
+ switch e := event.(type) {
+ case *oidcsession.AddedEvent:
+ wm.reduceAdded(e)
+ case *oidcsession.AccessTokenAddedEvent:
+ wm.reduceAccessTokenAdded(e)
+ case *oidcsession.AccessTokenRevokedEvent:
+ wm.reduceAccessTokenRevoked(e)
+ case *oidcsession.RefreshTokenAddedEvent:
+ wm.reduceRefreshTokenAdded(e)
+ case *oidcsession.RefreshTokenRenewedEvent:
+ wm.reduceRefreshTokenRenewed(e)
+ case *oidcsession.RefreshTokenRevokedEvent:
+ wm.reduceRefreshTokenRevoked(e)
+ }
+ }
+ return wm.WriteModel.Reduce()
+}
+
+func (wm *OIDCSessionWriteModel) Query() *eventstore.SearchQueryBuilder {
+ query := eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent).
+ AddQuery().
+ AggregateTypes(oidcsession.AggregateType).
+ AggregateIDs(wm.AggregateID).
+ EventTypes(
+ oidcsession.AddedType,
+ oidcsession.AccessTokenAddedType,
+ oidcsession.RefreshTokenAddedType,
+ oidcsession.RefreshTokenRenewedType,
+ oidcsession.RefreshTokenRevokedType,
+ ).
+ Builder()
+
+ if wm.ResourceOwner != "" {
+ query.ResourceOwner(wm.ResourceOwner)
+ }
+ return query
+}
+
+func (wm *OIDCSessionWriteModel) reduceAdded(e *oidcsession.AddedEvent) {
+ wm.UserID = e.UserID
+ wm.SessionID = e.SessionID
+ wm.ClientID = e.ClientID
+ wm.Audience = e.Audience
+ wm.Scope = e.Scope
+ wm.AuthMethods = e.AuthMethods
+ wm.AuthTime = e.AuthTime
+ wm.State = domain.OIDCSessionStateActive
+ // the write model might be initialized without resource owner,
+ // so update the aggregate
+ if wm.ResourceOwner == "" {
+ wm.aggregate = &oidcsession.NewAggregate(wm.AggregateID, e.Aggregate().ResourceOwner).Aggregate
+ }
+}
+
+func (wm *OIDCSessionWriteModel) reduceAccessTokenAdded(e *oidcsession.AccessTokenAddedEvent) {
+ wm.AccessTokenID = e.ID
+ wm.AccessTokenExpiration = e.CreationDate().Add(e.Lifetime)
+}
+
+func (wm *OIDCSessionWriteModel) reduceAccessTokenRevoked(e *oidcsession.AccessTokenRevokedEvent) {
+ wm.AccessTokenID = ""
+ wm.AccessTokenExpiration = e.CreationDate()
+}
+
+func (wm *OIDCSessionWriteModel) reduceRefreshTokenAdded(e *oidcsession.RefreshTokenAddedEvent) {
+ wm.RefreshTokenID = e.ID
+ wm.RefreshTokenExpiration = e.CreationDate().Add(e.Lifetime)
+ wm.RefreshTokenIdleExpiration = e.CreationDate().Add(e.IdleLifetime)
+}
+
+func (wm *OIDCSessionWriteModel) reduceRefreshTokenRenewed(e *oidcsession.RefreshTokenRenewedEvent) {
+ wm.RefreshTokenID = e.ID
+ wm.RefreshTokenIdleExpiration = e.CreationDate().Add(e.IdleLifetime)
+}
+
+func (wm *OIDCSessionWriteModel) reduceRefreshTokenRevoked(e *oidcsession.RefreshTokenRevokedEvent) {
+ wm.RefreshTokenID = ""
+ wm.RefreshTokenExpiration = e.CreationDate()
+ wm.RefreshTokenIdleExpiration = e.CreationDate()
+ wm.AccessTokenID = ""
+ wm.AccessTokenExpiration = e.CreationDate()
+}
+
+func (wm *OIDCSessionWriteModel) CheckRefreshToken(refreshTokenID string) error {
+ if wm.State != domain.OIDCSessionStateActive {
+ return caos_errs.ThrowPreconditionFailed(nil, "OIDCS-s3hjk", "Errors.OIDCSession.RefreshTokenInvalid")
+ }
+ if wm.RefreshTokenID != refreshTokenID {
+ return caos_errs.ThrowPreconditionFailed(nil, "OIDCS-28ubl", "Errors.OIDCSession.RefreshTokenInvalid")
+ }
+ now := time.Now()
+ if wm.RefreshTokenExpiration.Before(now) || wm.RefreshTokenIdleExpiration.Before(now) {
+ return caos_errs.ThrowPreconditionFailed(nil, "OIDCS-3jt2w", "Errors.OIDCSession.RefreshTokenInvalid")
+ }
+ return nil
+}
+
+func (wm *OIDCSessionWriteModel) CheckAccessToken(accessTokenID string) error {
+ if wm.State != domain.OIDCSessionStateActive {
+ return caos_errs.ThrowPreconditionFailed(nil, "OIDCS-KL2pk", "Errors.OIDCSession.Token.Invalid")
+ }
+ if wm.AccessTokenID != accessTokenID {
+ return caos_errs.ThrowPreconditionFailed(nil, "OIDCS-JLKW2", "Errors.OIDCSession.Token.Invalid")
+ }
+ if wm.AccessTokenExpiration.Before(time.Now()) {
+ return caos_errs.ThrowPreconditionFailed(nil, "OIDCS-3j3md", "Errors.OIDCSession.Token.Invalid")
+ }
+ return nil
+}
+
+func (wm *OIDCSessionWriteModel) CheckClient(clientID string) error {
+ for _, aud := range wm.Audience {
+ if aud == clientID {
+ return nil
+ }
+ }
+ return caos_errs.ThrowPreconditionFailed(nil, "OIDCS-SKjl3", "Errors.OIDCSession.InvalidClient")
+}
+
+func (wm *OIDCSessionWriteModel) OIDCRefreshTokenID(refreshTokenID string) string {
+ return wm.AggregateID + TokenDelimiter + refreshTokenID
+}
diff --git a/internal/command/oidc_session_test.go b/internal/command/oidc_session_test.go
new file mode 100644
index 000000000..b8a3a462e
--- /dev/null
+++ b/internal/command/oidc_session_test.go
@@ -0,0 +1,1014 @@
+package command
+
+import (
+ "context"
+ "testing"
+ "time"
+
+ "github.com/golang/mock/gomock"
+ "github.com/muhlemmer/gu"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+ "golang.org/x/text/language"
+
+ "github.com/zitadel/zitadel/internal/api/authz"
+ "github.com/zitadel/zitadel/internal/crypto"
+ "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/repository"
+ "github.com/zitadel/zitadel/internal/id"
+ "github.com/zitadel/zitadel/internal/id/mock"
+ "github.com/zitadel/zitadel/internal/repository/authrequest"
+ "github.com/zitadel/zitadel/internal/repository/oidcsession"
+ "github.com/zitadel/zitadel/internal/repository/session"
+ "github.com/zitadel/zitadel/internal/repository/user"
+)
+
+var (
+ testNow = time.Now()
+ tokenCreationNow = time.Time{}
+)
+
+func TestCommands_AddOIDCSessionAccessToken(t *testing.T) {
+ type fields struct {
+ eventstore *eventstore.Eventstore
+ idGenerator id.Generator
+ defaultAccessTokenLifetime time.Duration
+ defaultRefreshTokenLifetime time.Duration
+ defaultRefreshTokenIdleLifetime time.Duration
+ keyAlgorithm crypto.EncryptionAlgorithm
+ }
+ type args struct {
+ ctx context.Context
+ authRequestID string
+ }
+ type res struct {
+ id string
+ expiration time.Time
+ err error
+ }
+ tests := []struct {
+ name string
+ fields fields
+ args args
+ res res
+ }{
+ {
+ "unauthenticated error",
+ fields{
+ eventstore: eventstoreExpect(t,
+ expectFilter(),
+ ),
+ },
+ args{
+ ctx: authz.WithInstanceID(context.Background(), "instanceID"),
+ authRequestID: "V2_authRequestID",
+ },
+ res{
+ err: caos_errs.ThrowPreconditionFailed(nil, "AUTHR-SF2r2", "Errors.AuthRequest.NotAuthenticated"),
+ },
+ },
+ {
+ "inactive session error",
+ fields{
+ eventstore: eventstoreExpect(t,
+ expectFilter(
+ eventFromEventPusher(
+ authrequest.NewAddedEvent(context.Background(), &authrequest.NewAggregate("V2_authRequestID", "instanceID").Aggregate,
+ "loginClient",
+ "clientID",
+ "redirectURI",
+ "state",
+ "nonce",
+ []string{"openid"},
+ []string{"audience"},
+ domain.OIDCResponseTypeCode,
+ &domain.OIDCCodeChallenge{
+ Challenge: "challenge",
+ Method: domain.CodeChallengeMethodS256,
+ },
+ []domain.Prompt{domain.PromptNone},
+ []string{"en", "de"},
+ gu.Ptr(time.Duration(0)),
+ gu.Ptr("loginHint"),
+ gu.Ptr("hintUserID"),
+ ),
+ ),
+ eventFromEventPusher(
+ authrequest.NewSessionLinkedEvent(context.Background(), &authrequest.NewAggregate("V2_authRequestID", "instanceID").Aggregate,
+ "sessionID",
+ "userID",
+ testNow,
+ []domain.UserAuthMethodType{domain.UserAuthMethodTypePassword},
+ ),
+ ),
+ eventFromEventPusher(
+ authrequest.NewCodeAddedEvent(context.Background(), &authrequest.NewAggregate("V2_authRequestID", "instanceID").Aggregate),
+ ),
+ eventFromEventPusher(
+ authrequest.NewCodeExchangedEvent(context.Background(), &authrequest.NewAggregate("V2_authRequestID", "instanceID").Aggregate),
+ ),
+ ),
+ expectFilter(), // inactive session
+ ),
+ },
+ args{
+ ctx: authz.WithInstanceID(context.Background(), "instanceID"),
+ authRequestID: "V2_authRequestID",
+ },
+ res{
+ err: caos_errs.ThrowPreconditionFailed(nil, "OIDCS-sjkl3", "Errors.Session.Terminated"),
+ },
+ },
+ {
+ "add successful",
+ fields{
+ eventstore: eventstoreExpect(t,
+ expectFilter(
+ eventFromEventPusher(
+ authrequest.NewAddedEvent(context.Background(), &authrequest.NewAggregate("V2_authRequestID", "instanceID").Aggregate,
+ "loginClient",
+ "clientID",
+ "redirectURI",
+ "state",
+ "nonce",
+ []string{"openid"},
+ []string{"audience"},
+ domain.OIDCResponseTypeCode,
+ &domain.OIDCCodeChallenge{
+ Challenge: "challenge",
+ Method: domain.CodeChallengeMethodS256,
+ },
+ []domain.Prompt{domain.PromptNone},
+ []string{"en", "de"},
+ gu.Ptr(time.Duration(0)),
+ gu.Ptr("loginHint"),
+ gu.Ptr("hintUserID"),
+ ),
+ ),
+ eventFromEventPusher(
+ authrequest.NewSessionLinkedEvent(context.Background(), &authrequest.NewAggregate("V2_authRequestID", "instanceID").Aggregate,
+ "sessionID",
+ "userID",
+ testNow,
+ []domain.UserAuthMethodType{domain.UserAuthMethodTypePassword},
+ ),
+ ),
+ eventFromEventPusher(
+ authrequest.NewCodeAddedEvent(context.Background(), &authrequest.NewAggregate("V2_authRequestID", "instanceID").Aggregate),
+ ),
+ eventFromEventPusher(
+ authrequest.NewCodeExchangedEvent(context.Background(), &authrequest.NewAggregate("V2_authRequestID", "instanceID").Aggregate),
+ ),
+ ),
+ expectFilter(
+ eventFromEventPusher(
+ session.NewAddedEvent(context.Background(), &session.NewAggregate("sessionID", "instanceID").Aggregate, "domain.tld"),
+ ),
+ eventFromEventPusher(
+ session.NewUserCheckedEvent(context.Background(), &session.NewAggregate("sessionID", "instanceID").Aggregate,
+ "userID", testNow),
+ ),
+ eventFromEventPusher(
+ session.NewPasswordCheckedEvent(context.Background(), &session.NewAggregate("sessionID", "instanceID").Aggregate,
+ testNow),
+ ),
+ ),
+ expectFilter(
+ eventFromEventPusher(
+ user.NewHumanAddedEvent(context.Background(), &user.NewAggregate("userID", "org1").Aggregate,
+ "username", "firstName", "lastName", "", "", language.English, domain.GenderUnspecified, "", false,
+ ),
+ ),
+ ),
+ expectFilter(), // token lifetime
+ expectPush(
+ []*repository.Event{
+ eventFromEventPusherWithInstanceID("instanceID",
+ oidcsession.NewAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "org1").Aggregate,
+ "userID", "sessionID", "clientID", []string{"audience"}, []string{"openid"}, []domain.UserAuthMethodType{domain.UserAuthMethodTypePassword}, testNow),
+ ),
+ eventFromEventPusherWithInstanceID("instanceID",
+ oidcsession.NewAccessTokenAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "org1").Aggregate,
+ "at_accessTokenID", []string{"openid"}, time.Hour),
+ ),
+ eventFromEventPusherWithInstanceID("instanceID",
+ authrequest.NewSucceededEvent(context.Background(), &authrequest.NewAggregate("V2_authRequestID", "instanceID").Aggregate),
+ ),
+ },
+ ),
+ ),
+ idGenerator: mock.NewIDGeneratorExpectIDs(t, "oidcSessionID", "accessTokenID"),
+ defaultAccessTokenLifetime: time.Hour,
+ },
+ args{
+ ctx: authz.WithInstanceID(context.Background(), "instanceID"),
+ authRequestID: "V2_authRequestID",
+ },
+ res{
+ id: "V2_oidcSessionID-at_accessTokenID",
+ expiration: tokenCreationNow.Add(time.Hour),
+ },
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ c := &Commands{
+ eventstore: tt.fields.eventstore,
+ idGenerator: tt.fields.idGenerator,
+ defaultAccessTokenLifetime: tt.fields.defaultAccessTokenLifetime,
+ defaultRefreshTokenLifetime: tt.fields.defaultRefreshTokenLifetime,
+ defaultRefreshTokenIdleLifetime: tt.fields.defaultRefreshTokenIdleLifetime,
+ keyAlgorithm: tt.fields.keyAlgorithm,
+ }
+ gotID, gotExpiration, err := c.AddOIDCSessionAccessToken(tt.args.ctx, tt.args.authRequestID)
+ assert.Equal(t, tt.res.id, gotID)
+ assert.Equal(t, tt.res.expiration, gotExpiration)
+ assert.ErrorIs(t, err, tt.res.err)
+ })
+ }
+}
+
+func TestCommands_AddOIDCSessionRefreshAndAccessToken(t *testing.T) {
+ type fields struct {
+ eventstore *eventstore.Eventstore
+ idGenerator id.Generator
+ defaultAccessTokenLifetime time.Duration
+ defaultRefreshTokenLifetime time.Duration
+ defaultRefreshTokenIdleLifetime time.Duration
+ keyAlgorithm crypto.EncryptionAlgorithm
+ }
+ type args struct {
+ ctx context.Context
+ authRequestID string
+ }
+ type res struct {
+ id string
+ refreshToken string
+ expiration time.Time
+ err error
+ }
+ tests := []struct {
+ name string
+ fields fields
+ args args
+ res res
+ }{
+ {
+ "unauthenticated error",
+ fields{
+ eventstore: eventstoreExpect(t,
+ expectFilter(),
+ ),
+ },
+ args{
+ ctx: authz.WithInstanceID(context.Background(), "instanceID"),
+ authRequestID: "V2_authRequestID",
+ },
+ res{
+ err: caos_errs.ThrowPreconditionFailed(nil, "AUTHR-SF2r2", "Errors.AuthRequest.NotAuthenticated"),
+ },
+ },
+ {
+ "inactive session error",
+ fields{
+ eventstore: eventstoreExpect(t,
+ expectFilter(
+ eventFromEventPusher(
+ authrequest.NewAddedEvent(context.Background(), &authrequest.NewAggregate("V2_authRequestID", "instanceID").Aggregate,
+ "loginClient",
+ "clientID",
+ "redirectURI",
+ "state",
+ "nonce",
+ []string{"openid", "offline_access"},
+ []string{"audience"},
+ domain.OIDCResponseTypeCode,
+ &domain.OIDCCodeChallenge{
+ Challenge: "challenge",
+ Method: domain.CodeChallengeMethodS256,
+ },
+ []domain.Prompt{domain.PromptNone},
+ []string{"en", "de"},
+ gu.Ptr(time.Duration(0)),
+ gu.Ptr("loginHint"),
+ gu.Ptr("hintUserID"),
+ ),
+ ),
+ eventFromEventPusher(
+ authrequest.NewSessionLinkedEvent(context.Background(), &authrequest.NewAggregate("V2_authRequestID", "instanceID").Aggregate,
+ "sessionID",
+ "userID",
+ testNow,
+ []domain.UserAuthMethodType{domain.UserAuthMethodTypePassword},
+ ),
+ ),
+ eventFromEventPusher(
+ authrequest.NewCodeAddedEvent(context.Background(), &authrequest.NewAggregate("V2_authRequestID", "instanceID").Aggregate),
+ ),
+ eventFromEventPusher(
+ authrequest.NewCodeExchangedEvent(context.Background(), &authrequest.NewAggregate("V2_authRequestID", "instanceID").Aggregate),
+ ),
+ ),
+ expectFilter(), // inactive session
+ ),
+ },
+ args{
+ ctx: authz.WithInstanceID(context.Background(), "instanceID"),
+ authRequestID: "V2_authRequestID",
+ },
+ res{
+ err: caos_errs.ThrowPreconditionFailed(nil, "OIDCS-sjkl3", "Errors.Session.Terminated"),
+ },
+ },
+ {
+ "add successful",
+ fields{
+ eventstore: eventstoreExpect(t,
+ expectFilter(
+ eventFromEventPusher(
+ authrequest.NewAddedEvent(context.Background(), &authrequest.NewAggregate("V2_authRequestID", "instanceID").Aggregate,
+ "loginClient",
+ "clientID",
+ "redirectURI",
+ "state",
+ "nonce",
+ []string{"openid", "offline_access"},
+ []string{"audience"},
+ domain.OIDCResponseTypeCode,
+ &domain.OIDCCodeChallenge{
+ Challenge: "challenge",
+ Method: domain.CodeChallengeMethodS256,
+ },
+ []domain.Prompt{domain.PromptNone},
+ []string{"en", "de"},
+ gu.Ptr(time.Duration(0)),
+ gu.Ptr("loginHint"),
+ gu.Ptr("hintUserID"),
+ ),
+ ),
+ eventFromEventPusher(
+ authrequest.NewSessionLinkedEvent(context.Background(), &authrequest.NewAggregate("V2_authRequestID", "instanceID").Aggregate,
+ "sessionID",
+ "userID",
+ testNow,
+ []domain.UserAuthMethodType{domain.UserAuthMethodTypePassword},
+ ),
+ ),
+ eventFromEventPusher(
+ authrequest.NewCodeAddedEvent(context.Background(), &authrequest.NewAggregate("V2_authRequestID", "instanceID").Aggregate),
+ ),
+ eventFromEventPusher(
+ authrequest.NewCodeExchangedEvent(context.Background(), &authrequest.NewAggregate("V2_authRequestID", "instanceID").Aggregate),
+ ),
+ ),
+ expectFilter(
+ eventFromEventPusher(
+ session.NewAddedEvent(context.Background(), &session.NewAggregate("sessionID", "instanceID").Aggregate, "domain.tld"),
+ ),
+ eventFromEventPusher(
+ session.NewUserCheckedEvent(context.Background(), &session.NewAggregate("sessionID", "instanceID").Aggregate,
+ "userID", testNow),
+ ),
+ eventFromEventPusher(
+ session.NewPasswordCheckedEvent(context.Background(), &session.NewAggregate("sessionID", "instanceID").Aggregate,
+ testNow),
+ ),
+ ),
+ expectFilter(
+ eventFromEventPusher(
+ user.NewHumanAddedEvent(context.Background(), &user.NewAggregate("userID", "org1").Aggregate,
+ "username", "firstName", "lastName", "", "", language.English, domain.GenderUnspecified, "", false,
+ ),
+ ),
+ ),
+ expectFilter(), // token lifetime
+ expectPush(
+ []*repository.Event{
+ eventFromEventPusherWithInstanceID("instanceID",
+ oidcsession.NewAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "org1").Aggregate,
+ "userID", "sessionID", "clientID", []string{"audience"}, []string{"openid", "offline_access"}, []domain.UserAuthMethodType{domain.UserAuthMethodTypePassword}, testNow),
+ ),
+ eventFromEventPusherWithInstanceID("instanceID",
+ oidcsession.NewAccessTokenAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "org1").Aggregate,
+ "at_accessTokenID", []string{"openid", "offline_access"}, time.Hour),
+ ),
+ eventFromEventPusherWithInstanceID("instanceID",
+ oidcsession.NewRefreshTokenAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "org1").Aggregate,
+ "rt_refreshTokenID", 7*24*time.Hour, 24*time.Hour),
+ ),
+ eventFromEventPusherWithInstanceID("instanceID",
+ authrequest.NewSucceededEvent(context.Background(), &authrequest.NewAggregate("V2_authRequestID", "instanceID").Aggregate),
+ ),
+ },
+ ),
+ ),
+ idGenerator: mock.NewIDGeneratorExpectIDs(t, "oidcSessionID", "accessTokenID", "refreshTokenID"),
+ defaultAccessTokenLifetime: time.Hour,
+ defaultRefreshTokenLifetime: 7 * 24 * time.Hour,
+ defaultRefreshTokenIdleLifetime: 24 * time.Hour,
+ keyAlgorithm: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
+ },
+ args{
+ ctx: authz.WithInstanceID(context.Background(), "instanceID"),
+ authRequestID: "V2_authRequestID",
+ },
+ res{
+ id: "V2_oidcSessionID-at_accessTokenID",
+ refreshToken: "VjJfb2lkY1Nlc3Npb25JRC1ydF9yZWZyZXNoVG9rZW5JRDp1c2VySUQ", //V2_oidcSessionID-rt_refreshTokenID:userID
+ expiration: tokenCreationNow.Add(time.Hour),
+ },
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ c := &Commands{
+ eventstore: tt.fields.eventstore,
+ idGenerator: tt.fields.idGenerator,
+ defaultAccessTokenLifetime: tt.fields.defaultAccessTokenLifetime,
+ defaultRefreshTokenLifetime: tt.fields.defaultRefreshTokenLifetime,
+ defaultRefreshTokenIdleLifetime: tt.fields.defaultRefreshTokenIdleLifetime,
+ keyAlgorithm: tt.fields.keyAlgorithm,
+ }
+ gotID, gotRefreshToken, gotExpiration, err := c.AddOIDCSessionRefreshAndAccessToken(tt.args.ctx, tt.args.authRequestID)
+ assert.Equal(t, tt.res.id, gotID)
+ assert.Equal(t, tt.res.refreshToken, gotRefreshToken)
+ assert.Equal(t, tt.res.expiration, gotExpiration)
+ assert.ErrorIs(t, err, tt.res.err)
+ })
+ }
+}
+
+func TestCommands_ExchangeOIDCSessionRefreshAndAccessToken(t *testing.T) {
+ type fields struct {
+ eventstore *eventstore.Eventstore
+ idGenerator id.Generator
+ defaultAccessTokenLifetime time.Duration
+ defaultRefreshTokenLifetime time.Duration
+ defaultRefreshTokenIdleLifetime time.Duration
+ keyAlgorithm crypto.EncryptionAlgorithm
+ }
+ type args struct {
+ ctx context.Context
+ oidcSessionID string
+ refreshToken string
+ scope []string
+ }
+ type res struct {
+ id string
+ refreshToken string
+ expiration time.Time
+ err error
+ }
+ tests := []struct {
+ name string
+ fields fields
+ args args
+ res res
+ }{
+ {
+ "invalid refresh token format error",
+ fields{
+ eventstore: eventstoreExpect(t),
+ keyAlgorithm: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
+ },
+ args{
+ ctx: authz.WithInstanceID(context.Background(), "instanceID"),
+ oidcSessionID: "V2_oidcSessionID",
+ refreshToken: "aW52YWxpZA", // invalid
+ },
+ res{
+ err: caos_errs.ThrowPreconditionFailed(nil, "OIDCS-JOI23", "Errors.OIDCSession.RefreshTokenInvalid"),
+ },
+ },
+ {
+ "inactive session error",
+ fields{
+ eventstore: eventstoreExpect(t,
+ expectFilter(),
+ ),
+ keyAlgorithm: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
+ },
+ args{
+ ctx: authz.WithInstanceID(context.Background(), "instanceID"),
+ oidcSessionID: "V2_oidcSessionID",
+ refreshToken: "VjJfb2lkY1Nlc3Npb25JRC1ydF9yZWZyZXNoVG9rZW5JRDp1c2VySUQ", //V2_oidcSessionID:rt_refreshTokenID:userID
+ },
+ res{
+ err: caos_errs.ThrowPreconditionFailed(nil, "OIDCS-s3hjk", "Errors.OIDCSession.RefreshTokenInvalid"),
+ },
+ },
+ {
+ "invalid refresh token error",
+ fields{
+ eventstore: eventstoreExpect(t,
+ expectFilter(
+ eventFromEventPusher(
+ oidcsession.NewAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "org1").Aggregate,
+ "userID", "sessionID", "clientID", []string{"audience"}, []string{"openid", "profile", "offline_access"}, []domain.UserAuthMethodType{domain.UserAuthMethodTypePassword}, testNow),
+ ),
+ eventFromEventPusher(
+ oidcsession.NewAccessTokenAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "org1").Aggregate,
+ "accessTokenID", []string{"openid", "profile", "offline_access"}, time.Hour),
+ ),
+ ),
+ ),
+ keyAlgorithm: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
+ },
+ args{
+ ctx: authz.WithInstanceID(context.Background(), "instanceID"),
+ oidcSessionID: "V2_oidcSessionID",
+ refreshToken: "VjJfb2lkY1Nlc3Npb25JRC1ydF9yZWZyZXNoVG9rZW5JRDp1c2VySUQ", //V2_oidcSessionID:rt_refreshTokenID:userID
+ },
+ res{
+ err: caos_errs.ThrowPreconditionFailed(nil, "OIDCS-28ubl", "Errors.OIDCSession.RefreshTokenInvalid"),
+ },
+ },
+ {
+ "expired refresh token error",
+ fields{
+ eventstore: eventstoreExpect(t,
+ expectFilter(
+ eventFromEventPusher(
+ oidcsession.NewAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "org1").Aggregate,
+ "userID", "sessionID", "clientID", []string{"audience"}, []string{"openid", "profile", "offline_access"}, []domain.UserAuthMethodType{domain.UserAuthMethodTypePassword}, testNow),
+ ),
+ eventFromEventPusher(
+ oidcsession.NewAccessTokenAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "org1").Aggregate,
+ "at_accessTokenID", []string{"openid", "profile", "offline_access"}, time.Hour),
+ ),
+ eventFromEventPusher(
+ oidcsession.NewRefreshTokenAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "org1").Aggregate,
+ "rt_refreshTokenID", 7*24*time.Hour, 24*time.Hour),
+ ),
+ ),
+ ),
+ keyAlgorithm: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
+ },
+ args{
+ ctx: authz.WithInstanceID(context.Background(), "instanceID"),
+ oidcSessionID: "V2_oidcSessionID",
+ refreshToken: "VjJfb2lkY1Nlc3Npb25JRC1ydF9yZWZyZXNoVG9rZW5JRDp1c2VySUQ", //V2_oidcSessionID:rt_refreshTokenID:userID
+ },
+ res{
+ err: caos_errs.ThrowPreconditionFailed(nil, "OIDCS-3jt2w", "Errors.OIDCSession.RefreshTokenInvalid"),
+ },
+ },
+ {
+ "refresh successful",
+ fields{
+ eventstore: eventstoreExpect(t,
+ expectFilter(
+ eventFromEventPusherWithCreationDateNow(
+ oidcsession.NewAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "org1").Aggregate,
+ "userID", "sessionID", "clientID", []string{"audience"}, []string{"openid", "profile", "offline_access"}, []domain.UserAuthMethodType{domain.UserAuthMethodTypePassword}, testNow),
+ ),
+ eventFromEventPusherWithCreationDateNow(
+ oidcsession.NewAccessTokenAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "org1").Aggregate,
+ "at_accessTokenID", []string{"openid", "profile", "offline_access"}, time.Hour),
+ ),
+ eventFromEventPusherWithCreationDateNow(
+ oidcsession.NewRefreshTokenAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "org1").Aggregate,
+ "rt_refreshTokenID", 7*24*time.Hour, 24*time.Hour),
+ ),
+ ),
+ expectFilter(), // token lifetime
+ expectPush(
+ []*repository.Event{
+ eventFromEventPusherWithInstanceID("instanceID",
+ oidcsession.NewAccessTokenAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "org1").Aggregate,
+ "at_accessTokenID", []string{"openid", "offline_access"}, time.Hour),
+ ),
+ eventFromEventPusherWithInstanceID("instanceID",
+ oidcsession.NewRefreshTokenRenewedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "org1").Aggregate,
+ "rt_refreshTokenID2", 24*time.Hour),
+ ),
+ },
+ ),
+ ),
+ idGenerator: mock.NewIDGeneratorExpectIDs(t, "accessTokenID", "refreshTokenID2"),
+ defaultAccessTokenLifetime: time.Hour,
+ defaultRefreshTokenLifetime: 7 * 24 * time.Hour,
+ defaultRefreshTokenIdleLifetime: 24 * time.Hour,
+ keyAlgorithm: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
+ },
+ args{
+ ctx: authz.WithInstanceID(context.Background(), "instanceID"),
+ oidcSessionID: "V2_oidcSessionID",
+ refreshToken: "VjJfb2lkY1Nlc3Npb25JRC1ydF9yZWZyZXNoVG9rZW5JRDp1c2VySUQ", //V2_oidcSessionID:rt_refreshTokenID:userID
+ scope: []string{"openid", "offline_access"},
+ },
+ res{
+ id: "V2_oidcSessionID-at_accessTokenID",
+ refreshToken: "VjJfb2lkY1Nlc3Npb25JRC1ydF9yZWZyZXNoVG9rZW5JRDI6dXNlcklE", // V2_oidcSessionID-rt_refreshTokenID2:userID%
+ expiration: time.Time{}.Add(time.Hour),
+ },
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ c := &Commands{
+ eventstore: tt.fields.eventstore,
+ idGenerator: tt.fields.idGenerator,
+ defaultAccessTokenLifetime: tt.fields.defaultAccessTokenLifetime,
+ defaultRefreshTokenLifetime: tt.fields.defaultRefreshTokenLifetime,
+ defaultRefreshTokenIdleLifetime: tt.fields.defaultRefreshTokenIdleLifetime,
+ keyAlgorithm: tt.fields.keyAlgorithm,
+ }
+ gotID, gotRefreshToken, gotExpiration, err := c.ExchangeOIDCSessionRefreshAndAccessToken(tt.args.ctx, tt.args.oidcSessionID, tt.args.refreshToken, tt.args.scope)
+ assert.Equal(t, tt.res.id, gotID)
+ assert.Equal(t, tt.res.refreshToken, gotRefreshToken)
+ assert.Equal(t, tt.res.expiration, gotExpiration)
+ assert.ErrorIs(t, err, tt.res.err)
+ })
+ }
+}
+
+func TestCommands_OIDCSessionByRefreshToken(t *testing.T) {
+ type fields struct {
+ eventstore *eventstore.Eventstore
+ idGenerator id.Generator
+ defaultAccessTokenLifetime time.Duration
+ defaultRefreshTokenLifetime time.Duration
+ defaultRefreshTokenIdleLifetime time.Duration
+ keyAlgorithm crypto.EncryptionAlgorithm
+ }
+ type args struct {
+ ctx context.Context
+ refreshToken string
+ }
+ type res struct {
+ model *OIDCSessionWriteModel
+ err error
+ }
+ tests := []struct {
+ name string
+ fields fields
+ args args
+ res res
+ }{
+ {
+ "invalid refresh token format error",
+ fields{
+ eventstore: eventstoreExpect(t),
+ keyAlgorithm: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
+ },
+ args{
+ ctx: authz.WithInstanceID(context.Background(), "instanceID"),
+ refreshToken: "invalid",
+ },
+ res{
+ err: caos_errs.ThrowPreconditionFailed(nil, "OIDCS-JOI23", "Errors.OIDCSession.RefreshTokenInvalid"),
+ },
+ },
+ {
+ "inactive session error",
+ fields{
+ eventstore: eventstoreExpect(t,
+ expectFilter(),
+ ),
+ keyAlgorithm: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
+ },
+ args{
+ ctx: authz.WithInstanceID(context.Background(), "instanceID"),
+ refreshToken: "V2_oidcSessionID-rt_refreshTokenID:userID",
+ },
+ res{
+ err: caos_errs.ThrowPreconditionFailed(nil, "OIDCS-s3hjk", "Errors.OIDCSession.RefreshTokenInvalid"),
+ },
+ },
+ {
+ "invalid refresh token error",
+ fields{
+ eventstore: eventstoreExpect(t,
+ expectFilter(
+ eventFromEventPusher(
+ oidcsession.NewAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "org1").Aggregate,
+ "userID", "sessionID", "clientID", []string{"audience"}, []string{"openid", "profile", "offline_access"}, []domain.UserAuthMethodType{domain.UserAuthMethodTypePassword}, testNow),
+ ),
+ eventFromEventPusher(
+ oidcsession.NewAccessTokenAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "org1").Aggregate,
+ "at_accessTokenID", []string{"openid", "profile", "offline_access"}, time.Hour),
+ ),
+ ),
+ ),
+ keyAlgorithm: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
+ },
+ args{
+ ctx: authz.WithInstanceID(context.Background(), "instanceID"),
+ refreshToken: "V2_oidcSessionID-rt_refreshTokenID:userID",
+ },
+ res{
+ err: caos_errs.ThrowPreconditionFailed(nil, "OIDCS-28ubl", "Errors.OIDCSession.RefreshTokenInvalid"),
+ },
+ },
+ {
+ "expired refresh token error",
+ fields{
+ eventstore: eventstoreExpect(t,
+ expectFilter(
+ eventFromEventPusher(
+ oidcsession.NewAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "org1").Aggregate,
+ "userID", "sessionID", "clientID", []string{"audience"}, []string{"openid", "profile", "offline_access"}, []domain.UserAuthMethodType{domain.UserAuthMethodTypePassword}, testNow),
+ ),
+ eventFromEventPusher(
+ oidcsession.NewAccessTokenAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "org1").Aggregate,
+ "at_accessTokenID", []string{"openid", "profile", "offline_access"}, time.Hour),
+ ),
+ eventFromEventPusher(
+ oidcsession.NewRefreshTokenAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "org1").Aggregate,
+ "rt_refreshTokenID", 7*24*time.Hour, 24*time.Hour),
+ ),
+ ),
+ ),
+ keyAlgorithm: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
+ },
+ args{
+ ctx: authz.WithInstanceID(context.Background(), "instanceID"),
+ refreshToken: "V2_oidcSessionID-rt_refreshTokenID:userID",
+ },
+ res{
+ err: caos_errs.ThrowPreconditionFailed(nil, "OIDCS-3jt2w", "Errors.OIDCSession.RefreshTokenInvalid"),
+ },
+ },
+ {
+ "get successful",
+ fields{
+ eventstore: eventstoreExpect(t,
+ expectFilter(
+ eventFromEventPusherWithCreationDateNow(
+ oidcsession.NewAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "org1").Aggregate,
+ "userID", "sessionID", "clientID", []string{"audience"}, []string{"openid", "profile", "offline_access"}, []domain.UserAuthMethodType{domain.UserAuthMethodTypePassword}, testNow),
+ ),
+ eventFromEventPusherWithCreationDateNow(
+ oidcsession.NewAccessTokenAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "org1").Aggregate,
+ "at_accessTokenID", []string{"openid", "profile", "offline_access"}, time.Hour),
+ ),
+ eventFromEventPusherWithCreationDateNow(
+ oidcsession.NewRefreshTokenAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "org1").Aggregate,
+ "rt_refreshTokenID", 7*24*time.Hour, 24*time.Hour),
+ ),
+ ),
+ ),
+ keyAlgorithm: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
+ },
+ args{
+ ctx: authz.WithInstanceID(context.Background(), "instanceID"),
+ refreshToken: "V2_oidcSessionID-rt_refreshTokenID:userID",
+ },
+ res{
+ model: &OIDCSessionWriteModel{
+ WriteModel: eventstore.WriteModel{
+ AggregateID: "V2_oidcSessionID",
+ ChangeDate: testNow,
+ },
+ UserID: "userID",
+ SessionID: "sessionID",
+ ClientID: "clientID",
+ Audience: []string{"audience"},
+ Scope: []string{"openid", "profile", "offline_access"},
+ AuthMethods: []domain.UserAuthMethodType{domain.UserAuthMethodTypePassword},
+ AuthTime: testNow,
+ State: domain.OIDCSessionStateActive,
+ RefreshTokenID: "rt_refreshTokenID",
+ RefreshTokenExpiration: testNow.Add(7 * 24 * time.Hour),
+ RefreshTokenIdleExpiration: testNow.Add(24 * time.Hour),
+ },
+ },
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ c := &Commands{
+ eventstore: tt.fields.eventstore,
+ idGenerator: tt.fields.idGenerator,
+ defaultAccessTokenLifetime: tt.fields.defaultAccessTokenLifetime,
+ defaultRefreshTokenLifetime: tt.fields.defaultRefreshTokenLifetime,
+ defaultRefreshTokenIdleLifetime: tt.fields.defaultRefreshTokenIdleLifetime,
+ keyAlgorithm: tt.fields.keyAlgorithm,
+ }
+ got, err := c.OIDCSessionByRefreshToken(tt.args.ctx, tt.args.refreshToken)
+ require.ErrorIs(t, err, tt.res.err)
+ if tt.res.err == nil {
+ assert.WithinRange(t, got.ChangeDate, tt.res.model.ChangeDate.Add(-2*time.Second), tt.res.model.ChangeDate.Add(2*time.Second))
+ assert.Equal(t, tt.res.model.AggregateID, got.AggregateID)
+ assert.Equal(t, tt.res.model.UserID, got.UserID)
+ assert.Equal(t, tt.res.model.SessionID, got.SessionID)
+ assert.Equal(t, tt.res.model.ClientID, got.ClientID)
+ assert.Equal(t, tt.res.model.Audience, got.Audience)
+ assert.Equal(t, tt.res.model.Scope, got.Scope)
+ assert.Equal(t, tt.res.model.AuthMethods, got.AuthMethods)
+ assert.WithinRange(t, got.AuthTime, tt.res.model.AuthTime.Add(-2*time.Second), tt.res.model.AuthTime.Add(2*time.Second))
+ assert.Equal(t, tt.res.model.State, got.State)
+ assert.Equal(t, tt.res.model.RefreshTokenID, got.RefreshTokenID)
+ assert.WithinRange(t, got.RefreshTokenExpiration, tt.res.model.RefreshTokenExpiration.Add(-2*time.Second), tt.res.model.RefreshTokenExpiration.Add(2*time.Second))
+ assert.WithinRange(t, got.RefreshTokenIdleExpiration, tt.res.model.RefreshTokenIdleExpiration.Add(-2*time.Second), tt.res.model.RefreshTokenIdleExpiration.Add(2*time.Second))
+ }
+ })
+ }
+}
+
+func TestCommands_RevokeOIDCSessionToken(t *testing.T) {
+ type fields struct {
+ eventstore *eventstore.Eventstore
+ keyAlgorithm crypto.EncryptionAlgorithm
+ }
+ type args struct {
+ ctx context.Context
+ token string
+ clientID string
+ }
+ type res struct {
+ err error
+ }
+ tests := []struct {
+ name string
+ fields fields
+ args args
+ res res
+ }{
+ {
+ "invalid token",
+ fields{
+ eventstore: eventstoreExpect(t),
+ keyAlgorithm: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
+ },
+ args{
+ ctx: authz.WithInstanceID(context.Background(), "instanceID"),
+ token: "invalid",
+ },
+ res{
+ err: nil,
+ },
+ },
+ {
+ "refresh_token inactive",
+ fields{
+ eventstore: eventstoreExpect(t,
+ expectFilter(
+ eventFromEventPusher(
+ oidcsession.NewAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "org1").Aggregate,
+ "userID", "sessionID", "clientID", []string{"clientID"}, []string{"openid", "profile", "offline_access"}, []domain.UserAuthMethodType{domain.UserAuthMethodTypePassword}, testNow),
+ ),
+ ),
+ ),
+ keyAlgorithm: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
+ },
+ args{
+ ctx: authz.WithInstanceID(context.Background(), "instanceID"),
+ token: "V2_oidcSessionID-rt_refreshTokenID",
+ clientID: "clientID",
+ },
+ res{
+ err: nil,
+ },
+ },
+ {
+ "refresh_token invalid client",
+ fields{
+ eventstore: eventstoreExpect(t,
+ expectFilter(
+ eventFromEventPusher(
+ oidcsession.NewAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "org1").Aggregate,
+ "userID", "sessionID", "otherClientID", []string{"otherClientID"}, []string{"openid", "profile", "offline_access"}, []domain.UserAuthMethodType{domain.UserAuthMethodTypePassword}, testNow),
+ ),
+ ),
+ ),
+ keyAlgorithm: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
+ },
+ args{
+ ctx: authz.WithInstanceID(context.Background(), "instanceID"),
+ token: "V2_oidcSessionID-rt_refreshTokenID",
+ clientID: "clientID",
+ },
+ res{
+ err: caos_errs.ThrowPreconditionFailed(nil, "OIDCS-SKjl3", "Errors.OIDCSession.InvalidClient"),
+ },
+ },
+ {
+ "refresh_token revoked",
+ fields{
+ eventstore: eventstoreExpect(t,
+ expectFilter(
+ eventFromEventPusher(
+ oidcsession.NewAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "org1").Aggregate,
+ "userID", "sessionID", "clientID", []string{"clientID"}, []string{"openid", "profile", "offline_access"}, []domain.UserAuthMethodType{domain.UserAuthMethodTypePassword}, testNow),
+ ),
+ eventFromEventPusherWithCreationDateNow(
+ oidcsession.NewAccessTokenAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "org1").Aggregate,
+ "at_accessTokenID", []string{"openid", "profile", "offline_access"}, time.Hour),
+ ),
+ eventFromEventPusherWithCreationDateNow(
+ oidcsession.NewRefreshTokenAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "org1").Aggregate,
+ "rt_refreshTokenID", 7*24*time.Hour, 24*time.Hour),
+ ),
+ ),
+ expectPush([]*repository.Event{
+ eventFromEventPusherWithInstanceID("instanceID",
+ oidcsession.NewRefreshTokenRevokedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "org1").Aggregate),
+ ),
+ }),
+ ),
+ keyAlgorithm: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
+ },
+ args{
+ ctx: authz.WithInstanceID(context.Background(), "instanceID"),
+ token: "V2_oidcSessionID-rt_refreshTokenID",
+ clientID: "clientID",
+ },
+ res{
+ err: nil,
+ },
+ },
+ {
+ "access_token inactive session",
+ fields{
+ eventstore: eventstoreExpect(t,
+ expectFilter(
+ eventFromEventPusher(
+ oidcsession.NewAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "org1").Aggregate,
+ "userID", "sessionID", "clientID", []string{"clientID"}, []string{"openid", "profile", "offline_access"}, []domain.UserAuthMethodType{domain.UserAuthMethodTypePassword}, testNow),
+ ),
+ ),
+ ),
+ keyAlgorithm: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
+ },
+ args{
+ ctx: authz.WithInstanceID(context.Background(), "instanceID"),
+ token: "V2_oidcSessionID-at_accessTokenID",
+ clientID: "clientID",
+ },
+ res{
+ err: nil,
+ },
+ },
+ {
+ "access_token invalid client",
+ fields{
+ eventstore: eventstoreExpect(t,
+ expectFilter(
+ eventFromEventPusher(
+ oidcsession.NewAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "org1").Aggregate,
+ "userID", "sessionID", "otherClientID", []string{"otherClientID"}, []string{"openid", "profile", "offline_access"}, []domain.UserAuthMethodType{domain.UserAuthMethodTypePassword}, testNow),
+ ),
+ ),
+ ),
+ keyAlgorithm: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
+ },
+ args{
+ ctx: authz.WithInstanceID(context.Background(), "instanceID"),
+ token: "V2_oidcSessionID-at_accessTokenID",
+ clientID: "clientID",
+ },
+ res{
+ err: caos_errs.ThrowPreconditionFailed(nil, "OIDCS-SKjl3", "Errors.OIDCSession.InvalidClient"),
+ },
+ },
+ {
+ "access_token revoked",
+ fields{
+ eventstore: eventstoreExpect(t,
+ expectFilter(
+ eventFromEventPusher(
+ oidcsession.NewAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "org1").Aggregate,
+ "userID", "sessionID", "clientID", []string{"clientID"}, []string{"openid", "profile", "offline_access"}, []domain.UserAuthMethodType{domain.UserAuthMethodTypePassword}, testNow),
+ ),
+ eventFromEventPusherWithCreationDateNow(
+ oidcsession.NewAccessTokenAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "org1").Aggregate,
+ "at_accessTokenID", []string{"openid", "profile", "offline_access"}, time.Hour),
+ ),
+ eventFromEventPusherWithCreationDateNow(
+ oidcsession.NewRefreshTokenAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "org1").Aggregate,
+ "rt_refreshTokenID", 7*24*time.Hour, 24*time.Hour),
+ ),
+ ),
+ expectPush([]*repository.Event{
+ eventFromEventPusherWithInstanceID("instanceID",
+ oidcsession.NewAccessTokenRevokedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "org1").Aggregate),
+ ),
+ }),
+ ),
+ keyAlgorithm: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
+ },
+ args{
+ ctx: authz.WithInstanceID(context.Background(), "instanceID"),
+ token: "V2_oidcSessionID-at_accessTokenID",
+ clientID: "clientID",
+ },
+ res{
+ err: nil,
+ },
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ c := &Commands{
+ eventstore: tt.fields.eventstore,
+ keyAlgorithm: tt.fields.keyAlgorithm,
+ }
+ err := c.RevokeOIDCSessionToken(tt.args.ctx, tt.args.token, tt.args.clientID)
+ require.ErrorIs(t, err, tt.res.err)
+ })
+ }
+}
diff --git a/internal/command/org.go b/internal/command/org.go
index c9fd3dbd4..ba64dccb5 100644
--- a/internal/command/org.go
+++ b/internal/command/org.go
@@ -42,7 +42,7 @@ func (c *Commands) setUpOrgWithIDs(ctx context.Context, o *OrgSetup, orgID strin
var pat *PersonalAccessToken
if o.Human != nil {
o.Human.ID = userID
- validations = append(validations, c.AddHumanCommand(o.Human, orgID, c.userPasswordAlg, c.userEncryption, true))
+ validations = append(validations, c.AddHumanCommand(o.Human, orgID, c.userPasswordHasher, c.userEncryption, true))
} else if o.Machine != nil {
validations = append(validations, AddMachineCommand(userAgg, o.Machine.Machine))
if o.Machine.Pat != nil {
diff --git a/internal/command/org_domain_test.go b/internal/command/org_domain_test.go
index 230fc5601..feb737cde 100644
--- a/internal/command/org_domain_test.go
+++ b/internal/command/org_domain_test.go
@@ -653,7 +653,7 @@ func TestCommandSide_GenerateOrgDomainValidation(t *testing.T) {
},
res: res{
wantToken: "a",
- wantURL: "https://domain.ch/.well-known/zitadel-challenge/a",
+ wantURL: "https://domain.ch/.well-known/zitadel-challenge/a.txt",
},
},
}
diff --git a/internal/command/org_policy_login.go b/internal/command/org_policy_login.go
index b8e551b25..ddfb9169d 100644
--- a/internal/command/org_policy_login.go
+++ b/internal/command/org_policy_login.go
@@ -21,6 +21,7 @@ type AddLoginPolicy struct {
AllowExternalIDP bool
IDPProviders []*AddLoginPolicyIDP
ForceMFA bool
+ ForceMFALocalOnly bool
SecondFactors []domain.SecondFactorType
MultiFactors []domain.MultiFactorType
PasswordlessType domain.PasswordlessType
@@ -47,6 +48,7 @@ type ChangeLoginPolicy struct {
AllowRegister bool
AllowExternalIDP bool
ForceMFA bool
+ ForceMFALocalOnly bool
PasswordlessType domain.PasswordlessType
HidePasswordReset bool
IgnoreUnknownUsernames bool
@@ -425,6 +427,7 @@ func prepareAddLoginPolicy(a *org.Aggregate, policy *AddLoginPolicy) preparation
policy.AllowRegister,
policy.AllowExternalIDP,
policy.ForceMFA,
+ policy.ForceMFALocalOnly,
policy.HidePasswordReset,
policy.IgnoreUnknownUsernames,
policy.AllowDomainDiscovery,
diff --git a/internal/command/org_policy_login_test.go b/internal/command/org_policy_login_test.go
index e4a3c0a2f..f7d1accfa 100644
--- a/internal/command/org_policy_login_test.go
+++ b/internal/command/org_policy_login_test.go
@@ -61,6 +61,7 @@ func TestCommandSide_AddLoginPolicy(t *testing.T) {
true,
true,
true,
+ true,
false,
false,
domain.PasswordlessTypeAllowed,
@@ -83,6 +84,7 @@ func TestCommandSide_AddLoginPolicy(t *testing.T) {
AllowUsernamePassword: true,
AllowExternalIDP: true,
ForceMFA: true,
+ ForceMFALocalOnly: true,
IgnoreUnknownUsernames: true,
AllowDomainDiscovery: true,
PasswordlessType: domain.PasswordlessTypeAllowed,
@@ -118,6 +120,7 @@ func TestCommandSide_AddLoginPolicy(t *testing.T) {
true,
true,
true,
+ true,
domain.PasswordlessTypeAllowed,
"https://example.com/redirect",
time.Hour*1,
@@ -139,6 +142,7 @@ func TestCommandSide_AddLoginPolicy(t *testing.T) {
AllowUsernamePassword: true,
AllowExternalIDP: true,
ForceMFA: true,
+ ForceMFALocalOnly: true,
HidePasswordReset: true,
IgnoreUnknownUsernames: true,
AllowDomainDiscovery: true,
@@ -174,6 +178,7 @@ func TestCommandSide_AddLoginPolicy(t *testing.T) {
AllowUsernamePassword: true,
AllowExternalIDP: true,
ForceMFA: true,
+ ForceMFALocalOnly: true,
HidePasswordReset: true,
IgnoreUnknownUsernames: true,
AllowDomainDiscovery: true,
@@ -213,6 +218,7 @@ func TestCommandSide_AddLoginPolicy(t *testing.T) {
true,
true,
true,
+ true,
domain.PasswordlessTypeAllowed,
"https://example.com/redirect",
time.Hour*1,
@@ -246,6 +252,7 @@ func TestCommandSide_AddLoginPolicy(t *testing.T) {
AllowUsernamePassword: true,
AllowExternalIDP: true,
ForceMFA: true,
+ ForceMFALocalOnly: true,
HidePasswordReset: true,
IgnoreUnknownUsernames: true,
AllowDomainDiscovery: true,
@@ -285,6 +292,7 @@ func TestCommandSide_AddLoginPolicy(t *testing.T) {
AllowUsernamePassword: true,
AllowExternalIDP: true,
ForceMFA: true,
+ ForceMFALocalOnly: true,
HidePasswordReset: true,
IgnoreUnknownUsernames: true,
AllowDomainDiscovery: true,
@@ -341,6 +349,7 @@ func TestCommandSide_AddLoginPolicy(t *testing.T) {
true,
true,
true,
+ true,
domain.PasswordlessTypeAllowed,
"https://example.com/redirect",
time.Hour*1,
@@ -369,6 +378,7 @@ func TestCommandSide_AddLoginPolicy(t *testing.T) {
AllowUsernamePassword: true,
AllowExternalIDP: true,
ForceMFA: true,
+ ForceMFALocalOnly: true,
HidePasswordReset: true,
IgnoreUnknownUsernames: true,
AllowDomainDiscovery: true,
@@ -450,6 +460,7 @@ func TestCommandSide_ChangeLoginPolicy(t *testing.T) {
AllowUsernamePassword: true,
AllowExternalIDP: true,
ForceMFA: true,
+ ForceMFALocalOnly: true,
IgnoreUnknownUsernames: true,
AllowDomainDiscovery: true,
DisableLoginWithEmail: true,
@@ -480,6 +491,7 @@ func TestCommandSide_ChangeLoginPolicy(t *testing.T) {
true,
true,
true,
+ true,
domain.PasswordlessTypeAllowed,
"https://example.com/redirect",
time.Hour*1,
@@ -500,6 +512,7 @@ func TestCommandSide_ChangeLoginPolicy(t *testing.T) {
AllowUsernamePassword: true,
AllowExternalIDP: true,
ForceMFA: true,
+ ForceMFALocalOnly: true,
HidePasswordReset: true,
IgnoreUnknownUsernames: true,
AllowDomainDiscovery: true,
@@ -536,6 +549,7 @@ func TestCommandSide_ChangeLoginPolicy(t *testing.T) {
true,
true,
true,
+ true,
domain.PasswordlessTypeAllowed,
"https://example.com/redirect",
time.Hour*1,
@@ -581,6 +595,7 @@ func TestCommandSide_ChangeLoginPolicy(t *testing.T) {
AllowUsernamePassword: false,
AllowExternalIDP: false,
ForceMFA: false,
+ ForceMFALocalOnly: false,
IgnoreUnknownUsernames: false,
AllowDomainDiscovery: false,
DisableLoginWithEmail: false,
@@ -686,6 +701,7 @@ func TestCommandSide_RemoveLoginPolicy(t *testing.T) {
true,
true,
true,
+ true,
domain.PasswordlessTypeAllowed,
"",
time.Hour*1,
@@ -829,6 +845,7 @@ func TestCommandSide_AddIDPProviderLoginPolicy(t *testing.T) {
true,
true,
true,
+ true,
domain.PasswordlessTypeAllowed,
"",
time.Hour*1,
@@ -873,6 +890,7 @@ func TestCommandSide_AddIDPProviderLoginPolicy(t *testing.T) {
true,
true,
true,
+ true,
domain.PasswordlessTypeAllowed,
"",
time.Hour*1,
@@ -937,6 +955,7 @@ func TestCommandSide_AddIDPProviderLoginPolicy(t *testing.T) {
true,
true,
true,
+ true,
domain.PasswordlessTypeAllowed,
"",
time.Hour*1,
@@ -1104,6 +1123,7 @@ func TestCommandSide_RemoveIDPProviderLoginPolicy(t *testing.T) {
true,
true,
true,
+ true,
domain.PasswordlessTypeAllowed,
"",
time.Hour*1,
@@ -1148,6 +1168,7 @@ func TestCommandSide_RemoveIDPProviderLoginPolicy(t *testing.T) {
true,
true,
true,
+ true,
domain.PasswordlessTypeAllowed,
"",
time.Hour*1,
@@ -1204,6 +1225,7 @@ func TestCommandSide_RemoveIDPProviderLoginPolicy(t *testing.T) {
true,
true,
true,
+ true,
domain.PasswordlessTypeAllowed,
"",
time.Hour*1,
@@ -1267,6 +1289,7 @@ func TestCommandSide_RemoveIDPProviderLoginPolicy(t *testing.T) {
true,
true,
true,
+ true,
domain.PasswordlessTypeAllowed,
"",
time.Hour*1,
@@ -1338,6 +1361,7 @@ func TestCommandSide_RemoveIDPProviderLoginPolicy(t *testing.T) {
true,
true,
true,
+ true,
domain.PasswordlessTypeAllowed,
"",
time.Hour*1,
diff --git a/internal/command/phone.go b/internal/command/phone.go
index 30cabb6fc..7f550ceea 100644
--- a/internal/command/phone.go
+++ b/internal/command/phone.go
@@ -13,6 +13,6 @@ type Phone struct {
Verified bool
}
-func newPhoneCode(ctx context.Context, filter preparation.FilterToQueryReducer, alg crypto.EncryptionAlgorithm) (*CryptoCodeWithExpiry, error) {
- return newCryptoCodeWithExpiry(ctx, filter, domain.SecretGeneratorTypeVerifyPhoneCode, alg)
+func (c *Commands) newPhoneCode(ctx context.Context, filter preparation.FilterToQueryReducer, alg crypto.EncryptionAlgorithm) (*CryptoCode, error) {
+ return c.newCode(ctx, filter, domain.SecretGeneratorTypeVerifyPhoneCode, alg)
}
diff --git a/internal/command/policy_login_model.go b/internal/command/policy_login_model.go
index 50ece0f2f..732c2a16c 100644
--- a/internal/command/policy_login_model.go
+++ b/internal/command/policy_login_model.go
@@ -15,6 +15,7 @@ type LoginPolicyWriteModel struct {
AllowRegister bool
AllowExternalIDP bool
ForceMFA bool
+ ForceMFALocalOnly bool
HidePasswordReset bool
IgnoreUnknownUsernames bool
AllowDomainDiscovery bool
@@ -38,6 +39,7 @@ func (wm *LoginPolicyWriteModel) Reduce() error {
wm.AllowUserNamePassword = e.AllowUserNamePassword
wm.AllowExternalIDP = e.AllowExternalIDP
wm.ForceMFA = e.ForceMFA
+ wm.ForceMFALocalOnly = e.ForceMFALocalOnly
wm.PasswordlessType = e.PasswordlessType
wm.HidePasswordReset = e.HidePasswordReset
wm.IgnoreUnknownUsernames = e.IgnoreUnknownUsernames
@@ -64,6 +66,9 @@ func (wm *LoginPolicyWriteModel) Reduce() error {
if e.ForceMFA != nil {
wm.ForceMFA = *e.ForceMFA
}
+ if e.ForceMFALocalOnly != nil {
+ wm.ForceMFALocalOnly = *e.ForceMFALocalOnly
+ }
if e.HidePasswordReset != nil {
wm.HidePasswordReset = *e.HidePasswordReset
}
diff --git a/internal/command/project_application.go b/internal/command/project_application.go
index dfab28483..3929be9e7 100644
--- a/internal/command/project_application.go
+++ b/internal/command/project_application.go
@@ -16,8 +16,8 @@ type AddApp struct {
Name string
}
-func newAppClientSecret(ctx context.Context, filter preparation.FilterToQueryReducer, alg crypto.HashAlgorithm) (value *crypto.CryptoValue, plain string, err error) {
- return newCryptoCodeWithPlain(ctx, filter, domain.SecretGeneratorTypeAppSecret, alg)
+func (c *Commands) newAppClientSecret(ctx context.Context, filter preparation.FilterToQueryReducer, alg crypto.HashAlgorithm) (*CryptoCode, error) {
+ return c.newCode(ctx, filter, domain.SecretGeneratorTypeAppSecret, alg)
}
func (c *Commands) ChangeApplication(ctx context.Context, projectID string, appChange domain.Application, resourceOwner string) (*domain.ObjectDetails, error) {
diff --git a/internal/command/project_application_api.go b/internal/command/project_application_api.go
index 7354e5f99..16ff044db 100644
--- a/internal/command/project_application_api.go
+++ b/internal/command/project_application_api.go
@@ -44,10 +44,11 @@ func (c *Commands) AddAPIAppCommand(app *addAPIApp, clientSecretAlg crypto.HashA
}
if app.AuthMethodType == domain.APIAuthMethodTypeBasic {
- app.ClientSecret, app.ClientSecretPlain, err = newAppClientSecret(ctx, filter, clientSecretAlg)
+ code, err := c.newAppClientSecret(ctx, filter, clientSecretAlg)
if err != nil {
return nil, err
}
+ app.ClientSecret, app.ClientSecretPlain = code.Crypted, code.Plain
}
return []eventstore.Command{
@@ -243,7 +244,7 @@ func (c *Commands) VerifyAPIClientSecret(ctx context.Context, projectID, appID,
projectAgg := ProjectAggregateFromWriteModel(&app.WriteModel)
ctx, spanPasswordComparison := tracing.NewNamedSpan(ctx, "crypto.CompareHash")
- err = crypto.CompareHash(app.ClientSecret, []byte(secret), c.userPasswordAlg)
+ err = crypto.CompareHash(app.ClientSecret, []byte(secret), c.codeAlg)
spanPasswordComparison.EndWithError(err)
if err == nil {
_, err = c.eventstore.Push(ctx, project_repo.NewAPIConfigSecretCheckSucceededEvent(ctx, projectAgg, app.AppID))
diff --git a/internal/command/project_application_oidc.go b/internal/command/project_application_oidc.go
index 44efd279a..2d14ecf3a 100644
--- a/internal/command/project_application_oidc.go
+++ b/internal/command/project_application_oidc.go
@@ -77,10 +77,11 @@ func (c *Commands) AddOIDCAppCommand(app *addOIDCApp, clientSecretAlg crypto.Has
}
if app.AuthMethodType == domain.OIDCAuthMethodTypeBasic || app.AuthMethodType == domain.OIDCAuthMethodTypePost {
- app.ClientSecret, app.ClientSecretPlain, err = newAppClientSecret(ctx, filter, clientSecretAlg)
+ code, err := c.newAppClientSecret(ctx, filter, clientSecretAlg)
if err != nil {
return nil, err
}
+ app.ClientSecret, app.ClientSecretPlain = code.Crypted, code.Plain
}
return []eventstore.Command{
@@ -325,7 +326,7 @@ func (c *Commands) VerifyOIDCClientSecret(ctx context.Context, projectID, appID,
projectAgg := ProjectAggregateFromWriteModel(&app.WriteModel)
ctx, spanPasswordComparison := tracing.NewNamedSpan(ctx, "crypto.CompareHash")
- err = crypto.CompareHash(app.ClientSecret, []byte(secret), c.userPasswordAlg)
+ err = crypto.CompareHash(app.ClientSecret, []byte(secret), c.codeAlg)
spanPasswordComparison.EndWithError(err)
if err == nil {
_, err = c.eventstore.Push(ctx, project_repo.NewOIDCConfigSecretCheckSucceededEvent(ctx, projectAgg, app.AppID))
diff --git a/internal/command/quota_report.go b/internal/command/quota_report.go
index b18b9e9b5..19855452e 100644
--- a/internal/command/quota_report.go
+++ b/internal/command/quota_report.go
@@ -7,8 +7,8 @@ import (
"github.com/zitadel/zitadel/internal/repository/quota"
)
-// ReportUsage calls notification hooks and emits the notified events
-func (c *Commands) ReportUsage(ctx context.Context, dueNotifications []*quota.NotificationDueEvent) error {
+// ReportQuotaUsage writes a slice of *quota.NotificationDueEvent directly to the eventstore
+func (c *Commands) ReportQuotaUsage(ctx context.Context, dueNotifications []*quota.NotificationDueEvent) error {
cmds := make([]eventstore.Command, len(dueNotifications))
for idx, notification := range dueNotifications {
cmds[idx] = notification
diff --git a/internal/command/session.go b/internal/command/session.go
index fb1dd42d0..86e249822 100644
--- a/internal/command/session.go
+++ b/internal/command/session.go
@@ -1,6 +1,7 @@
package command
import (
+ "bytes"
"context"
"encoding/base64"
"fmt"
@@ -13,30 +14,34 @@ import (
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/id"
"github.com/zitadel/zitadel/internal/repository/session"
+ "github.com/zitadel/zitadel/internal/repository/user"
+ usr_repo "github.com/zitadel/zitadel/internal/repository/user"
"github.com/zitadel/zitadel/internal/telemetry/tracing"
)
type SessionCommand func(ctx context.Context, cmd *SessionCommands) error
type SessionCommands struct {
- cmds []SessionCommand
+ sessionCommands []SessionCommand
sessionWriteModel *SessionWriteModel
passwordWriteModel *HumanPasswordWriteModel
intentWriteModel *IDPIntentWriteModel
eventstore *eventstore.Eventstore
- userPasswordAlg crypto.HashAlgorithm
- intentAlg crypto.EncryptionAlgorithm
- createToken func(sessionID string) (id string, token string, err error)
- now func() time.Time
+ eventCommands []eventstore.Command
+
+ hasher *crypto.PasswordHasher
+ intentAlg crypto.EncryptionAlgorithm
+ createToken func(sessionID string) (id string, token string, err error)
+ now func() time.Time
}
func (c *Commands) NewSessionCommands(cmds []SessionCommand, session *SessionWriteModel) *SessionCommands {
return &SessionCommands{
- cmds: cmds,
+ sessionCommands: cmds,
sessionWriteModel: session,
eventstore: c.eventstore,
- userPasswordAlg: c.userPasswordAlg,
+ hasher: c.userPasswordHasher,
intentAlg: c.idpConfigEncryption,
createToken: c.sessionTokenCreator,
now: time.Now,
@@ -49,7 +54,7 @@ func CheckUser(id string) SessionCommand {
if cmd.sessionWriteModel.UserID != "" && id != "" && cmd.sessionWriteModel.UserID != id {
return caos_errs.ThrowInvalidArgument(nil, "", "user change not possible")
}
- return cmd.sessionWriteModel.UserChecked(ctx, id, cmd.now())
+ return cmd.UserChecked(ctx, id, cmd.now())
}
}
@@ -68,17 +73,21 @@ func CheckPassword(password string) SessionCommand {
return caos_errs.ThrowPreconditionFailed(nil, "COMMAND-Df4b3", "Errors.User.NotFound")
}
- if cmd.passwordWriteModel.Secret == nil {
+ if cmd.passwordWriteModel.EncodedHash == "" {
return caos_errs.ThrowPreconditionFailed(nil, "COMMAND-WEf3t", "Errors.User.Password.NotSet")
}
- ctx, spanPasswordComparison := tracing.NewNamedSpan(ctx, "crypto.CompareHash")
- err = crypto.CompareHash(cmd.passwordWriteModel.Secret, []byte(password), cmd.userPasswordAlg)
+ ctx, spanPasswordComparison := tracing.NewNamedSpan(ctx, "passwap.Verify")
+ updated, err := cmd.hasher.Verify(cmd.passwordWriteModel.EncodedHash, password)
spanPasswordComparison.EndWithError(err)
if err != nil {
//TODO: maybe we want to reset the session in the future https://github.com/zitadel/zitadel/issues/5807
return caos_errs.ThrowInvalidArgument(err, "COMMAND-SAF3g", "Errors.User.Password.Invalid")
}
- cmd.sessionWriteModel.PasswordChecked(ctx, cmd.now())
+ if updated != "" {
+ cmd.eventCommands = append(cmd.eventCommands, user.NewHumanPasswordHashUpdatedEvent(ctx, UserAggregateFromWriteModel(&cmd.passwordWriteModel.WriteModel), updated))
+ }
+
+ cmd.PasswordChecked(ctx, cmd.now())
return nil
}
}
@@ -114,14 +123,14 @@ func CheckIntent(intentID, token string) SessionCommand {
return caos_errs.ThrowPreconditionFailed(nil, "COMMAND-O8xk3w", "Errors.Intent.OtherUser")
}
}
- cmd.sessionWriteModel.IntentChecked(ctx, cmd.now())
+ cmd.IntentChecked(ctx, cmd.now())
return 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 {
+ for _, cmd := range s.sessionCommands {
if err := cmd(ctx, s); err != nil {
return err
}
@@ -129,6 +138,66 @@ func (s *SessionCommands) Exec(ctx context.Context) error {
return nil
}
+func (s *SessionCommands) Start(ctx context.Context, domain string) {
+ s.eventCommands = append(s.eventCommands, session.NewAddedEvent(ctx, s.sessionWriteModel.aggregate, domain))
+ // set the domain so checks can use it
+ s.sessionWriteModel.Domain = domain
+}
+
+func (s *SessionCommands) UserChecked(ctx context.Context, userID string, checkedAt time.Time) error {
+ s.eventCommands = append(s.eventCommands, session.NewUserCheckedEvent(ctx, s.sessionWriteModel.aggregate, userID, checkedAt))
+ // set the userID so other checks can use it
+ s.sessionWriteModel.UserID = userID
+ return nil
+}
+
+func (s *SessionCommands) PasswordChecked(ctx context.Context, checkedAt time.Time) {
+ s.eventCommands = append(s.eventCommands, session.NewPasswordCheckedEvent(ctx, s.sessionWriteModel.aggregate, checkedAt))
+}
+
+func (s *SessionCommands) IntentChecked(ctx context.Context, checkedAt time.Time) {
+ s.eventCommands = append(s.eventCommands, session.NewIntentCheckedEvent(ctx, s.sessionWriteModel.aggregate, checkedAt))
+}
+
+func (s *SessionCommands) PasskeyChallenged(ctx context.Context, challenge string, allowedCrentialIDs [][]byte, userVerification domain.UserVerificationRequirement) {
+ s.eventCommands = append(s.eventCommands, session.NewPasskeyChallengedEvent(ctx, s.sessionWriteModel.aggregate, challenge, allowedCrentialIDs, userVerification))
+}
+
+func (s *SessionCommands) PasskeyChecked(ctx context.Context, checkedAt time.Time, tokenID string, signCount uint32) {
+ s.eventCommands = append(s.eventCommands,
+ session.NewPasskeyCheckedEvent(ctx, s.sessionWriteModel.aggregate, checkedAt),
+ usr_repo.NewHumanPasswordlessSignCountChangedEvent(ctx, s.sessionWriteModel.aggregate, tokenID, signCount),
+ )
+}
+
+func (s *SessionCommands) SetToken(ctx context.Context, tokenID string) {
+ s.eventCommands = append(s.eventCommands, session.NewTokenSetEvent(ctx, s.sessionWriteModel.aggregate, tokenID))
+}
+
+func (s *SessionCommands) ChangeMetadata(ctx context.Context, metadata map[string][]byte) {
+ var changed bool
+ for key, value := range metadata {
+ currentValue, exists := s.sessionWriteModel.Metadata[key]
+
+ if len(value) != 0 {
+ // if a value is provided, and it's not equal, change it
+ if !bytes.Equal(currentValue, value) {
+ s.sessionWriteModel.Metadata[key] = value
+ changed = true
+ }
+ } else {
+ // if there's no / an empty value, we only need to remove it on existing entries
+ if exists {
+ delete(s.sessionWriteModel.Metadata, key)
+ changed = true
+ }
+ }
+ }
+ if changed {
+ s.eventCommands = append(s.eventCommands, session.NewMetadataSetEvent(ctx, s.sessionWriteModel.aggregate, s.sessionWriteModel.Metadata))
+ }
+}
+
func (s *SessionCommands) gethumanWriteModel(ctx context.Context) (*HumanWriteModel, error) {
if s.sessionWriteModel.UserID == "" {
return nil, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-eeR2e", "Errors.User.UserIDMissing")
@@ -145,7 +214,7 @@ func (s *SessionCommands) gethumanWriteModel(ctx context.Context) (*HumanWriteMo
}
func (s *SessionCommands) commands(ctx context.Context) (string, []eventstore.Command, error) {
- if len(s.sessionWriteModel.commands) == 0 {
+ if len(s.eventCommands) == 0 {
return "", nil, nil
}
@@ -153,8 +222,8 @@ func (s *SessionCommands) commands(ctx context.Context) (string, []eventstore.Co
if err != nil {
return "", nil, err
}
- s.sessionWriteModel.SetToken(ctx, tokenID)
- return token, s.sessionWriteModel.commands, nil
+ s.SetToken(ctx, tokenID)
+ return token, s.eventCommands, nil
}
func (c *Commands) CreateSession(ctx context.Context, cmds []SessionCommand, sessionDomain string, metadata map[string][]byte) (set *SessionChanged, err error) {
@@ -167,8 +236,8 @@ func (c *Commands) CreateSession(ctx context.Context, cmds []SessionCommand, ses
if err != nil {
return nil, err
}
- sessionWriteModel.Start(ctx, sessionDomain)
cmd := c.NewSessionCommands(cmds, sessionWriteModel)
+ cmd.Start(ctx, sessionDomain)
return c.updateSession(ctx, cmd, metadata)
}
@@ -185,13 +254,23 @@ func (c *Commands) UpdateSession(ctx context.Context, sessionID, sessionToken st
return c.updateSession(ctx, cmd, metadata)
}
-func (c *Commands) TerminateSession(ctx context.Context, sessionID, sessionToken string) (*domain.ObjectDetails, error) {
+func (c *Commands) TerminateSession(ctx context.Context, sessionID string, sessionToken string) (*domain.ObjectDetails, error) {
+ return c.terminateSession(ctx, sessionID, sessionToken, true)
+}
+
+func (c *Commands) TerminateSessionWithoutTokenCheck(ctx context.Context, sessionID string) (*domain.ObjectDetails, error) {
+ return c.terminateSession(ctx, sessionID, "", false)
+}
+
+func (c *Commands) terminateSession(ctx context.Context, sessionID, sessionToken string, mustCheckToken bool) (*domain.ObjectDetails, error) {
sessionWriteModel := NewSessionWriteModel(sessionID, "")
if err := c.eventstore.FilterToQueryReducer(ctx, sessionWriteModel); err != nil {
return nil, err
}
- if err := c.sessionPermission(ctx, sessionWriteModel, sessionToken, domain.PermissionSessionDelete); err != nil {
- return nil, err
+ if mustCheckToken {
+ if err := c.sessionPermission(ctx, sessionWriteModel, sessionToken, domain.PermissionSessionDelete); err != nil {
+ return nil, err
+ }
}
if sessionWriteModel.State != domain.SessionStateActive {
return writeModelToObjectDetails(&sessionWriteModel.WriteModel), nil
@@ -217,7 +296,7 @@ func (c *Commands) updateSession(ctx context.Context, checks *SessionCommands, m
// TODO: how to handle failed checks (e.g. pw wrong) https://github.com/zitadel/zitadel/issues/5807
return nil, err
}
- checks.sessionWriteModel.ChangeMetadata(ctx, metadata)
+ checks.ChangeMetadata(ctx, metadata)
sessionToken, cmds, err := checks.commands(ctx)
if err != nil {
return nil, err
diff --git a/internal/command/session_model.go b/internal/command/session_model.go
index aed542697..dce787ff7 100644
--- a/internal/command/session_model.go
+++ b/internal/command/session_model.go
@@ -1,15 +1,12 @@
package command
import (
- "bytes"
- "context"
"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 {
@@ -48,7 +45,6 @@ type SessionWriteModel struct {
PasskeyChallenge *PasskeyChallengeModel
- commands []eventstore.Command
aggregate *eventstore.Aggregate
}
@@ -151,62 +147,42 @@ func (wm *SessionWriteModel) reduceTerminate() {
wm.State = domain.SessionStateTerminated
}
-func (wm *SessionWriteModel) Start(ctx context.Context, domain string) {
- wm.commands = append(wm.commands, session.NewAddedEvent(ctx, wm.aggregate, domain))
- // set the domain so checks can use it
- wm.Domain = domain
-}
-
-func (wm *SessionWriteModel) UserChecked(ctx context.Context, userID string, checkedAt time.Time) error {
- wm.commands = append(wm.commands, session.NewUserCheckedEvent(ctx, wm.aggregate, userID, checkedAt))
- // set the userID so other checks can use it
- wm.UserID = userID
- return nil
-}
-
-func (wm *SessionWriteModel) PasswordChecked(ctx context.Context, checkedAt time.Time) {
- wm.commands = append(wm.commands, session.NewPasswordCheckedEvent(ctx, wm.aggregate, checkedAt))
-}
-
-func (wm *SessionWriteModel) IntentChecked(ctx context.Context, checkedAt time.Time) {
- wm.commands = append(wm.commands, session.NewIntentCheckedEvent(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))
-}
-
-func (wm *SessionWriteModel) ChangeMetadata(ctx context.Context, metadata map[string][]byte) {
- var changed bool
- for key, value := range metadata {
- currentValue, exists := wm.Metadata[key]
-
- if len(value) != 0 {
- // if a value is provided, and it's not equal, change it
- if !bytes.Equal(currentValue, value) {
- wm.Metadata[key] = value
- changed = true
- }
- } else {
- // if there's no / an empty value, we only need to remove it on existing entries
- if exists {
- delete(wm.Metadata, key)
- changed = true
- }
+// AuthenticationTime returns the time the user authenticated using the latest time of all checks
+func (wm *SessionWriteModel) AuthenticationTime() time.Time {
+ var authTime time.Time
+ for _, check := range []time.Time{
+ wm.PasswordCheckedAt,
+ wm.PasskeyCheckedAt,
+ wm.IntentCheckedAt,
+ // TODO: add U2F and OTP check https://github.com/zitadel/zitadel/issues/5477
+ } {
+ if check.After(authTime) {
+ authTime = check
}
}
- if changed {
- wm.commands = append(wm.commands, session.NewMetadataSetEvent(ctx, wm.aggregate, wm.Metadata))
+ return authTime
+}
+
+// AuthMethodTypes returns a list of UserAuthMethodTypes based on succeeded checks
+func (wm *SessionWriteModel) AuthMethodTypes() []domain.UserAuthMethodType {
+ types := make([]domain.UserAuthMethodType, 0, domain.UserAuthMethodTypeIDP)
+ if !wm.PasswordCheckedAt.IsZero() {
+ types = append(types, domain.UserAuthMethodTypePassword)
}
+ if !wm.PasskeyCheckedAt.IsZero() {
+ types = append(types, domain.UserAuthMethodTypePasswordless)
+ }
+ if !wm.IntentCheckedAt.IsZero() {
+ types = append(types, domain.UserAuthMethodTypeIDP)
+ }
+ // TODO: add checks with https://github.com/zitadel/zitadel/issues/5477
+ /*
+ if !wm.TOTPCheckedAt.IsZero() {
+ types = append(types, domain.UserAuthMethodTypeOTP)
+ }
+ if !wm.U2FCheckedAt.IsZero() {
+ types = append(types, domain.UserAuthMethodTypeU2F)
+ }
+ */
+ return types
}
diff --git a/internal/command/session_passkey.go b/internal/command/session_passkey.go
index 6de850b23..6ef3fccb2 100644
--- a/internal/command/session_passkey.go
+++ b/internal/command/session_passkey.go
@@ -51,7 +51,7 @@ func (c *Commands) CreatePasskeyChallenge(userVerification domain.UserVerificati
return caos_errs.ThrowInternal(err, "COMMAND-Yah6A", "Errors.Internal")
}
- cmd.sessionWriteModel.PasskeyChallenged(ctx, webAuthNLogin.Challenge, webAuthNLogin.AllowedCredentialIDs, webAuthNLogin.UserVerification)
+ cmd.PasskeyChallenged(ctx, webAuthNLogin.Challenge, webAuthNLogin.AllowedCredentialIDs, webAuthNLogin.UserVerification)
return nil
}
}
@@ -78,7 +78,7 @@ func (c *Commands) CheckPasskey(credentialAssertionData json.Marshaler) SessionC
if token == nil {
return caos_errs.ThrowPreconditionFailed(nil, "COMMAND-Aej7i", "Errors.User.WebAuthN.NotFound")
}
- cmd.sessionWriteModel.PasskeyChecked(ctx, cmd.now(), token.WebAuthNTokenID, signCount)
+ cmd.PasskeyChecked(ctx, cmd.now(), token.WebAuthNTokenID, signCount)
return nil
}
}
diff --git a/internal/command/session_test.go b/internal/command/session_test.go
index 92d19ac24..2eee78cc0 100644
--- a/internal/command/session_test.go
+++ b/internal/command/session_test.go
@@ -140,7 +140,6 @@ func TestSessionCommands_getHumanWriteModel(t *testing.T) {
func TestCommands_CreateSession(t *testing.T) {
type fields struct {
- eventstore *eventstore.Eventstore
idGenerator id.Generator
tokenCreator func(sessionID string) (string, string, error)
}
@@ -158,6 +157,7 @@ func TestCommands_CreateSession(t *testing.T) {
name string
fields fields
args args
+ expect []expect
res res
}{
{
@@ -168,6 +168,7 @@ func TestCommands_CreateSession(t *testing.T) {
args{
ctx: context.Background(),
},
+ []expect{},
res{
err: caos_errs.ThrowInternal(nil, "id", "generator failed"),
},
@@ -176,13 +177,13 @@ func TestCommands_CreateSession(t *testing.T) {
"eventstore failed",
fields{
idGenerator: mock.NewIDGeneratorExpectIDs(t, "sessionID"),
- eventstore: eventstoreExpect(t,
- expectFilterError(caos_errs.ThrowInternal(nil, "id", "filter failed")),
- ),
},
args{
ctx: context.Background(),
},
+ []expect{
+ expectFilterError(caos_errs.ThrowInternal(nil, "id", "filter failed")),
+ },
res{
err: caos_errs.ThrowInternal(nil, "id", "filter failed"),
},
@@ -191,17 +192,6 @@ func TestCommands_CreateSession(t *testing.T) {
"empty session",
fields{
idGenerator: mock.NewIDGeneratorExpectIDs(t, "sessionID"),
- eventstore: eventstoreExpect(t,
- expectFilter(),
- expectPush(
- eventPusherToEvents(
- session.NewAddedEvent(context.Background(), &session.NewAggregate("sessionID", "org1").Aggregate, ""),
- session.NewTokenSetEvent(context.Background(), &session.NewAggregate("sessionID", "org1").Aggregate,
- "tokenID",
- ),
- ),
- ),
- ),
tokenCreator: func(sessionID string) (string, string, error) {
return "tokenID",
"token",
@@ -211,6 +201,17 @@ func TestCommands_CreateSession(t *testing.T) {
args{
ctx: authz.NewMockContext("", "org1", ""),
},
+ []expect{
+ expectFilter(),
+ expectPush(
+ eventPusherToEvents(
+ session.NewAddedEvent(context.Background(), &session.NewAggregate("sessionID", "org1").Aggregate, ""),
+ session.NewTokenSetEvent(context.Background(), &session.NewAggregate("sessionID", "org1").Aggregate,
+ "tokenID",
+ ),
+ ),
+ ),
+ },
res{
want: &SessionChanged{
ObjectDetails: &domain.ObjectDetails{ResourceOwner: "org1"},
@@ -223,17 +224,6 @@ func TestCommands_CreateSession(t *testing.T) {
"empty session with domain",
fields{
idGenerator: mock.NewIDGeneratorExpectIDs(t, "sessionID"),
- eventstore: eventstoreExpect(t,
- expectFilter(),
- expectPush(
- eventPusherToEvents(
- session.NewAddedEvent(context.Background(), &session.NewAggregate("sessionID", "org1").Aggregate, "domain.tld"),
- session.NewTokenSetEvent(context.Background(), &session.NewAggregate("sessionID", "org1").Aggregate,
- "tokenID",
- ),
- ),
- ),
- ),
tokenCreator: func(sessionID string) (string, string, error) {
return "tokenID",
"token",
@@ -244,6 +234,17 @@ func TestCommands_CreateSession(t *testing.T) {
ctx: authz.NewMockContext("", "org1", ""),
domain: "domain.tld",
},
+ []expect{
+ expectFilter(),
+ expectPush(
+ eventPusherToEvents(
+ session.NewAddedEvent(context.Background(), &session.NewAggregate("sessionID", "org1").Aggregate, "domain.tld"),
+ session.NewTokenSetEvent(context.Background(), &session.NewAggregate("sessionID", "org1").Aggregate,
+ "tokenID",
+ ),
+ ),
+ ),
+ },
res{
want: &SessionChanged{
ObjectDetails: &domain.ObjectDetails{ResourceOwner: "org1"},
@@ -257,7 +258,7 @@ func TestCommands_CreateSession(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := &Commands{
- eventstore: tt.fields.eventstore,
+ eventstore: eventstoreExpect(t, tt.expect...),
idGenerator: tt.fields.idGenerator,
sessionTokenCreator: tt.fields.tokenCreator,
}
@@ -432,7 +433,7 @@ func TestCommands_updateSession(t *testing.T) {
ctx: context.Background(),
checks: &SessionCommands{
sessionWriteModel: NewSessionWriteModel("sessionID", "org1"),
- cmds: []SessionCommand{
+ sessionCommands: []SessionCommand{
func(ctx context.Context, cmd *SessionCommands) error {
return caos_errs.ThrowInternal(nil, "id", "check failed")
},
@@ -452,7 +453,7 @@ func TestCommands_updateSession(t *testing.T) {
ctx: context.Background(),
checks: &SessionCommands{
sessionWriteModel: NewSessionWriteModel("sessionID", "org1"),
- cmds: []SessionCommand{},
+ sessionCommands: []SessionCommand{},
},
},
res{
@@ -487,7 +488,7 @@ func TestCommands_updateSession(t *testing.T) {
ctx: context.Background(),
checks: &SessionCommands{
sessionWriteModel: NewSessionWriteModel("sessionID", "org1"),
- cmds: []SessionCommand{
+ sessionCommands: []SessionCommand{
CheckUser("userID"),
CheckPassword("password"),
},
@@ -499,12 +500,7 @@ func TestCommands_updateSession(t *testing.T) {
),
eventFromEventPusher(
user.NewHumanPasswordChangedEvent(context.Background(), &user.NewAggregate("userID", "org1").Aggregate,
- &crypto.CryptoValue{
- CryptoType: crypto.TypeHash,
- Algorithm: "hash",
- KeyID: "",
- Crypted: []byte("password"),
- }, false, ""),
+ "$plain$x$password", false, ""),
),
),
),
@@ -513,7 +509,7 @@ func TestCommands_updateSession(t *testing.T) {
"token",
nil
},
- userPasswordAlg: crypto.CreateMockHashAlg(gomock.NewController(t)),
+ hasher: mockPasswordHasher("x"),
now: func() time.Time {
return testNow
},
@@ -541,7 +537,7 @@ func TestCommands_updateSession(t *testing.T) {
ctx: context.Background(),
checks: &SessionCommands{
sessionWriteModel: NewSessionWriteModel("sessionID", "org1"),
- cmds: []SessionCommand{
+ sessionCommands: []SessionCommand{
CheckUser("userID"),
CheckIntent("intent", "aW50ZW50"),
},
@@ -580,7 +576,7 @@ func TestCommands_updateSession(t *testing.T) {
ctx: context.Background(),
checks: &SessionCommands{
sessionWriteModel: NewSessionWriteModel("sessionID", "org1"),
- cmds: []SessionCommand{
+ sessionCommands: []SessionCommand{
CheckUser("userID"),
CheckIntent("intent", "aW50ZW50"),
},
@@ -629,7 +625,7 @@ func TestCommands_updateSession(t *testing.T) {
ctx: context.Background(),
checks: &SessionCommands{
sessionWriteModel: NewSessionWriteModel("sessionID", "org1"),
- cmds: []SessionCommand{
+ sessionCommands: []SessionCommand{
CheckUser("userID"),
CheckIntent("intent2", "aW50ZW50"),
},
@@ -674,7 +670,7 @@ func TestCommands_updateSession(t *testing.T) {
ctx: context.Background(),
checks: &SessionCommands{
sessionWriteModel: NewSessionWriteModel("sessionID", "org1"),
- cmds: []SessionCommand{
+ sessionCommands: []SessionCommand{
CheckUser("userID"),
CheckIntent("intent", "aW50ZW50"),
},
diff --git a/internal/command/system_model.go b/internal/command/system_model.go
index 40847cfab..6637013d5 100644
--- a/internal/command/system_model.go
+++ b/internal/command/system_model.go
@@ -181,8 +181,5 @@ func (wm *SystemConfigWriteModel) changeConfig(validation *SystemConfigChangesVa
}
func (wm *SystemConfigWriteModel) changeURIs(validation *SystemConfigChangesValidation, inst *systemConfigChangesInstanceModel, commands *Commands, domain string) {
- if commands.checkUpdateConsoleRedirectURIs(domain, inst.RedirectUris, inst.PostLogoutRedirectUris) {
- return
- }
validation.Validations = append(validation.Validations, commands.prepareUpdateConsoleRedirectURIs(domain))
}
diff --git a/internal/command/user.go b/internal/command/user.go
index 34ba7c521..77b9acbcc 100644
--- a/internal/command/user.go
+++ b/internal/command/user.go
@@ -439,8 +439,8 @@ func ExistsUser(ctx context.Context, filter preparation.FilterToQueryReducer, id
return exists, nil
}
-func newUserInitCode(ctx context.Context, filter preparation.FilterToQueryReducer, alg crypto.EncryptionAlgorithm) (*CryptoCodeWithExpiry, error) {
- return newCryptoCodeWithExpiry(ctx, filter, domain.SecretGeneratorTypeInitCode, alg)
+func (c *Commands) newUserInitCode(ctx context.Context, filter preparation.FilterToQueryReducer, alg crypto.EncryptionAlgorithm) (*CryptoCode, error) {
+ return c.newCode(ctx, filter, domain.SecretGeneratorTypeInitCode, alg)
}
func userWriteModelByID(ctx context.Context, filter preparation.FilterToQueryReducer, userID, resourceOwner string) (*UserWriteModel, error) {
diff --git a/internal/command/user_human.go b/internal/command/user_human.go
index 061dc2232..1bbaec3ca 100644
--- a/internal/command/user_human.go
+++ b/internal/command/user_human.go
@@ -49,8 +49,8 @@ type AddHuman struct {
Phone Phone
// Password is optional
Password string
- // BcryptedPassword is optional
- BcryptedPassword string
+ // EncodedPasswordHash is optional
+ EncodedPasswordHash string
// PasswordChangeRequired is used if the `Password`-field is set
PasswordChangeRequired bool
Passwordless bool
@@ -74,7 +74,7 @@ type AddLink struct {
IDPExternalID string
}
-func (h *AddHuman) Validate() (err error) {
+func (h *AddHuman) Validate(hasher *crypto.PasswordHasher) (err error) {
if err := h.Email.Validate(); err != nil {
return err
}
@@ -101,6 +101,11 @@ func (h *AddHuman) Validate() (err error) {
return err
}
}
+ if h.EncodedPasswordHash != "" {
+ if !hasher.EncodingSupported(h.EncodedPasswordHash) {
+ return errors.ThrowInvalidArgument(nil, "USER-JDk4t", "Errors.User.Password.NotSupported")
+ }
+ }
return nil
}
@@ -127,7 +132,7 @@ func (c *Commands) AddHuman(ctx context.Context, resourceOwner string, human *Ad
c.AddHumanCommand(
human,
resourceOwner,
- c.userPasswordAlg,
+ c.userPasswordHasher,
c.userEncryption,
allowInitMail,
))
@@ -151,12 +156,13 @@ func (c *Commands) AddHuman(ctx context.Context, resourceOwner string, human *Ad
type humanCreationCommand interface {
eventstore.Command
AddPhoneData(phoneNumber domain.PhoneNumber)
- AddPasswordData(secret *crypto.CryptoValue, changeRequired bool)
+ AddPasswordData(encoded string, changeRequired bool)
}
-func (c *Commands) AddHumanCommand(human *AddHuman, orgID string, passwordAlg crypto.HashAlgorithm, codeAlg crypto.EncryptionAlgorithm, allowInitMail bool) preparation.Validation {
+//nolint:gocognit
+func (c *Commands) AddHumanCommand(human *AddHuman, orgID string, hasher *crypto.PasswordHasher, codeAlg crypto.EncryptionAlgorithm, allowInitMail bool) preparation.Validation {
return func() (_ preparation.CreateCommands, err error) {
- if err := human.Validate(); err != nil {
+ if err := human.Validate(hasher); err != nil {
return nil, err
}
@@ -210,7 +216,7 @@ func (c *Commands) AddHumanCommand(human *AddHuman, orgID string, passwordAlg cr
createCmd.AddPhoneData(human.Phone.Number)
}
- if err := addHumanCommandPassword(ctx, filter, createCmd, human, passwordAlg); err != nil {
+ if err := addHumanCommandPassword(ctx, filter, createCmd, human, hasher); err != nil {
return nil, err
}
@@ -258,7 +264,7 @@ func (c *Commands) addHumanCommandEmail(ctx context.Context, filter preparation.
// email not verified or
// user not registered and password set
if allowInitMail && human.shouldAddInitCode() {
- initCode, err := newUserInitCode(ctx, filter, codeAlg)
+ initCode, err := c.newUserInitCode(ctx, filter, codeAlg)
if err != nil {
return nil, err
}
@@ -292,7 +298,7 @@ func (c *Commands) addHumanCommandPhone(ctx context.Context, filter preparation.
if human.Phone.Verified {
return append(cmds, user.NewHumanPhoneVerifiedEvent(ctx, &a.Aggregate)), nil
}
- phoneCode, err := newPhoneCode(ctx, filter, codeAlg)
+ phoneCode, err := c.newPhoneCode(ctx, filter, codeAlg)
if err != nil {
return nil, err
}
@@ -316,13 +322,13 @@ func (c *Commands) addHumanCommandCheckID(ctx context.Context, filter preparatio
return nil
}
-func addHumanCommandPassword(ctx context.Context, filter preparation.FilterToQueryReducer, createCmd humanCreationCommand, human *AddHuman, passwordAlg crypto.HashAlgorithm) (err error) {
+func addHumanCommandPassword(ctx context.Context, filter preparation.FilterToQueryReducer, createCmd humanCreationCommand, human *AddHuman, hasher *crypto.PasswordHasher) (err error) {
if human.Password != "" {
if err = humanValidatePassword(ctx, filter, human.Password); err != nil {
return err
}
- secret, err := crypto.Hash([]byte(human.Password), passwordAlg)
+ secret, err := hasher.Hash(human.Password)
if err != nil {
return err
}
@@ -330,8 +336,8 @@ func addHumanCommandPassword(ctx context.Context, filter preparation.FilterToQue
return nil
}
- if human.BcryptedPassword != "" {
- createCmd.AddPasswordData(crypto.FillHash([]byte(human.BcryptedPassword), passwordAlg), human.PasswordChangeRequired)
+ if human.EncodedPasswordHash != "" {
+ createCmd.AddPasswordData(human.EncodedPasswordHash, human.PasswordChangeRequired)
}
return nil
}
@@ -578,7 +584,7 @@ func (c *Commands) createHuman(ctx context.Context, orgID string, human *domain.
human.EnsureDisplayName()
if human.Password != nil {
- if err := human.HashPasswordIfExisting(pwPolicy, c.userPasswordAlg, human.Password.ChangeRequired); err != nil {
+ if err := human.HashPasswordIfExisting(pwPolicy, c.userPasswordHasher, human.Password.ChangeRequired); err != nil {
return nil, nil, err
}
}
@@ -677,10 +683,10 @@ func createAddHumanEvent(ctx context.Context, aggregate *eventstore.Aggregate, h
human.StreetAddress)
}
if human.Password != nil {
- addEvent.AddPasswordData(human.Password.SecretCrypto, human.Password.ChangeRequired)
+ addEvent.AddPasswordData(human.Password.EncodedSecret, human.Password.ChangeRequired)
}
- if human.HashedPassword != nil {
- addEvent.AddPasswordData(human.HashedPassword.SecretCrypto, false)
+ if human.HashedPassword != "" {
+ addEvent.AddPasswordData(human.HashedPassword, false)
}
return addEvent
}
@@ -711,10 +717,10 @@ func createRegisterHumanEvent(ctx context.Context, aggregate *eventstore.Aggrega
human.StreetAddress)
}
if human.Password != nil {
- addEvent.AddPasswordData(human.Password.SecretCrypto, human.Password.ChangeRequired)
+ addEvent.AddPasswordData(human.Password.EncodedSecret, human.Password.ChangeRequired)
}
- if human.HashedPassword != nil {
- addEvent.AddPasswordData(human.HashedPassword.SecretCrypto, false)
+ if human.HashedPassword != "" {
+ addEvent.AddPasswordData(human.HashedPassword, false)
}
return addEvent
}
diff --git a/internal/command/user_human_init.go b/internal/command/user_human_init.go
index 74deb4ced..5af8eb9a5 100644
--- a/internal/command/user_human_init.go
+++ b/internal/command/user_human_init.go
@@ -4,6 +4,7 @@ import (
"context"
"github.com/zitadel/logging"
+
"github.com/zitadel/zitadel/internal/crypto"
"github.com/zitadel/zitadel/internal/domain"
caos_errs "github.com/zitadel/zitadel/internal/errors"
@@ -49,7 +50,7 @@ func (c *Commands) ResendInitialMail(ctx context.Context, userID string, email d
return writeModelToObjectDetails(&existingCode.WriteModel), nil
}
-func (c *Commands) HumanVerifyInitCode(ctx context.Context, userID, resourceOwner, code, passwordString string, initCodeGenerator crypto.Generator) error {
+func (c *Commands) HumanVerifyInitCode(ctx context.Context, userID, resourceOwner, code, password string, initCodeGenerator crypto.Generator) error {
if userID == "" {
return caos_errs.ThrowInvalidArgument(nil, "COMMAND-mkM9f", "Errors.User.UserIDMissing")
}
@@ -72,26 +73,22 @@ func (c *Commands) HumanVerifyInitCode(ctx context.Context, userID, resourceOwne
logging.WithFields("userID", userAgg.ID).OnError(err).Error("NewHumanInitializedCheckFailedEvent push failed")
return caos_errs.ThrowInvalidArgument(err, "COMMAND-11v6G", "Errors.User.Code.Invalid")
}
- events := []eventstore.Command{
+ commands := []eventstore.Command{
user.NewHumanInitializedCheckSucceededEvent(ctx, userAgg),
}
if !existingCode.IsEmailVerified {
- events = append(events, user.NewHumanEmailVerifiedEvent(ctx, userAgg))
+ commands = append(commands, user.NewHumanEmailVerifiedEvent(ctx, userAgg))
}
- if passwordString != "" {
+ if password != "" {
passwordWriteModel := NewHumanPasswordWriteModel(userID, existingCode.ResourceOwner)
passwordWriteModel.UserState = domain.UserStateActive
- password := &domain.Password{
- SecretString: passwordString,
- ChangeRequired: false,
- }
- passwordEvent, err := c.changePassword(ctx, "", password, userAgg, passwordWriteModel)
+ passwordCommand, err := c.setPasswordCommand(ctx, passwordWriteModel, password, false)
if err != nil {
return err
}
- events = append(events, passwordEvent)
+ commands = append(commands, passwordCommand)
}
- _, err = c.eventstore.Push(ctx, events...)
+ _, err = c.eventstore.Push(ctx, commands...)
return err
}
diff --git a/internal/command/user_human_init_test.go b/internal/command/user_human_init_test.go
index 838459bd2..69fce8366 100644
--- a/internal/command/user_human_init_test.go
+++ b/internal/command/user_human_init_test.go
@@ -5,7 +5,6 @@ import (
"testing"
"time"
- "github.com/golang/mock/gomock"
"github.com/stretchr/testify/assert"
"golang.org/x/text/language"
@@ -305,8 +304,8 @@ func TestCommandSide_ResendInitialMail(t *testing.T) {
func TestCommandSide_VerifyInitCode(t *testing.T) {
type fields struct {
- eventstore *eventstore.Eventstore
- userPasswordAlg crypto.HashAlgorithm
+ eventstore *eventstore.Eventstore
+ userPasswordHasher *crypto.PasswordHasher
}
type args struct {
ctx context.Context
@@ -584,18 +583,13 @@ func TestCommandSide_VerifyInitCode(t *testing.T) {
eventFromEventPusher(
user.NewHumanPasswordChangedEvent(context.Background(),
&user.NewAggregate("user1", "org1").Aggregate,
- &crypto.CryptoValue{
- CryptoType: crypto.TypeHash,
- Algorithm: "hash",
- KeyID: "",
- Crypted: []byte("password"),
- },
+ "$plain$x$password",
false,
"")),
},
),
),
- userPasswordAlg: crypto.CreateMockHashAlg(gomock.NewController(t)),
+ userPasswordHasher: mockPasswordHasher("x"),
},
args: args{
ctx: context.Background(),
@@ -615,8 +609,8 @@ func TestCommandSide_VerifyInitCode(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := &Commands{
- eventstore: tt.fields.eventstore,
- userPasswordAlg: tt.fields.userPasswordAlg,
+ eventstore: tt.fields.eventstore,
+ userPasswordHasher: tt.fields.userPasswordHasher,
}
err := r.HumanVerifyInitCode(tt.args.ctx, tt.args.userID, tt.args.resourceOwner, tt.args.code, tt.args.password, tt.args.secretGenerator)
if tt.res.err == nil {
diff --git a/internal/command/user_human_otp_test.go b/internal/command/user_human_otp_test.go
index e11f771e6..ae44489ae 100644
--- a/internal/command/user_human_otp_test.go
+++ b/internal/command/user_human_otp_test.go
@@ -528,7 +528,7 @@ func TestCommands_createHumanOTP(t *testing.T) {
}
func TestCommands_HumanCheckMFAOTPSetup(t *testing.T) {
- ctx := authz.NewMockContext("inst1", "org1", "user1")
+ ctx := authz.NewMockContext("", "org1", "user1")
cryptoAlg := crypto.CreateMockEncryptionAlg(gomock.NewController(t))
key, secret, err := domain.NewOTPKey("example.com", "user1", cryptoAlg)
diff --git a/internal/command/user_human_password.go b/internal/command/user_human_password.go
index 8d54727e2..0a2992a46 100644
--- a/internal/command/user_human_password.go
+++ b/internal/command/user_human_password.go
@@ -2,8 +2,10 @@ package command
import (
"context"
+ "errors"
"github.com/zitadel/logging"
+ "github.com/zitadel/passwap"
"github.com/zitadel/zitadel/internal/crypto"
"github.com/zitadel/zitadel/internal/domain"
@@ -13,80 +15,75 @@ import (
"github.com/zitadel/zitadel/internal/telemetry/tracing"
)
-func (c *Commands) SetPassword(ctx context.Context, orgID, userID, passwordString string, oneTime bool) (objectDetails *domain.ObjectDetails, err error) {
+func (c *Commands) SetPassword(ctx context.Context, orgID, userID, password string, oneTime bool) (objectDetails *domain.ObjectDetails, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
if userID == "" {
return nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-3M0fs", "Errors.IDMissing")
}
- existingPassword, err := c.passwordWriteModel(ctx, userID, orgID)
+ wm, err := c.passwordWriteModel(ctx, userID, orgID)
if err != nil {
return nil, err
}
- if !existingPassword.UserState.Exists() {
+ if !wm.UserState.Exists() {
return nil, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-3M0fs", "Errors.User.NotFound")
}
- if err = c.checkPermission(ctx, domain.PermissionUserWrite, existingPassword.ResourceOwner, userID); err != nil {
+ if err = c.checkPermission(ctx, domain.PermissionUserWrite, wm.ResourceOwner, userID); err != nil {
return nil, err
}
- password := &domain.Password{
- SecretString: passwordString,
- ChangeRequired: oneTime,
- }
- userAgg := UserAggregateFromWriteModel(&existingPassword.WriteModel)
- passwordEvent, err := c.changePassword(ctx, "", password, userAgg, existingPassword)
- if err != nil {
- return nil, err
- }
- pushedEvents, err := c.eventstore.Push(ctx, passwordEvent)
- if err != nil {
- return nil, err
- }
- err = AppendAndReduce(existingPassword, pushedEvents...)
- if err != nil {
- return nil, err
- }
- return writeModelToObjectDetails(&existingPassword.WriteModel), nil
+ return c.setPassword(ctx, wm, password, oneTime)
}
-func (c *Commands) SetPasswordWithVerifyCode(ctx context.Context, orgID, userID, code, passwordString, userAgentID string) (objectDetails *domain.ObjectDetails, err error) {
+func (c *Commands) SetPasswordWithVerifyCode(ctx context.Context, orgID, userID, code, password, userAgentID string) (objectDetails *domain.ObjectDetails, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
if userID == "" {
return nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-3M9fs", "Errors.IDMissing")
}
- if passwordString == "" {
+ if password == "" {
return nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-Mf0sd", "Errors.User.Password.Empty")
}
- existingCode, err := c.passwordWriteModel(ctx, userID, orgID)
+ wm, err := c.passwordWriteModel(ctx, userID, orgID)
if err != nil {
return nil, err
}
- if existingCode.Code == nil || existingCode.UserState == domain.UserStateUnspecified || existingCode.UserState == domain.UserStateDeleted {
+ if wm.Code == nil || wm.UserState == domain.UserStateUnspecified || wm.UserState == domain.UserStateDeleted {
return nil, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-2M9fs", "Errors.User.Code.NotFound")
}
- err = crypto.VerifyCodeWithAlgorithm(existingCode.CodeCreationDate, existingCode.CodeExpiry, existingCode.Code, code, c.userEncryption)
+ err = crypto.VerifyCodeWithAlgorithm(wm.CodeCreationDate, wm.CodeExpiry, wm.Code, code, c.userEncryption)
if err != nil {
return nil, err
}
- password := &domain.Password{
- SecretString: passwordString,
- ChangeRequired: false,
- }
- userAgg := UserAggregateFromWriteModel(&existingCode.WriteModel)
- passwordEvent, err := c.changePassword(ctx, userAgentID, password, userAgg, existingCode)
+ return c.setPassword(ctx, wm, password, false)
+}
+
+func (c *Commands) setPassword(ctx context.Context, wm *HumanPasswordWriteModel, password string, changeRequired bool) (objectDetails *domain.ObjectDetails, err error) {
+ command, err := c.setPasswordCommand(ctx, wm, password, changeRequired)
if err != nil {
return nil, err
}
- err = c.pushAppendAndReduce(ctx, existingCode, passwordEvent)
+ err = c.pushAppendAndReduce(ctx, wm, command)
if err != nil {
return nil, err
}
- return writeModelToObjectDetails(&existingCode.WriteModel), nil
+ return writeModelToObjectDetails(&wm.WriteModel), nil
+}
+
+func (c *Commands) setPasswordCommand(ctx context.Context, wm *HumanPasswordWriteModel, password string, changeRequired bool) (_ eventstore.Command, err error) {
+ if err = c.canUpdatePassword(ctx, password, wm); err != nil {
+ return nil, err
+ }
+ ctx, span := tracing.NewNamedSpan(ctx, "passwap.Hash")
+ encoded, err := c.userPasswordHasher.Hash(password)
+ span.EndWithError(err)
+ if err = convertPasswapErr(err); err != nil {
+ return nil, err
+ }
+ return user.NewHumanPasswordChangedEvent(ctx, UserAggregateFromWriteModel(&wm.WriteModel), encoded, changeRequired, ""), nil
}
func (c *Commands) ChangePassword(ctx context.Context, orgID, userID, oldPassword, newPassword, userAgentID string) (objectDetails *domain.ObjectDetails, err error) {
@@ -99,59 +96,50 @@ func (c *Commands) ChangePassword(ctx context.Context, orgID, userID, oldPasswor
if oldPassword == "" || newPassword == "" {
return nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-3M0fs", "Errors.User.Password.Empty")
}
- existingPassword, err := c.passwordWriteModel(ctx, userID, orgID)
+ wm, err := c.passwordWriteModel(ctx, userID, orgID)
if err != nil {
return nil, err
}
- if existingPassword.Secret == nil {
+ if wm.EncodedHash == "" {
return nil, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-Fds3s", "Errors.User.Password.Empty")
}
- ctx, spanPasswordComparison := tracing.NewNamedSpan(ctx, "crypto.CompareHash")
- err = crypto.CompareHash(existingPassword.Secret, []byte(oldPassword), c.userPasswordAlg)
- spanPasswordComparison.EndWithError(err)
-
- if err != nil {
- return nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-3M0fs", "Errors.User.Password.Invalid")
- }
- password := &domain.Password{
- SecretString: newPassword,
- ChangeRequired: false,
+ if err = c.canUpdatePassword(ctx, newPassword, wm); err != nil {
+ return nil, err
}
- userAgg := UserAggregateFromWriteModel(&existingPassword.WriteModel)
- command, err := c.changePassword(ctx, userAgentID, password, userAgg, existingPassword)
+ ctx, spanPasswap := tracing.NewNamedSpan(ctx, "passwap.VerifyAndUpdate")
+ updated, err := c.userPasswordHasher.VerifyAndUpdate(wm.EncodedHash, oldPassword, newPassword)
+ spanPasswap.EndWithError(err)
+ if err = convertPasswapErr(err); err != nil {
+ return nil, err
+ }
+ err = c.pushAppendAndReduce(ctx, wm,
+ user.NewHumanPasswordChangedEvent(ctx, UserAggregateFromWriteModel(&wm.WriteModel), updated, false, userAgentID))
if err != nil {
return nil, err
}
- pushedEvents, err := c.eventstore.Push(ctx, command)
- if err != nil {
- return nil, err
- }
- err = AppendAndReduce(existingPassword, pushedEvents...)
- if err != nil {
- return nil, err
- }
- return writeModelToObjectDetails(&existingPassword.WriteModel), nil
+ return writeModelToObjectDetails(&wm.WriteModel), nil
}
-func (c *Commands) changePassword(ctx context.Context, userAgentID string, password *domain.Password, userAgg *eventstore.Aggregate, existingPassword *HumanPasswordWriteModel) (event eventstore.Command, err error) {
+func (c *Commands) canUpdatePassword(ctx context.Context, newPassword string, wm *HumanPasswordWriteModel) (err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
- if existingPassword.UserState == domain.UserStateUnspecified || existingPassword.UserState == domain.UserStateDeleted {
- return nil, caos_errs.ThrowNotFound(nil, "COMMAND-G8dh3", "Errors.User.Password.NotFound")
+ if wm.UserState == domain.UserStateUnspecified || wm.UserState == domain.UserStateDeleted {
+ return caos_errs.ThrowNotFound(nil, "COMMAND-G8dh3", "Errors.User.Password.NotFound")
}
- if existingPassword.UserState == domain.UserStateInitial {
- return nil, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-M9dse", "Errors.User.NotInitialised")
+ if wm.UserState == domain.UserStateInitial {
+ return caos_errs.ThrowPreconditionFailed(nil, "COMMAND-M9dse", "Errors.User.NotInitialised")
}
- pwPolicy, err := c.getOrgPasswordComplexityPolicy(ctx, userAgg.ResourceOwner)
+ policy, err := c.getOrgPasswordComplexityPolicy(ctx, wm.ResourceOwner)
if err != nil {
- return nil, err
+ return err
}
- if err := password.HashPasswordIfExisting(pwPolicy, c.userPasswordAlg); err != nil {
- return nil, err
+
+ if err := policy.Check(newPassword); err != nil {
+ return err
}
- return user.NewHumanPasswordChangedEvent(ctx, userAgg, password.SecretCrypto, password.ChangeRequired, userAgentID), nil
+ return nil
}
func (c *Commands) RequestSetPassword(ctx context.Context, userID, resourceOwner string, notifyType domain.NotificationType, passwordVerificationCode crypto.Generator) (objectDetails *domain.ObjectDetails, err error) {
@@ -238,37 +226,44 @@ func (c *Commands) HumanCheckPassword(ctx context.Context, orgID, userID, passwo
return caos_errs.ThrowPreconditionFailed(err, "COMMAND-Dft32", "Errors.Org.LoginPolicy.UsernamePasswordNotAllowed")
}
- existingPassword, err := c.passwordWriteModel(ctx, userID, orgID)
+ wm, err := c.passwordWriteModel(ctx, userID, orgID)
if err != nil {
return err
}
- if existingPassword.UserState == domain.UserStateUnspecified || existingPassword.UserState == domain.UserStateDeleted {
+ if wm.UserState == domain.UserStateUnspecified || wm.UserState == domain.UserStateDeleted {
return caos_errs.ThrowPreconditionFailed(nil, "COMMAND-3n77z", "Errors.User.NotFound")
}
- if existingPassword.Secret == nil {
+ if wm.EncodedHash == "" {
return caos_errs.ThrowPreconditionFailed(nil, "COMMAND-3n77z", "Errors.User.Password.NotSet")
}
- userAgg := UserAggregateFromWriteModel(&existingPassword.WriteModel)
- ctx, spanPasswordComparison := tracing.NewNamedSpan(ctx, "crypto.CompareHash")
- err = crypto.CompareHash(existingPassword.Secret, []byte(password), c.userPasswordAlg)
+ userAgg := UserAggregateFromWriteModel(&wm.WriteModel)
+ ctx, spanPasswordComparison := tracing.NewNamedSpan(ctx, "passwap.Verify")
+ updated, err := c.userPasswordHasher.Verify(wm.EncodedHash, password)
spanPasswordComparison.EndWithError(err)
+ err = convertPasswapErr(err)
+
+ commands := make([]eventstore.Command, 0, 2)
if err == nil {
- _, err = c.eventstore.Push(ctx, user.NewHumanPasswordCheckSucceededEvent(ctx, userAgg, authRequestDomainToAuthRequestInfo(authRequest)))
+ commands = append(commands, user.NewHumanPasswordCheckSucceededEvent(ctx, userAgg, authRequestDomainToAuthRequestInfo(authRequest)))
+ if updated != "" {
+ commands = append(commands, user.NewHumanPasswordHashUpdatedEvent(ctx, userAgg, updated))
+ }
+ _, err = c.eventstore.Push(ctx, commands...)
return err
}
- events := make([]eventstore.Command, 0)
- events = append(events, user.NewHumanPasswordCheckFailedEvent(ctx, userAgg, authRequestDomainToAuthRequestInfo(authRequest)))
+
+ commands = append(commands, user.NewHumanPasswordCheckFailedEvent(ctx, userAgg, authRequestDomainToAuthRequestInfo(authRequest)))
if lockoutPolicy != nil && lockoutPolicy.MaxPasswordAttempts > 0 {
- if existingPassword.PasswordCheckFailedCount+1 >= lockoutPolicy.MaxPasswordAttempts {
- events = append(events, user.NewUserLockedEvent(ctx, userAgg))
+ if wm.PasswordCheckFailedCount+1 >= lockoutPolicy.MaxPasswordAttempts {
+ commands = append(commands, user.NewUserLockedEvent(ctx, userAgg))
}
}
- _, err = c.eventstore.Push(ctx, events...)
- logging.Log("COMMAND-9fj7s").OnError(err).Error("error create password check failed event")
- return caos_errs.ThrowInvalidArgument(nil, "COMMAND-452ad", "Errors.User.Password.Invalid")
+ _, pushErr := c.eventstore.Push(ctx, commands...)
+ logging.OnError(pushErr).Error("error create password check failed event")
+ return err
}
func (c *Commands) passwordWriteModel(ctx context.Context, userID, resourceOwner string) (writeModel *HumanPasswordWriteModel, err error) {
@@ -282,3 +277,16 @@ func (c *Commands) passwordWriteModel(ctx context.Context, userID, resourceOwner
}
return writeModel, nil
}
+
+func convertPasswapErr(err error) error {
+ if err == nil {
+ return nil
+ }
+ if errors.Is(err, passwap.ErrPasswordMismatch) {
+ return caos_errs.ThrowInvalidArgument(err, "COMMAND-3M0fs", "Errors.User.Password.Invalid")
+ }
+ if errors.Is(err, passwap.ErrPasswordNoChange) {
+ return caos_errs.ThrowPreconditionFailed(err, "COMMAND-Aesh5", "Errors.User.Password.NotChanged")
+ }
+ return caos_errs.ThrowInternal(err, "COMMAND-CahN2", "Errors.Internal")
+}
diff --git a/internal/command/user_human_password_model.go b/internal/command/user_human_password_model.go
index 68513a853..c0ca58f58 100644
--- a/internal/command/user_human_password_model.go
+++ b/internal/command/user_human_password_model.go
@@ -13,7 +13,7 @@ import (
type HumanPasswordWriteModel struct {
eventstore.WriteModel
- Secret *crypto.CryptoValue
+ EncodedHash string
SecretChangeRequired bool
Code *crypto.CryptoValue
@@ -37,11 +37,11 @@ func (wm *HumanPasswordWriteModel) Reduce() error {
for _, event := range wm.Events {
switch e := event.(type) {
case *user.HumanAddedEvent:
- wm.Secret = e.Secret
+ wm.EncodedHash = user.SecretOrEncodedHash(e.Secret, e.EncodedHash)
wm.SecretChangeRequired = e.ChangeRequired
wm.UserState = domain.UserStateActive
case *user.HumanRegisteredEvent:
- wm.Secret = e.Secret
+ wm.EncodedHash = user.SecretOrEncodedHash(e.Secret, e.EncodedHash)
wm.SecretChangeRequired = e.ChangeRequired
wm.UserState = domain.UserStateActive
case *user.HumanInitialCodeAddedEvent:
@@ -49,7 +49,7 @@ func (wm *HumanPasswordWriteModel) Reduce() error {
case *user.HumanInitializedCheckSucceededEvent:
wm.UserState = domain.UserStateActive
case *user.HumanPasswordChangedEvent:
- wm.Secret = e.Secret
+ wm.EncodedHash = user.SecretOrEncodedHash(e.Secret, e.EncodedHash)
wm.SecretChangeRequired = e.ChangeRequired
wm.Code = nil
wm.PasswordCheckFailedCount = 0
@@ -69,6 +69,8 @@ func (wm *HumanPasswordWriteModel) Reduce() error {
wm.PasswordCheckFailedCount = 0
case *user.UserRemovedEvent:
wm.UserState = domain.UserStateDeleted
+ case *user.HumanPasswordHashUpdatedEvent:
+ wm.EncodedHash = e.EncodedHash
}
}
return wm.WriteModel.Reduce()
@@ -98,7 +100,8 @@ func (wm *HumanPasswordWriteModel) Query() *eventstore.SearchQueryBuilder {
user.UserV1PasswordCodeAddedType,
user.UserV1EmailVerifiedType,
user.UserV1PasswordCheckFailedType,
- user.UserV1PasswordCheckSucceededType).
+ user.UserV1PasswordCheckSucceededType,
+ user.UserV1PasswordHashUpdatedType).
Builder()
if wm.ResourceOwner != "" {
diff --git a/internal/command/user_human_password_test.go b/internal/command/user_human_password_test.go
index 6eabda952..0a59b2ad3 100644
--- a/internal/command/user_human_password_test.go
+++ b/internal/command/user_human_password_test.go
@@ -3,11 +3,13 @@ package command
import (
"context"
"errors"
+ "io"
"testing"
"time"
"github.com/golang/mock/gomock"
"github.com/stretchr/testify/assert"
+ "github.com/zitadel/passwap"
"golang.org/x/text/language"
"github.com/zitadel/zitadel/internal/crypto"
@@ -21,9 +23,9 @@ import (
func TestCommandSide_SetOneTimePassword(t *testing.T) {
type fields struct {
- eventstore *eventstore.Eventstore
- userPasswordAlg crypto.HashAlgorithm
- checkPermission domain.PermissionCheck
+ eventstore *eventstore.Eventstore
+ userPasswordHasher *crypto.PasswordHasher
+ checkPermission domain.PermissionCheck
}
type args struct {
ctx context.Context
@@ -101,8 +103,8 @@ func TestCommandSide_SetOneTimePassword(t *testing.T) {
),
),
),
- userPasswordAlg: crypto.CreateMockHashAlg(gomock.NewController(t)),
- checkPermission: newMockPermissionCheckNotAllowed(),
+ userPasswordHasher: mockPasswordHasher("x"),
+ checkPermission: newMockPermissionCheckNotAllowed(),
},
args: args{
ctx: context.Background(),
@@ -160,12 +162,7 @@ func TestCommandSide_SetOneTimePassword(t *testing.T) {
eventFromEventPusher(
user.NewHumanPasswordChangedEvent(context.Background(),
&user.NewAggregate("user1", "org1").Aggregate,
- &crypto.CryptoValue{
- CryptoType: crypto.TypeHash,
- Algorithm: "hash",
- KeyID: "",
- Crypted: []byte("password"),
- },
+ "$plain$x$password",
true,
"",
),
@@ -173,8 +170,8 @@ func TestCommandSide_SetOneTimePassword(t *testing.T) {
},
),
),
- userPasswordAlg: crypto.CreateMockHashAlg(gomock.NewController(t)),
- checkPermission: newMockPermissionCheckAllowed(),
+ userPasswordHasher: mockPasswordHasher("x"),
+ checkPermission: newMockPermissionCheckAllowed(),
},
args: args{
ctx: context.Background(),
@@ -232,12 +229,7 @@ func TestCommandSide_SetOneTimePassword(t *testing.T) {
eventFromEventPusher(
user.NewHumanPasswordChangedEvent(context.Background(),
&user.NewAggregate("user1", "org1").Aggregate,
- &crypto.CryptoValue{
- CryptoType: crypto.TypeHash,
- Algorithm: "hash",
- KeyID: "",
- Crypted: []byte("password"),
- },
+ "$plain$x$password",
false,
"",
),
@@ -245,8 +237,8 @@ func TestCommandSide_SetOneTimePassword(t *testing.T) {
},
),
),
- userPasswordAlg: crypto.CreateMockHashAlg(gomock.NewController(t)),
- checkPermission: newMockPermissionCheckAllowed(),
+ userPasswordHasher: mockPasswordHasher("x"),
+ checkPermission: newMockPermissionCheckAllowed(),
},
args: args{
ctx: context.Background(),
@@ -265,9 +257,9 @@ func TestCommandSide_SetOneTimePassword(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := &Commands{
- eventstore: tt.fields.eventstore,
- userPasswordAlg: tt.fields.userPasswordAlg,
- checkPermission: tt.fields.checkPermission,
+ eventstore: tt.fields.eventstore,
+ userPasswordHasher: tt.fields.userPasswordHasher,
+ checkPermission: tt.fields.checkPermission,
}
got, err := r.SetPassword(tt.args.ctx, tt.args.resourceOwner, tt.args.userID, tt.args.password, tt.args.oneTime)
if tt.res.err == nil {
@@ -285,9 +277,9 @@ func TestCommandSide_SetOneTimePassword(t *testing.T) {
func TestCommandSide_SetPasswordWithVerifyCode(t *testing.T) {
type fields struct {
- eventstore *eventstore.Eventstore
- userEncryption crypto.EncryptionAlgorithm
- userPasswordAlg crypto.HashAlgorithm
+ eventstore *eventstore.Eventstore
+ userEncryption crypto.EncryptionAlgorithm
+ userPasswordHasher *crypto.PasswordHasher
}
type args struct {
ctx context.Context
@@ -494,12 +486,7 @@ func TestCommandSide_SetPasswordWithVerifyCode(t *testing.T) {
eventFromEventPusher(
user.NewHumanPasswordChangedEvent(context.Background(),
&user.NewAggregate("user1", "org1").Aggregate,
- &crypto.CryptoValue{
- CryptoType: crypto.TypeHash,
- Algorithm: "hash",
- KeyID: "",
- Crypted: []byte("password"),
- },
+ "$plain$x$password",
false,
"",
),
@@ -507,8 +494,8 @@ func TestCommandSide_SetPasswordWithVerifyCode(t *testing.T) {
},
),
),
- userPasswordAlg: crypto.CreateMockHashAlg(gomock.NewController(t)),
- userEncryption: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
+ userPasswordHasher: mockPasswordHasher("x"),
+ userEncryption: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
},
args: args{
ctx: context.Background(),
@@ -527,9 +514,9 @@ func TestCommandSide_SetPasswordWithVerifyCode(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := &Commands{
- eventstore: tt.fields.eventstore,
- userPasswordAlg: tt.fields.userPasswordAlg,
- userEncryption: tt.fields.userEncryption,
+ eventstore: tt.fields.eventstore,
+ userPasswordHasher: tt.fields.userPasswordHasher,
+ userEncryption: tt.fields.userEncryption,
}
got, err := r.SetPasswordWithVerifyCode(tt.args.ctx, tt.args.resourceOwner, tt.args.userID, tt.args.code, tt.args.password, tt.args.agentID)
if tt.res.err == nil {
@@ -547,8 +534,7 @@ func TestCommandSide_SetPasswordWithVerifyCode(t *testing.T) {
func TestCommandSide_ChangePassword(t *testing.T) {
type fields struct {
- eventstore *eventstore.Eventstore
- userPasswordAlg crypto.HashAlgorithm
+ userPasswordHasher *crypto.PasswordHasher
}
type args struct {
ctx context.Context
@@ -566,67 +552,54 @@ func TestCommandSide_ChangePassword(t *testing.T) {
name string
fields fields
args args
+ expect []expect
res res
}{
{
- name: "userid missing, invalid argument error",
- fields: fields{
- eventstore: eventstoreExpect(
- t,
- ),
- },
+ name: "userid missing, invalid argument error",
+ fields: fields{},
args: args{
ctx: context.Background(),
oldPassword: "password",
newPassword: "password1",
resourceOwner: "org1",
},
+ expect: []expect{},
res: res{
err: caos_errs.IsErrorInvalidArgument,
},
},
{
- name: "old password missing, invalid argument error",
- fields: fields{
- eventstore: eventstoreExpect(
- t,
- ),
- },
+ name: "old password missing, invalid argument error",
+ fields: fields{},
args: args{
ctx: context.Background(),
userID: "user1",
newPassword: "password1",
resourceOwner: "org1",
},
+ expect: []expect{},
res: res{
err: caos_errs.IsErrorInvalidArgument,
},
},
{
- name: "new password missing, invalid argument error",
- fields: fields{
- eventstore: eventstoreExpect(
- t,
- ),
- },
+ name: "new password missing, invalid argument error",
+ fields: fields{},
args: args{
ctx: context.Background(),
userID: "user1",
oldPassword: "password",
resourceOwner: "org1",
},
+ expect: []expect{},
res: res{
err: caos_errs.IsErrorInvalidArgument,
},
},
{
- name: "user not existing, precondition error",
- fields: fields{
- eventstore: eventstoreExpect(
- t,
- expectFilter(),
- ),
- },
+ name: "user not existing, precondition error",
+ fields: fields{},
args: args{
ctx: context.Background(),
userID: "user1",
@@ -634,6 +607,9 @@ func TestCommandSide_ChangePassword(t *testing.T) {
oldPassword: "password",
newPassword: "password1",
},
+ expect: []expect{
+ expectFilter(),
+ },
res: res{
err: caos_errs.IsPreconditionFailed,
},
@@ -641,26 +617,7 @@ func TestCommandSide_ChangePassword(t *testing.T) {
{
name: "existing password empty, precondition error",
fields: fields{
- eventstore: eventstoreExpect(
- t,
- expectFilter(
- eventFromEventPusher(
- user.NewHumanAddedEvent(context.Background(),
- &user.NewAggregate("user1", "org1").Aggregate,
- "username",
- "firstname",
- "lastname",
- "nickname",
- "displayname",
- language.German,
- domain.GenderUnspecified,
- "email@test.ch",
- true,
- ),
- ),
- ),
- ),
- userPasswordAlg: crypto.CreateMockHashAlg(gomock.NewController(t)),
+ userPasswordHasher: mockPasswordHasher("x"),
},
args: args{
ctx: context.Background(),
@@ -669,49 +626,32 @@ func TestCommandSide_ChangePassword(t *testing.T) {
newPassword: "password1",
resourceOwner: "org1",
},
+ expect: []expect{
+ expectFilter(
+ eventFromEventPusher(
+ user.NewHumanAddedEvent(context.Background(),
+ &user.NewAggregate("user1", "org1").Aggregate,
+ "username",
+ "firstname",
+ "lastname",
+ "nickname",
+ "displayname",
+ language.German,
+ domain.GenderUnspecified,
+ "email@test.ch",
+ true,
+ ),
+ ),
+ ),
+ },
res: res{
err: caos_errs.IsPreconditionFailed,
},
},
{
- name: "password not matching, precondition error",
+ name: "password not matching, invalid argument error",
fields: fields{
- eventstore: eventstoreExpect(
- t,
- expectFilter(
- eventFromEventPusher(
- user.NewHumanAddedEvent(context.Background(),
- &user.NewAggregate("user1", "org1").Aggregate,
- "username",
- "firstname",
- "lastname",
- "nickname",
- "displayname",
- language.German,
- domain.GenderUnspecified,
- "email@test.ch",
- true,
- ),
- ),
- eventFromEventPusher(
- user.NewHumanEmailVerifiedEvent(context.Background(),
- &user.NewAggregate("user1", "org1").Aggregate,
- ),
- ),
- eventFromEventPusher(
- user.NewHumanPasswordChangedEvent(context.Background(),
- &user.NewAggregate("user1", "org1").Aggregate,
- &crypto.CryptoValue{
- CryptoType: crypto.TypeHash,
- Algorithm: "hash",
- KeyID: "",
- Crypted: []byte("password"),
- },
- false,
- "")),
- ),
- ),
- userPasswordAlg: crypto.CreateMockHashAlg(gomock.NewController(t)),
+ userPasswordHasher: mockPasswordHasher("x"),
},
args: args{
ctx: context.Background(),
@@ -720,6 +660,47 @@ func TestCommandSide_ChangePassword(t *testing.T) {
newPassword: "password1",
resourceOwner: "org1",
},
+ expect: []expect{
+ expectFilter(
+ eventFromEventPusher(
+ user.NewHumanAddedEvent(context.Background(),
+ &user.NewAggregate("user1", "org1").Aggregate,
+ "username",
+ "firstname",
+ "lastname",
+ "nickname",
+ "displayname",
+ language.German,
+ domain.GenderUnspecified,
+ "email@test.ch",
+ true,
+ ),
+ ),
+ eventFromEventPusher(
+ user.NewHumanEmailVerifiedEvent(context.Background(),
+ &user.NewAggregate("user1", "org1").Aggregate,
+ ),
+ ),
+ eventFromEventPusher(
+ user.NewHumanPasswordChangedEvent(context.Background(),
+ &user.NewAggregate("user1", "org1").Aggregate,
+ "$plain$x$password",
+ false,
+ "")),
+ ),
+ expectFilter(
+ eventFromEventPusher(
+ org.NewPasswordComplexityPolicyAddedEvent(context.Background(),
+ &user.NewAggregate("user1", "org1").Aggregate,
+ 1,
+ false,
+ false,
+ false,
+ false,
+ ),
+ ),
+ ),
+ },
res: res{
err: caos_errs.IsErrorInvalidArgument,
},
@@ -727,71 +708,7 @@ func TestCommandSide_ChangePassword(t *testing.T) {
{
name: "change password, ok",
fields: fields{
- eventstore: eventstoreExpect(
- t,
- expectFilter(
- eventFromEventPusher(
- user.NewHumanAddedEvent(context.Background(),
- &user.NewAggregate("user1", "org1").Aggregate,
- "username",
- "firstname",
- "lastname",
- "nickname",
- "displayname",
- language.German,
- domain.GenderUnspecified,
- "email@test.ch",
- true,
- ),
- ),
- eventFromEventPusher(
- user.NewHumanEmailVerifiedEvent(context.Background(),
- &user.NewAggregate("user1", "org1").Aggregate,
- ),
- ),
- eventFromEventPusher(
- user.NewHumanPasswordChangedEvent(context.Background(),
- &user.NewAggregate("user1", "org1").Aggregate,
- &crypto.CryptoValue{
- CryptoType: crypto.TypeHash,
- Algorithm: "hash",
- KeyID: "",
- Crypted: []byte("password"),
- },
- false,
- "")),
- ),
- expectFilter(
- eventFromEventPusher(
- org.NewPasswordComplexityPolicyAddedEvent(context.Background(),
- &user.NewAggregate("user1", "org1").Aggregate,
- 1,
- false,
- false,
- false,
- false,
- ),
- ),
- ),
- expectPush(
- []*repository.Event{
- eventFromEventPusher(
- user.NewHumanPasswordChangedEvent(context.Background(),
- &user.NewAggregate("user1", "org1").Aggregate,
- &crypto.CryptoValue{
- CryptoType: crypto.TypeHash,
- Algorithm: "hash",
- KeyID: "",
- Crypted: []byte("password1"),
- },
- false,
- "",
- ),
- ),
- },
- ),
- ),
- userPasswordAlg: crypto.CreateMockHashAlg(gomock.NewController(t)),
+ userPasswordHasher: mockPasswordHasher("x"),
},
args: args{
ctx: context.Background(),
@@ -800,6 +717,59 @@ func TestCommandSide_ChangePassword(t *testing.T) {
oldPassword: "password",
newPassword: "password1",
},
+ expect: []expect{
+ expectFilter(
+ eventFromEventPusher(
+ user.NewHumanAddedEvent(context.Background(),
+ &user.NewAggregate("user1", "org1").Aggregate,
+ "username",
+ "firstname",
+ "lastname",
+ "nickname",
+ "displayname",
+ language.German,
+ domain.GenderUnspecified,
+ "email@test.ch",
+ true,
+ ),
+ ),
+ eventFromEventPusher(
+ user.NewHumanEmailVerifiedEvent(context.Background(),
+ &user.NewAggregate("user1", "org1").Aggregate,
+ ),
+ ),
+ eventFromEventPusher(
+ user.NewHumanPasswordChangedEvent(context.Background(),
+ &user.NewAggregate("user1", "org1").Aggregate,
+ "$plain$x$password",
+ false,
+ "")),
+ ),
+ expectFilter(
+ eventFromEventPusher(
+ org.NewPasswordComplexityPolicyAddedEvent(context.Background(),
+ &user.NewAggregate("user1", "org1").Aggregate,
+ 1,
+ false,
+ false,
+ false,
+ false,
+ ),
+ ),
+ ),
+ expectPush(
+ []*repository.Event{
+ eventFromEventPusher(
+ user.NewHumanPasswordChangedEvent(context.Background(),
+ &user.NewAggregate("user1", "org1").Aggregate,
+ "$plain$x$password1",
+ false,
+ "",
+ ),
+ ),
+ },
+ ),
+ },
res: res{
want: &domain.ObjectDetails{
ResourceOwner: "org1",
@@ -810,8 +780,8 @@ func TestCommandSide_ChangePassword(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := &Commands{
- eventstore: tt.fields.eventstore,
- userPasswordAlg: tt.fields.userPasswordAlg,
+ eventstore: eventstoreExpect(t, tt.expect...),
+ userPasswordHasher: tt.fields.userPasswordHasher,
}
got, err := r.ChangePassword(tt.args.ctx, tt.args.resourceOwner, tt.args.userID, tt.args.oldPassword, tt.args.newPassword, tt.args.agentID)
if tt.res.err == nil {
@@ -1123,8 +1093,8 @@ func TestCommandSide_PasswordCodeSent(t *testing.T) {
func TestCommandSide_CheckPassword(t *testing.T) {
type fields struct {
- eventstore *eventstore.Eventstore
- userPasswordAlg crypto.HashAlgorithm
+ eventstore *eventstore.Eventstore
+ userPasswordHasher *crypto.PasswordHasher
}
type args struct {
ctx context.Context
@@ -1212,6 +1182,7 @@ func TestCommandSide_CheckPassword(t *testing.T) {
false,
false,
false,
+ false,
domain.PasswordlessTypeNotAllowed,
"",
time.Hour*1,
@@ -1252,6 +1223,7 @@ func TestCommandSide_CheckPassword(t *testing.T) {
false,
false,
false,
+ false,
domain.PasswordlessTypeNotAllowed,
"",
time.Hour*1,
@@ -1293,6 +1265,7 @@ func TestCommandSide_CheckPassword(t *testing.T) {
false,
false,
false,
+ false,
domain.PasswordlessTypeNotAllowed,
"",
time.Hour*1,
@@ -1320,7 +1293,7 @@ func TestCommandSide_CheckPassword(t *testing.T) {
),
),
),
- userPasswordAlg: crypto.CreateMockHashAlg(gomock.NewController(t)),
+ userPasswordHasher: mockPasswordHasher("x"),
},
args: args{
ctx: context.Background(),
@@ -1350,6 +1323,7 @@ func TestCommandSide_CheckPassword(t *testing.T) {
false,
false,
false,
+ false,
domain.PasswordlessTypeNotAllowed,
"",
time.Hour*1,
@@ -1383,12 +1357,7 @@ func TestCommandSide_CheckPassword(t *testing.T) {
eventFromEventPusher(
user.NewHumanPasswordChangedEvent(context.Background(),
&user.NewAggregate("user1", "org1").Aggregate,
- &crypto.CryptoValue{
- CryptoType: crypto.TypeHash,
- Algorithm: "hash",
- KeyID: "",
- Crypted: []byte("password"),
- },
+ "$plain$x$password",
false,
"")),
),
@@ -1406,7 +1375,7 @@ func TestCommandSide_CheckPassword(t *testing.T) {
},
),
),
- userPasswordAlg: crypto.CreateMockHashAlg(gomock.NewController(t)),
+ userPasswordHasher: mockPasswordHasher("x"),
},
args: args{
ctx: context.Background(),
@@ -1441,6 +1410,7 @@ func TestCommandSide_CheckPassword(t *testing.T) {
false,
false,
false,
+ false,
domain.PasswordlessTypeNotAllowed,
"",
time.Hour*1,
@@ -1474,12 +1444,7 @@ func TestCommandSide_CheckPassword(t *testing.T) {
eventFromEventPusher(
user.NewHumanPasswordChangedEvent(context.Background(),
&user.NewAggregate("user1", "org1").Aggregate,
- &crypto.CryptoValue{
- CryptoType: crypto.TypeHash,
- Algorithm: "hash",
- KeyID: "",
- Crypted: []byte("password"),
- },
+ "$plain$x$password",
false,
"")),
),
@@ -1502,7 +1467,7 @@ func TestCommandSide_CheckPassword(t *testing.T) {
},
),
),
- userPasswordAlg: crypto.CreateMockHashAlg(gomock.NewController(t)),
+ userPasswordHasher: mockPasswordHasher("x"),
},
args: args{
ctx: context.Background(),
@@ -1539,6 +1504,7 @@ func TestCommandSide_CheckPassword(t *testing.T) {
false,
false,
false,
+ false,
domain.PasswordlessTypeNotAllowed,
"",
time.Hour*1,
@@ -1572,12 +1538,7 @@ func TestCommandSide_CheckPassword(t *testing.T) {
eventFromEventPusher(
user.NewHumanPasswordChangedEvent(context.Background(),
&user.NewAggregate("user1", "org1").Aggregate,
- &crypto.CryptoValue{
- CryptoType: crypto.TypeHash,
- Algorithm: "hash",
- KeyID: "",
- Crypted: []byte("password"),
- },
+ "$plain$x$password",
false,
"")),
),
@@ -1595,7 +1556,197 @@ func TestCommandSide_CheckPassword(t *testing.T) {
},
),
),
- userPasswordAlg: crypto.CreateMockHashAlg(gomock.NewController(t)),
+ userPasswordHasher: mockPasswordHasher("x"),
+ },
+ args: args{
+ ctx: context.Background(),
+ userID: "user1",
+ resourceOwner: "org1",
+ password: "password",
+ authReq: &domain.AuthRequest{
+ ID: "request1",
+ AgentID: "agent1",
+ },
+ },
+ res: res{},
+ },
+ {
+ name: "check password, ok, updated hash",
+ fields: fields{
+ eventstore: eventstoreExpect(
+ t,
+ expectFilter(
+ eventFromEventPusher(
+ org.NewLoginPolicyAddedEvent(context.Background(),
+ &org.NewAggregate("org1").Aggregate,
+ true,
+ false,
+ false,
+ false,
+ false,
+ false,
+ false,
+ false,
+ false,
+ false,
+ domain.PasswordlessTypeNotAllowed,
+ "",
+ time.Hour*1,
+ time.Hour*2,
+ time.Hour*3,
+ time.Hour*4,
+ time.Hour*5,
+ ),
+ ),
+ ),
+ expectFilter(
+ eventFromEventPusher(
+ user.NewHumanAddedEvent(context.Background(),
+ &user.NewAggregate("user1", "org1").Aggregate,
+ "username",
+ "firstname",
+ "lastname",
+ "nickname",
+ "displayname",
+ language.German,
+ domain.GenderUnspecified,
+ "email@test.ch",
+ true,
+ ),
+ ),
+ eventFromEventPusher(
+ user.NewHumanEmailVerifiedEvent(context.Background(),
+ &user.NewAggregate("user1", "org1").Aggregate,
+ ),
+ ),
+ eventFromEventPusher(
+ user.NewHumanPasswordChangedEvent(context.Background(),
+ &user.NewAggregate("user1", "org1").Aggregate,
+ "$plain$v$password",
+ false,
+ "")),
+ ),
+ expectPush(
+ []*repository.Event{
+ eventFromEventPusher(
+ user.NewHumanPasswordCheckSucceededEvent(context.Background(),
+ &user.NewAggregate("user1", "org1").Aggregate,
+ &user.AuthRequestInfo{
+ ID: "request1",
+ UserAgentID: "agent1",
+ },
+ ),
+ ),
+ eventFromEventPusher(
+ user.NewHumanPasswordHashUpdatedEvent(context.Background(),
+ &user.NewAggregate("user1", "org1").Aggregate,
+ "$plain$x$password",
+ ),
+ ),
+ },
+ ),
+ ),
+ userPasswordHasher: mockPasswordHasher("x"),
+ },
+ args: args{
+ ctx: context.Background(),
+ userID: "user1",
+ resourceOwner: "org1",
+ password: "password",
+ authReq: &domain.AuthRequest{
+ ID: "request1",
+ AgentID: "agent1",
+ },
+ },
+ res: res{},
+ },
+ {
+ name: "regression test old version event",
+ fields: fields{
+ eventstore: eventstoreExpect(
+ t,
+ expectFilter(
+ eventFromEventPusher(
+ org.NewLoginPolicyAddedEvent(context.Background(),
+ &org.NewAggregate("org1").Aggregate,
+ true,
+ false,
+ false,
+ false,
+ false,
+ false,
+ false,
+ false,
+ false,
+ false,
+ domain.PasswordlessTypeNotAllowed,
+ "",
+ time.Hour*1,
+ time.Hour*2,
+ time.Hour*3,
+ time.Hour*4,
+ time.Hour*5,
+ ),
+ ),
+ ),
+ expectFilter(
+ eventFromEventPusher(
+ user.NewHumanAddedEvent(context.Background(),
+ &user.NewAggregate("user1", "org1").Aggregate,
+ "username",
+ "firstname",
+ "lastname",
+ "nickname",
+ "displayname",
+ language.German,
+ domain.GenderUnspecified,
+ "email@test.ch",
+ true,
+ ),
+ ),
+ eventFromEventPusher(
+ user.NewHumanEmailVerifiedEvent(context.Background(),
+ &user.NewAggregate("user1", "org1").Aggregate,
+ ),
+ ),
+ eventFromEventPusher(
+ &user.HumanPasswordChangedEvent{
+ BaseEvent: *eventstore.NewBaseEventForPush(
+ context.Background(),
+ &user.NewAggregate("user1", "org1").Aggregate,
+ user.HumanPasswordChangedType,
+ ),
+ Secret: &crypto.CryptoValue{
+ CryptoType: crypto.TypeHash,
+ Algorithm: "plain",
+ KeyID: "",
+ Crypted: []byte("$plain$v$password"),
+ },
+ ChangeRequired: false,
+ },
+ ),
+ ),
+ expectPush(
+ []*repository.Event{
+ eventFromEventPusher(
+ user.NewHumanPasswordCheckSucceededEvent(context.Background(),
+ &user.NewAggregate("user1", "org1").Aggregate,
+ &user.AuthRequestInfo{
+ ID: "request1",
+ UserAgentID: "agent1",
+ },
+ ),
+ ),
+ eventFromEventPusher(
+ user.NewHumanPasswordHashUpdatedEvent(context.Background(),
+ &user.NewAggregate("user1", "org1").Aggregate,
+ "$plain$x$password",
+ ),
+ ),
+ },
+ ),
+ ),
+ userPasswordHasher: mockPasswordHasher("x"),
},
args: args{
ctx: context.Background(),
@@ -1613,8 +1764,8 @@ func TestCommandSide_CheckPassword(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := &Commands{
- eventstore: tt.fields.eventstore,
- userPasswordAlg: tt.fields.userPasswordAlg,
+ eventstore: tt.fields.eventstore,
+ userPasswordHasher: tt.fields.userPasswordHasher,
}
err := r.HumanCheckPassword(tt.args.ctx, tt.args.resourceOwner, tt.args.userID, tt.args.password, tt.args.authReq, tt.args.lockoutPolicy)
if tt.res.err == nil {
@@ -1626,3 +1777,41 @@ func TestCommandSide_CheckPassword(t *testing.T) {
})
}
}
+
+func Test_convertPasswapErr(t *testing.T) {
+ type args struct {
+ err error
+ }
+ tests := []struct {
+ name string
+ args args
+ wantErr error
+ }{
+ {
+ name: "nil",
+ args: args{nil},
+ wantErr: nil,
+ },
+ {
+ name: "mismatch",
+ args: args{passwap.ErrPasswordMismatch},
+ wantErr: caos_errs.ThrowInvalidArgument(passwap.ErrPasswordMismatch, "COMMAND-3M0fs", "Errors.User.Password.Invalid"),
+ },
+ {
+ name: "no change",
+ args: args{passwap.ErrPasswordNoChange},
+ wantErr: caos_errs.ThrowPreconditionFailed(passwap.ErrPasswordNoChange, "COMMAND-Aesh5", "Errors.User.Password.NotChanged"),
+ },
+ {
+ name: "other",
+ args: args{io.ErrClosedPipe},
+ wantErr: caos_errs.ThrowInternal(io.ErrClosedPipe, "COMMAND-CahN2", "Errors.Internal"),
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ err := convertPasswapErr(tt.args.err)
+ assert.ErrorIs(t, err, tt.wantErr)
+ })
+ }
+}
diff --git a/internal/command/user_human_test.go b/internal/command/user_human_test.go
index 5d6f131c2..384ef4757 100644
--- a/internal/command/user_human_test.go
+++ b/internal/command/user_human_test.go
@@ -20,18 +20,17 @@ import (
"github.com/zitadel/zitadel/internal/eventstore/v1/models"
"github.com/zitadel/zitadel/internal/id"
id_mock "github.com/zitadel/zitadel/internal/id/mock"
- "github.com/zitadel/zitadel/internal/repository/instance"
"github.com/zitadel/zitadel/internal/repository/org"
"github.com/zitadel/zitadel/internal/repository/user"
)
func TestCommandSide_AddHuman(t *testing.T) {
type fields struct {
- eventstore *eventstore.Eventstore
- idGenerator id.Generator
- userPasswordAlg crypto.HashAlgorithm
- codeAlg crypto.EncryptionAlgorithm
- newCode cryptoCodeFunc
+ eventstore func(t *testing.T) *eventstore.Eventstore
+ idGenerator id.Generator
+ userPasswordHasher *crypto.PasswordHasher
+ codeAlg crypto.EncryptionAlgorithm
+ newCode cryptoCodeFunc
}
type args struct {
ctx context.Context
@@ -48,7 +47,6 @@ func TestCommandSide_AddHuman(t *testing.T) {
}
userAgg := user.NewAggregate("user1", "org1")
- instanceAgg := instance.NewAggregate("instance")
tests := []struct {
name string
@@ -59,9 +57,7 @@ func TestCommandSide_AddHuman(t *testing.T) {
{
name: "orgid missing, invalid argument error",
fields: fields{
- eventstore: eventstoreExpect(
- t,
- ),
+ eventstore: expectEventstore(),
},
args: args{
ctx: context.Background(),
@@ -85,9 +81,7 @@ func TestCommandSide_AddHuman(t *testing.T) {
{
name: "user invalid, invalid argument error",
fields: fields{
- eventstore: eventstoreExpect(
- t,
- ),
+ eventstore: expectEventstore(),
},
args: args{
ctx: context.Background(),
@@ -107,11 +101,10 @@ func TestCommandSide_AddHuman(t *testing.T) {
{
name: "with id, already exists, precondition error",
fields: fields{
- eventstore: eventstoreExpect(
- t,
+ eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
- newAddHumanEvent("password", true, ""),
+ newAddHumanEvent("$plain$x$password", true, ""),
),
),
),
@@ -141,8 +134,7 @@ func TestCommandSide_AddHuman(t *testing.T) {
name: "domain policy not found, precondition error",
fields: fields{
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"),
- eventstore: eventstoreExpect(
- t,
+ eventstore: expectEventstore(
expectFilter(),
expectFilter(),
expectFilter(),
@@ -172,8 +164,7 @@ func TestCommandSide_AddHuman(t *testing.T) {
name: "password policy not found, precondition error",
fields: fields{
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"),
- eventstore: eventstoreExpect(
- t,
+ eventstore: expectEventstore(
expectFilter(),
expectFilter(
eventFromEventPusher(
@@ -214,8 +205,7 @@ func TestCommandSide_AddHuman(t *testing.T) {
{
name: "add human (with initial code), ok",
fields: fields{
- eventstore: eventstoreExpect(
- t,
+ eventstore: expectEventstore(
expectFilter(),
expectFilter(
eventFromEventPusher(
@@ -227,21 +217,6 @@ func TestCommandSide_AddHuman(t *testing.T) {
),
),
),
- expectFilter(
- eventFromEventPusher(
- instance.NewSecretGeneratorAddedEvent(
- context.Background(),
- &instanceAgg.Aggregate,
- domain.SecretGeneratorTypeInitCode,
- 0,
- 1*time.Hour,
- true,
- true,
- true,
- true,
- ),
- ),
- ),
expectPush(
[]*repository.Event{
eventFromEventPusher(
@@ -265,7 +240,7 @@ func TestCommandSide_AddHuman(t *testing.T) {
CryptoType: crypto.TypeEncryption,
Algorithm: "enc",
KeyID: "id",
- Crypted: []byte(""),
+ Crypted: []byte("userinit"),
},
time.Hour*1,
),
@@ -276,6 +251,7 @@ func TestCommandSide_AddHuman(t *testing.T) {
),
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"),
codeAlg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
+ newCode: mockCode("userinit", time.Hour),
},
args: args{
ctx: context.Background(),
@@ -304,8 +280,7 @@ func TestCommandSide_AddHuman(t *testing.T) {
{
name: "add human (with password and initial code), ok",
fields: fields{
- eventstore: eventstoreExpect(
- t,
+ eventstore: expectEventstore(
expectFilter(),
expectFilter(
eventFromEventPusher(
@@ -329,25 +304,10 @@ func TestCommandSide_AddHuman(t *testing.T) {
),
),
),
- expectFilter(
- eventFromEventPusher(
- instance.NewSecretGeneratorAddedEvent(
- context.Background(),
- &instanceAgg.Aggregate,
- domain.SecretGeneratorTypeInitCode,
- 0,
- 1*time.Hour,
- true,
- true,
- true,
- true,
- ),
- ),
- ),
expectPush(
[]*repository.Event{
eventFromEventPusher(
- newAddHumanEvent("password", false, ""),
+ newAddHumanEvent("$plain$x$password", false, ""),
),
eventFromEventPusher(
user.NewHumanInitialCodeAddedEvent(context.Background(),
@@ -356,7 +316,7 @@ func TestCommandSide_AddHuman(t *testing.T) {
CryptoType: crypto.TypeEncryption,
Algorithm: "enc",
KeyID: "id",
- Crypted: []byte(""),
+ Crypted: []byte("userinit"),
},
1*time.Hour,
),
@@ -365,9 +325,10 @@ func TestCommandSide_AddHuman(t *testing.T) {
uniqueConstraintsFromEventConstraint(user.NewAddUsernameUniqueConstraint("username", "org1", true)),
),
),
- idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"),
- userPasswordAlg: crypto.CreateMockHashAlg(gomock.NewController(t)),
- codeAlg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
+ idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"),
+ userPasswordHasher: mockPasswordHasher("x"),
+ codeAlg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
+ newCode: mockCode("userinit", time.Hour),
},
args: args{
ctx: context.Background(),
@@ -395,8 +356,7 @@ func TestCommandSide_AddHuman(t *testing.T) {
{
name: "add human (with password and email code custom template), ok",
fields: fields{
- eventstore: eventstoreExpect(
- t,
+ eventstore: expectEventstore(
expectFilter(),
expectFilter(
eventFromEventPusher(
@@ -423,7 +383,7 @@ func TestCommandSide_AddHuman(t *testing.T) {
expectPush(
[]*repository.Event{
eventFromEventPusher(
- newAddHumanEvent("password", false, ""),
+ newAddHumanEvent("$plain$x$password", false, ""),
),
eventFromEventPusher(
user.NewHumanEmailCodeAddedEventV2(context.Background(),
@@ -443,10 +403,10 @@ func TestCommandSide_AddHuman(t *testing.T) {
uniqueConstraintsFromEventConstraint(user.NewAddUsernameUniqueConstraint("username", "org1", true)),
),
),
- idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"),
- userPasswordAlg: crypto.CreateMockHashAlg(gomock.NewController(t)),
- codeAlg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
- newCode: mockCode("emailCode", time.Hour),
+ idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"),
+ userPasswordHasher: mockPasswordHasher("x"),
+ codeAlg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
+ newCode: mockCode("emailCode", time.Hour),
},
args: args{
ctx: context.Background(),
@@ -475,8 +435,7 @@ func TestCommandSide_AddHuman(t *testing.T) {
{
name: "add human (with password and return email code), ok",
fields: fields{
- eventstore: eventstoreExpect(
- t,
+ eventstore: expectEventstore(
expectFilter(),
expectFilter(
eventFromEventPusher(
@@ -503,7 +462,7 @@ func TestCommandSide_AddHuman(t *testing.T) {
expectPush(
[]*repository.Event{
eventFromEventPusher(
- newAddHumanEvent("password", false, ""),
+ newAddHumanEvent("$plain$x$password", false, ""),
),
eventFromEventPusher(
user.NewHumanEmailCodeAddedEventV2(context.Background(),
@@ -523,10 +482,10 @@ func TestCommandSide_AddHuman(t *testing.T) {
uniqueConstraintsFromEventConstraint(user.NewAddUsernameUniqueConstraint("username", "org1", true)),
),
),
- idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"),
- userPasswordAlg: crypto.CreateMockHashAlg(gomock.NewController(t)),
- codeAlg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
- newCode: mockCode("emailCode", time.Hour),
+ idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"),
+ userPasswordHasher: mockPasswordHasher("x"),
+ codeAlg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
+ newCode: mockCode("emailCode", time.Hour),
},
args: args{
ctx: context.Background(),
@@ -556,8 +515,7 @@ func TestCommandSide_AddHuman(t *testing.T) {
{
name: "add human email verified, ok",
fields: fields{
- eventstore: eventstoreExpect(
- t,
+ eventstore: expectEventstore(
expectFilter(),
expectFilter(
eventFromEventPusher(
@@ -584,7 +542,7 @@ func TestCommandSide_AddHuman(t *testing.T) {
expectPush(
[]*repository.Event{
eventFromEventPusher(
- newAddHumanEvent("password", true, ""),
+ newAddHumanEvent("$plain$x$password", true, ""),
),
eventFromEventPusher(
user.NewHumanEmailVerifiedEvent(context.Background(),
@@ -594,9 +552,9 @@ func TestCommandSide_AddHuman(t *testing.T) {
uniqueConstraintsFromEventConstraint(user.NewAddUsernameUniqueConstraint("username", "org1", true)),
),
),
- idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"),
- userPasswordAlg: crypto.CreateMockHashAlg(gomock.NewController(t)),
- codeAlg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
+ idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"),
+ userPasswordHasher: mockPasswordHasher("x"),
+ codeAlg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
},
args: args{
ctx: context.Background(),
@@ -626,8 +584,7 @@ func TestCommandSide_AddHuman(t *testing.T) {
{
name: "add human email verified, trim spaces, ok",
fields: fields{
- eventstore: eventstoreExpect(
- t,
+ eventstore: expectEventstore(
expectFilter(),
expectFilter(
eventFromEventPusher(
@@ -654,7 +611,7 @@ func TestCommandSide_AddHuman(t *testing.T) {
expectPush(
[]*repository.Event{
eventFromEventPusher(
- newAddHumanEvent("password", true, ""),
+ newAddHumanEvent("$plain$x$password", true, ""),
),
eventFromEventPusher(
user.NewHumanEmailVerifiedEvent(context.Background(),
@@ -664,9 +621,9 @@ func TestCommandSide_AddHuman(t *testing.T) {
uniqueConstraintsFromEventConstraint(user.NewAddUsernameUniqueConstraint("username", "org1", true)),
),
),
- idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"),
- userPasswordAlg: crypto.CreateMockHashAlg(gomock.NewController(t)),
- codeAlg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
+ idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"),
+ userPasswordHasher: mockPasswordHasher("x"),
+ codeAlg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
},
args: args{
ctx: context.Background(),
@@ -696,8 +653,7 @@ func TestCommandSide_AddHuman(t *testing.T) {
{
name: "add human, email verified, userLoginMustBeDomain false, ok",
fields: fields{
- eventstore: eventstoreExpect(
- t,
+ eventstore: expectEventstore(
expectFilter(),
expectFilter(
eventFromEventPusher(
@@ -724,7 +680,7 @@ func TestCommandSide_AddHuman(t *testing.T) {
expectPush(
[]*repository.Event{
eventFromEventPusher(
- newAddHumanEvent("password", true, ""),
+ newAddHumanEvent("$plain$x$password", true, ""),
),
eventFromEventPusher(
user.NewHumanEmailVerifiedEvent(context.Background(),
@@ -734,9 +690,9 @@ func TestCommandSide_AddHuman(t *testing.T) {
uniqueConstraintsFromEventConstraint(user.NewAddUsernameUniqueConstraint("username", "org1", false)),
),
),
- idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"),
- userPasswordAlg: crypto.CreateMockHashAlg(gomock.NewController(t)),
- codeAlg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
+ idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"),
+ userPasswordHasher: mockPasswordHasher("x"),
+ codeAlg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
},
args: args{
ctx: context.Background(),
@@ -766,8 +722,7 @@ func TestCommandSide_AddHuman(t *testing.T) {
{
name: "add human claimed domain, userLoginMustBeDomain false, error",
fields: fields{
- eventstore: eventstoreExpect(
- t,
+ eventstore: expectEventstore(
expectFilter(),
expectFilter(
eventFromEventPusher(
@@ -788,9 +743,9 @@ func TestCommandSide_AddHuman(t *testing.T) {
),
),
),
- idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"),
- userPasswordAlg: crypto.CreateMockHashAlg(gomock.NewController(t)),
- codeAlg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
+ idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"),
+ userPasswordHasher: mockPasswordHasher("x"),
+ codeAlg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
},
args: args{
ctx: context.Background(),
@@ -819,8 +774,7 @@ func TestCommandSide_AddHuman(t *testing.T) {
{
name: "add human domain, userLoginMustBeDomain false, ok",
fields: fields{
- eventstore: eventstoreExpect(
- t,
+ eventstore: expectEventstore(
expectFilter(),
expectFilter(
eventFromEventPusher(
@@ -868,12 +822,7 @@ func TestCommandSide_AddHuman(t *testing.T) {
"email@test.ch",
true,
)
- event.AddPasswordData(&crypto.CryptoValue{
- CryptoType: crypto.TypeHash,
- Algorithm: "hash",
- KeyID: "",
- Crypted: []byte("password"),
- }, true)
+ event.AddPasswordData("$plain$x$password", true)
return event
}(),
),
@@ -885,9 +834,9 @@ func TestCommandSide_AddHuman(t *testing.T) {
uniqueConstraintsFromEventConstraint(user.NewAddUsernameUniqueConstraint("username@test.ch", "org1", false)),
),
),
- idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"),
- userPasswordAlg: crypto.CreateMockHashAlg(gomock.NewController(t)),
- codeAlg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
+ idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"),
+ userPasswordHasher: mockPasswordHasher("x"),
+ codeAlg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
},
args: args{
ctx: context.Background(),
@@ -918,8 +867,7 @@ func TestCommandSide_AddHuman(t *testing.T) {
{
name: "add human (with phone), ok",
fields: fields{
- eventstore: eventstoreExpect(
- t,
+ eventstore: expectEventstore(
expectFilter(),
expectFilter(
eventFromEventPusher(
@@ -943,25 +891,10 @@ func TestCommandSide_AddHuman(t *testing.T) {
),
),
),
- expectFilter(
- eventFromEventPusher(
- instance.NewSecretGeneratorAddedEvent(
- context.Background(),
- &instanceAgg.Aggregate,
- domain.SecretGeneratorTypeVerifyPhoneCode,
- 0,
- 1*time.Hour,
- true,
- true,
- true,
- true,
- ),
- ),
- ),
expectPush(
[]*repository.Event{
eventFromEventPusher(
- newAddHumanEvent("password", false, "+41711234567"),
+ newAddHumanEvent("$plain$x$password", false, "+41711234567"),
),
eventFromEventPusher(
user.NewHumanEmailVerifiedEvent(
@@ -976,16 +909,17 @@ func TestCommandSide_AddHuman(t *testing.T) {
CryptoType: crypto.TypeEncryption,
Algorithm: "enc",
KeyID: "id",
- Crypted: []byte(""),
+ Crypted: []byte("phonecode"),
},
time.Hour*1)),
},
uniqueConstraintsFromEventConstraint(user.NewAddUsernameUniqueConstraint("username", "org1", true)),
),
),
- idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"),
- userPasswordAlg: crypto.CreateMockHashAlg(gomock.NewController(t)),
- codeAlg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
+ idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"),
+ userPasswordHasher: mockPasswordHasher("x"),
+ codeAlg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
+ newCode: mockCode("phonecode", time.Hour),
},
args: args{
ctx: context.Background(),
@@ -1017,8 +951,7 @@ func TestCommandSide_AddHuman(t *testing.T) {
{
name: "add human (with verified phone), ok",
fields: fields{
- eventstore: eventstoreExpect(
- t,
+ eventstore: expectEventstore(
expectFilter(),
expectFilter(
eventFromEventPusher(
@@ -1030,21 +963,6 @@ func TestCommandSide_AddHuman(t *testing.T) {
),
),
),
- expectFilter(
- eventFromEventPusher(
- instance.NewSecretGeneratorAddedEvent(
- context.Background(),
- &instanceAgg.Aggregate,
- domain.SecretGeneratorTypeInitCode,
- 0,
- 1*time.Hour,
- true,
- true,
- true,
- true,
- ),
- ),
- ),
expectPush(
[]*repository.Event{
eventFromEventPusher(
@@ -1058,7 +976,7 @@ func TestCommandSide_AddHuman(t *testing.T) {
CryptoType: crypto.TypeEncryption,
Algorithm: "enc",
KeyID: "id",
- Crypted: []byte(""),
+ Crypted: []byte("userinit"),
},
1*time.Hour,
),
@@ -1075,6 +993,7 @@ func TestCommandSide_AddHuman(t *testing.T) {
),
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"),
codeAlg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
+ newCode: mockCode("userinit", time.Hour),
},
args: args{
ctx: context.Background(),
@@ -1105,8 +1024,7 @@ func TestCommandSide_AddHuman(t *testing.T) {
{
name: "add human with metadata, ok",
fields: fields{
- eventstore: eventstoreExpect(
- t,
+ eventstore: expectEventstore(
expectFilter(),
expectFilter(
eventFromEventPusher(
@@ -1118,21 +1036,6 @@ func TestCommandSide_AddHuman(t *testing.T) {
),
),
),
- expectFilter(
- eventFromEventPusher(
- instance.NewSecretGeneratorAddedEvent(
- context.Background(),
- &instanceAgg.Aggregate,
- domain.SecretGeneratorTypeInitCode,
- 0,
- 1*time.Hour,
- true,
- true,
- true,
- true,
- ),
- ),
- ),
expectPush(
[]*repository.Event{
eventFromEventPusher(
@@ -1146,7 +1049,7 @@ func TestCommandSide_AddHuman(t *testing.T) {
CryptoType: crypto.TypeEncryption,
Algorithm: "enc",
KeyID: "id",
- Crypted: []byte(""),
+ Crypted: []byte("userinit"),
},
1*time.Hour,
),
@@ -1165,6 +1068,7 @@ func TestCommandSide_AddHuman(t *testing.T) {
),
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"),
codeAlg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
+ newCode: mockCode("userinit", time.Hour),
},
args: args{
ctx: context.Background(),
@@ -1198,11 +1102,11 @@ func TestCommandSide_AddHuman(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := &Commands{
- eventstore: tt.fields.eventstore,
- userPasswordAlg: tt.fields.userPasswordAlg,
- userEncryption: tt.fields.codeAlg,
- idGenerator: tt.fields.idGenerator,
- newCode: tt.fields.newCode,
+ eventstore: tt.fields.eventstore(t),
+ userPasswordHasher: tt.fields.userPasswordHasher,
+ userEncryption: tt.fields.codeAlg,
+ idGenerator: tt.fields.idGenerator,
+ newCode: tt.fields.newCode,
}
err := r.AddHuman(tt.args.ctx, tt.args.orgID, tt.args.human, tt.args.allowInitMail)
if tt.res.err == nil {
@@ -1224,9 +1128,9 @@ func TestCommandSide_AddHuman(t *testing.T) {
func TestCommandSide_ImportHuman(t *testing.T) {
type fields struct {
- eventstore *eventstore.Eventstore
- idGenerator id.Generator
- userPasswordAlg crypto.HashAlgorithm
+ eventstore *eventstore.Eventstore
+ idGenerator id.Generator
+ userPasswordHasher *crypto.PasswordHasher
}
type args struct {
ctx context.Context
@@ -1410,7 +1314,7 @@ func TestCommandSide_ImportHuman(t *testing.T) {
expectPush(
[]*repository.Event{
eventFromEventPusher(
- newAddHumanEvent("password", true, ""),
+ newAddHumanEvent("$plain$x$password", true, ""),
),
eventFromEventPusher(
user.NewHumanInitialCodeAddedEvent(context.Background(),
@@ -1428,8 +1332,8 @@ func TestCommandSide_ImportHuman(t *testing.T) {
uniqueConstraintsFromEventConstraint(user.NewAddUsernameUniqueConstraint("username", "org1", true)),
),
),
- idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"),
- userPasswordAlg: crypto.CreateMockHashAlg(gomock.NewController(t)),
+ idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"),
+ userPasswordHasher: mockPasswordHasher("x"),
},
args: args{
ctx: context.Background(),
@@ -1501,7 +1405,7 @@ func TestCommandSide_ImportHuman(t *testing.T) {
expectPush(
[]*repository.Event{
eventFromEventPusher(
- newAddHumanEvent("password", false, ""),
+ newAddHumanEvent("$plain$x$password", false, ""),
),
eventFromEventPusher(
user.NewHumanEmailVerifiedEvent(context.Background(),
@@ -1511,8 +1415,8 @@ func TestCommandSide_ImportHuman(t *testing.T) {
uniqueConstraintsFromEventConstraint(user.NewAddUsernameUniqueConstraint("username", "org1", true)),
),
),
- idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"),
- userPasswordAlg: crypto.CreateMockHashAlg(gomock.NewController(t)),
+ idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"),
+ userPasswordHasher: mockPasswordHasher("x"),
},
args: args{
ctx: context.Background(),
@@ -1610,8 +1514,8 @@ func TestCommandSide_ImportHuman(t *testing.T) {
uniqueConstraintsFromEventConstraint(user.NewAddUsernameUniqueConstraint("username", "org1", true)),
),
),
- idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1", "code1"),
- userPasswordAlg: crypto.CreateMockHashAlg(gomock.NewController(t)),
+ idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1", "code1"),
+ userPasswordHasher: mockPasswordHasher("x"),
},
args: args{
ctx: context.Background(),
@@ -1694,7 +1598,7 @@ func TestCommandSide_ImportHuman(t *testing.T) {
expectPush(
[]*repository.Event{
eventFromEventPusher(
- newAddHumanEvent("password", false, ""),
+ newAddHumanEvent("$plain$x$password", false, ""),
),
eventFromEventPusher(
user.NewHumanEmailVerifiedEvent(context.Background(),
@@ -1717,8 +1621,8 @@ func TestCommandSide_ImportHuman(t *testing.T) {
uniqueConstraintsFromEventConstraint(user.NewAddUsernameUniqueConstraint("username", "org1", true)),
),
),
- idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1", "code1"),
- userPasswordAlg: crypto.CreateMockHashAlg(gomock.NewController(t)),
+ idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1", "code1"),
+ userPasswordHasher: mockPasswordHasher("x"),
},
args: args{
ctx: context.Background(),
@@ -1804,7 +1708,7 @@ func TestCommandSide_ImportHuman(t *testing.T) {
expectPush(
[]*repository.Event{
eventFromEventPusher(
- newAddHumanEvent("password", false, "+41711234567"),
+ newAddHumanEvent("$plain$x$password", false, "+41711234567"),
),
eventFromEventPusher(
user.NewHumanInitialCodeAddedEvent(context.Background(),
@@ -1832,8 +1736,8 @@ func TestCommandSide_ImportHuman(t *testing.T) {
uniqueConstraintsFromEventConstraint(user.NewAddUsernameUniqueConstraint("username", "org1", true)),
),
),
- idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"),
- userPasswordAlg: crypto.CreateMockHashAlg(gomock.NewController(t)),
+ idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"),
+ userPasswordHasher: mockPasswordHasher("x"),
},
args: args{
ctx: context.Background(),
@@ -1911,7 +1815,7 @@ func TestCommandSide_ImportHuman(t *testing.T) {
expectPush(
[]*repository.Event{
eventFromEventPusher(
- newAddHumanEvent("password", false, "+41711234567"),
+ newAddHumanEvent("$plain$x$password", false, "+41711234567"),
),
eventFromEventPusher(
user.NewHumanInitialCodeAddedEvent(context.Background(),
@@ -1933,8 +1837,8 @@ func TestCommandSide_ImportHuman(t *testing.T) {
uniqueConstraintsFromEventConstraint(user.NewAddUsernameUniqueConstraint("username", "org1", true)),
),
),
- idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"),
- userPasswordAlg: crypto.CreateMockHashAlg(gomock.NewController(t)),
+ idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"),
+ userPasswordHasher: mockPasswordHasher("x"),
},
args: args{
ctx: context.Background(),
@@ -2064,8 +1968,8 @@ func TestCommandSide_ImportHuman(t *testing.T) {
uniqueConstraintsFromEventConstraint(user.NewAddUserIDPLinkUniqueConstraint("idpID", "externalID")),
),
),
- idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"),
- userPasswordAlg: crypto.CreateMockHashAlg(gomock.NewController(t)),
+ idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"),
+ userPasswordHasher: mockPasswordHasher("x"),
},
args: args{
ctx: context.Background(),
@@ -2116,9 +2020,9 @@ func TestCommandSide_ImportHuman(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := &Commands{
- eventstore: tt.fields.eventstore,
- idGenerator: tt.fields.idGenerator,
- userPasswordAlg: tt.fields.userPasswordAlg,
+ eventstore: tt.fields.eventstore,
+ idGenerator: tt.fields.idGenerator,
+ userPasswordHasher: tt.fields.userPasswordHasher,
}
gotHuman, gotCode, err := r.ImportHuman(tt.args.ctx, tt.args.orgID, tt.args.human, tt.args.passwordless, tt.args.links, tt.args.secretGenerator, tt.args.secretGenerator, tt.args.secretGenerator, tt.args.secretGenerator)
if tt.res.err == nil {
@@ -2137,9 +2041,9 @@ func TestCommandSide_ImportHuman(t *testing.T) {
func TestCommandSide_RegisterHuman(t *testing.T) {
type fields struct {
- eventstore *eventstore.Eventstore
- idGenerator id.Generator
- userPasswordAlg crypto.HashAlgorithm
+ eventstore *eventstore.Eventstore
+ idGenerator id.Generator
+ userPasswordHasher *crypto.PasswordHasher
}
type args struct {
ctx context.Context
@@ -2342,6 +2246,7 @@ func TestCommandSide_RegisterHuman(t *testing.T) {
false,
false,
false,
+ false,
domain.PasswordlessTypeNotAllowed,
"",
time.Hour*1,
@@ -2411,6 +2316,7 @@ func TestCommandSide_RegisterHuman(t *testing.T) {
false,
false,
false,
+ false,
domain.PasswordlessTypeNotAllowed,
"",
time.Hour*1,
@@ -2480,6 +2386,7 @@ func TestCommandSide_RegisterHuman(t *testing.T) {
false,
false,
false,
+ false,
domain.PasswordlessTypeNotAllowed,
"",
time.Hour*1,
@@ -2566,6 +2473,7 @@ func TestCommandSide_RegisterHuman(t *testing.T) {
false,
false,
false,
+ false,
domain.PasswordlessTypeNotAllowed,
"",
time.Hour*1,
@@ -2612,7 +2520,7 @@ func TestCommandSide_RegisterHuman(t *testing.T) {
expectPush(
[]*repository.Event{
eventFromEventPusher(
- newRegisterHumanEvent("email@test.ch", "password", false, ""),
+ newRegisterHumanEvent("email@test.ch", "$plain$x$password", false, ""),
),
eventFromEventPusher(
user.NewHumanInitialCodeAddedEvent(context.Background(),
@@ -2630,8 +2538,8 @@ func TestCommandSide_RegisterHuman(t *testing.T) {
uniqueConstraintsFromEventConstraint(user.NewAddUsernameUniqueConstraint("email@test.ch", "org1", false)),
),
),
- idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"),
- userPasswordAlg: crypto.CreateMockHashAlg(gomock.NewController(t)),
+ idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"),
+ userPasswordHasher: mockPasswordHasher("x"),
},
args: args{
ctx: context.Background(),
@@ -2710,6 +2618,7 @@ func TestCommandSide_RegisterHuman(t *testing.T) {
false,
false,
false,
+ false,
domain.PasswordlessTypeNotAllowed,
"",
time.Hour*1,
@@ -2723,7 +2632,7 @@ func TestCommandSide_RegisterHuman(t *testing.T) {
expectPush(
[]*repository.Event{
eventFromEventPusher(
- newRegisterHumanEvent("username", "password", false, ""),
+ newRegisterHumanEvent("username", "$plain$x$password", false, ""),
),
eventFromEventPusher(
user.NewHumanInitialCodeAddedEvent(context.Background(),
@@ -2741,8 +2650,8 @@ func TestCommandSide_RegisterHuman(t *testing.T) {
uniqueConstraintsFromEventConstraint(user.NewAddUsernameUniqueConstraint("username", "org1", false)),
),
),
- idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"),
- userPasswordAlg: crypto.CreateMockHashAlg(gomock.NewController(t)),
+ idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"),
+ userPasswordHasher: mockPasswordHasher("x"),
},
args: args{
ctx: context.Background(),
@@ -2822,6 +2731,7 @@ func TestCommandSide_RegisterHuman(t *testing.T) {
false,
false,
false,
+ false,
domain.PasswordlessTypeNotAllowed,
"",
time.Hour*1,
@@ -2835,7 +2745,7 @@ func TestCommandSide_RegisterHuman(t *testing.T) {
expectPush(
[]*repository.Event{
eventFromEventPusher(
- newRegisterHumanEvent("username", "password", false, ""),
+ newRegisterHumanEvent("username", "$plain$x$password", false, ""),
),
eventFromEventPusher(
user.NewHumanInitialCodeAddedEvent(context.Background(),
@@ -2853,8 +2763,8 @@ func TestCommandSide_RegisterHuman(t *testing.T) {
uniqueConstraintsFromEventConstraint(user.NewAddUsernameUniqueConstraint("username", "org1", true)),
),
),
- idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"),
- userPasswordAlg: crypto.CreateMockHashAlg(gomock.NewController(t)),
+ idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"),
+ userPasswordHasher: mockPasswordHasher("x"),
},
args: args{
ctx: context.Background(),
@@ -2934,6 +2844,7 @@ func TestCommandSide_RegisterHuman(t *testing.T) {
false,
false,
false,
+ false,
domain.PasswordlessTypeNotAllowed,
"",
time.Hour*1,
@@ -2947,7 +2858,7 @@ func TestCommandSide_RegisterHuman(t *testing.T) {
expectPush(
[]*repository.Event{
eventFromEventPusher(
- newRegisterHumanEvent("username", "password", false, ""),
+ newRegisterHumanEvent("username", "$plain$x$password", false, ""),
),
eventFromEventPusher(
user.NewHumanEmailVerifiedEvent(context.Background(),
@@ -2957,8 +2868,8 @@ func TestCommandSide_RegisterHuman(t *testing.T) {
uniqueConstraintsFromEventConstraint(user.NewAddUsernameUniqueConstraint("username", "org1", true)),
),
),
- idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"),
- userPasswordAlg: crypto.CreateMockHashAlg(gomock.NewController(t)),
+ idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"),
+ userPasswordHasher: mockPasswordHasher("x"),
},
args: args{
ctx: context.Background(),
@@ -3040,6 +2951,7 @@ func TestCommandSide_RegisterHuman(t *testing.T) {
false,
false,
false,
+ false,
domain.PasswordlessTypeNotAllowed,
"",
time.Hour*1,
@@ -3053,7 +2965,7 @@ func TestCommandSide_RegisterHuman(t *testing.T) {
expectPush(
[]*repository.Event{
eventFromEventPusher(
- newRegisterHumanEvent("username", "password", false, "+41711234567"),
+ newRegisterHumanEvent("username", "$plain$x$password", false, "+41711234567"),
),
eventFromEventPusher(
user.NewHumanInitialCodeAddedEvent(context.Background(),
@@ -3081,8 +2993,8 @@ func TestCommandSide_RegisterHuman(t *testing.T) {
uniqueConstraintsFromEventConstraint(user.NewAddUsernameUniqueConstraint("username", "org1", true)),
),
),
- idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"),
- userPasswordAlg: crypto.CreateMockHashAlg(gomock.NewController(t)),
+ idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"),
+ userPasswordHasher: mockPasswordHasher("x"),
},
args: args{
ctx: context.Background(),
@@ -3168,6 +3080,7 @@ func TestCommandSide_RegisterHuman(t *testing.T) {
false,
false,
false,
+ false,
domain.PasswordlessTypeNotAllowed,
"",
time.Hour*1,
@@ -3181,7 +3094,7 @@ func TestCommandSide_RegisterHuman(t *testing.T) {
expectPush(
[]*repository.Event{
eventFromEventPusher(
- newRegisterHumanEvent("username", "password", false, "+41711234567"),
+ newRegisterHumanEvent("username", "$plain$x$password", false, "+41711234567"),
),
eventFromEventPusher(
user.NewHumanInitialCodeAddedEvent(context.Background(),
@@ -3203,8 +3116,8 @@ func TestCommandSide_RegisterHuman(t *testing.T) {
uniqueConstraintsFromEventConstraint(user.NewAddUsernameUniqueConstraint("username", "org1", true)),
),
),
- idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"),
- userPasswordAlg: crypto.CreateMockHashAlg(gomock.NewController(t)),
+ idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"),
+ userPasswordHasher: mockPasswordHasher("x"),
},
args: args{
ctx: context.Background(),
@@ -3291,6 +3204,7 @@ func TestCommandSide_RegisterHuman(t *testing.T) {
false,
false,
false,
+ false,
domain.PasswordlessTypeNotAllowed,
"",
time.Hour*1,
@@ -3336,7 +3250,7 @@ func TestCommandSide_RegisterHuman(t *testing.T) {
expectPush(
[]*repository.Event{
eventFromEventPusher(
- newRegisterHumanEvent("username", "password", false, ""),
+ newRegisterHumanEvent("username", "$plain$x$password", false, ""),
),
eventFromEventPusher(
user.NewUserIDPLinkAddedEvent(context.Background(),
@@ -3355,8 +3269,8 @@ func TestCommandSide_RegisterHuman(t *testing.T) {
uniqueConstraintsFromEventConstraint(user.NewAddUserIDPLinkUniqueConstraint("idpID", "externalID")),
),
),
- idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"),
- userPasswordAlg: crypto.CreateMockHashAlg(gomock.NewController(t)),
+ idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"),
+ userPasswordHasher: mockPasswordHasher("x"),
},
args: args{
ctx: context.Background(),
@@ -3407,9 +3321,9 @@ func TestCommandSide_RegisterHuman(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := &Commands{
- eventstore: tt.fields.eventstore,
- idGenerator: tt.fields.idGenerator,
- userPasswordAlg: tt.fields.userPasswordAlg,
+ eventstore: tt.fields.eventstore,
+ idGenerator: tt.fields.idGenerator,
+ userPasswordHasher: tt.fields.userPasswordHasher,
}
got, err := r.RegisterHuman(tt.args.ctx, tt.args.orgID, tt.args.human, tt.args.link, tt.args.orgMemberRoles, tt.args.secretGenerator, tt.args.secretGenerator, tt.args.secretGenerator)
if tt.res.err == nil {
@@ -3748,13 +3662,7 @@ func newAddHumanEvent(password string, changeRequired bool, phone string) *user.
true,
)
if password != "" {
- event.AddPasswordData(&crypto.CryptoValue{
- CryptoType: crypto.TypeHash,
- Algorithm: "hash",
- KeyID: "",
- Crypted: []byte(password),
- },
- changeRequired)
+ event.AddPasswordData(password, changeRequired)
}
if phone != "" {
event.AddPhoneData(domain.PhoneNumber(phone))
@@ -3776,13 +3684,7 @@ func newRegisterHumanEvent(username, password string, changeRequired bool, phone
true,
)
if password != "" {
- event.AddPasswordData(&crypto.CryptoValue{
- CryptoType: crypto.TypeHash,
- Algorithm: "hash",
- KeyID: "",
- Crypted: []byte(password),
- },
- changeRequired)
+ event.AddPasswordData(password, changeRequired)
}
if phone != "" {
event.AddPhoneData(domain.PhoneNumber(phone))
@@ -3797,7 +3699,7 @@ func TestAddHumanCommand(t *testing.T) {
type args struct {
human *AddHuman
orgID string
- passwordAlg crypto.HashAlgorithm
+ hasher *crypto.PasswordHasher
filter preparation.FilterToQueryReducer
codeAlg crypto.EncryptionAlgorithm
allowInitMail bool
@@ -3854,6 +3756,24 @@ func TestAddHumanCommand(t *testing.T) {
ValidationErr: caos_errs.ThrowInvalidArgument(nil, "USER-4hB7d", "Errors.User.Profile.LastNameEmpty"),
},
},
+ {
+ name: "unsupported password hash encoding",
+ args: args{
+ human: &AddHuman{
+ Email: Email{Address: "support@zitadel.com", Verified: true},
+ PreferredLanguage: language.English,
+ FirstName: "gigi",
+ LastName: "giraffe",
+ EncodedPasswordHash: "$foo$x$password",
+ Username: "username",
+ },
+ orgID: "ro",
+ hasher: mockPasswordHasher("x"),
+ },
+ want: Want{
+ ValidationErr: caos_errs.ThrowInvalidArgument(nil, "USER-JDk4t", "Errors.User.Password.NotSupported"),
+ },
+ },
{
name: "invalid password",
fields: fields{
@@ -3919,9 +3839,9 @@ func TestAddHumanCommand(t *testing.T) {
Password: "password",
Username: "username",
},
- orgID: "ro",
- passwordAlg: crypto.CreateMockHashAlg(gomock.NewController(t)),
- codeAlg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
+ orgID: "ro",
+ hasher: mockPasswordHasher("x"),
+ codeAlg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
filter: NewMultiFilter().Append(
func(ctx context.Context, queryFactory *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) {
return []eventstore.Event{}, nil
@@ -3970,12 +3890,79 @@ func TestAddHumanCommand(t *testing.T) {
"support@zitadel.com",
true,
)
- event.AddPasswordData(&crypto.CryptoValue{
- CryptoType: crypto.TypeHash,
- Algorithm: "hash",
- KeyID: "",
- Crypted: []byte("password"),
- }, false)
+ event.AddPasswordData("$plain$x$password", false)
+ return event
+ }(),
+ user.NewHumanEmailVerifiedEvent(context.Background(), &agg.Aggregate),
+ },
+ },
+ },
+ {
+ name: "hashed password",
+ fields: fields{
+ idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "id"),
+ },
+ args: args{
+ human: &AddHuman{
+ Email: Email{Address: "support@zitadel.com", Verified: true},
+ PreferredLanguage: language.English,
+ FirstName: "gigi",
+ LastName: "giraffe",
+ EncodedPasswordHash: "$plain$x$password",
+ Username: "username",
+ },
+ orgID: "ro",
+ hasher: mockPasswordHasher("x"),
+ codeAlg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
+ filter: NewMultiFilter().Append(
+ func(ctx context.Context, queryFactory *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) {
+ return []eventstore.Event{}, nil
+ }).
+ Append(
+ func(ctx context.Context, queryFactory *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) {
+ return []eventstore.Event{
+ org.NewDomainPolicyAddedEvent(
+ ctx,
+ &org.NewAggregate("id").Aggregate,
+ true,
+ true,
+ true,
+ ),
+ }, nil
+ }).
+ Append(
+ func(ctx context.Context, queryFactory *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) {
+ return []eventstore.Event{
+ org.NewPasswordComplexityPolicyAddedEvent(
+ ctx,
+ &org.NewAggregate("id").Aggregate,
+ 2,
+ false,
+ false,
+ false,
+ false,
+ ),
+ }, nil
+ }).
+ Filter(),
+ },
+ want: Want{
+ Commands: []eventstore.Command{
+ func() *user.HumanAddedEvent {
+ event := user.NewHumanAddedEvent(
+ context.Background(),
+ &agg.Aggregate,
+ "username",
+ "gigi",
+ "giraffe",
+ "",
+ "gigi giraffe",
+ language.English,
+ 0,
+ "support@zitadel.com",
+ true,
+ )
+ event.AddPasswordData("$plain$x$password", false)
return event
}(),
user.NewHumanEmailVerifiedEvent(context.Background(), &agg.Aggregate),
@@ -3988,7 +3975,7 @@ func TestAddHumanCommand(t *testing.T) {
c := &Commands{
idGenerator: tt.fields.idGenerator,
}
- AssertValidation(t, context.Background(), c.AddHumanCommand(tt.args.human, tt.args.orgID, tt.args.passwordAlg, tt.args.codeAlg, tt.args.allowInitMail), tt.args.filter, tt.want)
+ AssertValidation(t, context.Background(), c.AddHumanCommand(tt.args.human, tt.args.orgID, tt.args.hasher, tt.args.codeAlg, tt.args.allowInitMail), tt.args.filter, tt.want)
})
}
}
diff --git a/internal/command/user_machine_secret.go b/internal/command/user_machine_secret.go
index a7a91b247..8f28b5e5d 100644
--- a/internal/command/user_machine_secret.go
+++ b/internal/command/user_machine_secret.go
@@ -113,7 +113,7 @@ func prepareRemoveMachineSecret(a *user.Aggregate) preparation.Validation {
func (c *Commands) VerifyMachineSecret(ctx context.Context, userID string, resourceOwner string, secret string) (*domain.ObjectDetails, error) {
agg := user.NewAggregate(userID, resourceOwner)
- cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, prepareVerifyMachineSecret(agg, secret, c.userPasswordAlg))
+ cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, prepareVerifyMachineSecret(agg, secret, c.codeAlg))
if err != nil {
return nil, err
}
diff --git a/internal/command/user_machine_secret_test.go b/internal/command/user_machine_secret_test.go
index 490797b96..42df770d9 100644
--- a/internal/command/user_machine_secret_test.go
+++ b/internal/command/user_machine_secret_test.go
@@ -529,8 +529,8 @@ func TestCommandSide_VerifyMachineSecret(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := &Commands{
- eventstore: tt.fields.eventstore,
- userPasswordAlg: crypto.NewBCrypt(14),
+ eventstore: tt.fields.eventstore,
+ codeAlg: crypto.NewBCrypt(14),
}
got, err := r.VerifyMachineSecret(tt.args.ctx, tt.args.userID, tt.args.resourceOwner, tt.args.secret)
if tt.res.err == nil {
diff --git a/internal/command/user_v2_passkey.go b/internal/command/user_v2_passkey.go
index f08a0632c..47af8c134 100644
--- a/internal/command/user_v2_passkey.go
+++ b/internal/command/user_v2_passkey.go
@@ -7,6 +7,7 @@ import (
"github.com/zitadel/logging"
"github.com/zitadel/zitadel/internal/api/authz"
+ "github.com/zitadel/zitadel/internal/command/preparation"
"github.com/zitadel/zitadel/internal/crypto"
"github.com/zitadel/zitadel/internal/domain"
caos_errs "github.com/zitadel/zitadel/internal/errors"
@@ -132,7 +133,7 @@ func (c *Commands) addUserPasskeyCode(ctx context.Context, userID, resourceOwner
if err != nil {
return nil, err
}
- code, err := c.newCode(ctx, c.eventstore.Filter, domain.SecretGeneratorTypePasswordlessInitCode, alg)
+ code, err := c.newPasskeyCode(ctx, c.eventstore.Filter, alg)
if err != nil {
return nil, err
}
@@ -154,3 +155,7 @@ func (c *Commands) addUserPasskeyCode(ctx context.Context, userID, resourceOwner
Code: code.Plain,
}, nil
}
+
+func (c *Commands) newPasskeyCode(ctx context.Context, filter preparation.FilterToQueryReducer, alg crypto.EncryptionAlgorithm) (*CryptoCode, error) {
+ return c.newCode(ctx, filter, domain.SecretGeneratorTypePasswordlessInitCode, alg)
+}
diff --git a/internal/command/user_v2_passkey_test.go b/internal/command/user_v2_passkey_test.go
index 1885f6ab2..8a96d4a21 100644
--- a/internal/command/user_v2_passkey_test.go
+++ b/internal/command/user_v2_passkey_test.go
@@ -139,7 +139,7 @@ func TestCommands_RegisterUserPasskeyWithCode(t *testing.T) {
es := eventstoreExpect(t,
expectFilter(eventFromEventPusher(testSecretGeneratorAddedEvent(domain.SecretGeneratorTypePasswordlessInitCode))),
)
- code, err := newCryptoCodeWithExpiry(ctx, es.Filter, domain.SecretGeneratorTypePasswordlessInitCode, alg)
+ code, err := newCryptoCode(ctx, es.Filter, domain.SecretGeneratorTypePasswordlessInitCode, alg)
require.NoError(t, err)
userAgg := &user.NewAggregate("user1", "org1").Aggregate
type fields struct {
@@ -237,7 +237,7 @@ func TestCommands_verifyUserPasskeyCode(t *testing.T) {
es := eventstoreExpect(t,
expectFilter(eventFromEventPusher(testSecretGeneratorAddedEvent(domain.SecretGeneratorTypePasswordlessInitCode))),
)
- code, err := newCryptoCodeWithExpiry(ctx, es.Filter, domain.SecretGeneratorTypePasswordlessInitCode, alg)
+ code, err := newCryptoCode(ctx, es.Filter, domain.SecretGeneratorTypePasswordlessInitCode, alg)
require.NoError(t, err)
userAgg := &user.NewAggregate("user1", "org1").Aggregate
@@ -463,7 +463,7 @@ func TestCommands_AddUserPasskeyCode(t *testing.T) {
userAgg := &user.NewAggregate("user1", "org1").Aggregate
type fields struct {
newCode cryptoCodeFunc
- eventstore *eventstore.Eventstore
+ eventstore func(t *testing.T) *eventstore.Eventstore
idGenerator id.Generator
}
type args struct {
@@ -480,8 +480,8 @@ func TestCommands_AddUserPasskeyCode(t *testing.T) {
{
name: "id generator error",
fields: fields{
- newCode: newCryptoCodeWithExpiry,
- eventstore: eventstoreExpect(t),
+ newCode: mockCode("passkey1", time.Hour),
+ eventstore: expectEventstore(),
idGenerator: id_mock.NewIDGeneratorExpectError(t, io.ErrClosedPipe),
},
args: args{
@@ -494,7 +494,7 @@ func TestCommands_AddUserPasskeyCode(t *testing.T) {
name: "success",
fields: fields{
newCode: mockCode("passkey1", time.Minute),
- eventstore: eventstoreExpect(t,
+ eventstore: expectEventstore(
expectFilter(eventFromEventPusher(
user.NewHumanAddedEvent(context.Background(),
userAgg,
@@ -538,7 +538,7 @@ func TestCommands_AddUserPasskeyCode(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
c := &Commands{
newCode: tt.fields.newCode,
- eventstore: tt.fields.eventstore,
+ eventstore: tt.fields.eventstore(t),
idGenerator: tt.fields.idGenerator,
}
got, err := c.AddUserPasskeyCode(context.Background(), tt.args.userID, tt.args.resourceOwner, alg)
@@ -572,7 +572,7 @@ func TestCommands_AddUserPasskeyCodeURLTemplate(t *testing.T) {
{
name: "template error",
fields: fields{
- newCode: newCryptoCodeWithExpiry,
+ newCode: newCryptoCode,
eventstore: eventstoreExpect(t),
},
args: args{
@@ -585,7 +585,7 @@ func TestCommands_AddUserPasskeyCodeURLTemplate(t *testing.T) {
{
name: "id generator error",
fields: fields{
- newCode: newCryptoCodeWithExpiry,
+ newCode: newCryptoCode,
eventstore: eventstoreExpect(t),
idGenerator: id_mock.NewIDGeneratorExpectError(t, io.ErrClosedPipe),
},
@@ -680,7 +680,7 @@ func TestCommands_AddUserPasskeyCodeReturn(t *testing.T) {
{
name: "id generator error",
fields: fields{
- newCode: newCryptoCodeWithExpiry,
+ newCode: newCryptoCode,
eventstore: eventstoreExpect(t),
idGenerator: id_mock.NewIDGeneratorExpectError(t, io.ErrClosedPipe),
},
@@ -774,7 +774,7 @@ func TestCommands_addUserPasskeyCode(t *testing.T) {
{
name: "id generator error",
fields: fields{
- newCode: newCryptoCodeWithExpiry,
+ newCode: newCryptoCode,
eventstore: eventstoreExpect(t),
idGenerator: id_mock.NewIDGeneratorExpectError(t, io.ErrClosedPipe),
},
@@ -787,7 +787,7 @@ func TestCommands_addUserPasskeyCode(t *testing.T) {
{
name: "crypto error",
fields: fields{
- newCode: newCryptoCodeWithExpiry,
+ newCode: newCryptoCode,
eventstore: eventstoreExpect(t, expectFilterError(io.ErrClosedPipe)),
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "123"),
},
@@ -800,7 +800,7 @@ func TestCommands_addUserPasskeyCode(t *testing.T) {
{
name: "filter query error",
fields: fields{
- newCode: newCryptoCodeWithExpiry,
+ newCode: newCryptoCode,
eventstore: eventstoreExpect(t,
expectFilter(eventFromEventPusher(testSecretGeneratorAddedEvent(domain.SecretGeneratorTypePasswordlessInitCode))),
expectFilterError(io.ErrClosedPipe),
diff --git a/internal/command/user_v2_totp_test.go b/internal/command/user_v2_totp_test.go
index d7b9f8246..90596428a 100644
--- a/internal/command/user_v2_totp_test.go
+++ b/internal/command/user_v2_totp_test.go
@@ -188,7 +188,7 @@ func TestCommands_AddUserTOTP(t *testing.T) {
}
func TestCommands_CheckUserTOTP(t *testing.T) {
- ctx := authz.NewMockContext("inst1", "org1", "user1")
+ ctx := authz.NewMockContext("", "org1", "user1")
cryptoAlg := crypto.CreateMockEncryptionAlg(gomock.NewController(t))
key, secret, err := domain.NewOTPKey("example.com", "user1", cryptoAlg)
diff --git a/internal/config/systemdefaults/system_defaults.go b/internal/config/systemdefaults/system_defaults.go
index 7ca33b04b..012c37ac0 100644
--- a/internal/config/systemdefaults/system_defaults.go
+++ b/internal/config/systemdefaults/system_defaults.go
@@ -8,6 +8,7 @@ import (
type SystemDefaults struct {
SecretGenerators SecretGenerators
+ PasswordHasher crypto.PasswordHashConfig
Multifactors MultifactorConfig
DomainVerification DomainVerification
Notifications Notifications
diff --git a/internal/crypto/crypto.go b/internal/crypto/crypto.go
index 748a124b5..5cc057590 100644
--- a/internal/crypto/crypto.go
+++ b/internal/crypto/crypto.go
@@ -10,7 +10,7 @@ import (
const (
TypeEncryption CryptoType = iota
- TypeHash
+ TypeHash // Depcrecated: use [passwap.Swapper] instead
)
type Crypto interface {
@@ -26,6 +26,7 @@ type EncryptionAlgorithm interface {
DecryptString(hashed []byte, keyID string) (string, error)
}
+// Depcrecated: use [passwap.Swapper] instead
type HashAlgorithm interface {
Crypto
Hash(value []byte) ([]byte, error)
diff --git a/internal/crypto/passwap.go b/internal/crypto/passwap.go
new file mode 100644
index 000000000..cf72a844f
--- /dev/null
+++ b/internal/crypto/passwap.go
@@ -0,0 +1,209 @@
+package crypto
+
+import (
+ "fmt"
+ "strings"
+
+ "github.com/mitchellh/mapstructure"
+ "github.com/zitadel/passwap"
+ "github.com/zitadel/passwap/argon2"
+ "github.com/zitadel/passwap/bcrypt"
+ "github.com/zitadel/passwap/md5"
+ "github.com/zitadel/passwap/scrypt"
+ "github.com/zitadel/passwap/verifier"
+
+ "github.com/zitadel/zitadel/internal/errors"
+)
+
+type PasswordHasher struct {
+ *passwap.Swapper
+ Prefixes []string
+}
+
+func (h *PasswordHasher) EncodingSupported(encodedHash string) bool {
+ for _, prefix := range h.Prefixes {
+ if strings.HasPrefix(encodedHash, prefix) {
+ return true
+ }
+ }
+ return false
+}
+
+type HashName string
+
+const (
+ HashNameArgon2 HashName = "argon2" // used for the common argon2 verifier
+ HashNameArgon2i HashName = "argon2i" // hash only
+ HashNameArgon2id HashName = "argon2id" // hash only
+ HashNameBcrypt HashName = "bcrypt" // hash and verify
+ HashNameMd5 HashName = "md5" // verify only, as hashing with md5 is insecure and deprecated
+ HashNameScrypt HashName = "scrypt" // hash and verify
+)
+
+type PasswordHashConfig struct {
+ Verifiers []HashName
+ Hasher HasherConfig
+}
+
+func (c *PasswordHashConfig) PasswordHasher() (*PasswordHasher, error) {
+ verifiers, vPrefixes, err := c.buildVerifiers()
+ if err != nil {
+ return nil, errors.ThrowInvalidArgument(err, "CRYPT-sahW9", "password hash config invalid")
+ }
+ hasher, hPrefixes, err := c.Hasher.buildHasher()
+ if err != nil {
+ return nil, errors.ThrowInvalidArgument(err, "CRYPT-Que4r", "password hash config invalid")
+ }
+ return &PasswordHasher{
+ Swapper: passwap.NewSwapper(hasher, verifiers...),
+ Prefixes: append(hPrefixes, vPrefixes...),
+ }, nil
+}
+
+type prefixVerifier struct {
+ prefixes []string
+ verifier verifier.Verifier
+}
+
+// map HashNames to Verifier instances.
+var knowVerifiers = map[HashName]prefixVerifier{
+ HashNameArgon2: {
+ // only argon2i and argon2id are suppored.
+ // The Prefix constant also covers argon2d.
+ prefixes: []string{argon2.Prefix},
+ verifier: argon2.Verifier,
+ },
+ HashNameBcrypt: {
+ prefixes: []string{bcrypt.Prefix},
+ verifier: bcrypt.Verifier,
+ },
+ HashNameMd5: {
+ prefixes: []string{md5.Prefix},
+ verifier: md5.Verifier,
+ },
+ HashNameScrypt: {
+ prefixes: []string{scrypt.Prefix, scrypt.Prefix_Linux},
+ verifier: scrypt.Verifier,
+ },
+}
+
+func (c *PasswordHashConfig) buildVerifiers() (verifiers []verifier.Verifier, prefixes []string, err error) {
+ verifiers = make([]verifier.Verifier, len(c.Verifiers))
+ prefixes = make([]string, 0, len(c.Verifiers)+1)
+ for i, name := range c.Verifiers {
+ v, ok := knowVerifiers[name]
+ if !ok {
+ return nil, nil, fmt.Errorf("invalid verifier %q", name)
+ }
+ verifiers[i] = v.verifier
+ prefixes = append(prefixes, v.prefixes...)
+ }
+ return verifiers, prefixes, nil
+}
+
+type HasherConfig struct {
+ Algorithm HashName
+ Params map[string]any `mapstructure:",remain"`
+}
+
+func (c *HasherConfig) buildHasher() (hasher passwap.Hasher, prefixes []string, err error) {
+ switch c.Algorithm {
+ case HashNameArgon2i:
+ return c.argon2i()
+ case HashNameArgon2id:
+ return c.argon2id()
+ case HashNameBcrypt:
+ return c.bcrypt()
+ case HashNameScrypt:
+ return c.scrypt()
+ case "":
+ return nil, nil, fmt.Errorf("missing hasher algorithm")
+ case HashNameArgon2, HashNameMd5:
+ fallthrough
+ default:
+ return nil, nil, fmt.Errorf("invalid algorithm %q", c.Algorithm)
+ }
+}
+
+func (c *HasherConfig) decodeParams(dst any) error {
+ decoder, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{
+ ErrorUnused: true,
+ ErrorUnset: true,
+ Result: dst,
+ })
+ if err != nil {
+ return err
+ }
+ return decoder.Decode(c.Params)
+}
+
+// argon2Params decodes [HasherConfig.Params] into a [argon2.Params] used as defaults.
+// p is passed a copy and therfore will not be modified.
+func (c *HasherConfig) argon2Params(p argon2.Params) (argon2.Params, error) {
+ var dst struct {
+ Time uint32 `mapstructure:"Time"`
+ Memory uint32 `mapstructure:"Memory"`
+ Threads uint8 `mapstructure:"Threads"`
+ }
+ if err := c.decodeParams(&dst); err != nil {
+ return argon2.Params{}, fmt.Errorf("decode argon2i params: %w", err)
+ }
+ p.Time = dst.Time
+ p.Memory = dst.Memory
+ p.Threads = dst.Threads
+ return p, nil
+}
+
+func (c *HasherConfig) argon2i() (passwap.Hasher, []string, error) {
+ p, err := c.argon2Params(argon2.RecommendedIParams)
+ if err != nil {
+ return nil, nil, err
+ }
+ return argon2.NewArgon2i(p), []string{argon2.Prefix}, nil
+}
+
+func (c *HasherConfig) argon2id() (passwap.Hasher, []string, error) {
+ p, err := c.argon2Params(argon2.RecommendedIDParams)
+ if err != nil {
+ return nil, nil, err
+ }
+ return argon2.NewArgon2id(p), []string{argon2.Prefix}, nil
+}
+
+func (c *HasherConfig) bcryptCost() (int, error) {
+ var dst = struct {
+ Cost int `mapstructure:"Cost"`
+ }{}
+ if err := c.decodeParams(&dst); err != nil {
+ return 0, fmt.Errorf("decode bcrypt params: %w", err)
+ }
+ return dst.Cost, nil
+}
+
+func (c *HasherConfig) bcrypt() (passwap.Hasher, []string, error) {
+ cost, err := c.bcryptCost()
+ if err != nil {
+ return nil, nil, err
+ }
+ return bcrypt.New(cost), []string{bcrypt.Prefix}, nil
+}
+
+func (c *HasherConfig) scryptParams() (scrypt.Params, error) {
+ var dst = struct {
+ Cost int `mapstructure:"Cost"`
+ }{}
+ if err := c.decodeParams(&dst); err != nil {
+ return scrypt.Params{}, fmt.Errorf("decode scrypt params: %w", err)
+ }
+ p := scrypt.RecommendedParams // copy
+ p.N = 1 << dst.Cost
+ return p, nil
+}
+
+func (c *HasherConfig) scrypt() (passwap.Hasher, []string, error) {
+ p, err := c.scryptParams()
+ if err != nil {
+ return nil, nil, err
+ }
+ return scrypt.New(p), []string{scrypt.Prefix, scrypt.Prefix_Linux}, nil
+}
diff --git a/internal/crypto/passwap_test.go b/internal/crypto/passwap_test.go
new file mode 100644
index 000000000..2cc5aa80e
--- /dev/null
+++ b/internal/crypto/passwap_test.go
@@ -0,0 +1,486 @@
+package crypto
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+ "github.com/zitadel/passwap/argon2"
+ "github.com/zitadel/passwap/bcrypt"
+ "github.com/zitadel/passwap/md5"
+ "github.com/zitadel/passwap/scrypt"
+)
+
+func TestPasswordHasher_EncodingSupported(t *testing.T) {
+ tests := []struct {
+ name string
+ encodedHash string
+ want bool
+ }{
+ {
+ name: "empty string, false",
+ encodedHash: "",
+ want: false,
+ },
+ {
+ name: "scrypt, false",
+ encodedHash: "$scrypt$ln=16,r=8,p=1$cmFuZG9tc2FsdGlzaGFyZA$Rh+NnJNo1I6nRwaNqbDm6kmADswD1+7FTKZ7Ln9D8nQ",
+ want: false,
+ },
+ {
+ name: "bcrypt, true",
+ encodedHash: "$2y$12$hXUrnqdq1RIIYZ2HPytIIe5lXdIvbhqrTvdPsSF7o.jFh817Z6lwm",
+ want: true,
+ },
+ {
+ name: "argo2i, true",
+ encodedHash: "$argon2i$v=19$m=4096,t=3,p=1$cmFuZG9tc2FsdGlzaGFyZA$YMvo8AUoNtnKYGqeODruCjHdiEbl1pKL2MsYy9VgU/E",
+ want: true,
+ },
+ {
+ name: "argo2id, true",
+ encodedHash: "$argon2d$v=19$m=4096,t=3,p=1$cmFuZG9tc2FsdGlzaGFyZA$CB0Du96aj3fQVcVSqb0LIA6Z6fpStjzjVkaC3RlpK9A",
+ want: true,
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ h := &PasswordHasher{
+ Prefixes: []string{bcrypt.Prefix, argon2.Prefix},
+ }
+ got := h.EncodingSupported(tt.encodedHash)
+ assert.Equal(t, tt.want, got)
+ })
+ }
+}
+
+func TestPasswordHashConfig_PasswordHasher(t *testing.T) {
+ type fields struct {
+ Verifiers []HashName
+ Hasher HasherConfig
+ }
+ tests := []struct {
+ name string
+ fields fields
+ wantPrefixes []string
+ wantErr bool
+ }{
+ {
+ name: "invalid verifier",
+ fields: fields{
+ Verifiers: []HashName{
+ HashNameArgon2,
+ HashNameBcrypt,
+ HashNameMd5,
+ HashNameScrypt,
+ "foobar",
+ },
+ Hasher: HasherConfig{
+ Algorithm: HashNameBcrypt,
+ Params: map[string]any{
+ "cost": 5,
+ },
+ },
+ },
+ wantErr: true,
+ },
+ {
+ name: "invalid hasher",
+ fields: fields{
+ Verifiers: []HashName{
+ HashNameArgon2,
+ HashNameBcrypt,
+ HashNameMd5,
+ HashNameScrypt,
+ },
+ Hasher: HasherConfig{
+ Algorithm: "foobar",
+ Params: map[string]any{
+ "cost": 5,
+ },
+ },
+ },
+ wantErr: true,
+ },
+ {
+ name: "missing algorithm",
+ fields: fields{
+ Hasher: HasherConfig{},
+ },
+ wantErr: true,
+ },
+ {
+ name: "invalid md5",
+ fields: fields{
+ Hasher: HasherConfig{
+ Algorithm: HashNameMd5,
+ },
+ },
+ wantErr: true,
+ },
+ {
+ name: "invalid argon2",
+ fields: fields{
+ Hasher: HasherConfig{
+ Algorithm: HashNameArgon2,
+ Params: map[string]any{
+ "time": 3,
+ "memory": 32768,
+ "threads": 4,
+ },
+ },
+ },
+ wantErr: true,
+ },
+ {
+ name: "argon2i, error",
+ fields: fields{
+ Hasher: HasherConfig{
+ Algorithm: HashNameArgon2i,
+ Params: map[string]any{
+ "time": 3,
+ "threads": 4,
+ },
+ },
+ },
+ wantErr: true,
+ },
+ {
+ name: "argon2i, ok",
+ fields: fields{
+ Hasher: HasherConfig{
+ Algorithm: HashNameArgon2i,
+ Params: map[string]any{
+ "time": 3,
+ "memory": 32768,
+ "threads": 4,
+ },
+ },
+ Verifiers: []HashName{HashNameBcrypt, HashNameMd5, HashNameScrypt},
+ },
+ wantPrefixes: []string{argon2.Prefix, bcrypt.Prefix, md5.Prefix, scrypt.Prefix, scrypt.Prefix_Linux},
+ },
+ {
+ name: "argon2id, error",
+ fields: fields{
+ Hasher: HasherConfig{
+ Algorithm: HashNameArgon2id,
+ Params: map[string]any{
+ "time": 3,
+ "threads": 4,
+ },
+ },
+ },
+ wantErr: true,
+ },
+ {
+ name: "argon2id, ok",
+ fields: fields{
+ Hasher: HasherConfig{
+ Algorithm: HashNameArgon2id,
+ Params: map[string]any{
+ "time": 3,
+ "memory": 32768,
+ "threads": 4,
+ },
+ },
+ Verifiers: []HashName{HashNameBcrypt, HashNameMd5, HashNameScrypt},
+ },
+ wantPrefixes: []string{argon2.Prefix, bcrypt.Prefix, md5.Prefix, scrypt.Prefix, scrypt.Prefix_Linux},
+ },
+ {
+ name: "bcrypt, error",
+ fields: fields{
+ Hasher: HasherConfig{
+ Algorithm: HashNameBcrypt,
+ Params: map[string]any{
+ "foo": 3,
+ },
+ },
+ },
+ wantErr: true,
+ },
+ {
+ name: "bcrypt, ok",
+ fields: fields{
+ Hasher: HasherConfig{
+ Algorithm: HashNameBcrypt,
+ Params: map[string]any{
+ "cost": 3,
+ },
+ },
+ Verifiers: []HashName{HashNameArgon2, HashNameMd5, HashNameScrypt},
+ },
+ wantPrefixes: []string{bcrypt.Prefix, argon2.Prefix, md5.Prefix, scrypt.Prefix, scrypt.Prefix_Linux},
+ },
+ {
+ name: "scrypt, error",
+ fields: fields{
+ Hasher: HasherConfig{
+ Algorithm: HashNameScrypt,
+ Params: map[string]any{
+ "cost": "bar",
+ },
+ },
+ },
+ wantErr: true,
+ },
+ {
+ name: "scrypt, ok",
+ fields: fields{
+ Hasher: HasherConfig{
+ Algorithm: HashNameScrypt,
+ Params: map[string]any{
+ "cost": 3,
+ },
+ },
+ Verifiers: []HashName{HashNameArgon2, HashNameBcrypt, HashNameMd5},
+ },
+ wantPrefixes: []string{scrypt.Prefix, scrypt.Prefix_Linux, argon2.Prefix, bcrypt.Prefix, md5.Prefix},
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ c := &PasswordHashConfig{
+ Verifiers: tt.fields.Verifiers,
+ Hasher: tt.fields.Hasher,
+ }
+ got, err := c.PasswordHasher()
+ if tt.wantErr {
+ assert.Error(t, err)
+ return
+ }
+ require.NoError(t, err)
+ if tt.wantPrefixes != nil {
+ require.NotNil(t, got)
+ assert.Equal(t, tt.wantPrefixes, got.Prefixes)
+ encoded, err := got.Hash("password")
+ require.NoError(t, err)
+ assert.NotEmpty(t, encoded)
+ }
+ })
+ }
+}
+
+func TestHasherConfig_decodeParams(t *testing.T) {
+ type dst struct {
+ A int
+ B uint32
+ }
+ tests := []struct {
+ name string
+ params map[string]any
+ want dst
+ wantErr bool
+ }{
+ {
+ name: "unused",
+ params: map[string]any{
+ "a": 1,
+ "b": 2,
+ "c": 3,
+ },
+ wantErr: true,
+ },
+ {
+ name: "unset",
+ params: map[string]any{
+ "a": 1,
+ },
+ wantErr: true,
+ },
+ {
+ name: "wrong type",
+ params: map[string]any{
+ "a": 1,
+ "b": "2",
+ },
+ wantErr: true,
+ },
+ {
+ name: "ok",
+ params: map[string]any{
+ "a": 1,
+ "b": 2,
+ },
+ want: dst{
+ A: 1,
+ B: 2,
+ },
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ c := &HasherConfig{
+ Params: tt.params,
+ }
+ var got dst
+ err := c.decodeParams(&got)
+ if tt.wantErr {
+ assert.Error(t, err)
+ return
+ }
+ require.NoError(t, err)
+ assert.Equal(t, tt.want, got)
+ })
+ }
+}
+
+func TestHasherConfig_argon2Params(t *testing.T) {
+ type fields struct {
+ Params map[string]any
+ }
+ type args struct {
+ p argon2.Params
+ }
+ tests := []struct {
+ name string
+ fields fields
+ args args
+ want argon2.Params
+ wantErr bool
+ }{
+ {
+ name: "decode error",
+ fields: fields{
+ Params: map[string]any{
+ "foo": "bar",
+ },
+ },
+ args: args{
+ p: argon2.RecommendedIDParams,
+ },
+ wantErr: true,
+ },
+ {
+ name: "ok",
+ fields: fields{
+ Params: map[string]any{
+ "time": 2,
+ "memory": 256,
+ "threads": 8,
+ },
+ },
+ args: args{
+ p: argon2.RecommendedIDParams,
+ },
+ want: argon2.Params{
+ Time: 2,
+ Memory: 256,
+ Threads: 8,
+ KeyLen: 32,
+ SaltLen: 16,
+ },
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ c := &HasherConfig{
+ Params: tt.fields.Params,
+ }
+ got, err := c.argon2Params(tt.args.p)
+ if tt.wantErr {
+ assert.Error(t, err)
+ return
+ }
+ require.NoError(t, err)
+ assert.Equal(t, tt.want, got)
+ })
+ }
+}
+
+func TestHasherConfig_bcryptCost(t *testing.T) {
+ type fields struct {
+ Params map[string]any
+ }
+ tests := []struct {
+ name string
+ fields fields
+ want int
+ wantErr bool
+ }{
+ {
+ name: "decode error",
+ fields: fields{
+ Params: map[string]any{
+ "foo": "bar",
+ },
+ },
+ wantErr: true,
+ },
+ {
+ name: "ok",
+ fields: fields{
+ Params: map[string]any{
+ "cost": 12,
+ },
+ },
+ want: 12,
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ c := &HasherConfig{
+ Params: tt.fields.Params,
+ }
+ got, err := c.bcryptCost()
+ if tt.wantErr {
+ assert.Error(t, err)
+ return
+ }
+ require.NoError(t, err)
+ assert.Equal(t, tt.want, got)
+ })
+ }
+}
+
+func TestHasherConfig_scryptParams(t *testing.T) {
+ type fields struct {
+ Params map[string]any
+ }
+ tests := []struct {
+ name string
+ fields fields
+ want scrypt.Params
+ wantErr bool
+ }{
+ {
+ name: "decode error",
+ fields: fields{
+ Params: map[string]any{
+ "foo": "bar",
+ },
+ },
+ wantErr: true,
+ },
+ {
+ name: "ok",
+ fields: fields{
+ Params: map[string]any{
+ "cost": 2,
+ },
+ },
+ want: scrypt.Params{
+ N: 4,
+ R: 8,
+ P: 1,
+ KeyLen: 32,
+ SaltLen: 16,
+ },
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ c := &HasherConfig{
+ Params: tt.fields.Params,
+ }
+ got, err := c.scryptParams()
+ if tt.wantErr {
+ assert.Error(t, err)
+ return
+ }
+ require.NoError(t, err)
+ assert.Equal(t, tt.want, got)
+ })
+ }
+}
diff --git a/internal/domain/auth_request.go b/internal/domain/auth_request.go
index c0b9e1a40..765fead8a 100644
--- a/internal/domain/auth_request.go
+++ b/internal/domain/auth_request.go
@@ -116,6 +116,17 @@ const (
MFALevelMultiFactorCertified
)
+type AuthRequestState int
+
+const (
+ AuthRequestStateUnspecified AuthRequestState = iota
+ AuthRequestStateAdded
+ AuthRequestStateCodeAdded
+ AuthRequestStateCodeExchanged
+ AuthRequestStateFailed
+ AuthRequestStateSucceeded
+)
+
func NewAuthRequestFromType(requestType AuthRequestType) (*AuthRequest, error) {
switch requestType {
case AuthRequestTypeOIDC:
diff --git a/internal/domain/human.go b/internal/domain/human.go
index 619eaf480..0454a3665 100644
--- a/internal/domain/human.go
+++ b/internal/domain/human.go
@@ -16,7 +16,7 @@ type Human struct {
Username string
State UserState
*Password
- *HashedPassword
+ HashedPassword string
*Profile
*Email
*Phone
@@ -103,10 +103,10 @@ func (u *Human) EnsureDisplayName() {
u.DisplayName = u.Username
}
-func (u *Human) HashPasswordIfExisting(policy *PasswordComplexityPolicy, passwordAlg crypto.HashAlgorithm, onetime bool) error {
+func (u *Human) HashPasswordIfExisting(policy *PasswordComplexityPolicy, hasher *crypto.PasswordHasher, onetime bool) error {
if u.Password != nil {
u.Password.ChangeRequired = onetime
- return u.Password.HashPasswordIfExisting(policy, passwordAlg)
+ return u.Password.HashPasswordIfExisting(policy, hasher)
}
return nil
}
@@ -115,7 +115,7 @@ func (u *Human) IsInitialState(passwordless, externalIDPs bool) bool {
if externalIDPs {
return false
}
- return u.Email == nil || !u.IsEmailVerified || !passwordless && (u.Password == nil || u.Password.SecretString == "") && (u.HashedPassword == nil || u.HashedPassword.SecretString == "")
+ return u.Email == nil || !u.IsEmailVerified || !passwordless && (u.Password == nil || u.Password.SecretString == "") && u.HashedPassword == ""
}
func NewInitUserCode(generator crypto.Generator) (*InitUserCode, error) {
diff --git a/internal/domain/human_hashed_password.go b/internal/domain/human_hashed_password.go
deleted file mode 100644
index 5aa144fe5..000000000
--- a/internal/domain/human_hashed_password.go
+++ /dev/null
@@ -1,24 +0,0 @@
-package domain
-
-import (
- "github.com/zitadel/zitadel/internal/crypto"
- es_models "github.com/zitadel/zitadel/internal/eventstore/v1/models"
-)
-
-type HashedPassword struct {
- es_models.ObjectRoot
-
- SecretString string
- SecretCrypto *crypto.CryptoValue
-}
-
-func NewHashedPassword(password, algorithm string) *HashedPassword {
- return &HashedPassword{
- SecretString: password,
- SecretCrypto: &crypto.CryptoValue{
- CryptoType: crypto.TypeHash,
- Algorithm: algorithm,
- Crypted: []byte(password),
- },
- }
-}
diff --git a/internal/domain/human_password.go b/internal/domain/human_password.go
index 070d00aca..35bcbeaf2 100644
--- a/internal/domain/human_password.go
+++ b/internal/domain/human_password.go
@@ -12,7 +12,7 @@ type Password struct {
es_models.ObjectRoot
SecretString string
- SecretCrypto *crypto.CryptoValue
+ EncodedSecret string
ChangeRequired bool
}
@@ -30,7 +30,7 @@ type PasswordCode struct {
NotificationType NotificationType
}
-func (p *Password) HashPasswordIfExisting(policy *PasswordComplexityPolicy, passwordAlg crypto.HashAlgorithm) error {
+func (p *Password) HashPasswordIfExisting(policy *PasswordComplexityPolicy, hasher *crypto.PasswordHasher) error {
if p.SecretString == "" {
return nil
}
@@ -40,11 +40,11 @@ func (p *Password) HashPasswordIfExisting(policy *PasswordComplexityPolicy, pass
if err := policy.Check(p.SecretString); err != nil {
return err
}
- secret, err := crypto.Hash([]byte(p.SecretString), passwordAlg)
+ encoded, err := hasher.Hash(p.SecretString)
if err != nil {
return err
}
- p.SecretCrypto = secret
+ p.EncodedSecret = encoded
return nil
}
diff --git a/internal/domain/idp.go b/internal/domain/idp.go
index d06fcdd1f..c2664c1ab 100644
--- a/internal/domain/idp.go
+++ b/internal/domain/idp.go
@@ -48,12 +48,13 @@ func (t IDPType) GetCSSClass() string {
case IDPTypeGitLab,
IDPTypeGitLabSelfHosted:
return "gitlab"
+ case IDPTypeAzureAD:
+ return "azure"
case IDPTypeUnspecified,
IDPTypeOIDC,
IDPTypeJWT,
IDPTypeOAuth,
- IDPTypeLDAP,
- IDPTypeAzureAD:
+ IDPTypeLDAP:
fallthrough
default:
return ""
diff --git a/internal/domain/oidc_error_reason.go b/internal/domain/oidc_error_reason.go
new file mode 100644
index 000000000..5a4c8d2c7
--- /dev/null
+++ b/internal/domain/oidc_error_reason.go
@@ -0,0 +1,23 @@
+package domain
+
+type OIDCErrorReason int32
+
+const (
+ OIDCErrorReasonUnspecified OIDCErrorReason = iota
+ OIDCErrorReasonInvalidRequest
+ OIDCErrorReasonUnauthorizedClient
+ OIDCErrorReasonAccessDenied
+ OIDCErrorReasonUnsupportedResponseType
+ OIDCErrorReasonInvalidScope
+ OIDCErrorReasonServerError
+ OIDCErrorReasonTemporaryUnavailable
+ OIDCErrorReasonInteractionRequired
+ OIDCErrorReasonLoginRequired
+ OIDCErrorReasonAccountSelectionRequired
+ OIDCErrorReasonConsentRequired
+ OIDCErrorReasonInvalidRequestURI
+ OIDCErrorReasonInvalidRequestObject
+ OIDCErrorReasonRequestNotSupported
+ OIDCErrorReasonRequestURINotSupported
+ OIDCErrorReasonRegistrationNotSupported
+)
diff --git a/internal/domain/oidc_session.go b/internal/domain/oidc_session.go
new file mode 100644
index 000000000..ad09f1073
--- /dev/null
+++ b/internal/domain/oidc_session.go
@@ -0,0 +1,9 @@
+package domain
+
+type OIDCSessionState int32
+
+const (
+ OIDCSessionStateUnspecified OIDCSessionState = iota
+ OIDCSessionStateActive
+ OIDCSessionStateTerminated
+)
diff --git a/internal/domain/org_domain.go b/internal/domain/org_domain.go
index 6009639ed..ad39e3679 100644
--- a/internal/domain/org_domain.go
+++ b/internal/domain/org_domain.go
@@ -1,6 +1,7 @@
package domain
import (
+ "regexp"
"strings"
http_util "github.com/zitadel/zitadel/internal/api/http"
@@ -32,7 +33,38 @@ func (domain *OrgDomain) GenerateVerificationCode(codeGenerator crypto.Generator
}
func NewIAMDomainName(orgName, iamDomain string) string {
- return strings.ToLower(strings.ReplaceAll(strings.TrimSpace(orgName), " ", "-") + "." + iamDomain)
+ // Reference: label domain requirements https://www.nic.ad.jp/timeline/en/20th/appendix1.html
+
+ // Replaces spaces in org name with hyphens
+ label := strings.ReplaceAll(orgName, " ", "-")
+
+ // The label must only contains alphanumeric characters and hyphens
+ // Invalid characters are replaced with and empty space
+ label = string(regexp.MustCompile(`[^a-zA-Z0-9-]`).ReplaceAll([]byte(label), []byte("")))
+
+ // The label cannot exceed 63 characters
+ if len(label) > 63 {
+ label = label[:63]
+ }
+
+ // The total length of the resulting domain can't exceed 253 characters
+ domain := label + "." + iamDomain
+ if len(domain) > 253 {
+ truncateNChars := len(domain) - 253
+ label = label[:len(label)-truncateNChars]
+ }
+
+ // Label (maybe truncated) can't start with a hyphen
+ if len(label) > 0 && label[0:1] == "-" {
+ label = label[1:]
+ }
+
+ // Label (maybe truncated) can't end with a hyphen
+ if len(label) > 0 && label[len(label)-1:] == "-" {
+ label = label[:len(label)-1]
+ }
+
+ return strings.ToLower(label + "." + iamDomain)
}
type OrgDomainValidationType int32
diff --git a/internal/domain/org_domain_test.go b/internal/domain/org_domain_test.go
new file mode 100644
index 000000000..8dd77ca32
--- /dev/null
+++ b/internal/domain/org_domain_test.go
@@ -0,0 +1,90 @@
+package domain
+
+import (
+ "testing"
+)
+
+func TestNewIAMDomainName(t *testing.T) {
+ type args struct {
+ orgName string
+ iamDomain string
+ }
+ tests := []struct {
+ name string
+ args args
+ result string
+ }{
+ {
+ name: "Single word domain is already valid",
+ args: args{
+ orgName: "single-word-domain",
+ iamDomain: "localhost",
+ },
+ result: "single-word-domain.localhost",
+ },
+ {
+ name: "resulting domain should be in lowercase",
+ args: args{
+ orgName: "Uppercase org Name",
+ iamDomain: "localhost",
+ },
+ result: "uppercase-org-name.localhost",
+ },
+ {
+ name: "replace spaces with hyphens",
+ args: args{
+ orgName: "my org name",
+ iamDomain: "localhost",
+ },
+ result: "my-org-name.localhost",
+ },
+ {
+ name: "replace invalid characters [^a-zA-Z0-9-] with empty spaces",
+ args: args{
+ orgName: "mí Örg name?",
+ iamDomain: "localhost",
+ },
+ result: "m-rg-name.localhost",
+ },
+ {
+ name: "label created from org name size is not greater than 63 chars",
+ args: args{
+ orgName: "my organization name must not exceed sixty-three characters 1234",
+ iamDomain: "localhost",
+ },
+ result: "my-organization-name-must-not-exceed-sixty-three-characters-123.localhost",
+ },
+ {
+ name: "resulting domain cannot exceed 253 chars",
+ args: args{
+ orgName: "Lorem ipsum dolor sit amet",
+ iamDomain: "llgwyngyllgogerychwyrndrobwllllantysiliogogogoch.llanfairpwllgwyngyllgogerychwyrndrobwllllantysiliogogogoch.llanfairpwllgwyngyllgogerychwyrndrobwllllantysiliogogogoch.llanfairpwllgwyngyllgogerychwyrndrobwllllantysiliogogogoch.co.uk",
+ },
+ result: "lorem-ipsum-dolor-sit.llgwyngyllgogerychwyrndrobwllllantysiliogogogoch.llanfairpwllgwyngyllgogerychwyrndrobwllllantysiliogogogoch.llanfairpwllgwyngyllgogerychwyrndrobwllllantysiliogogogoch.llanfairpwllgwyngyllgogerychwyrndrobwllllantysiliogogogoch.co.uk",
+ },
+ {
+ name: "label based on org name should not end with a hyphen",
+ args: args{
+ orgName: "my super long organization name with many many many characters ",
+ iamDomain: "localhost",
+ },
+ result: "my-super-long-organization-name-with-many-many-many-characters.localhost",
+ },
+ {
+ name: "label based on org name should not start with a hyphen",
+ args: args{
+ orgName: " my super long organization name with many many many characters",
+ iamDomain: "localhost",
+ },
+ result: "my-super-long-organization-name-with-many-many-many-characters.localhost",
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ domain := NewIAMDomainName(tt.args.orgName, tt.args.iamDomain)
+ if tt.result != domain {
+ t.Errorf("got wrong result: expected: %v, actual: %v ", tt.result, domain)
+ }
+ })
+ }
+}
diff --git a/internal/domain/policy_login.go b/internal/domain/policy_login.go
index b9c169193..c898ea254 100644
--- a/internal/domain/policy_login.go
+++ b/internal/domain/policy_login.go
@@ -16,6 +16,7 @@ type LoginPolicy struct {
AllowExternalIDP bool
IDPProviders []*IDPProvider
ForceMFA bool
+ ForceMFALocalOnly bool
SecondFactors []SecondFactorType
MultiFactors []MultiFactorType
PasswordlessType PasswordlessType
diff --git a/internal/domain/user.go b/internal/domain/user.go
index a5e01361f..b46d4227d 100644
--- a/internal/domain/user.go
+++ b/internal/domain/user.go
@@ -60,6 +60,41 @@ func (f UserAuthMethodType) Valid() bool {
return f >= 0 && f < userAuthMethodTypeCount
}
+// HasMFA checks whether the user authenticated with multiple auth factors.
+// This can either be true if the list contains a [UserAuthMethodType] which by itself is MFA (e.g. [UserAuthMethodTypePasswordless])
+// or if multiple factors were used (e.g. [UserAuthMethodTypePassword] and [UserAuthMethodTypeU2F])
+func HasMFA(methods []UserAuthMethodType) bool {
+ var factors int
+ for _, method := range methods {
+ switch method {
+ case UserAuthMethodTypePassword:
+ factors++
+ case UserAuthMethodTypePasswordless:
+ return true
+ case UserAuthMethodTypeU2F:
+ factors++
+ case UserAuthMethodTypeOTP:
+ factors++
+ case UserAuthMethodTypeIDP:
+ factors++
+ case UserAuthMethodTypeUnspecified,
+ userAuthMethodTypeCount:
+ // ignore
+ }
+ }
+ return factors > 1
+}
+
+// RequiresMFA checks whether the user requires to authenticate with multiple auth factors based on the LoginPolicy and the authentication type.
+// Internal authentication will require MFA if either option is activated.
+// External authentication will only require MFA if it's forced generally and not local only.
+func RequiresMFA(forceMFA, forceMFALocalOnly, isInternalLogin bool) bool {
+ if isInternalLogin {
+ return forceMFA || forceMFALocalOnly
+ }
+ return forceMFA && !forceMFALocalOnly
+}
+
type PersonalAccessTokenState int32
const (
diff --git a/internal/eventstore/handler/crdb/handler_stmt.go b/internal/eventstore/handler/crdb/handler_stmt.go
index 8f6d9481f..e49fe007b 100644
--- a/internal/eventstore/handler/crdb/handler_stmt.go
+++ b/internal/eventstore/handler/crdb/handler_stmt.go
@@ -11,6 +11,7 @@ import (
"github.com/zitadel/zitadel/internal/errors"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/eventstore/handler"
+ "github.com/zitadel/zitadel/internal/repository/pseudo"
)
var (
@@ -49,6 +50,8 @@ type StatementHandler struct {
initialized chan bool
bulkLimit uint64
+
+ reduceScheduledPseudoEvent bool
}
func NewStatementHandler(
@@ -57,30 +60,40 @@ func NewStatementHandler(
) StatementHandler {
aggregateTypes := make([]eventstore.AggregateType, 0, len(config.Reducers))
reduces := make(map[eventstore.EventType]handler.Reduce, len(config.Reducers))
+ reduceScheduledPseudoEvent := false
for _, aggReducer := range config.Reducers {
aggregateTypes = append(aggregateTypes, aggReducer.Aggregate)
+ if aggReducer.Aggregate == pseudo.AggregateType {
+ reduceScheduledPseudoEvent = true
+ if len(config.Reducers) != 1 ||
+ len(aggReducer.EventRedusers) != 1 ||
+ aggReducer.EventRedusers[0].Event != pseudo.ScheduledEventType {
+ panic("if a pseudo.AggregateType is reduced, exactly one event reducer for pseudo.ScheduledEventType is supported and no other aggregate can be reduced")
+ }
+ }
for _, eventReducer := range aggReducer.EventRedusers {
reduces[eventReducer.Event] = eventReducer.Reduce
}
}
h := StatementHandler{
- client: config.Client,
- sequenceTable: config.SequenceTable,
- maxFailureCount: config.MaxFailureCount,
- currentSequenceStmt: fmt.Sprintf(currentSequenceStmtFormat, config.SequenceTable),
- updateSequencesBaseStmt: fmt.Sprintf(updateCurrentSequencesStmtFormat, config.SequenceTable),
- failureCountStmt: fmt.Sprintf(failureCountStmtFormat, config.FailedEventsTable),
- setFailureCountStmt: fmt.Sprintf(setFailureCountStmtFormat, config.FailedEventsTable),
- aggregates: aggregateTypes,
- reduces: reduces,
- bulkLimit: config.BulkLimit,
- Locker: NewLocker(config.Client.DB, config.LockTable, config.ProjectionName),
- initCheck: config.InitCheck,
- initialized: make(chan bool),
+ client: config.Client,
+ sequenceTable: config.SequenceTable,
+ maxFailureCount: config.MaxFailureCount,
+ currentSequenceStmt: fmt.Sprintf(currentSequenceStmtFormat, config.SequenceTable),
+ updateSequencesBaseStmt: fmt.Sprintf(updateCurrentSequencesStmtFormat, config.SequenceTable),
+ failureCountStmt: fmt.Sprintf(failureCountStmtFormat, config.FailedEventsTable),
+ setFailureCountStmt: fmt.Sprintf(setFailureCountStmtFormat, config.FailedEventsTable),
+ aggregates: aggregateTypes,
+ reduces: reduces,
+ bulkLimit: config.BulkLimit,
+ Locker: NewLocker(config.Client.DB, config.LockTable, config.ProjectionName),
+ initCheck: config.InitCheck,
+ initialized: make(chan bool),
+ reduceScheduledPseudoEvent: reduceScheduledPseudoEvent,
}
- h.ProjectionHandler = handler.NewProjectionHandler(ctx, config.ProjectionHandlerConfig, h.reduce, h.Update, h.SearchQuery, h.Lock, h.Unlock, h.initialized)
+ h.ProjectionHandler = handler.NewProjectionHandler(ctx, config.ProjectionHandlerConfig, h.reduce, h.Update, h.searchQuery, h.Lock, h.Unlock, h.initialized, reduceScheduledPseudoEvent)
return h
}
@@ -88,10 +101,19 @@ func NewStatementHandler(
func (h *StatementHandler) Start() {
h.initialized <- true
close(h.initialized)
- h.Subscribe(h.aggregates...)
+ if !h.reduceScheduledPseudoEvent {
+ h.Subscribe(h.aggregates...)
+ }
}
-func (h *StatementHandler) SearchQuery(ctx context.Context, instanceIDs []string) (*eventstore.SearchQueryBuilder, uint64, error) {
+func (h *StatementHandler) searchQuery(ctx context.Context, instanceIDs []string) (*eventstore.SearchQueryBuilder, uint64, error) {
+ if h.reduceScheduledPseudoEvent {
+ return nil, 1, nil
+ }
+ return h.dbSearchQuery(ctx, instanceIDs)
+}
+
+func (h *StatementHandler) dbSearchQuery(ctx context.Context, instanceIDs []string) (*eventstore.SearchQueryBuilder, uint64, error) {
sequences, err := h.currentSequences(ctx, h.client.QueryContext, instanceIDs)
if err != nil {
return nil, 0, err
@@ -115,7 +137,6 @@ func (h *StatementHandler) SearchQuery(ctx context.Context, instanceIDs []string
InstanceID(instanceID)
}
}
-
return queryBuilder, h.bulkLimit, nil
}
diff --git a/internal/eventstore/handler/crdb/handler_stmt_test.go b/internal/eventstore/handler/crdb/handler_stmt_test.go
index 8c0d1537f..b72db40eb 100644
--- a/internal/eventstore/handler/crdb/handler_stmt_test.go
+++ b/internal/eventstore/handler/crdb/handler_stmt_test.go
@@ -18,6 +18,7 @@ import (
"github.com/zitadel/zitadel/internal/eventstore/repository"
es_repo_mock "github.com/zitadel/zitadel/internal/eventstore/repository/mock"
"github.com/zitadel/zitadel/internal/id"
+ "github.com/zitadel/zitadel/internal/repository/pseudo"
)
var (
@@ -60,7 +61,7 @@ func TestProjectionHandler_SearchQuery(t *testing.T) {
type fields struct {
sequenceTable string
projectionName string
- aggregates []eventstore.AggregateType
+ reducers []handler.AggregateReducer
bulkLimit uint64
}
type args struct {
@@ -77,7 +78,7 @@ func TestProjectionHandler_SearchQuery(t *testing.T) {
fields: fields{
sequenceTable: "my_sequences",
projectionName: "my_projection",
- aggregates: []eventstore.AggregateType{"testAgg"},
+ reducers: failingAggregateReducers("testAgg"),
bulkLimit: 5,
},
args: args{
@@ -99,7 +100,7 @@ func TestProjectionHandler_SearchQuery(t *testing.T) {
fields: fields{
sequenceTable: "my_sequences",
projectionName: "my_projection",
- aggregates: []eventstore.AggregateType{"testAgg"},
+ reducers: failingAggregateReducers("testAgg"),
bulkLimit: 5,
},
args: args{
@@ -129,7 +130,7 @@ func TestProjectionHandler_SearchQuery(t *testing.T) {
fields: fields{
sequenceTable: "my_sequences",
projectionName: "my_projection",
- aggregates: []eventstore.AggregateType{"testAgg"},
+ reducers: failingAggregateReducers("testAgg"),
bulkLimit: 5,
},
args: args{
@@ -158,6 +159,32 @@ func TestProjectionHandler_SearchQuery(t *testing.T) {
Limit(5),
},
},
+ {
+ name: "scheduled pseudo event",
+ fields: fields{
+ sequenceTable: "my_sequences",
+ projectionName: "my_projection",
+ reducers: []handler.AggregateReducer{{
+ Aggregate: pseudo.AggregateType,
+ EventRedusers: []handler.EventReducer{
+ {
+ Event: pseudo.ScheduledEventType,
+ Reduce: testReduceErr(errors.New("should not be called")),
+ },
+ },
+ }},
+ bulkLimit: 5,
+ },
+ args: args{
+ instanceIDs: []string{"instanceID1", "instanceID2"},
+ },
+ want: want{
+ limit: 1,
+ isErr: func(err error) bool {
+ return err == nil
+ },
+ },
+ },
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
@@ -177,15 +204,14 @@ func TestProjectionHandler_SearchQuery(t *testing.T) {
Client: &database.DB{
DB: client,
},
+ Reducers: tt.fields.reducers,
})
- h.aggregates = tt.fields.aggregates
-
for _, expectation := range tt.want.expectations {
expectation(mock)
}
- query, limit, err := h.SearchQuery(context.Background(), tt.args.instanceIDs)
+ query, limit, err := h.searchQuery(context.Background(), tt.args.instanceIDs)
if !tt.want.isErr(err) {
t.Errorf("ProjectionHandler.prepareBulkStmts() error = %v", err)
return
@@ -1768,3 +1794,17 @@ func testReduceErr(err error) handler.Reduce {
return nil, err
}
}
+
+func failingAggregateReducers(aggregates ...eventstore.AggregateType) []handler.AggregateReducer {
+ reducers := make([]handler.AggregateReducer, len(aggregates))
+ for idx := range aggregates {
+ reducers[idx] = handler.AggregateReducer{
+ Aggregate: aggregates[idx],
+ EventRedusers: []handler.EventReducer{{
+ Event: "any.event",
+ Reduce: testReduceErr(errors.New("should not be called")),
+ }},
+ }
+ }
+ return reducers
+}
diff --git a/internal/eventstore/handler/crdb/statement.go b/internal/eventstore/handler/crdb/statement.go
index 591751e66..efcc4952e 100644
--- a/internal/eventstore/handler/crdb/statement.go
+++ b/internal/eventstore/handler/crdb/statement.go
@@ -235,12 +235,6 @@ func AddDeleteStatement(conditions []handler.Condition, opts ...execOption) func
}
}
-func AddCopyStatement(conflict, from, to []handler.Column, conditions []handler.Condition, opts ...execOption) func(eventstore.Event) Exec {
- return func(event eventstore.Event) Exec {
- return NewCopyStatement(event, conflict, from, to, conditions, opts...).Execute
- }
-}
-
func NewArrayAppendCol(column string, value interface{}) handler.Column {
return handler.Column{
Name: column,
@@ -286,12 +280,30 @@ func NewCopyCol(column, from string) handler.Column {
}
func NewLessThanCond(column string, value interface{}) handler.Condition {
- return handler.Condition{
- Name: column,
- Value: value,
- ParameterOpt: func(placeholder string) string {
- return " < " + placeholder
- },
+ return func(param string) (string, interface{}) {
+ return column + " < " + param, value
+ }
+}
+
+func NewIsNullCond(column string) handler.Condition {
+ return func(param string) (string, interface{}) {
+ return column + " IS NULL", nil
+ }
+}
+
+// NewTextArrayContainsCond returns a handler.Condition that checks if the column that stores an array of text contains the given value
+func NewTextArrayContainsCond(column string, value string) handler.Condition {
+ return func(param string) (string, interface{}) {
+ return column + " @> " + param, database.StringArray{value}
+ }
+}
+
+// Not is a function and not a method, so that calling it is well readable
+// For example conditions := []handler.Condition{ Not(NewTextArrayContainsCond())}
+func Not(condition handler.Condition) handler.Condition {
+ return func(param string) (string, interface{}) {
+ cond, value := condition(param)
+ return "NOT (" + cond + ")", value
}
}
@@ -300,7 +312,7 @@ func NewLessThanCond(column string, value interface{}) handler.Condition {
// if the value of a col is empty the data will be copied from the selected row
// if the value of a col is not empty the data will be set by the static value
// conds represent the conditions for the selection subquery
-func NewCopyStatement(event eventstore.Event, conflictCols, from, to []handler.Column, conds []handler.Condition, opts ...execOption) *handler.Statement {
+func NewCopyStatement(event eventstore.Event, conflictCols, from, to []handler.Column, nsCond []handler.NamespacedCondition, opts ...execOption) *handler.Statement {
columnNames := make([]string, len(to))
selectColumns := make([]string, len(from))
updateColumns := make([]string, len(columnNames))
@@ -319,13 +331,12 @@ func NewCopyStatement(event eventstore.Event, conflictCols, from, to []handler.C
}
}
-
- wheres := make([]string, len(conds))
- for i, cond := range conds {
- argCounter++
- wheres[i] = "copy_table." + cond.Name + " = $" + strconv.Itoa(argCounter)
- args = append(args, cond.Value)
+ cond := make([]handler.Condition, len(nsCond))
+ for i := range nsCond {
+ cond[i] = nsCond[i]("copy_table")
}
+ wheres, values := conditionsToWhere(cond, len(args))
+ args = append(args, values...)
conflictTargets := make([]string, len(conflictCols))
for i, conflictCol := range conflictCols {
@@ -340,7 +351,7 @@ func NewCopyStatement(event eventstore.Event, conflictCols, from, to []handler.C
config.err = handler.ErrNoValues
}
- if len(conds) == 0 {
+ if len(cond) == 0 {
config.err = handler.ErrNoCondition
}
@@ -394,18 +405,16 @@ func columnsToQuery(cols []handler.Column) (names []string, parameters []string,
return names, parameters, values[:parameterIndex]
}
-func conditionsToWhere(cols []handler.Condition, paramOffset int) (wheres []string, values []interface{}) {
- wheres = make([]string, len(cols))
- values = make([]interface{}, len(cols))
-
- for i, col := range cols {
- wheres[i] = "(" + col.Name + " = $" + strconv.Itoa(i+1+paramOffset) + ")"
- if col.ParameterOpt != nil {
- wheres[i] = "(" + col.Name + col.ParameterOpt("$"+strconv.Itoa(i+1+paramOffset)) + ")"
+func conditionsToWhere(conditions []handler.Condition, paramOffset int) (wheres []string, values []interface{}) {
+ wheres = make([]string, len(conditions))
+ values = make([]interface{}, 0, len(conditions))
+ for i, conditionFunc := range conditions {
+ condition, value := conditionFunc("$" + strconv.Itoa(i+1+paramOffset))
+ wheres[i] = "(" + condition + ")"
+ if value != nil {
+ values = append(values, value)
}
- values[i] = col.Value
}
-
return wheres, values
}
diff --git a/internal/eventstore/handler/crdb/statement_test.go b/internal/eventstore/handler/crdb/statement_test.go
index fd9f451d9..e023f0079 100644
--- a/internal/eventstore/handler/crdb/statement_test.go
+++ b/internal/eventstore/handler/crdb/statement_test.go
@@ -6,6 +6,7 @@ import (
"reflect"
"testing"
+ "github.com/zitadel/zitadel/internal/database"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/eventstore/handler"
)
@@ -420,10 +421,7 @@ func TestNewUpdateStatement(t *testing.T) {
},
},
conditions: []handler.Condition{
- {
- Name: "col2",
- Value: 1,
- },
+ handler.NewCond("col2", 1),
},
},
want: want{
@@ -450,10 +448,7 @@ func TestNewUpdateStatement(t *testing.T) {
},
values: []handler.Column{},
conditions: []handler.Condition{
- {
- Name: "col2",
- Value: 1,
- },
+ handler.NewCond("col2", 1),
},
},
want: want{
@@ -515,10 +510,7 @@ func TestNewUpdateStatement(t *testing.T) {
},
},
conditions: []handler.Condition{
- {
- Name: "col2",
- Value: 1,
- },
+ handler.NewCond("col2", 1),
},
},
want: want{
@@ -560,10 +552,7 @@ func TestNewUpdateStatement(t *testing.T) {
},
},
conditions: []handler.Condition{
- {
- Name: "col2",
- Value: 1,
- },
+ handler.NewCond("col2", 1),
},
},
want: want{
@@ -630,10 +619,7 @@ func TestNewDeleteStatement(t *testing.T) {
previousSequence: 0,
},
conditions: []handler.Condition{
- {
- Name: "col2",
- Value: 1,
- },
+ handler.NewCond("col2", 1),
},
},
want: want{
@@ -683,10 +669,7 @@ func TestNewDeleteStatement(t *testing.T) {
aggregateType: "agg",
},
conditions: []handler.Condition{
- {
- Name: "col1",
- Value: 1,
- },
+ handler.NewCond("col1", 1),
},
},
want: want{
@@ -842,11 +825,9 @@ func TestNewMultiStatement(t *testing.T) {
execs: []func(eventstore.Event) Exec{
AddDeleteStatement(
[]handler.Condition{
- {
- Name: "col1",
- Value: 1,
- },
- }),
+ handler.NewCond("col1", 1),
+ },
+ ),
AddCreateStatement(
[]handler.Column{
{
@@ -876,11 +857,9 @@ func TestNewMultiStatement(t *testing.T) {
},
},
[]handler.Condition{
- {
- Name: "col1",
- Value: 1,
- },
- }),
+ handler.NewCond("col1", 1),
+ },
+ ),
},
},
want: want{
@@ -942,7 +921,7 @@ func TestNewCopyStatement(t *testing.T) {
conflictingCols []handler.Column
from []handler.Column
to []handler.Column
- conds []handler.Condition
+ conds []handler.NamespacedCondition
}
type want struct {
aggregateType eventstore.AggregateType
@@ -966,11 +945,8 @@ func TestNewCopyStatement(t *testing.T) {
sequence: 1,
previousSequence: 0,
},
- conds: []handler.Condition{
- {
- Name: "col2",
- Value: 1,
- },
+ conds: []handler.NamespacedCondition{
+ handler.NewNamespacedCondition("col2", 1),
},
},
want: want{
@@ -995,7 +971,7 @@ func TestNewCopyStatement(t *testing.T) {
sequence: 1,
previousSequence: 0,
},
- conds: []handler.Condition{},
+ conds: []handler.NamespacedCondition{},
from: []handler.Column{
{
Name: "col",
@@ -1029,7 +1005,7 @@ func TestNewCopyStatement(t *testing.T) {
sequence: 1,
previousSequence: 0,
},
- conds: []handler.Condition{},
+ conds: []handler.NamespacedCondition{},
from: []handler.Column{
{
Name: "col",
@@ -1066,10 +1042,8 @@ func TestNewCopyStatement(t *testing.T) {
sequence: 1,
previousSequence: 0,
},
- conds: []handler.Condition{
- {
- Name: "col",
- },
+ conds: []handler.NamespacedCondition{
+ handler.NewNamespacedCondition("col2", nil),
},
from: []handler.Column{},
},
@@ -1124,15 +1098,9 @@ func TestNewCopyStatement(t *testing.T) {
Name: "col_b",
},
},
- conds: []handler.Condition{
- {
- Name: "id",
- Value: 2,
- },
- {
- Name: "state",
- Value: 3,
- },
+ conds: []handler.NamespacedCondition{
+ handler.NewNamespacedCondition("id", 2),
+ handler.NewNamespacedCondition("state", 3),
},
},
want: want{
@@ -1143,7 +1111,7 @@ func TestNewCopyStatement(t *testing.T) {
executer: &wantExecuter{
params: []params{
{
- query: "INSERT INTO my_table (state, id, col_a, col_b) SELECT $1, id, col_a, col_b FROM my_table AS copy_table WHERE copy_table.id = $2 AND copy_table.state = $3 ON CONFLICT () DO UPDATE SET (state, id, col_a, col_b) = ($1, EXCLUDED.id, EXCLUDED.col_a, EXCLUDED.col_b)",
+ query: "INSERT INTO my_table (state, id, col_a, col_b) SELECT $1, id, col_a, col_b FROM my_table AS copy_table WHERE (copy_table.id = $2) AND (copy_table.state = $3) ON CONFLICT () DO UPDATE SET (state, id, col_a, col_b) = ($1, EXCLUDED.id, EXCLUDED.col_a, EXCLUDED.col_b)",
args: []interface{}{1, 2, 3},
},
},
@@ -1191,15 +1159,9 @@ func TestNewCopyStatement(t *testing.T) {
Name: "col_d",
},
},
- conds: []handler.Condition{
- {
- Name: "id",
- Value: 2,
- },
- {
- Name: "state",
- Value: 3,
- },
+ conds: []handler.NamespacedCondition{
+ handler.NewNamespacedCondition("id", 2),
+ handler.NewNamespacedCondition("state", 3),
},
},
want: want{
@@ -1210,7 +1172,7 @@ func TestNewCopyStatement(t *testing.T) {
executer: &wantExecuter{
params: []params{
{
- query: "INSERT INTO my_table (state, id, col_c, col_d) SELECT $1, id, col_a, col_b FROM my_table AS copy_table WHERE copy_table.id = $2 AND copy_table.state = $3 ON CONFLICT () DO UPDATE SET (state, id, col_c, col_d) = ($1, EXCLUDED.id, EXCLUDED.col_a, EXCLUDED.col_b)",
+ query: "INSERT INTO my_table (state, id, col_c, col_d) SELECT $1, id, col_a, col_b FROM my_table AS copy_table WHERE (copy_table.id = $2) AND (copy_table.state = $3) ON CONFLICT () DO UPDATE SET (state, id, col_c, col_d) = ($1, EXCLUDED.id, EXCLUDED.col_a, EXCLUDED.col_b)",
args: []interface{}{1, 2, 3},
},
},
@@ -1395,7 +1357,7 @@ func Test_columnsToQuery(t *testing.T) {
}
}
-func Test_columnsToWhere(t *testing.T) {
+func Test_conditionsToWhere(t *testing.T) {
type args struct {
conds []handler.Condition
paramOffset int
@@ -1421,10 +1383,7 @@ func Test_columnsToWhere(t *testing.T) {
name: "no offset",
args: args{
conds: []handler.Condition{
- {
- Name: "col1",
- Value: "val1",
- },
+ handler.NewCond("col1", "val1"),
},
paramOffset: 0,
},
@@ -1437,14 +1396,8 @@ func Test_columnsToWhere(t *testing.T) {
name: "multiple cols",
args: args{
conds: []handler.Condition{
- {
- Name: "col1",
- Value: "val1",
- },
- {
- Name: "col2",
- Value: "val2",
- },
+ handler.NewCond("col1", "val1"),
+ handler.NewCond("col2", "val2"),
},
paramOffset: 0,
},
@@ -1457,10 +1410,7 @@ func Test_columnsToWhere(t *testing.T) {
name: "2 offset",
args: args{
conds: []handler.Condition{
- {
- Name: "col1",
- Value: "val1",
- },
+ handler.NewCond("col1", "val1"),
},
paramOffset: 2,
},
@@ -1469,6 +1419,54 @@ func Test_columnsToWhere(t *testing.T) {
values: []interface{}{"val1"},
},
},
+ {
+ name: "less than",
+ args: args{
+ conds: []handler.Condition{
+ NewLessThanCond("col1", "val1"),
+ },
+ },
+ want: want{
+ wheres: []string{"(col1 < $1)"},
+ values: []interface{}{"val1"},
+ },
+ },
+ {
+ name: "is null",
+ args: args{
+ conds: []handler.Condition{
+ NewIsNullCond("col1"),
+ },
+ },
+ want: want{
+ wheres: []string{"(col1 IS NULL)"},
+ values: []interface{}{},
+ },
+ },
+ {
+ name: "text array contains",
+ args: args{
+ conds: []handler.Condition{
+ NewTextArrayContainsCond("col1", "val1"),
+ },
+ },
+ want: want{
+ wheres: []string{"(col1 @> $1)"},
+ values: []interface{}{database.StringArray{"val1"}},
+ },
+ },
+ {
+ name: "not",
+ args: args{
+ conds: []handler.Condition{
+ Not(handler.NewCond("col1", "val1")),
+ },
+ },
+ want: want{
+ wheres: []string{"(NOT (col1 = $1))"},
+ values: []interface{}{"val1"},
+ },
+ },
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
diff --git a/internal/eventstore/handler/handler_projection.go b/internal/eventstore/handler/handler_projection.go
index 808da1dbb..baab2d3a6 100644
--- a/internal/eventstore/handler/handler_projection.go
+++ b/internal/eventstore/handler/handler_projection.go
@@ -6,10 +6,13 @@ import (
"runtime/debug"
"time"
+ "github.com/sirupsen/logrus"
"github.com/zitadel/logging"
"github.com/zitadel/zitadel/internal/api/authz"
+ "github.com/zitadel/zitadel/internal/api/call"
"github.com/zitadel/zitadel/internal/eventstore"
+ "github.com/zitadel/zitadel/internal/repository/pseudo"
)
const (
@@ -49,19 +52,20 @@ type NowFunc func() time.Time
type ProjectionHandler struct {
Handler
- ProjectionName string
- reduce Reduce
- update Update
- searchQuery SearchQuery
- triggerProjection *time.Timer
- lock Lock
- unlock Unlock
- requeueAfter time.Duration
- retryFailedAfter time.Duration
- retries int
- concurrentInstances int
- handleActiveInstances time.Duration
- nowFunc NowFunc
+ ProjectionName string
+ reduce Reduce
+ update Update
+ searchQuery SearchQuery
+ triggerProjection *time.Timer
+ lock Lock
+ unlock Unlock
+ requeueAfter time.Duration
+ retryFailedAfter time.Duration
+ retries int
+ concurrentInstances int
+ handleActiveInstances time.Duration
+ nowFunc NowFunc
+ reduceScheduledPseudoEvent bool
}
func NewProjectionHandler(
@@ -73,59 +77,86 @@ func NewProjectionHandler(
lock Lock,
unlock Unlock,
initialized <-chan bool,
+ reduceScheduledPseudoEvent bool,
) *ProjectionHandler {
concurrentInstances := int(config.ConcurrentInstances)
if concurrentInstances < 1 {
concurrentInstances = 1
}
h := &ProjectionHandler{
- Handler: NewHandler(config.HandlerConfig),
- ProjectionName: config.ProjectionName,
- reduce: reduce,
- update: update,
- searchQuery: query,
- lock: lock,
- unlock: unlock,
- requeueAfter: config.RequeueEvery,
- triggerProjection: time.NewTimer(0), // first trigger is instant on startup
- retryFailedAfter: config.RetryFailedAfter,
- retries: int(config.Retries),
- concurrentInstances: concurrentInstances,
- handleActiveInstances: config.HandleActiveInstances,
- nowFunc: time.Now,
+ Handler: NewHandler(config.HandlerConfig),
+ ProjectionName: config.ProjectionName,
+ reduce: reduce,
+ update: update,
+ searchQuery: query,
+ lock: lock,
+ unlock: unlock,
+ requeueAfter: config.RequeueEvery,
+ triggerProjection: time.NewTimer(0), // first trigger is instant on startup
+ retryFailedAfter: config.RetryFailedAfter,
+ retries: int(config.Retries),
+ concurrentInstances: concurrentInstances,
+ handleActiveInstances: config.HandleActiveInstances,
+ nowFunc: time.Now,
+ reduceScheduledPseudoEvent: reduceScheduledPseudoEvent,
}
go func() {
<-initialized
- go h.subscribe(ctx)
-
+ if !h.reduceScheduledPseudoEvent {
+ go h.subscribe(ctx)
+ }
go h.schedule(ctx)
}()
return h
}
-// Trigger handles all events for the provided instances (or current instance from context if non specified)
-// by calling FetchEvents and Process until the amount of events is smaller than the BulkLimit
-func (h *ProjectionHandler) Trigger(ctx context.Context, instances ...string) error {
- ids := []string{authz.GetInstance(ctx).InstanceID()}
- if len(instances) > 0 {
- ids = instances
+func triggerInstances(ctx context.Context, instances []string) []string {
+ if len(instances) == 0 {
+ instances = append(instances, authz.GetInstance(ctx).InstanceID())
}
+ return instances
+}
+
+// Trigger handles all events for the provided instances (or current instance from context if non specified)
+// by calling FetchEvents and Process until the amount of events is smaller than the BulkLimit.
+// If a bulk action was executed, the call timestamp in context will be reset for subsequent queries.
+// The returned context is never nil. It is either the original context or an updated context.
+//
+// If Trigger encounters an error, it is only logged. If the error is important for the caller,
+// use TriggerErr instead.
+func (h *ProjectionHandler) Trigger(ctx context.Context, instances ...string) context.Context {
+ instances = triggerInstances(ctx, instances)
+ ctx, err := h.TriggerErr(ctx, instances...)
+ logging.OnError(err).WithFields(logrus.Fields{
+ "projection": h.ProjectionName,
+ "instanceIDs": instances,
+ }).Error("trigger failed")
+ return ctx
+}
+
+// TriggerErr handles all events for the provided instances (or current instance from context if non specified)
+// by calling FetchEvents and Process until the amount of events is smaller than the BulkLimit.
+// If a bulk action was executed, the call timestamp in context will be reset for subsequent queries.
+// The returned context is never nil. It is either the original context or an updated context.
+func (h *ProjectionHandler) TriggerErr(ctx context.Context, instances ...string) (context.Context, error) {
+ instances = triggerInstances(ctx, instances)
for {
- events, hasLimitExceeded, err := h.FetchEvents(ctx, ids...)
+ events, hasLimitExceeded, err := h.FetchEvents(ctx, instances...)
if err != nil {
- return err
+ return ctx, err
}
if len(events) == 0 {
- return nil
+ return ctx, nil
}
_, err = h.Process(ctx, events...)
+ ctx = call.ResetTimestamp(ctx)
if err != nil {
- return err
+ return ctx, err
}
if !hasLimitExceeded {
- return nil
+ return ctx, nil
}
}
}
@@ -158,6 +189,13 @@ func (h *ProjectionHandler) Process(ctx context.Context, events ...eventstore.Ev
// FetchEvents checks the current sequences and filters for newer events
func (h *ProjectionHandler) FetchEvents(ctx context.Context, instances ...string) ([]eventstore.Event, bool, error) {
+ if h.reduceScheduledPseudoEvent {
+ return h.fetchPseudoEvents(ctx, instances...)
+ }
+ return h.fetchDBEvents(ctx, instances...)
+}
+
+func (h *ProjectionHandler) fetchDBEvents(ctx context.Context, instances ...string) ([]eventstore.Event, bool, error) {
eventQuery, eventsLimit, err := h.searchQuery(ctx, instances)
if err != nil {
return nil, false, err
@@ -169,6 +207,10 @@ func (h *ProjectionHandler) FetchEvents(ctx context.Context, instances ...string
return events, int(eventsLimit) == len(events), err
}
+func (h *ProjectionHandler) fetchPseudoEvents(ctx context.Context, instances ...string) ([]eventstore.Event, bool, error) {
+ return []eventstore.Event{pseudo.NewScheduledEvent(ctx, time.Now(), instances...)}, false, nil
+}
+
func (h *ProjectionHandler) subscribe(ctx context.Context) {
ctx, cancel := context.WithCancel(ctx)
defer func() {
@@ -258,7 +300,7 @@ func (h *ProjectionHandler) schedule(ctx context.Context) {
continue
}
go h.cancelOnErr(lockInstanceCtx, errs, cancelInstanceLock)
- err = h.Trigger(lockInstanceCtx, instances...)
+ _, err = h.TriggerErr(lockInstanceCtx, instances...)
if err != nil {
logging.WithFields("projection", h.ProjectionName, "instanceIDs", instances).WithError(err).Error("trigger failed")
failed = true
diff --git a/internal/eventstore/handler/handler_projection_test.go b/internal/eventstore/handler/handler_projection_test.go
index 3ab88ef4a..3ee791a2d 100644
--- a/internal/eventstore/handler/handler_projection_test.go
+++ b/internal/eventstore/handler/handler_projection_test.go
@@ -10,6 +10,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/zitadel/zitadel/internal/api/authz"
+ "github.com/zitadel/zitadel/internal/api/call"
"github.com/zitadel/zitadel/internal/api/service"
"github.com/zitadel/zitadel/internal/database"
"github.com/zitadel/zitadel/internal/eventstore"
@@ -29,6 +30,47 @@ var (
)
func TestProjectionHandler_Trigger(t *testing.T) {
+ const pause = time.Millisecond
+
+ startCtx := call.WithTimestamp(context.Background())
+ start := call.FromContext(startCtx)
+
+ h := &ProjectionHandler{
+ Handler: Handler{
+ Eventstore: eventstore.NewEventstore(eventstore.TestConfig(
+ es_repo_mock.NewRepo(t).ExpectFilterEvents(
+ &repository.Event{
+ ID: "id",
+ Sequence: 1,
+ PreviousAggregateSequence: 0,
+ CreationDate: time.Now(),
+ Type: "test.added",
+ Version: "v1",
+ AggregateID: "testid",
+ AggregateType: "testAgg",
+ },
+ ),
+ )),
+ },
+ ProjectionName: "test",
+ reduce: testReduce(newTestStatement("testAgg", 1, 0)),
+ update: testUpdate(t, 1, 0, nil),
+ searchQuery: testQuery(
+ eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent).
+ AddQuery().
+ AggregateTypes("test").
+ Builder(),
+ 5, nil,
+ ),
+ }
+
+ time.Sleep(pause)
+ endCtx := h.Trigger(startCtx)
+ // check if the new context has a call timestamp that's later than start+pause.
+ assert.WithinRange(t, call.FromContext(endCtx), start.Add(pause), start.Add(pause+time.Second))
+}
+
+func TestProjectionHandler_TriggerErr(t *testing.T) {
type fields struct {
reduce Reduce
update Update
@@ -223,7 +265,8 @@ func TestProjectionHandler_Trigger(t *testing.T) {
searchQuery: tt.fields.query,
}
- err := h.Trigger(tt.args.ctx, tt.args.instances...)
+ // context timestamp is checked in [TestProjectionHandler_Trigger]
+ _, err := h.TriggerErr(tt.args.ctx, tt.args.instances...)
if !tt.want.isErr(err) {
t.Errorf("unexpected error %v", err)
}
@@ -342,6 +385,7 @@ func TestProjectionHandler_Process(t *testing.T) {
nil,
nil,
nil,
+ false,
)
index, err := h.Process(tt.args.ctx, tt.args.events...)
diff --git a/internal/eventstore/handler/statement.go b/internal/eventstore/handler/statement.go
index ae7bec92e..1eecd78f2 100644
--- a/internal/eventstore/handler/statement.go
+++ b/internal/eventstore/handler/statement.go
@@ -4,7 +4,6 @@ import (
"database/sql"
"encoding/json"
"errors"
-
"github.com/zitadel/logging"
"github.com/zitadel/zitadel/internal/eventstore"
@@ -62,11 +61,18 @@ func NewJSONCol(name string, value interface{}) Column {
return NewCol(name, marshalled)
}
-type Condition Column
+type Condition func(param string) (string, interface{})
+
+type NamespacedCondition func(namespace string) Condition
func NewCond(name string, value interface{}) Condition {
- return Condition{
- Name: name,
- Value: value,
+ return func(param string) (string, interface{}) {
+ return name + " = " + param, value
+ }
+}
+
+func NewNamespacedCondition(name string, value interface{}) NamespacedCondition {
+ return func(namespace string) Condition {
+ return NewCond(namespace+"."+name, value)
}
}
diff --git a/internal/integration/client.go b/internal/integration/client.go
index 86b78bb87..65e0e9ab1 100644
--- a/internal/integration/client.go
+++ b/internal/integration/client.go
@@ -17,9 +17,12 @@ import (
openid "github.com/zitadel/zitadel/internal/idp/providers/oidc"
"github.com/zitadel/zitadel/internal/repository/idp"
"github.com/zitadel/zitadel/pkg/grpc/admin"
+ "github.com/zitadel/zitadel/pkg/grpc/auth"
mgmt "github.com/zitadel/zitadel/pkg/grpc/management"
object "github.com/zitadel/zitadel/pkg/grpc/object/v2alpha"
+ oidc_pb "github.com/zitadel/zitadel/pkg/grpc/oidc/v2alpha"
session "github.com/zitadel/zitadel/pkg/grpc/session/v2alpha"
+ "github.com/zitadel/zitadel/pkg/grpc/system"
user "github.com/zitadel/zitadel/pkg/grpc/user/v2alpha"
)
@@ -27,8 +30,11 @@ type Client struct {
CC *grpc.ClientConn
Admin admin.AdminServiceClient
Mgmt mgmt.ManagementServiceClient
+ Auth auth.AuthServiceClient
UserV2 user.UserServiceClient
SessionV2 session.SessionServiceClient
+ OIDCv2 oidc_pb.OIDCServiceClient
+ System system.SystemServiceClient
}
func newClient(cc *grpc.ClientConn) Client {
@@ -36,11 +42,38 @@ func newClient(cc *grpc.ClientConn) Client {
CC: cc,
Admin: admin.NewAdminServiceClient(cc),
Mgmt: mgmt.NewManagementServiceClient(cc),
+ Auth: auth.NewAuthServiceClient(cc),
UserV2: user.NewUserServiceClient(cc),
SessionV2: session.NewSessionServiceClient(cc),
+ OIDCv2: oidc_pb.NewOIDCServiceClient(cc),
+ System: system.NewSystemServiceClient(cc),
}
}
+func (t *Tester) UseIsolatedInstance(iamOwnerCtx, systemCtx context.Context) (primaryDomain, instanceId string, authenticatedIamOwnerCtx context.Context) {
+ primaryDomain = randString(5) + ".integration"
+ instance, err := t.Client.System.CreateInstance(systemCtx, &system.CreateInstanceRequest{
+ InstanceName: "testinstance",
+ CustomDomain: primaryDomain,
+ Owner: &system.CreateInstanceRequest_Machine_{
+ Machine: &system.CreateInstanceRequest_Machine{
+ UserName: "owner",
+ Name: "owner",
+ PersonalAccessToken: &system.CreateInstanceRequest_PersonalAccessToken{},
+ },
+ },
+ })
+ if err != nil {
+ panic(err)
+ }
+ t.createClientConn(iamOwnerCtx, grpc.WithAuthority(primaryDomain))
+ instanceId = instance.GetInstanceId()
+ t.Users.Set(instanceId, IAMOwner, &User{
+ Token: instance.GetPat(),
+ })
+ return primaryDomain, instanceId, t.WithInstanceAuthorization(iamOwnerCtx, IAMOwner, instanceId)
+}
+
func (s *Tester) CreateHumanUser(ctx context.Context) *user.AddHumanUserResponse {
resp, err := s.Client.UserV2.AddHumanUser(ctx, &user.AddHumanUserRequest{
Organisation: &object.Organisation{
@@ -104,6 +137,14 @@ func (s *Tester) RegisterUserPasskey(ctx context.Context, userID string) {
logging.OnError(err).Fatal("create user passkey")
}
+func (s *Tester) SetUserPassword(ctx context.Context, userID, password string) {
+ _, err := s.Client.UserV2.SetPassword(ctx, &user.SetPasswordRequest{
+ UserId: userID,
+ NewPassword: &user.Password{Password: password},
+ })
+ logging.OnError(err).Fatal("set user password")
+}
+
func (s *Tester) AddGenericOAuthProvider(t *testing.T) string {
ctx := authz.WithInstance(context.Background(), s.Instance)
id, _, err := s.Commands.AddOrgGenericOAuthProvider(ctx, s.Organisation.ID, command.GenericOAuthProvider{
@@ -158,3 +199,51 @@ func (s *Tester) CreateSuccessfulIntent(t *testing.T, idpID, userID, idpUserID s
require.NoError(t, err)
return intentID, token, writeModel.ChangeDate, writeModel.ProcessedSequence
}
+
+func (s *Tester) CreatePasskeySession(t *testing.T, ctx context.Context, userID string) (id, token string, start, change time.Time) {
+ createResp, err := s.Client.SessionV2.CreateSession(ctx, &session.CreateSessionRequest{
+ Checks: &session.Checks{
+ User: &session.CheckUser{
+ Search: &session.CheckUser_UserId{UserId: userID},
+ },
+ },
+ Challenges: []session.ChallengeKind{
+ session.ChallengeKind_CHALLENGE_KIND_PASSKEY,
+ },
+ Domain: s.Config.ExternalDomain,
+ })
+ require.NoError(t, err)
+
+ assertion, err := s.WebAuthN.CreateAssertionResponse(createResp.GetChallenges().GetPasskey().GetPublicKeyCredentialRequestOptions())
+ require.NoError(t, err)
+
+ updateResp, err := s.Client.SessionV2.SetSession(ctx, &session.SetSessionRequest{
+ SessionId: createResp.GetSessionId(),
+ SessionToken: createResp.GetSessionToken(),
+ Checks: &session.Checks{
+ Passkey: &session.CheckPasskey{
+ CredentialAssertionData: assertion,
+ },
+ },
+ })
+ require.NoError(t, err)
+ return createResp.GetSessionId(), updateResp.GetSessionToken(),
+ createResp.GetDetails().GetChangeDate().AsTime(), updateResp.GetDetails().GetChangeDate().AsTime()
+}
+
+func (s *Tester) CreatePasswordSession(t *testing.T, ctx context.Context, userID, password string) (id, token string, start, change time.Time) {
+ createResp, err := s.Client.SessionV2.CreateSession(ctx, &session.CreateSessionRequest{
+ Checks: &session.Checks{
+ User: &session.CheckUser{
+ Search: &session.CheckUser_UserId{UserId: userID},
+ },
+ Password: &session.CheckPassword{
+ Password: password,
+ },
+ },
+ Domain: s.Config.ExternalDomain,
+ })
+ require.NoError(t, err)
+ return createResp.GetSessionId(), createResp.GetSessionToken(),
+ createResp.GetDetails().GetChangeDate().AsTime(), createResp.GetDetails().GetChangeDate().AsTime()
+}
diff --git a/internal/integration/config/system-user-key.pem b/internal/integration/config/system-user-key.pem
new file mode 100644
index 000000000..2c7a6e7f1
--- /dev/null
+++ b/internal/integration/config/system-user-key.pem
@@ -0,0 +1,27 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIIEowIBAAKCAQEAzi+FFSJL7f5yw4KTwzgMP34ePGycm/M+kT0M7V4Cgx5V3EaD
+IvTQKTLfBaEB45zb9LtjIXzDw0rXRoS2hO6th+CYQCz3KCvh09C0IzxZiB2IS3H/
+aT+5Bx9EFY+vnAkZjccbyG5YNRvmtOlnvIeIH7qZ0tEwkPfF5GEZNPJPtmy3UGV7
+iofdVQS1xRj73+aMw5rvH4D8IdyiAC3VekIbpt0Vj0SUX3DwKtog337BzTiPk3aX
+RF0sbFhQoqdJRI8NqgZjCwjq9yfI5tyxYswn+JGzHGdHvW3idODlmwEt5K2pasiR
+IWK2OGfq+w0EcltQHabuqEPgZlmhCkRdNfixBwIDAQABAoIBAA9jNoBkRdxmH/R9
+Wz+3gBqA9Aq4ZFuzJJk8QCm62V8ltWyyCnliYeKhPEm0QWrWOwghr/1AzW9Wt4g4
+wVJcabD5TwODF5L0626eZcM3bsscwR44TMJzEgD5EWC2j3mKqFCPaoBj08tq4KXh
+wW8tgjgz+eTk3cYD583qfTIZX1+SzSMBpetTBsssQtGhhOB/xPiuL7hi+fXmV2rh
+8mc9X6+wJ5u3zepsyK0vBeEDmurD4ZUIXFrZ0WCB/wNkSW9VKyoH+RC1asQAgqTz
+glJ/NPbDJSKGvSBQydoKkqoXx7MVJ8VObFddfgo4dtOoz6YCfUVBHt8qy+E5rz5y
+CICjL/kCgYEA9MnHntVVKNXtEFZPo02xgCwS3eG27ZwjYgJ1ZkCHM5BuL4MS7qbr
+743/POs1Ctaok0udHl1PFB4uAG0URnmkUnWzcoJYb6Plv03F0LRdsnfuhehfIxLP
+nWvxSm5n21H4ytfxm0BWY09JkLDnJZtXrgTILbuqb9Wy6TmAvUaF2YUCgYEA16Ec
+ywSaLVdqPaVpsTxi7XpRJAB2Isjp6RffNEecta4S0LL7s/IO3QXDH9SYpgmgCTah
+3aXhpT4hIFlpg3eBjVfbOwgqub8DgirnSQyQt99edUtHIK+K8nMdGxz6X6pfTKzK
+asSH7qPlt5tz1621vC0ocXSZR7zm99/FgwILwBsCgYBOsP8nJFV4By1qbxSy3qsN
+FR4LjiAMSoFlZHzxHhVYkjmZtH1FkwuNuwwuPT6T+WW/1DLyK/Tb9se7A1XdQgV9
+LLE/Qn/Dg+C7mvjYmuL0GHHpQkYzNDzh0m2DC/L/Il7kdn8I9anPyxFPHk9wW3vY
+SVlAum+T/BLDvuSP9DfbMQKBgCc1j7PG8XYfOB1fj7l/volqPYjrYI/wssAE7Dxo
+bTGIJrm2YhiVgmhkXNfT47IFfAlQ2twgBsjyZDmqqIoUWAVonV+9m29NMYkg3g+l
+bkdRIa74ckWaRgzSK8+7VDfDFjMuFFyXwhP9z460gLsORkaie4Et75Vg3yrhkNvC
+qnpTAoGBAMguDSWBbCewXnHlKGFpm+LH+OIvVKGEhtCSvfZojtNrg/JBeBebSL1n
+mmT1cONO+0O5bz7uVaRd3JdnH2JFevY698zFfhVsjVCrm+fz31i5cxAgC39G2Lfl
+YkTaa1AFLstnf348ZjuvBN3USUYZo3X3mxnS+uluVuRSGwIKsN0a
+-----END RSA PRIVATE KEY-----
diff --git a/internal/integration/config/zitadel.yaml b/internal/integration/config/zitadel.yaml
index 60389d6de..14222e263 100644
--- a/internal/integration/config/zitadel.yaml
+++ b/internal/integration/config/zitadel.yaml
@@ -1,9 +1,21 @@
Log:
Level: debug
+ExternalSecure: false
+
TLS:
Enabled: false
+Telemetry:
+ Enabled: true
+ Endpoints:
+ - http://localhost:8081
+ Headers:
+ single-value: "single-value"
+ multi-value:
+ - "multi-value-1"
+ - "multi-value-2"
+
FirstInstance:
Org:
Human:
@@ -31,7 +43,13 @@ Projections:
Customizations:
NotificationsQuotas:
RequeueEvery: 1s
+ Telemetry:
+ RequeueEvery: 5s
DefaultInstance:
LoginPolicy:
MfaInitSkipLifetime: "0"
+
+SystemAPIUsers:
+ - tester:
+ KeyData: "LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUlJQklqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FROEFNSUlCQ2dLQ0FRRUF6aStGRlNKTDdmNXl3NEtUd3pnTQpQMzRlUEd5Y20vTStrVDBNN1Y0Q2d4NVYzRWFESXZUUUtUTGZCYUVCNDV6YjlMdGpJWHpEdzByWFJvUzJoTzZ0CmgrQ1lRQ3ozS0N2aDA5QzBJenhaaUIySVMzSC9hVCs1Qng5RUZZK3ZuQWtaamNjYnlHNVlOUnZtdE9sbnZJZUkKSDdxWjB0RXdrUGZGNUdFWk5QSlB0bXkzVUdWN2lvZmRWUVMxeFJqNzMrYU13NXJ2SDREOElkeWlBQzNWZWtJYgpwdDBWajBTVVgzRHdLdG9nMzM3QnpUaVBrM2FYUkYwc2JGaFFvcWRKUkk4TnFnWmpDd2pxOXlmSTV0eXhZc3duCitKR3pIR2RIdlczaWRPRGxtd0V0NUsycGFzaVJJV0syT0dmcSt3MEVjbHRRSGFidXFFUGdabG1oQ2tSZE5maXgKQndJREFRQUIKLS0tLS1FTkQgUFVCTElDIEtFWS0tLS0tCg=="
diff --git a/internal/integration/integration.go b/internal/integration/integration.go
index 6ffec11e1..8634095f3 100644
--- a/internal/integration/integration.go
+++ b/internal/integration/integration.go
@@ -15,6 +15,7 @@ import (
"github.com/spf13/viper"
"github.com/zitadel/logging"
+ "github.com/zitadel/oidc/v2/pkg/client"
"github.com/zitadel/oidc/v2/pkg/oidc"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
@@ -23,6 +24,7 @@ import (
"github.com/zitadel/zitadel/cmd"
"github.com/zitadel/zitadel/cmd/start"
"github.com/zitadel/zitadel/internal/api/authz"
+ http_util "github.com/zitadel/zitadel/internal/api/http"
z_oidc "github.com/zitadel/zitadel/internal/api/oidc"
"github.com/zitadel/zitadel/internal/command"
"github.com/zitadel/zitadel/internal/domain"
@@ -40,6 +42,8 @@ var (
cockroachYAML []byte
//go:embed config/postgres.yaml
postgresYAML []byte
+ //go:embed config/system-user-key.pem
+ systemUserKey []byte
)
// UserType provides constants that give
@@ -53,6 +57,14 @@ type UserType int
const (
Unspecified UserType = iota
OrgOwner
+ Login
+ IAMOwner
+ SystemUser // SystemUser is a user with access to the system service.
+)
+
+const (
+ FirstInstanceUsersKey = "first"
+ UserPassword = "VeryS3cret!"
)
// User information with a Personal Access Token.
@@ -61,13 +73,29 @@ type User struct {
Token string
}
+type InstanceUserMap map[string]map[UserType]*User
+
+func (m InstanceUserMap) Set(instanceID string, typ UserType, user *User) {
+ if m[instanceID] == nil {
+ m[instanceID] = make(map[UserType]*User)
+ }
+ m[instanceID][typ] = user
+}
+
+func (m InstanceUserMap) Get(instanceID string, typ UserType) *User {
+ if users, ok := m[instanceID]; ok {
+ return users[typ]
+ }
+ return nil
+}
+
// Tester is a Zitadel server and client with all resources available for testing.
type Tester struct {
*start.Server
Instance authz.Instance
Organisation *query.Org
- Users map[UserType]User
+ Users InstanceUserMap
Client Client
WebAuthN *webauthn.Client
@@ -80,11 +108,12 @@ func (s *Tester) Host() string {
return fmt.Sprintf("%s:%d", s.Config.ExternalDomain, s.Config.Port)
}
-func (s *Tester) createClientConn(ctx context.Context) {
+func (s *Tester) createClientConn(ctx context.Context, opts ...grpc.DialOption) {
target := s.Host()
- cc, err := grpc.DialContext(ctx, target,
- grpc.WithBlock(), grpc.WithTransportCredentials(insecure.NewCredentials()),
- )
+ cc, err := grpc.DialContext(ctx, target, append(opts,
+ grpc.WithBlock(),
+ grpc.WithTransportCredentials(insecure.NewCredentials()),
+ )...)
if err != nil {
s.Shutdown <- os.Interrupt
s.wg.Wait()
@@ -124,10 +153,11 @@ func (s *Tester) pollHealth(ctx context.Context) (err error) {
}
const (
- SystemUser = "integration"
+ LoginUser = "loginClient"
+ MachineUser = "integration"
)
-func (s *Tester) createSystemUser(ctx context.Context) {
+func (s *Tester) createMachineUser(ctx context.Context, instanceId string) {
var err error
s.Instance, err = s.Queries.InstanceByHost(ctx, s.Host())
@@ -137,25 +167,23 @@ func (s *Tester) createSystemUser(ctx context.Context) {
s.Organisation, err = s.Queries.OrgByID(ctx, true, s.Instance.DefaultOrganisationID())
logging.OnError(err).Fatal("query organisation")
- query, err := query.NewUserUsernameSearchQuery(SystemUser, query.TextEquals)
+ usernameQuery, err := query.NewUserUsernameSearchQuery(MachineUser, query.TextEquals)
logging.OnError(err).Fatal("user query")
- user, err := s.Queries.GetUser(ctx, true, true, query)
-
+ user, err := s.Queries.GetUser(ctx, true, true, usernameQuery)
if errors.Is(err, sql.ErrNoRows) {
_, err = s.Commands.AddMachine(ctx, &command.Machine{
ObjectRoot: models.ObjectRoot{
ResourceOwner: s.Organisation.ID,
},
- Username: SystemUser,
- Name: SystemUser,
+ Username: MachineUser,
+ Name: MachineUser,
Description: "who cares?",
AccessTokenType: domain.OIDCTokenTypeJWT,
})
- logging.OnError(err).Fatal("add machine user")
- user, err = s.Queries.GetUser(ctx, true, true, query)
-
+ logging.WithFields("username", SystemUser).OnError(err).Fatal("add machine user")
+ user, err = s.Queries.GetUser(ctx, true, true, usernameQuery)
}
- logging.OnError(err).Fatal("get user")
+ logging.WithFields("username", SystemUser).OnError(err).Fatal("get user")
_, err = s.Commands.AddOrgMember(ctx, s.Organisation.ID, user.ID, "ORG_OWNER")
target := new(caos_errs.AlreadyExistsError)
@@ -163,21 +191,81 @@ func (s *Tester) createSystemUser(ctx context.Context) {
logging.OnError(err).Fatal("add org member")
}
+ scopes := []string{oidc.ScopeOpenID, oidc.ScopeProfile, z_oidc.ScopeUserMetaData, z_oidc.ScopeResourceOwner}
+ pat := command.NewPersonalAccessToken(user.ResourceOwner, user.ID, time.Now().Add(time.Hour), scopes, domain.UserTypeMachine)
+ _, err = s.Commands.AddPersonalAccessToken(ctx, pat)
+ logging.WithFields("username", SystemUser).OnError(err).Fatal("add pat")
+ s.Users.Set(instanceId, OrgOwner, &User{
+ User: user,
+ Token: pat.Token,
+ })
+}
+
+func (s *Tester) createLoginClient(ctx context.Context) {
+ var err error
+
+ s.Instance, err = s.Queries.InstanceByHost(ctx, s.Host())
+ logging.OnError(err).Fatal("query instance")
+ ctx = authz.WithInstance(ctx, s.Instance)
+
+ s.Organisation, err = s.Queries.OrgByID(ctx, true, s.Instance.DefaultOrganisationID())
+ logging.OnError(err).Fatal("query organisation")
+
+ usernameQuery, err := query.NewUserUsernameSearchQuery(LoginUser, query.TextEquals)
+ logging.WithFields("username", LoginUser).OnError(err).Fatal("user query")
+ user, err := s.Queries.GetUser(ctx, true, true, usernameQuery)
+ if errors.Is(err, sql.ErrNoRows) {
+ _, err = s.Commands.AddMachine(ctx, &command.Machine{
+ ObjectRoot: models.ObjectRoot{
+ ResourceOwner: s.Organisation.ID,
+ },
+ Username: LoginUser,
+ Name: LoginUser,
+ Description: "who cares?",
+ AccessTokenType: domain.OIDCTokenTypeJWT,
+ })
+ logging.WithFields("username", LoginUser).OnError(err).Fatal("add machine user")
+ user, err = s.Queries.GetUser(ctx, true, true, usernameQuery)
+ }
+ logging.WithFields("username", LoginUser).OnError(err).Fatal("get user")
+
scopes := []string{oidc.ScopeOpenID, z_oidc.ScopeUserMetaData, z_oidc.ScopeResourceOwner}
pat := command.NewPersonalAccessToken(user.ResourceOwner, user.ID, time.Now().Add(time.Hour), scopes, domain.UserTypeMachine)
_, err = s.Commands.AddPersonalAccessToken(ctx, pat)
logging.OnError(err).Fatal("add pat")
- s.Users = map[UserType]User{
- OrgOwner: {
- User: user,
- Token: pat.Token,
- },
- }
+ s.Users.Set(FirstInstanceUsersKey, Login, &User{
+ User: user,
+ Token: pat.Token,
+ })
}
-func (s *Tester) WithSystemAuthorization(ctx context.Context, u UserType) context.Context {
- return metadata.AppendToOutgoingContext(ctx, "Authorization", fmt.Sprintf("Bearer %s", s.Users[u].Token))
+func (s *Tester) WithAuthorization(ctx context.Context, u UserType) context.Context {
+ return s.WithInstanceAuthorization(ctx, u, FirstInstanceUsersKey)
+}
+
+func (s *Tester) WithInstanceAuthorization(ctx context.Context, u UserType, instanceID string) context.Context {
+ if u == SystemUser {
+ s.ensureSystemUser()
+ }
+ return metadata.AppendToOutgoingContext(ctx, "Authorization", fmt.Sprintf("Bearer %s", s.Users.Get(instanceID, u).Token))
+}
+
+func (s *Tester) ensureSystemUser() {
+ const ISSUER = "tester"
+ if s.Users.Get(FirstInstanceUsersKey, SystemUser) != nil {
+ return
+ }
+ audience := http_util.BuildOrigin(s.Host(), s.Server.Config.ExternalSecure)
+ signer, err := client.NewSignerFromPrivateKeyByte(systemUserKey, "")
+ logging.OnError(err).Fatal("system key signer")
+ jwt, err := client.SignedJWTProfileAssertion(ISSUER, []string{audience}, time.Hour, signer)
+ logging.OnError(err).Fatal("system key jwt")
+ s.Users.Set(FirstInstanceUsersKey, SystemUser, &User{Token: jwt})
+}
+
+func (s *Tester) WithSystemAuthorizationHTTP(u UserType) map[string]string {
+ return map[string]string{"Authorization": fmt.Sprintf("Bearer %s", s.Users.Get(FirstInstanceUsersKey, u).Token)}
}
// Done send an interrupt signal to cleanly shutdown the server.
@@ -224,7 +312,9 @@ func NewTester(ctx context.Context) *Tester {
}
logging.OnError(err).Fatal()
- tester := new(Tester)
+ tester := Tester{
+ Users: make(InstanceUserMap),
+ }
tester.wg.Add(1)
go func(wg *sync.WaitGroup) {
logging.OnError(cmd.Execute()).Fatal()
@@ -237,10 +327,12 @@ func NewTester(ctx context.Context) *Tester {
logging.OnError(ctx.Err()).Fatal("waiting for integration tester server")
}
tester.createClientConn(ctx)
- tester.createSystemUser(ctx)
+ tester.createLoginClient(ctx)
+ tester.WebAuthN = webauthn.NewClient(tester.Config.WebAuthNName, tester.Config.ExternalDomain, http_util.BuildOrigin(tester.Host(), tester.Config.ExternalSecure))
+ tester.createMachineUser(ctx, FirstInstanceUsersKey)
tester.WebAuthN = webauthn.NewClient(tester.Config.WebAuthNName, tester.Config.ExternalDomain, "https://"+tester.Host())
- return tester
+ return &tester
}
func Contexts(timeout time.Duration) (ctx, errCtx context.Context, cancel context.CancelFunc) {
diff --git a/internal/integration/oidc.go b/internal/integration/oidc.go
new file mode 100644
index 000000000..e34850d5e
--- /dev/null
+++ b/internal/integration/oidc.go
@@ -0,0 +1,185 @@
+package integration
+
+import (
+ "context"
+ "fmt"
+ "net/http"
+ "net/url"
+ "strings"
+ "time"
+
+ "github.com/zitadel/oidc/v2/pkg/client"
+ "github.com/zitadel/oidc/v2/pkg/client/rp"
+ "github.com/zitadel/oidc/v2/pkg/client/rs"
+ "github.com/zitadel/oidc/v2/pkg/oidc"
+
+ http_util "github.com/zitadel/zitadel/internal/api/http"
+ oidc_internal "github.com/zitadel/zitadel/internal/api/oidc"
+ "github.com/zitadel/zitadel/pkg/grpc/app"
+ "github.com/zitadel/zitadel/pkg/grpc/management"
+)
+
+func (s *Tester) CreateOIDCNativeClient(ctx context.Context, redirectURI, logoutRedirectURI, projectID string) (*management.AddOIDCAppResponse, error) {
+ return s.Client.Mgmt.AddOIDCApp(ctx, &management.AddOIDCAppRequest{
+ ProjectId: projectID,
+ Name: fmt.Sprintf("app-%d", time.Now().UnixNano()),
+ RedirectUris: []string{redirectURI},
+ ResponseTypes: []app.OIDCResponseType{app.OIDCResponseType_OIDC_RESPONSE_TYPE_CODE},
+ GrantTypes: []app.OIDCGrantType{app.OIDCGrantType_OIDC_GRANT_TYPE_AUTHORIZATION_CODE, app.OIDCGrantType_OIDC_GRANT_TYPE_REFRESH_TOKEN},
+ AppType: app.OIDCAppType_OIDC_APP_TYPE_NATIVE,
+ AuthMethodType: app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_NONE,
+ PostLogoutRedirectUris: []string{logoutRedirectURI},
+ Version: app.OIDCVersion_OIDC_VERSION_1_0,
+ DevMode: false,
+ AccessTokenType: app.OIDCTokenType_OIDC_TOKEN_TYPE_JWT,
+ AccessTokenRoleAssertion: false,
+ IdTokenRoleAssertion: false,
+ IdTokenUserinfoAssertion: false,
+ ClockSkew: nil,
+ AdditionalOrigins: nil,
+ SkipNativeAppSuccessPage: false,
+ })
+}
+
+func (s *Tester) CreateOIDCImplicitFlowClient(ctx context.Context, redirectURI string) (*management.AddOIDCAppResponse, error) {
+ project, err := s.Client.Mgmt.AddProject(ctx, &management.AddProjectRequest{
+ Name: fmt.Sprintf("project-%d", time.Now().UnixNano()),
+ })
+ if err != nil {
+ return nil, err
+ }
+ return s.Client.Mgmt.AddOIDCApp(ctx, &management.AddOIDCAppRequest{
+ ProjectId: project.GetId(),
+ Name: fmt.Sprintf("app-%d", time.Now().UnixNano()),
+ RedirectUris: []string{redirectURI},
+ ResponseTypes: []app.OIDCResponseType{app.OIDCResponseType_OIDC_RESPONSE_TYPE_ID_TOKEN_TOKEN},
+ GrantTypes: []app.OIDCGrantType{app.OIDCGrantType_OIDC_GRANT_TYPE_IMPLICIT},
+ AppType: app.OIDCAppType_OIDC_APP_TYPE_USER_AGENT,
+ AuthMethodType: app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_NONE,
+ PostLogoutRedirectUris: nil,
+ Version: app.OIDCVersion_OIDC_VERSION_1_0,
+ DevMode: true,
+ AccessTokenType: app.OIDCTokenType_OIDC_TOKEN_TYPE_JWT,
+ AccessTokenRoleAssertion: false,
+ IdTokenRoleAssertion: false,
+ IdTokenUserinfoAssertion: false,
+ ClockSkew: nil,
+ AdditionalOrigins: nil,
+ SkipNativeAppSuccessPage: false,
+ })
+}
+
+func (s *Tester) CreateProject(ctx context.Context) (*management.AddProjectResponse, error) {
+ return s.Client.Mgmt.AddProject(ctx, &management.AddProjectRequest{
+ Name: fmt.Sprintf("project-%d", time.Now().UnixNano()),
+ })
+}
+
+func (s *Tester) CreateAPIClient(ctx context.Context, projectID string) (*management.AddAPIAppResponse, error) {
+ return s.Client.Mgmt.AddAPIApp(ctx, &management.AddAPIAppRequest{
+ ProjectId: projectID,
+ Name: fmt.Sprintf("api-%d", time.Now().UnixNano()),
+ AuthMethodType: app.APIAuthMethodType_API_AUTH_METHOD_TYPE_PRIVATE_KEY_JWT,
+ })
+}
+
+func (s *Tester) CreateOIDCAuthRequest(clientID, loginClient, redirectURI string, scope ...string) (authRequestID string, err error) {
+ provider, err := s.CreateRelyingParty(clientID, redirectURI, scope...)
+ if err != nil {
+ return "", err
+ }
+
+ codeVerifier := "codeVerifier"
+ codeChallenge := oidc.NewSHACodeChallenge(codeVerifier)
+ authURL := rp.AuthURL("state", provider, rp.WithCodeChallenge(codeChallenge))
+
+ loc, err := CheckRedirect(authURL, map[string]string{oidc_internal.LoginClientHeader: loginClient})
+ if err != nil {
+ return "", err
+ }
+
+ prefixWithHost := provider.Issuer() + s.Config.OIDC.DefaultLoginURLV2
+ if !strings.HasPrefix(loc.String(), prefixWithHost) {
+ return "", fmt.Errorf("login location has not prefix %s, but is %s", prefixWithHost, loc.String())
+ }
+ return strings.TrimPrefix(loc.String(), prefixWithHost), nil
+}
+
+func (s *Tester) CreateOIDCAuthRequestImplicit(clientID, loginClient, redirectURI string, scope ...string) (authRequestID string, err error) {
+ provider, err := s.CreateRelyingParty(clientID, redirectURI, scope...)
+ if err != nil {
+ return "", err
+ }
+
+ authURL := rp.AuthURL("state", provider)
+
+ // implicit is not natively supported so let's just overwrite the response type
+ parsed, _ := url.Parse(authURL)
+ queries := parsed.Query()
+ queries.Set("response_type", string(oidc.ResponseTypeIDToken))
+ parsed.RawQuery = queries.Encode()
+ authURL = parsed.String()
+
+ loc, err := CheckRedirect(authURL, map[string]string{oidc_internal.LoginClientHeader: loginClient})
+ if err != nil {
+ return "", err
+ }
+
+ prefixWithHost := provider.Issuer() + s.Config.OIDC.DefaultLoginURLV2
+ if !strings.HasPrefix(loc.String(), prefixWithHost) {
+ return "", fmt.Errorf("login location has not prefix %s, but is %s", prefixWithHost, loc.String())
+ }
+ return strings.TrimPrefix(loc.String(), prefixWithHost), nil
+}
+
+func (s *Tester) OIDCIssuer() string {
+ return http_util.BuildHTTP(s.Config.ExternalDomain, s.Config.Port, s.Config.ExternalSecure)
+}
+
+func (s *Tester) CreateRelyingParty(clientID, redirectURI string, scope ...string) (rp.RelyingParty, error) {
+ if len(scope) == 0 {
+ scope = []string{oidc.ScopeOpenID}
+ }
+ loginClient := &http.Client{Transport: &loginRoundTripper{http.DefaultTransport}}
+ return rp.NewRelyingPartyOIDC(s.OIDCIssuer(), clientID, "", redirectURI, scope, rp.WithHTTPClient(loginClient))
+}
+
+type loginRoundTripper struct {
+ http.RoundTripper
+}
+
+func (c *loginRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
+ req.Header.Set(oidc_internal.LoginClientHeader, LoginUser)
+ return c.RoundTripper.RoundTrip(req)
+}
+
+func (s *Tester) CreateResourceServer(keyFileData []byte) (rs.ResourceServer, error) {
+ keyFile, err := client.ConfigFromKeyFileData(keyFileData)
+ if err != nil {
+ return nil, err
+ }
+ return rs.NewResourceServerJWTProfile(s.OIDCIssuer(), keyFile.ClientID, keyFile.KeyID, []byte(keyFile.Key))
+}
+
+func CheckRedirect(url string, headers map[string]string) (*url.URL, error) {
+ req, err := http.NewRequest(http.MethodGet, url, nil)
+ if err != nil {
+ return nil, err
+ }
+ for key, value := range headers {
+ req.Header.Set(key, value)
+ }
+
+ client := &http.Client{
+ CheckRedirect: func(req *http.Request, via []*http.Request) error {
+ return http.ErrUseLastResponse
+ },
+ }
+ resp, err := client.Do(req)
+ if err != nil {
+ return nil, err
+ }
+ defer resp.Body.Close()
+
+ return resp.Location()
+}
diff --git a/internal/integration/rand.go b/internal/integration/rand.go
new file mode 100644
index 000000000..4425c97c8
--- /dev/null
+++ b/internal/integration/rand.go
@@ -0,0 +1,20 @@
+package integration
+
+import (
+ "math/rand"
+ "time"
+)
+
+func init() {
+ rand.Seed(time.Now().UnixNano())
+}
+
+var letterRunes = []rune("abcdefghijklmnopqrstuvwxyz")
+
+func randString(n int) string {
+ b := make([]rune, n)
+ for i := range b {
+ b[i] = letterRunes[rand.Intn(len(letterRunes))]
+ }
+ return string(b)
+}
diff --git a/internal/notification/channels/webhook/channel.go b/internal/notification/channels/webhook/channel.go
index 6dbed74eb..c587f593b 100644
--- a/internal/notification/channels/webhook/channel.go
+++ b/internal/notification/channels/webhook/channel.go
@@ -21,10 +21,8 @@ func InitChannel(ctx context.Context, cfg Config) (channels.NotificationChannel,
logging.Debug("successfully initialized webhook json channel")
return channels.HandleMessageFunc(func(message channels.Message) error {
-
requestCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
-
msg, ok := message.(*messages.JSON)
if !ok {
return errors.ThrowInternal(nil, "WEBH-K686U", "message is not JSON")
@@ -33,27 +31,24 @@ func InitChannel(ctx context.Context, cfg Config) (channels.NotificationChannel,
if err != nil {
return err
}
-
req, err := http.NewRequestWithContext(requestCtx, cfg.Method, cfg.CallURL, strings.NewReader(payload))
if err != nil {
return err
}
-
+ if cfg.Headers != nil {
+ req.Header = cfg.Headers
+ }
req.Header.Set("Content-Type", "application/json")
-
resp, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
-
if err = resp.Body.Close(); err != nil {
return err
}
-
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return errors.ThrowUnknown(fmt.Errorf("calling url %s returned %s", cfg.CallURL, resp.Status), "WEBH-LBxU0", "webhook didn't return a success status")
}
-
logging.WithFields("calling_url", cfg.CallURL, "method", cfg.Method).Debug("webhook called")
return nil
}), nil
diff --git a/internal/notification/channels/webhook/config.go b/internal/notification/channels/webhook/config.go
index 4af0c1540..9772151d8 100644
--- a/internal/notification/channels/webhook/config.go
+++ b/internal/notification/channels/webhook/config.go
@@ -1,12 +1,14 @@
package webhook
import (
+ "net/http"
"net/url"
)
type Config struct {
CallURL string
Method string
+ Headers http.Header
}
func (w *Config) Validate() error {
diff --git a/internal/notification/handlers/already_handled.go b/internal/notification/handlers/already_handled.go
index abf7eb901..556a3e33c 100644
--- a/internal/notification/handlers/already_handled.go
+++ b/internal/notification/handlers/already_handled.go
@@ -4,16 +4,15 @@ import (
"context"
"github.com/zitadel/zitadel/internal/eventstore"
- "github.com/zitadel/zitadel/internal/repository/user"
)
-func (n *NotificationQueries) IsAlreadyHandled(ctx context.Context, event eventstore.Event, data map[string]interface{}, eventTypes ...eventstore.EventType) (bool, error) {
+func (n *NotificationQueries) IsAlreadyHandled(ctx context.Context, event eventstore.Event, data map[string]interface{}, aggregateType eventstore.AggregateType, eventTypes ...eventstore.EventType) (bool, error) {
events, err := n.es.Filter(
ctx,
eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent).
InstanceID(event.Aggregate().InstanceID).
AddQuery().
- AggregateTypes(user.AggregateType).
+ AggregateTypes(aggregateType).
AggregateIDs(event.Aggregate().ID).
SequenceGreater(event.Sequence()).
EventTypes(eventTypes...).
diff --git a/internal/notification/handlers/handlers_integration_test.go b/internal/notification/handlers/handlers_integration_test.go
new file mode 100644
index 000000000..37f6a344c
--- /dev/null
+++ b/internal/notification/handlers/handlers_integration_test.go
@@ -0,0 +1,30 @@
+//go:build integration
+
+package handlers_test
+
+import (
+ "context"
+ "os"
+ "testing"
+ "time"
+
+ "github.com/zitadel/zitadel/internal/integration"
+)
+
+var (
+ CTX context.Context
+ SystemCTX context.Context
+ Tester *integration.Tester
+)
+
+func TestMain(m *testing.M) {
+ os.Exit(func() int {
+ ctx, _, cancel := integration.Contexts(5 * time.Minute)
+ CTX = ctx
+ defer cancel()
+ Tester = integration.NewTester(ctx)
+ SystemCTX = Tester.WithAuthorization(ctx, integration.SystemUser)
+ defer Tester.Done()
+ return m.Run()
+ }())
+}
diff --git a/internal/notification/handlers/queries.go b/internal/notification/handlers/queries.go
index 63e0ab4d4..96880445b 100644
--- a/internal/notification/handlers/queries.go
+++ b/internal/notification/handlers/queries.go
@@ -12,6 +12,7 @@ import (
type NotificationQueries struct {
*query.Queries
es *eventstore.Eventstore
+ externalDomain string
externalPort uint16
externalSecure bool
fileSystemPath string
@@ -24,6 +25,7 @@ type NotificationQueries struct {
func NewNotificationQueries(
baseQueries *query.Queries,
es *eventstore.Eventstore,
+ externalDomain string,
externalPort uint16,
externalSecure bool,
fileSystemPath string,
@@ -35,6 +37,7 @@ func NewNotificationQueries(
return &NotificationQueries{
Queries: baseQueries,
es: es,
+ externalDomain: externalDomain,
externalPort: externalPort,
externalSecure: externalSecure,
fileSystemPath: fileSystemPath,
diff --git a/internal/notification/handlers/quotanotifier.go b/internal/notification/handlers/quota_notifier.go
similarity index 96%
rename from internal/notification/handlers/quotanotifier.go
rename to internal/notification/handlers/quota_notifier.go
index e53998490..21daa92ff 100644
--- a/internal/notification/handlers/quotanotifier.go
+++ b/internal/notification/handlers/quota_notifier.go
@@ -68,7 +68,7 @@ func (u *quotaNotifier) reduceNotificationDue(event eventstore.Event) (*handler.
return nil, errors.ThrowInvalidArgumentf(nil, "HANDL-DLxdE", "reduce.wrong.event.type %s", quota.NotificationDueEventType)
}
ctx := HandlerContext(event.Aggregate())
- alreadyHandled, err := u.queries.IsAlreadyHandled(ctx, event, map[string]interface{}{"dueEventID": e.ID}, quota.NotifiedEventType)
+ alreadyHandled, err := u.queries.IsAlreadyHandled(ctx, event, map[string]interface{}{"dueEventID": e.ID}, quota.AggregateType, quota.NotifiedEventType)
if err != nil {
return nil, err
}
diff --git a/internal/notification/handlers/telemetry_pusher.go b/internal/notification/handlers/telemetry_pusher.go
new file mode 100644
index 000000000..86f8420c7
--- /dev/null
+++ b/internal/notification/handlers/telemetry_pusher.go
@@ -0,0 +1,156 @@
+package handlers
+
+import (
+ "context"
+ "fmt"
+ "net/http"
+ "time"
+
+ "github.com/zitadel/logging"
+
+ "github.com/zitadel/zitadel/internal/api/authz"
+ "github.com/zitadel/zitadel/internal/api/call"
+ "github.com/zitadel/zitadel/internal/command"
+ "github.com/zitadel/zitadel/internal/errors"
+ "github.com/zitadel/zitadel/internal/eventstore"
+ "github.com/zitadel/zitadel/internal/eventstore/handler"
+ "github.com/zitadel/zitadel/internal/eventstore/handler/crdb"
+ "github.com/zitadel/zitadel/internal/notification/channels/webhook"
+ _ "github.com/zitadel/zitadel/internal/notification/statik"
+ "github.com/zitadel/zitadel/internal/notification/types"
+ "github.com/zitadel/zitadel/internal/query"
+ "github.com/zitadel/zitadel/internal/query/projection"
+ "github.com/zitadel/zitadel/internal/repository/milestone"
+ "github.com/zitadel/zitadel/internal/repository/pseudo"
+)
+
+const (
+ TelemetryProjectionTable = "projections.telemetry"
+)
+
+type TelemetryPusherConfig struct {
+ Enabled bool
+ Endpoints []string
+ Headers http.Header
+ Limit uint64
+}
+
+type telemetryPusher struct {
+ crdb.StatementHandler
+ cfg TelemetryPusherConfig
+ commands *command.Commands
+ queries *NotificationQueries
+ metricSuccessfulDeliveriesJSON string
+ metricFailedDeliveriesJSON string
+}
+
+func NewTelemetryPusher(
+ ctx context.Context,
+ telemetryCfg TelemetryPusherConfig,
+ handlerCfg crdb.StatementHandlerConfig,
+ commands *command.Commands,
+ queries *NotificationQueries,
+ metricSuccessfulDeliveriesJSON,
+ metricFailedDeliveriesJSON string,
+) *telemetryPusher {
+ p := new(telemetryPusher)
+ handlerCfg.ProjectionName = TelemetryProjectionTable
+ handlerCfg.Reducers = p.reducers()
+ p.cfg = telemetryCfg
+ p.StatementHandler = crdb.NewStatementHandler(ctx, handlerCfg)
+ p.commands = commands
+ p.queries = queries
+ p.metricSuccessfulDeliveriesJSON = metricSuccessfulDeliveriesJSON
+ p.metricFailedDeliveriesJSON = metricFailedDeliveriesJSON
+ projection.TelemetryPusherProjection = p
+ return p
+}
+
+func (t *telemetryPusher) reducers() []handler.AggregateReducer {
+ return []handler.AggregateReducer{{
+ Aggregate: pseudo.AggregateType,
+ EventRedusers: []handler.EventReducer{{
+ Event: pseudo.ScheduledEventType,
+ Reduce: t.pushMilestones,
+ }},
+ }}
+}
+
+func (t *telemetryPusher) pushMilestones(event eventstore.Event) (*handler.Statement, error) {
+ ctx := call.WithTimestamp(context.Background())
+ scheduledEvent, ok := event.(*pseudo.ScheduledEvent)
+ if !ok {
+ return nil, errors.ThrowInvalidArgumentf(nil, "HANDL-lDTs5", "reduce.wrong.event.type %s", event.Type())
+ }
+
+ isReached, err := query.NewNotNullQuery(query.MilestoneReachedDateColID)
+ if err != nil {
+ return nil, err
+ }
+ isNotPushed, err := query.NewIsNullQuery(query.MilestonePushedDateColID)
+ if err != nil {
+ return nil, err
+ }
+ hasPrimaryDomain, err := query.NewNotNullQuery(query.MilestonePrimaryDomainColID)
+ if err != nil {
+ return nil, err
+ }
+ unpushedMilestones, err := t.queries.Queries.SearchMilestones(ctx, scheduledEvent.InstanceIDs, &query.MilestonesSearchQueries{
+ SearchRequest: query.SearchRequest{
+ Limit: t.cfg.Limit,
+ SortingColumn: query.MilestoneReachedDateColID,
+ Asc: true,
+ },
+ Queries: []query.SearchQuery{isReached, isNotPushed, hasPrimaryDomain},
+ })
+ if err != nil {
+ return nil, err
+ }
+ var errs int
+ for _, ms := range unpushedMilestones.Milestones {
+ if err = t.pushMilestone(ctx, scheduledEvent, ms); err != nil {
+ errs++
+ logging.Warnf("pushing milestone %+v failed: %s", *ms, err.Error())
+ }
+ }
+ if errs > 0 {
+ return nil, fmt.Errorf("pushing %d of %d milestones failed", errs, unpushedMilestones.Count)
+ }
+
+ return crdb.NewNoOpStatement(scheduledEvent), nil
+}
+
+func (t *telemetryPusher) pushMilestone(ctx context.Context, event *pseudo.ScheduledEvent, ms *query.Milestone) error {
+ ctx = authz.WithInstanceID(ctx, ms.InstanceID)
+ for _, endpoint := range t.cfg.Endpoints {
+ if err := types.SendJSON(
+ ctx,
+ webhook.Config{
+ CallURL: endpoint,
+ Method: http.MethodPost,
+ Headers: t.cfg.Headers,
+ },
+ t.queries.GetFileSystemProvider,
+ t.queries.GetLogProvider,
+ &struct {
+ InstanceID string `json:"instanceId"`
+ ExternalDomain string `json:"externalDomain"`
+ PrimaryDomain string `json:"primaryDomain"`
+ Type milestone.Type `json:"type"`
+ ReachedDate time.Time `json:"reached"`
+ }{
+ InstanceID: ms.InstanceID,
+ ExternalDomain: t.queries.externalDomain,
+ PrimaryDomain: ms.PrimaryDomain,
+ Type: ms.Type,
+ ReachedDate: ms.ReachedDate,
+ },
+ event,
+ t.metricSuccessfulDeliveriesJSON,
+ t.metricFailedDeliveriesJSON,
+ ).WithoutTemplate(); err != nil {
+ return err
+ }
+ }
+ return t.commands.MilestonePushed(ctx, ms.Type, t.cfg.Endpoints, ms.PrimaryDomain)
+}
diff --git a/internal/notification/handlers/telemetry_pusher_integration_test.go b/internal/notification/handlers/telemetry_pusher_integration_test.go
new file mode 100644
index 000000000..e3709db17
--- /dev/null
+++ b/internal/notification/handlers/telemetry_pusher_integration_test.go
@@ -0,0 +1,89 @@
+//go:build integration
+
+package handlers_test
+
+import (
+ "bytes"
+ "encoding/json"
+ "io"
+ "net"
+ "net/http"
+ "net/http/httptest"
+ "reflect"
+ "testing"
+ "time"
+
+ "github.com/zitadel/zitadel/pkg/grpc/management"
+ "github.com/zitadel/zitadel/pkg/grpc/system"
+)
+
+func TestServer_TelemetryPushMilestones(t *testing.T) {
+ bodies := make(chan []byte, 0)
+ mockServer := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ body, err := io.ReadAll(r.Body)
+ if err != nil {
+ t.Error(err)
+ }
+ if r.Header.Get("single-value") != "single-value" {
+ t.Error("single-value header not set")
+ }
+ if reflect.DeepEqual(r.Header.Get("multi-value"), "multi-value-1,multi-value-2") {
+ t.Error("single-value header not set")
+ }
+ bodies <- body
+ w.WriteHeader(http.StatusOK)
+ }))
+ listener, err := net.Listen("tcp", "localhost:8081")
+ if err != nil {
+ t.Fatal(err)
+ }
+ mockServer.Listener = listener
+ mockServer.Start()
+ t.Cleanup(mockServer.Close)
+ primaryDomain, instanceID, iamOwnerCtx := Tester.UseIsolatedInstance(CTX, SystemCTX)
+ t.Log("testing against instance with primary domain", primaryDomain)
+ awaitMilestone(t, bodies, primaryDomain, "InstanceCreated")
+ project, err := Tester.Client.Mgmt.AddProject(iamOwnerCtx, &management.AddProjectRequest{Name: "integration"})
+ if err != nil {
+ t.Fatal(err)
+ }
+ awaitMilestone(t, bodies, primaryDomain, "ProjectCreated")
+ if _, err = Tester.Client.Mgmt.AddOIDCApp(iamOwnerCtx, &management.AddOIDCAppRequest{
+ ProjectId: project.GetId(),
+ Name: "integration",
+ }); err != nil {
+ t.Fatal(err)
+ }
+ awaitMilestone(t, bodies, primaryDomain, "ApplicationCreated")
+ // TODO: trigger and await milestone AuthenticationSucceededOnInstance
+ // TODO: trigger and await milestone AuthenticationSucceededOnApplication
+ if _, err = Tester.Client.System.RemoveInstance(SystemCTX, &system.RemoveInstanceRequest{InstanceId: instanceID}); err != nil {
+ t.Fatal(err)
+ }
+ awaitMilestone(t, bodies, primaryDomain, "InstanceDeleted")
+}
+
+func awaitMilestone(t *testing.T, bodies chan []byte, primaryDomain, expectMilestoneType string) {
+ for {
+ select {
+ case body := <-bodies:
+ plain := new(bytes.Buffer)
+ if err := json.Indent(plain, body, "", " "); err != nil {
+ t.Fatal(err)
+ }
+ t.Log("received milestone", plain.String())
+ milestone := struct {
+ Type string `json:"type"`
+ PrimaryDomain string `json:"primaryDomain"`
+ }{}
+ if err := json.Unmarshal(body, &milestone); err != nil {
+ t.Error(err)
+ }
+ if milestone.Type == expectMilestoneType && milestone.PrimaryDomain == primaryDomain {
+ return
+ }
+ case <-time.After(60 * time.Second):
+ t.Fatalf("timed out waiting for milestone %s in domain %s", expectMilestoneType, primaryDomain)
+ }
+ }
+}
diff --git a/internal/notification/handlers/usernotifier.go b/internal/notification/handlers/user_notifier.go
similarity index 99%
rename from internal/notification/handlers/usernotifier.go
rename to internal/notification/handlers/user_notifier.go
index cd2a48f09..d726fb62a 100644
--- a/internal/notification/handlers/usernotifier.go
+++ b/internal/notification/handlers/user_notifier.go
@@ -337,7 +337,7 @@ func (u *userNotifier) reduceDomainClaimed(event eventstore.Event) (*handler.Sta
return nil, errors.ThrowInvalidArgumentf(nil, "HANDL-Drh5w", "reduce.wrong.event.type %s", user.UserDomainClaimedType)
}
ctx := HandlerContext(event.Aggregate())
- alreadyHandled, err := u.queries.IsAlreadyHandled(ctx, event, nil,
+ alreadyHandled, err := u.queries.IsAlreadyHandled(ctx, event, nil, user.AggregateType,
user.UserDomainClaimedType, user.UserDomainClaimedSentType)
if err != nil {
return nil, err
@@ -465,7 +465,7 @@ func (u *userNotifier) reducePasswordChanged(event eventstore.Event) (*handler.S
return nil, errors.ThrowInvalidArgumentf(nil, "HANDL-Yko2z8", "reduce.wrong.event.type %s", user.HumanPasswordChangedType)
}
ctx := HandlerContext(event.Aggregate())
- alreadyHandled, err := u.queries.IsAlreadyHandled(ctx, event, nil, user.HumanPasswordChangeSentType)
+ alreadyHandled, err := u.queries.IsAlreadyHandled(ctx, event, nil, user.AggregateType, user.HumanPasswordChangeSentType)
if err != nil {
return nil, err
}
@@ -594,5 +594,5 @@ func (u *userNotifier) checkIfCodeAlreadyHandledOrExpired(ctx context.Context, e
if event.CreationDate().Add(expiry).Before(time.Now().UTC()) {
return true, nil
}
- return u.queries.IsAlreadyHandled(ctx, event, data, eventTypes...)
+ return u.queries.IsAlreadyHandled(ctx, event, data, user.AggregateType, eventTypes...)
}
diff --git a/internal/notification/projections.go b/internal/notification/projections.go
index f4abf3b7b..0a6f6f659 100644
--- a/internal/notification/projections.go
+++ b/internal/notification/projections.go
@@ -29,6 +29,9 @@ func Start(
ctx context.Context,
userHandlerCustomConfig projection.CustomConfig,
quotaHandlerCustomConfig projection.CustomConfig,
+ telemetryHandlerCustomConfig projection.CustomConfig,
+ telemetryCfg handlers.TelemetryPusherConfig,
+ externalDomain string,
externalPort uint16,
externalSecure bool,
commands *command.Commands,
@@ -54,7 +57,7 @@ func Start(
logging.WithFields("metric", metricSuccessfulDeliveriesJSON).OnError(err).Panic("unable to register counter")
err = metrics.RegisterCounter(metricFailedDeliveriesJSON, "Failed JSON message deliveries")
logging.WithFields("metric", metricFailedDeliveriesJSON).OnError(err).Panic("unable to register counter")
- q := handlers.NewNotificationQueries(queries, es, externalPort, externalSecure, fileSystemPath, userEncryption, smtpEncryption, smsEncryption, statikFS)
+ q := handlers.NewNotificationQueries(queries, es, externalDomain, externalPort, externalSecure, fileSystemPath, userEncryption, smtpEncryption, smsEncryption, statikFS)
handlers.NewUserNotifier(
ctx,
projection.ApplyCustomConfig(userHandlerCustomConfig),
@@ -74,4 +77,15 @@ func Start(
metricSuccessfulDeliveriesJSON,
metricFailedDeliveriesJSON,
).Start()
+ if telemetryCfg.Enabled {
+ handlers.NewTelemetryPusher(
+ ctx,
+ telemetryCfg,
+ projection.ApplyCustomConfig(telemetryHandlerCustomConfig),
+ commands,
+ q,
+ metricSuccessfulDeliveriesJSON,
+ metricFailedDeliveriesJSON,
+ ).Start()
+ }
}
diff --git a/internal/notification/static/i18n/mk.yaml b/internal/notification/static/i18n/mk.yaml
new file mode 100644
index 000000000..b2c5573ae
--- /dev/null
+++ b/internal/notification/static/i18n/mk.yaml
@@ -0,0 +1,49 @@
+InitCode:
+ Title: ZITADEL - Иницијализација на Корисник
+ PreHeader: Иницијализација на Корисник
+ Subject: Иницијализација на Корисник
+ Greeting: Здраво {{.DisplayName}},
+ Text: Овој корисник е креиран во ZITADEL. Користете го корисничкото име {{.PreferredLoginName}} за најава. Ве молиме кликнете на копчето подолу за да го завршите процесот на иницијализација. (Код {{.Code}}) Ако не сте барале/и оваа е-пошта, ве молиме игнорирајте ја.
+ ButtonText: Заврши иницијализација
+PasswordReset:
+ Title: ZITADEL - Ресетирање на лозинка
+ PreHeader: Ресетирање на лозинка
+ Subject: Ресетирање на лозинка
+ Greeting: Здраво {{.DisplayName}},
+ Text: Добивме барање за ресетирање на лозинката. Ве молиме користете го копчето подолу за да ја ресетирате вашата лозинка. (Код {{.Code}}) Ако не сте ја побарале ресетирање на лозинка, ве молиме игнорирајте ја оваа е-пошта.
+ ButtonText: Ресетирај лозинка
+VerifyEmail:
+ Title: ZITADEL - Верификација на е-пошта
+ PreHeader: Верификација на е-пошта
+ Subject: Верификација на е-пошта
+ Greeting: Здраво {{.DisplayName}},
+ Text: Додадена е нова е-пошта. Ве молиме користете го копчето подолу за да ја верифицирате вашата е-пошта. (Код {{.Code}}) Ако не сте додале нова е-пошта, ве молиме игнорирајте ја оваа е-пошта.
+ ButtonText: Верифицирај е-пошта
+VerifyPhone:
+ Title: ZITADEL - Верификација на телефонски број
+ PreHeader: Верификација на телефонски број
+ Subject: Верификација на телефонски број
+ Greeting: Здраво {{.DisplayName}},
+ Text: Додаден е нов телефонски број. Ве молиме користете го следниот код за да го верифицирате. {{.Code}}
+ ButtonText: Верифицирај телефонски број
+DomainClaimed:
+ Title: ZITADEL - Доменот е преземен
+ PreHeader: Промена на е-пошта / корисничко име
+ Subject: Доменот е преземен
+ Greeting: Здраво {{.DisplayName}},
+ Text: Доменот {{.Domain}} е преземен од страна на организација. Вашиот моментален корисник {{.Username}} не е дел од оваа организација. Затоа, при најава, треба да го промените вашиот е-поштенска адреса. Направивме привремено корисничко име ({{.TempUsername}}) за оваа најава.
+ ButtonText: Најави се
+PasswordlessRegistration:
+ Title: ZITADEL - Додади Најава без Лозинка
+ PreHeader: Додади Најава без Лозинка
+ Subject: Додади Најава без Лозинка
+ Greeting: Здраво {{.DisplayName}},
+ Text: Добивме барање да додадеме токен за најава без лозинка. Ве молиме користете го копчето подолу за да додадете вашиот токен или уред за најава без лозинка.
+ ButtonText: Додади Најава без Лозинка
+PasswordChange:
+ Title: ZITADEL - Лозинката на корисникот е променета
+ PreHeader: Промена на лозинка
+ Subject: Лозинката на корисникот е променета
+ Greeting: Здраво {{.DisplayName}},
+ Text: Лозинката на вашиот корисник е променета. Ако оваа промена не е извршена од вас, ве молиме веднаш ресетирајте ја вашата лозинка.
+ ButtonText: Најава
diff --git a/internal/notification/static/i18n/pt.yaml b/internal/notification/static/i18n/pt.yaml
new file mode 100644
index 000000000..7aa46efd8
--- /dev/null
+++ b/internal/notification/static/i18n/pt.yaml
@@ -0,0 +1,49 @@
+InitCode:
+ Title: ZITADEL - Inicializar Usuário
+ PreHeader: Inicializar Usuário
+ Subject: Inicializar Usuário
+ Greeting: Olá {{.DisplayName}},
+ Text: Este usuário foi criado no ZITADEL. Use o nome de usuário {{.PreferredLoginName}} para fazer login. Por favor, clique no botão abaixo para finalizar o processo de inicialização. (Código {{.Code}}) Se você não solicitou este e-mail, por favor, ignore-o.
+ ButtonText: Finalizar inicialização
+PasswordReset:
+ Title: ZITADEL - Redefinir senha
+ PreHeader: Redefinir senha
+ Subject: Redefinir senha
+ Greeting: Olá {{.DisplayName}},
+ Text: Recebemos uma solicitação de redefinição de senha. Por favor, use o botão abaixo para redefinir sua senha. (Código {{.Code}}) Se você não solicitou este e-mail, por favor, ignore-o.
+ ButtonText: Redefinir senha
+VerifyEmail:
+ Title: ZITADEL - Verificar e-mail
+ PreHeader: Verificar e-mail
+ Subject: Verificar e-mail
+ Greeting: Olá {{.DisplayName}},
+ Text: Um novo e-mail foi adicionado. Por favor, use o botão abaixo para verificar seu e-mail. (Código {{.Code}}) Se você não adicionou um novo e-mail, por favor, ignore este e-mail.
+ ButtonText: Verificar e-mail
+VerifyPhone:
+ Title: ZITADEL - Verificar telefone
+ PreHeader: Verificar telefone
+ Subject: Verificar telefone
+ Greeting: Olá {{.DisplayName}},
+ Text: Um novo número de telefone foi adicionado. Por favor, use o código a seguir para verificá-lo {{.Code}}
+ ButtonText: Verificar telefone
+DomainClaimed:
+ Title: ZITADEL - Domínio foi reivindicado
+ PreHeader: Alterar e-mail / nome de usuário
+ Subject: Domínio foi reivindicado
+ Greeting: Olá {{.DisplayName}},
+ Text: O domínio {{.Domain}} foi reivindicado por uma organização. Seu usuário atual, {{.Username}}, não faz parte dessa organização. Portanto, você terá que alterar seu e-mail quando fizer o login. Criamos um nome de usuário temporário ({{.TempUsername}}) para este login.
+ ButtonText: Fazer login
+PasswordlessRegistration:
+ Title: ZITADEL - Adicionar Login sem Senha
+ PreHeader: Adicionar Login sem Senha
+ Subject: Adicionar Login sem Senha
+ Greeting: Olá {{.DisplayName}},
+ Text: Recebemos uma solicitação para adicionar um token ou dispositivo para login sem senha. Por favor, use o botão abaixo para adicionar seu token ou dispositivo para login sem senha.
+ ButtonText: Adicionar Login sem Senha
+PasswordChange:
+ Title: ZITADEL - Senha do usuário foi alterada
+ PreHeader: Alterar senha
+ Subject: Senha do usuário foi alterada
+ Greeting: Olá {{.DisplayName}},
+ Text: A senha do seu usuário foi alterada. Se esta alteração não foi feita por você, recomendamos que você redefina sua senha imediatamente.
+ ButtonText: Fazer login
diff --git a/internal/org/model/org.go b/internal/org/model/org.go
index a944a6897..4267506b5 100644
--- a/internal/org/model/org.go
+++ b/internal/org/model/org.go
@@ -1,8 +1,7 @@
package model
import (
- "strings"
-
+ "github.com/zitadel/zitadel/internal/domain"
es_models "github.com/zitadel/zitadel/internal/eventstore/v1/models"
iam_model "github.com/zitadel/zitadel/internal/iam/model"
)
@@ -46,10 +45,6 @@ func (o *Org) GetPrimaryDomain() *OrgDomain {
return nil
}
-func (o *Org) nameForDomain(iamDomain string) string {
- return strings.ToLower(strings.ReplaceAll(o.Name, " ", "-") + "." + iamDomain)
-}
-
func (o *Org) AddIAMDomain(iamDomain string) {
- o.Domains = append(o.Domains, &OrgDomain{Domain: o.nameForDomain(iamDomain), Verified: true, Primary: true})
+ o.Domains = append(o.Domains, &OrgDomain{Domain: domain.NewIAMDomainName(o.Name, iamDomain), Verified: true, Primary: true})
}
diff --git a/internal/protoc/protoc-base/protoc_helper.go b/internal/protoc/protoc-base/protoc_helper.go
deleted file mode 100644
index ecf9150c8..000000000
--- a/internal/protoc/protoc-base/protoc_helper.go
+++ /dev/null
@@ -1,113 +0,0 @@
-package protocbase
-
-import (
- "flag"
- "fmt"
- "io"
- "io/ioutil"
- "os"
- "strings"
- "text/template"
-
- "github.com/golang/glog"
- "github.com/golang/protobuf/proto"
- plugin "github.com/golang/protobuf/protoc-gen-go/plugin"
- "github.com/grpc-ecosystem/grpc-gateway/protoc-gen-grpc-gateway/descriptor"
-)
-
-type GeneratorFunc func(target string, registry *descriptor.Registry, file *descriptor.File) (string, string, error)
-
-type ProtocGenerator interface {
- Generate(target string, registry *descriptor.Registry, file *descriptor.File) (string, string, error)
-}
-
-func (f GeneratorFunc) Generate(target string, registry *descriptor.Registry, file *descriptor.File) (string, string, error) {
- return f(target, registry, file) //TODO: in my opinion we should use file.GoPkg here analog https://github.com/grpc-ecosystem/grpc-gateway/blob/0cc2680a4990244dcc7602bad34fef935310c0e8/protoc-gen-grpc-gateway/internal/gengateway/generator.go#L111
-}
-
-func parseReq(r io.Reader) (*plugin.CodeGeneratorRequest, error) {
- glog.V(1).Info("Parsing code generator request")
-
- input, err := ioutil.ReadAll(r)
-
- if err != nil {
- glog.Errorf("Failed to read code generator request: %v", err)
- return nil, err
- }
-
- req := &plugin.CodeGeneratorRequest{}
-
- if err = proto.Unmarshal(input, req); err != nil {
- glog.Errorf("Failed to unmarshal code generator request: %v", err)
- return nil, err
- }
-
- glog.V(1).Info("Parsed code generator request")
-
- return req, nil
-}
-
-func RunWithBaseTemplate(targetFileNameFmt string, tmpl *template.Template) {
- Run(GeneratorFunc(func(target string, registry *descriptor.Registry, file *descriptor.File) (string, string, error) {
- fileName := fmt.Sprintf(targetFileNameFmt, strings.Split(target, ".")[0])
- fContent, err := GenerateFromBaseTemplate(tmpl, registry, file)
- return fileName, fContent, err
- }))
-}
-
-func Run(generator ProtocGenerator) {
- flag.Parse()
- defer glog.Flush()
-
- req, err := parseReq(os.Stdin)
- if err != nil {
- glog.Fatal(err)
- }
-
- registry := descriptor.NewRegistry()
- registry.SetAllowDeleteBody(true)
- if err = registry.Load(req); err != nil {
- glog.Fatal(err)
- }
-
- var result []*plugin.CodeGeneratorResponse_File
-
- for _, t := range req.FileToGenerate {
- file, err := registry.LookupFile(t)
- if err != nil {
- EmitError(err)
- return
- }
-
- fName, fContent, err := generator.Generate(t, registry, file)
- if err != nil {
- EmitError(err)
- return
- }
-
- result = append(result, &plugin.CodeGeneratorResponse_File{
- Name: &fName,
- Content: &fContent,
- })
- }
-
- EmitFiles(result)
-}
-
-func EmitFiles(out []*plugin.CodeGeneratorResponse_File) {
- EmitResp(&plugin.CodeGeneratorResponse{File: out})
-}
-
-func EmitError(err error) {
- EmitResp(&plugin.CodeGeneratorResponse{Error: proto.String(err.Error())})
-}
-
-func EmitResp(resp *plugin.CodeGeneratorResponse) {
- buf, err := proto.Marshal(resp)
- if err != nil {
- glog.Fatal(err)
- }
- if _, err := os.Stdout.Write(buf); err != nil {
- glog.Fatal(err)
- }
-}
diff --git a/internal/protoc/protoc-base/templates.go b/internal/protoc/protoc-base/templates.go
deleted file mode 100644
index 57e1c0364..000000000
--- a/internal/protoc/protoc-base/templates.go
+++ /dev/null
@@ -1,106 +0,0 @@
-package protocbase
-
-import (
- "bytes"
- "fmt"
- "text/template"
- "time"
-
- "github.com/Masterminds/sprig"
- "github.com/golang/protobuf/proto"
- "github.com/grpc-ecosystem/grpc-gateway/protoc-gen-grpc-gateway/descriptor"
- "golang.org/x/tools/imports"
-)
-
-var extensions = map[string]*proto.ExtensionDesc{}
-
-type BaseTemplateData struct {
- Now time.Time
- File *descriptor.File
-
- registry *descriptor.Registry
-}
-
-var templateFuncs = map[string]interface{}{
- "option": getOption,
-}
-
-func RegisterTmplFunc(name string, f interface{}) {
- if _, found := templateFuncs[name]; found {
- panic(fmt.Sprintf("func with name %v is already registered", name))
- }
-
- templateFuncs[name] = f
-}
-
-func RegisterExtension(ext *proto.ExtensionDesc) {
- extensions[ext.Name] = ext
-}
-
-func GetBaseTemplateData(registry *descriptor.Registry, file *descriptor.File) *BaseTemplateData {
- return &BaseTemplateData{
- Now: time.Now().UTC(),
- File: file,
- registry: registry,
- }
-}
-
-func getOption(opts proto.Message, extName string) interface{} {
- extDesc := extensions[extName]
-
- if !proto.HasExtension(opts, extDesc) {
- return nil
- }
-
- ext, err := proto.GetExtension(opts, extDesc)
- if err != nil {
- panic(err)
- }
-
- return ext
-}
-
-func (data *BaseTemplateData) ResolveMsgType(msgType string) string {
- msg, err := data.registry.LookupMsg(data.File.GetPackage(), msgType)
- if err != nil {
- panic(err)
- }
-
- return msg.GoType(data.File.GoPkg.Path)
-}
-
-func (data *BaseTemplateData) ResolveFile(fileName string) *descriptor.File {
- file, err := data.registry.LookupFile(fileName)
- if err != nil {
- panic(err)
- }
-
- return file
-}
-
-func LoadTemplate(templateData []byte, err error) *template.Template {
- if err != nil {
- panic(err)
- }
-
- return template.Must(template.New("").
- Funcs(sprig.TxtFuncMap()).
- Funcs(templateFuncs).
- Parse(string(templateData)))
-}
-
-func GenerateFromTemplate(tmpl *template.Template, data interface{}) (string, error) {
- var tpl bytes.Buffer
- err := tmpl.Execute(&tpl, data)
- if err != nil {
- return "", err
- }
-
- tmplResult := tpl.Bytes()
- tmplResult, err = imports.Process(".", tmplResult, nil)
- return string(tmplResult), err
-}
-
-func GenerateFromBaseTemplate(tmpl *template.Template, registry *descriptor.Registry, file *descriptor.File) (string, error) {
- return GenerateFromTemplate(tmpl, GetBaseTemplateData(registry, file))
-}
diff --git a/internal/protoc/protoc-gen-auth/auth_method_mapping.go.tmpl b/internal/protoc/protoc-gen-authoption/auth_method_mapping.go.tmpl
similarity index 100%
rename from internal/protoc/protoc-gen-auth/auth_method_mapping.go.tmpl
rename to internal/protoc/protoc-gen-authoption/auth_method_mapping.go.tmpl
diff --git a/internal/protoc/protoc-gen-authoption/authoption/generate.go b/internal/protoc/protoc-gen-authoption/authoption/generate.go
deleted file mode 100644
index 24ab586e3..000000000
--- a/internal/protoc/protoc-gen-authoption/authoption/generate.go
+++ /dev/null
@@ -1,3 +0,0 @@
-package authoption
-
-//go:generate protoc -I. -I$GOPATH/src --go-grpc_out=plugins=grpc:$GOPATH/src options.proto
diff --git a/internal/protoc/protoc-gen-authoption/authoption/options.pb.go b/internal/protoc/protoc-gen-authoption/authoption/options.pb.go
new file mode 100644
index 000000000..18d84aa42
--- /dev/null
+++ b/internal/protoc/protoc-gen-authoption/authoption/options.pb.go
@@ -0,0 +1,187 @@
+// Code generated by protoc-gen-go. DO NOT EDIT.
+// versions:
+// protoc-gen-go v1.28.1
+// protoc (unknown)
+// source: zitadel/options.proto
+
+package authoption
+
+import (
+ protoreflect "google.golang.org/protobuf/reflect/protoreflect"
+ protoimpl "google.golang.org/protobuf/runtime/protoimpl"
+ descriptorpb "google.golang.org/protobuf/types/descriptorpb"
+ reflect "reflect"
+ sync "sync"
+)
+
+const (
+ // Verify that this generated code is sufficiently up-to-date.
+ _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
+ // Verify that runtime/protoimpl is sufficiently up-to-date.
+ _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
+)
+
+type AuthOption struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ Permission string `protobuf:"bytes,1,opt,name=permission,proto3" json:"permission,omitempty"`
+ CheckFieldName string `protobuf:"bytes,2,opt,name=check_field_name,json=checkFieldName,proto3" json:"check_field_name,omitempty"`
+}
+
+func (x *AuthOption) Reset() {
+ *x = AuthOption{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_zitadel_options_proto_msgTypes[0]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *AuthOption) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*AuthOption) ProtoMessage() {}
+
+func (x *AuthOption) ProtoReflect() protoreflect.Message {
+ mi := &file_zitadel_options_proto_msgTypes[0]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use AuthOption.ProtoReflect.Descriptor instead.
+func (*AuthOption) Descriptor() ([]byte, []int) {
+ return file_zitadel_options_proto_rawDescGZIP(), []int{0}
+}
+
+func (x *AuthOption) GetPermission() string {
+ if x != nil {
+ return x.Permission
+ }
+ return ""
+}
+
+func (x *AuthOption) GetCheckFieldName() string {
+ if x != nil {
+ return x.CheckFieldName
+ }
+ return ""
+}
+
+var file_zitadel_options_proto_extTypes = []protoimpl.ExtensionInfo{
+ {
+ ExtendedType: (*descriptorpb.MethodOptions)(nil),
+ ExtensionType: (*AuthOption)(nil),
+ Field: 50000,
+ Name: "zitadel.v1.auth_option",
+ Tag: "bytes,50000,opt,name=auth_option",
+ Filename: "zitadel/options.proto",
+ },
+}
+
+// Extension fields to descriptorpb.MethodOptions.
+var (
+ // optional zitadel.v1.AuthOption auth_option = 50000;
+ E_AuthOption = &file_zitadel_options_proto_extTypes[0]
+)
+
+var File_zitadel_options_proto protoreflect.FileDescriptor
+
+var file_zitadel_options_proto_rawDesc = []byte{
+ 0x0a, 0x15, 0x7a, 0x69, 0x74, 0x61, 0x64, 0x65, 0x6c, 0x2f, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e,
+ 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x0a, 0x7a, 0x69, 0x74, 0x61, 0x64, 0x65, 0x6c,
+ 0x2e, 0x76, 0x31, 0x1a, 0x20, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74,
+ 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x6f, 0x72, 0x2e,
+ 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x56, 0x0a, 0x0a, 0x41, 0x75, 0x74, 0x68, 0x4f, 0x70, 0x74,
+ 0x69, 0x6f, 0x6e, 0x12, 0x1e, 0x0a, 0x0a, 0x70, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f,
+ 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x70, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73,
+ 0x69, 0x6f, 0x6e, 0x12, 0x28, 0x0a, 0x10, 0x63, 0x68, 0x65, 0x63, 0x6b, 0x5f, 0x66, 0x69, 0x65,
+ 0x6c, 0x64, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x63,
+ 0x68, 0x65, 0x63, 0x6b, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x4e, 0x61, 0x6d, 0x65, 0x3a, 0x59, 0x0a,
+ 0x0b, 0x61, 0x75, 0x74, 0x68, 0x5f, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x1e, 0x2e, 0x67,
+ 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x4d,
+ 0x65, 0x74, 0x68, 0x6f, 0x64, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0xd0, 0x86, 0x03,
+ 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x7a, 0x69, 0x74, 0x61, 0x64, 0x65, 0x6c, 0x2e, 0x76,
+ 0x31, 0x2e, 0x41, 0x75, 0x74, 0x68, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x0a, 0x61, 0x75,
+ 0x74, 0x68, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x42, 0x47, 0x5a, 0x45, 0x67, 0x69, 0x74, 0x68,
+ 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x7a, 0x69, 0x74, 0x61, 0x64, 0x65, 0x6c, 0x2f, 0x7a,
+ 0x69, 0x74, 0x61, 0x64, 0x65, 0x6c, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f,
+ 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x2d, 0x67, 0x65,
+ 0x6e, 0x2d, 0x61, 0x75, 0x74, 0x68, 0x2f, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x70, 0x74, 0x69, 0x6f,
+ 0x6e, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
+}
+
+var (
+ file_zitadel_options_proto_rawDescOnce sync.Once
+ file_zitadel_options_proto_rawDescData = file_zitadel_options_proto_rawDesc
+)
+
+func file_zitadel_options_proto_rawDescGZIP() []byte {
+ file_zitadel_options_proto_rawDescOnce.Do(func() {
+ file_zitadel_options_proto_rawDescData = protoimpl.X.CompressGZIP(file_zitadel_options_proto_rawDescData)
+ })
+ return file_zitadel_options_proto_rawDescData
+}
+
+var file_zitadel_options_proto_msgTypes = make([]protoimpl.MessageInfo, 1)
+var file_zitadel_options_proto_goTypes = []interface{}{
+ (*AuthOption)(nil), // 0: zitadel.v1.AuthOption
+ (*descriptorpb.MethodOptions)(nil), // 1: google.protobuf.MethodOptions
+}
+var file_zitadel_options_proto_depIdxs = []int32{
+ 1, // 0: zitadel.v1.auth_option:extendee -> google.protobuf.MethodOptions
+ 0, // 1: zitadel.v1.auth_option:type_name -> zitadel.v1.AuthOption
+ 2, // [2:2] is the sub-list for method output_type
+ 2, // [2:2] is the sub-list for method input_type
+ 1, // [1:2] is the sub-list for extension type_name
+ 0, // [0:1] is the sub-list for extension extendee
+ 0, // [0:0] is the sub-list for field type_name
+}
+
+func init() { file_zitadel_options_proto_init() }
+func file_zitadel_options_proto_init() {
+ if File_zitadel_options_proto != nil {
+ return
+ }
+ if !protoimpl.UnsafeEnabled {
+ file_zitadel_options_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*AuthOption); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ }
+ type x struct{}
+ out := protoimpl.TypeBuilder{
+ File: protoimpl.DescBuilder{
+ GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
+ RawDescriptor: file_zitadel_options_proto_rawDesc,
+ NumEnums: 0,
+ NumMessages: 1,
+ NumExtensions: 1,
+ NumServices: 0,
+ },
+ GoTypes: file_zitadel_options_proto_goTypes,
+ DependencyIndexes: file_zitadel_options_proto_depIdxs,
+ MessageInfos: file_zitadel_options_proto_msgTypes,
+ ExtensionInfos: file_zitadel_options_proto_extTypes,
+ }.Build()
+ File_zitadel_options_proto = out.File
+ file_zitadel_options_proto_rawDesc = nil
+ file_zitadel_options_proto_goTypes = nil
+ file_zitadel_options_proto_depIdxs = nil
+}
diff --git a/internal/protoc/protoc-gen-authoption/authoption/options.proto b/internal/protoc/protoc-gen-authoption/authoption/options.proto
deleted file mode 100644
index c62a6378e..000000000
--- a/internal/protoc/protoc-gen-authoption/authoption/options.proto
+++ /dev/null
@@ -1,18 +0,0 @@
-syntax = "proto3";
-
-package caos.zitadel.utils.v1;
-
-import "google/protobuf/descriptor.proto";
-
-option go_package = "github.com/zitadel/zitadel/internal/protoc/protoc-gen-authoption/authoption";
-
-
-extend google.protobuf.MethodOptions {
- AuthOption auth_option = 50000;
-}
-
-message AuthOption {
- string permission = 1;
- string check_field_name = 2;
- string feature = 3;
-}
\ No newline at end of file
diff --git a/internal/protoc/protoc-gen-auth/main.go b/internal/protoc/protoc-gen-authoption/main.go
similarity index 100%
rename from internal/protoc/protoc-gen-auth/main.go
rename to internal/protoc/protoc-gen-authoption/main.go
diff --git a/internal/query/access_token.go b/internal/query/access_token.go
new file mode 100644
index 000000000..25121af05
--- /dev/null
+++ b/internal/query/access_token.go
@@ -0,0 +1,152 @@
+package query
+
+import (
+ "context"
+ "strings"
+ "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/oidcsession"
+ "github.com/zitadel/zitadel/internal/repository/session"
+ "github.com/zitadel/zitadel/internal/telemetry/tracing"
+)
+
+type OIDCSessionAccessTokenReadModel struct {
+ eventstore.WriteModel
+
+ UserID string
+ SessionID string
+ ClientID string
+ Audience []string
+ Scope []string
+ AuthMethods []domain.UserAuthMethodType
+ AuthTime time.Time
+ State domain.OIDCSessionState
+ AccessTokenID string
+ AccessTokenCreation time.Time
+ AccessTokenExpiration time.Time
+}
+
+func newOIDCSessionAccessTokenReadModel(id string) *OIDCSessionAccessTokenReadModel {
+ return &OIDCSessionAccessTokenReadModel{
+ WriteModel: eventstore.WriteModel{
+ AggregateID: id,
+ },
+ }
+}
+
+func (wm *OIDCSessionAccessTokenReadModel) Reduce() error {
+ for _, event := range wm.Events {
+ switch e := event.(type) {
+ case *oidcsession.AddedEvent:
+ wm.reduceAdded(e)
+ case *oidcsession.AccessTokenAddedEvent:
+ wm.reduceAccessTokenAdded(e)
+ case *oidcsession.AccessTokenRevokedEvent,
+ *oidcsession.RefreshTokenRevokedEvent:
+ wm.reduceTokenRevoked(event)
+ }
+ }
+ return wm.WriteModel.Reduce()
+}
+
+func (wm *OIDCSessionAccessTokenReadModel) Query() *eventstore.SearchQueryBuilder {
+ return eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent).
+ AllowTimeTravel().
+ AddQuery().
+ AggregateTypes(oidcsession.AggregateType).
+ AggregateIDs(wm.AggregateID).
+ EventTypes(
+ oidcsession.AddedType,
+ oidcsession.AccessTokenAddedType,
+ oidcsession.AccessTokenRevokedType,
+ oidcsession.RefreshTokenRevokedType,
+ ).
+ Builder()
+}
+
+func (wm *OIDCSessionAccessTokenReadModel) reduceAdded(e *oidcsession.AddedEvent) {
+ wm.UserID = e.UserID
+ wm.SessionID = e.SessionID
+ wm.ClientID = e.ClientID
+ wm.Audience = e.Audience
+ wm.Scope = e.Scope
+ wm.AuthMethods = e.AuthMethods
+ wm.AuthTime = e.AuthTime
+ wm.State = domain.OIDCSessionStateActive
+}
+
+func (wm *OIDCSessionAccessTokenReadModel) reduceAccessTokenAdded(e *oidcsession.AccessTokenAddedEvent) {
+ wm.AccessTokenID = e.ID
+ wm.AccessTokenCreation = e.CreationDate()
+ wm.AccessTokenExpiration = e.CreationDate().Add(e.Lifetime)
+}
+
+func (wm *OIDCSessionAccessTokenReadModel) reduceTokenRevoked(e eventstore.Event) {
+ wm.AccessTokenID = ""
+ wm.AccessTokenExpiration = e.CreationDate()
+}
+
+// ActiveAccessTokenByToken will check if the token is active by retrieving the OIDCSession events from the eventstore.
+// Refreshed or expired tokens will return an error as well as if the underlying sessions has been terminated.
+func (q *Queries) ActiveAccessTokenByToken(ctx context.Context, token string) (model *OIDCSessionAccessTokenReadModel, err error) {
+ ctx, span := tracing.NewSpan(ctx)
+ defer func() { span.EndWithError(err) }()
+
+ split := strings.Split(token, "-")
+ if len(split) != 2 {
+ return nil, caos_errs.ThrowPermissionDenied(nil, "QUERY-LJK2W", "Errors.OIDCSession.Token.Invalid")
+ }
+ model, err = q.accessTokenByOIDCSessionAndTokenID(ctx, split[0], split[1])
+ if err != nil {
+ return nil, err
+ }
+ if !model.AccessTokenExpiration.After(time.Now()) {
+ return nil, caos_errs.ThrowPermissionDenied(nil, "QUERY-SAF3rf", "Errors.OIDCSession.Token.Expired")
+ }
+ if err = q.checkSessionNotTerminatedAfter(ctx, model.SessionID, model.AccessTokenCreation); err != nil {
+ return nil, err
+ }
+ return model, nil
+}
+
+func (q *Queries) accessTokenByOIDCSessionAndTokenID(ctx context.Context, oidcSessionID, tokenID string) (model *OIDCSessionAccessTokenReadModel, err error) {
+ ctx, span := tracing.NewSpan(ctx)
+ defer func() { span.EndWithError(err) }()
+
+ model = newOIDCSessionAccessTokenReadModel(oidcSessionID)
+ if err = q.eventstore.FilterToQueryReducer(ctx, model); err != nil {
+ return nil, caos_errs.ThrowPermissionDenied(err, "QUERY-ASfe2", "Errors.OIDCSession.Token.Invalid")
+ }
+ if model.AccessTokenID != tokenID {
+ return nil, caos_errs.ThrowPermissionDenied(nil, "QUERY-M2u9w", "Errors.OIDCSession.Token.Invalid")
+ }
+ return model, nil
+}
+
+// checkSessionNotTerminatedAfter checks if a [session.TerminateType] event occurred after a certain time
+// and will return an error if so.
+func (q *Queries) checkSessionNotTerminatedAfter(ctx context.Context, sessionID string, creation time.Time) (err error) {
+ ctx, span := tracing.NewSpan(ctx)
+ defer func() { span.EndWithError(err) }()
+
+ events, err := q.eventstore.Filter(ctx, eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent).
+ AllowTimeTravel().
+ AddQuery().
+ AggregateTypes(session.AggregateType).
+ AggregateIDs(sessionID).
+ EventTypes(
+ session.TerminateType,
+ ).
+ CreationDateAfter(creation).
+ Builder())
+ if err != nil {
+ return caos_errs.ThrowPermissionDenied(err, "QUERY-SJ642", "Errors.Internal")
+ }
+ if len(events) > 0 {
+ return caos_errs.ThrowPermissionDenied(nil, "QUERY-IJL3H", "Errors.OIDCSession.Token.Invalid")
+ }
+ return nil
+}
diff --git a/internal/query/app.go b/internal/query/app.go
index 6b40f712e..577f6c181 100644
--- a/internal/query/app.go
+++ b/internal/query/app.go
@@ -253,7 +253,7 @@ func (q *Queries) AppByProjectAndAppID(ctx context.Context, shouldTriggerBulk bo
defer func() { span.EndWithError(err) }()
if shouldTriggerBulk {
- projection.AppProjection.Trigger(ctx)
+ ctx = projection.AppProjection.Trigger(ctx)
}
stmt, scan := prepareAppQuery(ctx, q.client)
diff --git a/internal/query/auth_request.go b/internal/query/auth_request.go
new file mode 100644
index 000000000..ee91bdebe
--- /dev/null
+++ b/internal/query/auth_request.go
@@ -0,0 +1,88 @@
+package query
+
+import (
+ "context"
+ "database/sql"
+ _ "embed"
+ errs "errors"
+ "fmt"
+ "time"
+
+ "github.com/zitadel/zitadel/internal/api/authz"
+ "github.com/zitadel/zitadel/internal/api/call"
+ "github.com/zitadel/zitadel/internal/database"
+ "github.com/zitadel/zitadel/internal/domain"
+ "github.com/zitadel/zitadel/internal/errors"
+ "github.com/zitadel/zitadel/internal/query/projection"
+ "github.com/zitadel/zitadel/internal/telemetry/tracing"
+)
+
+type AuthRequest struct {
+ ID string
+ CreationDate time.Time
+ LoginClient string
+ ClientID string
+ Scope []string
+ RedirectURI string
+ Prompt []domain.Prompt
+ UiLocales []string
+ LoginHint *string
+ MaxAge *time.Duration
+ HintUserID *string
+}
+
+func (a *AuthRequest) checkLoginClient(ctx context.Context) error {
+ if uid := authz.GetCtxData(ctx).UserID; uid != a.LoginClient {
+ return errors.ThrowPermissionDenied(nil, "OIDCv2-aL0ag", "Errors.AuthRequest.WrongLoginClient")
+ }
+ return nil
+}
+
+//go:embed embed/auth_request_by_id.sql
+var authRequestByIDQuery string
+
+func (q *Queries) authRequestByIDQuery(ctx context.Context) string {
+ return fmt.Sprintf(authRequestByIDQuery, q.client.Timetravel(call.Took(ctx)))
+}
+
+func (q *Queries) AuthRequestByID(ctx context.Context, shouldTriggerBulk bool, id string, checkLoginClient bool) (_ *AuthRequest, err error) {
+ ctx, span := tracing.NewSpan(ctx)
+ defer func() { span.EndWithError(err) }()
+
+ if shouldTriggerBulk {
+ ctx = projection.AuthRequestProjection.Trigger(ctx)
+ }
+
+ var (
+ scope database.StringArray
+ prompt database.EnumArray[domain.Prompt]
+ locales database.StringArray
+ )
+
+ dst := new(AuthRequest)
+ err = q.client.DB.QueryRowContext(
+ ctx, q.authRequestByIDQuery(ctx),
+ id, authz.GetInstance(ctx).InstanceID(),
+ ).Scan(
+ &dst.ID, &dst.CreationDate, &dst.LoginClient, &dst.ClientID, &scope, &dst.RedirectURI,
+ &prompt, &locales, &dst.LoginHint, &dst.MaxAge, &dst.HintUserID,
+ )
+ if errs.Is(err, sql.ErrNoRows) {
+ return nil, errors.ThrowNotFound(err, "QUERY-Thee9", "Errors.AuthRequest.NotExisting")
+ }
+ if err != nil {
+ return nil, errors.ThrowInternal(err, "QUERY-Ou8ue", "Errors.Internal")
+ }
+
+ dst.Scope = scope
+ dst.Prompt = prompt
+ dst.UiLocales = locales
+
+ if checkLoginClient {
+ if err = dst.checkLoginClient(ctx); err != nil {
+ return nil, err
+ }
+ }
+
+ return dst, nil
+}
diff --git a/internal/query/auth_request_test.go b/internal/query/auth_request_test.go
new file mode 100644
index 000000000..7348855e7
--- /dev/null
+++ b/internal/query/auth_request_test.go
@@ -0,0 +1,180 @@
+package query
+
+import (
+ "database/sql"
+ "database/sql/driver"
+ _ "embed"
+ "fmt"
+ "regexp"
+ "testing"
+ "time"
+
+ "github.com/muhlemmer/gu"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+
+ "github.com/zitadel/zitadel/internal/api/authz"
+ "github.com/zitadel/zitadel/internal/database"
+ "github.com/zitadel/zitadel/internal/domain"
+ "github.com/zitadel/zitadel/internal/errors"
+ "github.com/zitadel/zitadel/internal/query/projection"
+)
+
+func TestQueries_AuthRequestByID(t *testing.T) {
+ expQuery := regexp.QuoteMeta(fmt.Sprintf(
+ authRequestByIDQuery,
+ asOfSystemTime,
+ ))
+
+ cols := []string{
+ projection.AuthRequestColumnID,
+ projection.AuthRequestColumnCreationDate,
+ projection.AuthRequestColumnLoginClient,
+ projection.AuthRequestColumnClientID,
+ projection.AuthRequestColumnScope,
+ projection.AuthRequestColumnRedirectURI,
+ projection.AuthRequestColumnPrompt,
+ projection.AuthRequestColumnUILocales,
+ projection.AuthRequestColumnLoginHint,
+ projection.AuthRequestColumnMaxAge,
+ projection.AuthRequestColumnHintUserID,
+ }
+ type args struct {
+ shouldTriggerBulk bool
+ id string
+ checkLoginClient bool
+ }
+ tests := []struct {
+ name string
+ args args
+ expect sqlExpectation
+ want *AuthRequest
+ wantErr error
+ }{
+ {
+ name: "success, all values",
+ args: args{
+ shouldTriggerBulk: false,
+ id: "123",
+ checkLoginClient: true,
+ },
+ expect: mockQuery(expQuery, cols, []driver.Value{
+ "id",
+ testNow,
+ "loginClient",
+ "clientID",
+ database.StringArray{"a", "b", "c"},
+ "example.com",
+ database.EnumArray[domain.Prompt]{domain.PromptLogin, domain.PromptConsent},
+ database.StringArray{"en", "fi"},
+ "me@example.com",
+ int64(time.Minute),
+ "userID",
+ }, "123", "instanceID"),
+ want: &AuthRequest{
+ ID: "id",
+ CreationDate: testNow,
+ LoginClient: "loginClient",
+ ClientID: "clientID",
+ Scope: []string{"a", "b", "c"},
+ RedirectURI: "example.com",
+ Prompt: []domain.Prompt{domain.PromptLogin, domain.PromptConsent},
+ UiLocales: []string{"en", "fi"},
+ LoginHint: gu.Ptr("me@example.com"),
+ MaxAge: gu.Ptr(time.Minute),
+ HintUserID: gu.Ptr("userID"),
+ },
+ },
+ {
+ name: "success, null values",
+ args: args{
+ shouldTriggerBulk: false,
+ id: "123",
+ checkLoginClient: true,
+ },
+ expect: mockQuery(expQuery, cols, []driver.Value{
+ "id",
+ testNow,
+ "loginClient",
+ "clientID",
+ database.StringArray{"a", "b", "c"},
+ "example.com",
+ database.EnumArray[domain.Prompt]{domain.PromptLogin, domain.PromptConsent},
+ database.StringArray{"en", "fi"},
+ sql.NullString{},
+ sql.NullInt64{},
+ sql.NullString{},
+ }, "123", "instanceID"),
+ want: &AuthRequest{
+ ID: "id",
+ CreationDate: testNow,
+ LoginClient: "loginClient",
+ ClientID: "clientID",
+ Scope: []string{"a", "b", "c"},
+ RedirectURI: "example.com",
+ Prompt: []domain.Prompt{domain.PromptLogin, domain.PromptConsent},
+ UiLocales: []string{"en", "fi"},
+ LoginHint: nil,
+ MaxAge: nil,
+ HintUserID: nil,
+ },
+ },
+ {
+ name: "no rows",
+ args: args{
+ shouldTriggerBulk: false,
+ id: "123",
+ },
+ expect: mockQuery(expQuery, cols, nil, "123", "instanceID"),
+ wantErr: errors.ThrowNotFound(sql.ErrNoRows, "QUERY-Thee9", "Errors.AuthRequest.NotExisting"),
+ },
+ {
+ name: "query error",
+ args: args{
+ shouldTriggerBulk: false,
+ id: "123",
+ },
+ expect: mockQueryErr(expQuery, sql.ErrConnDone, "123", "instanceID"),
+ wantErr: errors.ThrowInternal(sql.ErrConnDone, "QUERY-Ou8ue", "Errors.Internal"),
+ },
+ {
+ name: "wrong login client",
+ args: args{
+ shouldTriggerBulk: false,
+ id: "123",
+ checkLoginClient: true,
+ },
+ expect: mockQuery(expQuery, cols, []driver.Value{
+ "id",
+ testNow,
+ "wrongLoginClient",
+ "clientID",
+ database.StringArray{"a", "b", "c"},
+ "example.com",
+ database.EnumArray[domain.Prompt]{domain.PromptLogin, domain.PromptConsent},
+ database.StringArray{"en", "fi"},
+ sql.NullString{},
+ sql.NullInt64{},
+ sql.NullString{},
+ }, "123", "instanceID"),
+ wantErr: errors.ThrowPermissionDeniedf(nil, "OIDCv2-aL0ag", "Errors.AuthRequest.WrongLoginClient"),
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ execMock(t, tt.expect, func(db *sql.DB) {
+ q := &Queries{
+ client: &database.DB{
+ DB: db,
+ Database: &prepareDB{},
+ },
+ }
+ ctx := authz.NewMockContext("instanceID", "orgID", "loginClient")
+
+ got, err := q.AuthRequestByID(ctx, tt.args.shouldTriggerBulk, tt.args.id, tt.args.checkLoginClient)
+ require.ErrorIs(t, err, tt.wantErr)
+ assert.Equal(t, tt.want, got)
+ })
+ })
+ }
+}
diff --git a/internal/query/authn_key.go b/internal/query/authn_key.go
index 3d9341c94..87140b216 100644
--- a/internal/query/authn_key.go
+++ b/internal/query/authn_key.go
@@ -191,7 +191,7 @@ func (q *Queries) GetAuthNKeyByID(ctx context.Context, shouldTriggerBulk bool, i
defer func() { span.EndWithError(err) }()
if shouldTriggerBulk {
- projection.AuthNKeyProjection.Trigger(ctx)
+ ctx = projection.AuthNKeyProjection.Trigger(ctx)
}
query, scan := prepareAuthNKeyQuery(ctx, q.client)
diff --git a/internal/query/domain_policy.go b/internal/query/domain_policy.go
index f9f5f94b7..2f7a596cd 100644
--- a/internal/query/domain_policy.go
+++ b/internal/query/domain_policy.go
@@ -91,7 +91,7 @@ func (q *Queries) DomainPolicyByOrg(ctx context.Context, shouldTriggerBulk bool,
defer func() { span.EndWithError(err) }()
if shouldTriggerBulk {
- projection.DomainPolicyProjection.Trigger(ctx)
+ ctx = projection.DomainPolicyProjection.Trigger(ctx)
}
eq := sq.And{
sq.Eq{DomainPolicyColInstanceID.identifier(): authz.GetInstance(ctx).InstanceID()},
diff --git a/internal/query/embed/auth_request_by_id.sql b/internal/query/embed/auth_request_by_id.sql
new file mode 100644
index 000000000..ffc18fccd
--- /dev/null
+++ b/internal/query/embed/auth_request_by_id.sql
@@ -0,0 +1,15 @@
+select
+ id,
+ creation_date,
+ login_client,
+ client_id,
+ scope,
+ redirect_uri,
+ prompt,
+ ui_locales,
+ login_hint,
+ max_age,
+ hint_user_id
+from projections.auth_requests %s
+where id = $1 and instance_id = $2
+limit 1;
diff --git a/internal/query/idp.go b/internal/query/idp.go
index e483755c8..35c0b7b5d 100644
--- a/internal/query/idp.go
+++ b/internal/query/idp.go
@@ -193,7 +193,7 @@ func (q *Queries) IDPByIDAndResourceOwner(ctx context.Context, shouldTriggerBulk
defer func() { span.EndWithError(err) }()
if shouldTriggerBulk {
- projection.IDPProjection.Trigger(ctx)
+ ctx = projection.IDPProjection.Trigger(ctx)
}
eq := sq.Eq{
diff --git a/internal/query/idp_login_policy_link_test.go b/internal/query/idp_login_policy_link_test.go
index 530780160..22f07e7fa 100644
--- a/internal/query/idp_login_policy_link_test.go
+++ b/internal/query/idp_login_policy_link_test.go
@@ -22,7 +22,7 @@ var (
` COUNT(*) OVER ()` +
` FROM projections.idp_login_policy_links5` +
` LEFT JOIN projections.idp_templates5 ON projections.idp_login_policy_links5.idp_id = projections.idp_templates5.id AND projections.idp_login_policy_links5.instance_id = projections.idp_templates5.instance_id` +
- ` RIGHT JOIN (SELECT login_policy_owner.aggregate_id, login_policy_owner.instance_id, login_policy_owner.owner_removed FROM projections.login_policies4 AS login_policy_owner` +
+ ` RIGHT JOIN (SELECT login_policy_owner.aggregate_id, login_policy_owner.instance_id, login_policy_owner.owner_removed FROM projections.login_policies5 AS login_policy_owner` +
` WHERE (login_policy_owner.instance_id = $1 AND (login_policy_owner.aggregate_id = $2 OR login_policy_owner.aggregate_id = $3)) ORDER BY login_policy_owner.is_default LIMIT 1) AS login_policy_owner` +
` ON login_policy_owner.aggregate_id = projections.idp_login_policy_links5.resource_owner AND login_policy_owner.instance_id = projections.idp_login_policy_links5.instance_id` +
` AS OF SYSTEM TIME '-1 ms'`)
diff --git a/internal/query/idp_template.go b/internal/query/idp_template.go
index 2639691a7..b83efe156 100644
--- a/internal/query/idp_template.go
+++ b/internal/query/idp_template.go
@@ -7,7 +7,6 @@ import (
"time"
sq "github.com/Masterminds/squirrel"
- "github.com/zitadel/logging"
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/api/call"
@@ -612,8 +611,7 @@ func (q *Queries) IDPTemplateByID(ctx context.Context, shouldTriggerBulk bool, i
defer func() { span.EndWithError(err) }()
if shouldTriggerBulk {
- err := projection.IDPTemplateProjection.Trigger(ctx)
- logging.OnError(err).WithField("projection", idpTemplateTable.identifier()).Warn("could not trigger projection for query")
+ ctx = projection.IDPTemplateProjection.Trigger(ctx)
}
eq := sq.Eq{
diff --git a/internal/query/instance.go b/internal/query/instance.go
index 2b4cfb185..9040478af 100644
--- a/internal/query/instance.go
+++ b/internal/query/instance.go
@@ -182,7 +182,7 @@ func (q *Queries) Instance(ctx context.Context, shouldTriggerBulk bool) (_ *Inst
defer func() { span.EndWithError(err) }()
if shouldTriggerBulk {
- projection.InstanceProjection.Trigger(ctx)
+ ctx = projection.InstanceProjection.Trigger(ctx)
}
stmt, scan := prepareInstanceDomainQuery(ctx, q.client, authz.GetInstance(ctx).RequestedDomain())
diff --git a/internal/query/lockout_policy.go b/internal/query/lockout_policy.go
index 84b0e27ee..ff9dfc663 100644
--- a/internal/query/lockout_policy.go
+++ b/internal/query/lockout_policy.go
@@ -86,7 +86,7 @@ func (q *Queries) LockoutPolicyByOrg(ctx context.Context, shouldTriggerBulk bool
defer func() { span.EndWithError(err) }()
if shouldTriggerBulk {
- projection.LockoutPolicyProjection.Trigger(ctx)
+ ctx = projection.LockoutPolicyProjection.Trigger(ctx)
}
eq := sq.Eq{
LockoutColInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(),
diff --git a/internal/query/login_policy.go b/internal/query/login_policy.go
index d575a26d3..dcf168d94 100644
--- a/internal/query/login_policy.go
+++ b/internal/query/login_policy.go
@@ -26,6 +26,7 @@ type LoginPolicy struct {
AllowUsernamePassword bool
AllowExternalIDPs bool
ForceMFA bool
+ ForceMFALocalOnly bool
SecondFactors database.EnumArray[domain.SecondFactorType]
MultiFactors database.EnumArray[domain.MultiFactorType]
PasswordlessType domain.PasswordlessType
@@ -95,6 +96,10 @@ var (
name: projection.LoginPolicyForceMFACol,
table: loginPolicyTable,
}
+ LoginPolicyColumnForceMFALocalOnly = Column{
+ name: projection.LoginPolicyForceMFALocalOnlyCol,
+ table: loginPolicyTable,
+ }
LoginPolicyColumnSecondFactors = Column{
name: projection.LoginPolicy2FAsCol,
table: loginPolicyTable,
@@ -166,7 +171,7 @@ func (q *Queries) LoginPolicyByID(ctx context.Context, shouldTriggerBulk bool, o
defer func() { span.EndWithError(err) }()
if shouldTriggerBulk {
- projection.LoginPolicyProjection.Trigger(ctx)
+ ctx = projection.LoginPolicyProjection.Trigger(ctx)
}
eq := sq.Eq{LoginPolicyColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID()}
if !withOwnerRemoved {
@@ -351,6 +356,7 @@ func prepareLoginPolicyQuery(ctx context.Context, db prepareDatabase) (sq.Select
LoginPolicyColumnAllowUsernamePassword.identifier(),
LoginPolicyColumnAllowExternalIDPs.identifier(),
LoginPolicyColumnForceMFA.identifier(),
+ LoginPolicyColumnForceMFALocalOnly.identifier(),
LoginPolicyColumnSecondFactors.identifier(),
LoginPolicyColumnMultiFactors.identifier(),
LoginPolicyColumnPasswordlessType.identifier(),
@@ -381,6 +387,7 @@ func prepareLoginPolicyQuery(ctx context.Context, db prepareDatabase) (sq.Select
&p.AllowUsernamePassword,
&p.AllowExternalIDPs,
&p.ForceMFA,
+ &p.ForceMFALocalOnly,
&p.SecondFactors,
&p.MultiFactors,
&p.PasswordlessType,
diff --git a/internal/query/login_policy_test.go b/internal/query/login_policy_test.go
index 144545633..2e1900f21 100644
--- a/internal/query/login_policy_test.go
+++ b/internal/query/login_policy_test.go
@@ -15,30 +15,31 @@ import (
)
var (
- loginPolicyQuery = `SELECT projections.login_policies4.aggregate_id,` +
- ` projections.login_policies4.creation_date,` +
- ` projections.login_policies4.change_date,` +
- ` projections.login_policies4.sequence,` +
- ` projections.login_policies4.allow_register,` +
- ` projections.login_policies4.allow_username_password,` +
- ` projections.login_policies4.allow_external_idps,` +
- ` projections.login_policies4.force_mfa,` +
- ` projections.login_policies4.second_factors,` +
- ` projections.login_policies4.multi_factors,` +
- ` projections.login_policies4.passwordless_type,` +
- ` projections.login_policies4.is_default,` +
- ` projections.login_policies4.hide_password_reset,` +
- ` projections.login_policies4.ignore_unknown_usernames,` +
- ` projections.login_policies4.allow_domain_discovery,` +
- ` projections.login_policies4.disable_login_with_email,` +
- ` projections.login_policies4.disable_login_with_phone,` +
- ` projections.login_policies4.default_redirect_uri,` +
- ` projections.login_policies4.password_check_lifetime,` +
- ` projections.login_policies4.external_login_check_lifetime,` +
- ` projections.login_policies4.mfa_init_skip_lifetime,` +
- ` projections.login_policies4.second_factor_check_lifetime,` +
- ` projections.login_policies4.multi_factor_check_lifetime` +
- ` FROM projections.login_policies4` +
+ loginPolicyQuery = `SELECT projections.login_policies5.aggregate_id,` +
+ ` projections.login_policies5.creation_date,` +
+ ` projections.login_policies5.change_date,` +
+ ` projections.login_policies5.sequence,` +
+ ` projections.login_policies5.allow_register,` +
+ ` projections.login_policies5.allow_username_password,` +
+ ` projections.login_policies5.allow_external_idps,` +
+ ` projections.login_policies5.force_mfa,` +
+ ` projections.login_policies5.force_mfa_local_only,` +
+ ` projections.login_policies5.second_factors,` +
+ ` projections.login_policies5.multi_factors,` +
+ ` projections.login_policies5.passwordless_type,` +
+ ` projections.login_policies5.is_default,` +
+ ` projections.login_policies5.hide_password_reset,` +
+ ` projections.login_policies5.ignore_unknown_usernames,` +
+ ` projections.login_policies5.allow_domain_discovery,` +
+ ` projections.login_policies5.disable_login_with_email,` +
+ ` projections.login_policies5.disable_login_with_phone,` +
+ ` projections.login_policies5.default_redirect_uri,` +
+ ` projections.login_policies5.password_check_lifetime,` +
+ ` projections.login_policies5.external_login_check_lifetime,` +
+ ` projections.login_policies5.mfa_init_skip_lifetime,` +
+ ` projections.login_policies5.second_factor_check_lifetime,` +
+ ` projections.login_policies5.multi_factor_check_lifetime` +
+ ` FROM projections.login_policies5` +
` AS OF SYSTEM TIME '-1 ms'`
loginPolicyCols = []string{
"aggregate_id",
@@ -49,6 +50,7 @@ var (
"allow_username_password",
"allow_external_idps",
"force_mfa",
+ "force_mfa_local_only",
"second_factors",
"multi_factors",
"passwordless_type",
@@ -66,15 +68,15 @@ var (
"multi_factor_check_lifetime",
}
- prepareLoginPolicy2FAsStmt = `SELECT projections.login_policies4.second_factors` +
- ` FROM projections.login_policies4` +
+ prepareLoginPolicy2FAsStmt = `SELECT projections.login_policies5.second_factors` +
+ ` FROM projections.login_policies5` +
` AS OF SYSTEM TIME '-1 ms'`
prepareLoginPolicy2FAsCols = []string{
"second_factors",
}
- prepareLoginPolicyMFAsStmt = `SELECT projections.login_policies4.multi_factors` +
- ` FROM projections.login_policies4` +
+ prepareLoginPolicyMFAsStmt = `SELECT projections.login_policies5.multi_factors` +
+ ` FROM projections.login_policies5` +
` AS OF SYSTEM TIME '-1 ms'`
prepareLoginPolicyMFAsCols = []string{
"multi_factors",
@@ -126,6 +128,7 @@ func Test_LoginPolicyPrepares(t *testing.T) {
true,
true,
true,
+ true,
database.EnumArray[domain.SecondFactorType]{domain.SecondFactorTypeOTP},
database.EnumArray[domain.MultiFactorType]{domain.MultiFactorTypeU2FWithPIN},
domain.PasswordlessTypeAllowed,
@@ -153,6 +156,7 @@ func Test_LoginPolicyPrepares(t *testing.T) {
AllowUsernamePassword: true,
AllowExternalIDPs: true,
ForceMFA: true,
+ ForceMFALocalOnly: true,
SecondFactors: database.EnumArray[domain.SecondFactorType]{domain.SecondFactorTypeOTP},
MultiFactors: database.EnumArray[domain.MultiFactorType]{domain.MultiFactorTypeU2FWithPIN},
PasswordlessType: domain.PasswordlessTypeAllowed,
diff --git a/internal/query/milestone.go b/internal/query/milestone.go
new file mode 100644
index 000000000..b4767495b
--- /dev/null
+++ b/internal/query/milestone.go
@@ -0,0 +1,146 @@
+package query
+
+import (
+ "context"
+ "database/sql"
+ "time"
+
+ sq "github.com/Masterminds/squirrel"
+
+ "github.com/zitadel/zitadel/internal/api/authz"
+ "github.com/zitadel/zitadel/internal/api/call"
+ "github.com/zitadel/zitadel/internal/errors"
+ "github.com/zitadel/zitadel/internal/query/projection"
+ "github.com/zitadel/zitadel/internal/repository/milestone"
+ "github.com/zitadel/zitadel/internal/telemetry/tracing"
+)
+
+type Milestones struct {
+ SearchResponse
+ Milestones []*Milestone
+}
+
+type Milestone struct {
+ InstanceID string
+ Type milestone.Type
+ ReachedDate time.Time
+ PushedDate time.Time
+ PrimaryDomain string
+}
+
+type MilestonesSearchQueries struct {
+ SearchRequest
+ Queries []SearchQuery
+}
+
+func (q *MilestonesSearchQueries) toQuery(query sq.SelectBuilder) sq.SelectBuilder {
+ query = q.SearchRequest.toQuery(query)
+ for _, q := range q.Queries {
+ query = q.toQuery(query)
+ }
+ return query
+}
+
+var (
+ milestonesTable = table{
+ name: projection.MilestonesProjectionTable,
+ instanceIDCol: projection.MilestoneColumnInstanceID,
+ }
+ MilestoneInstanceIDColID = Column{
+ name: projection.MilestoneColumnInstanceID,
+ table: milestonesTable,
+ }
+ MilestoneTypeColID = Column{
+ name: projection.MilestoneColumnType,
+ table: milestonesTable,
+ }
+ MilestonePrimaryDomainColID = Column{
+ name: projection.MilestoneColumnPrimaryDomain,
+ table: milestonesTable,
+ }
+ MilestoneReachedDateColID = Column{
+ name: projection.MilestoneColumnReachedDate,
+ table: milestonesTable,
+ }
+ MilestonePushedDateColID = Column{
+ name: projection.MilestoneColumnPushedDate,
+ table: milestonesTable,
+ }
+)
+
+// SearchMilestones tries to defer the instanceID from the passed context if no instanceIDs are passed
+func (q *Queries) SearchMilestones(ctx context.Context, instanceIDs []string, queries *MilestonesSearchQueries) (_ *Milestones, err error) {
+ ctx, span := tracing.NewSpan(ctx)
+ defer func() { span.EndWithError(err) }()
+ query, scan := prepareMilestonesQuery(ctx, q.client)
+ if len(instanceIDs) == 0 {
+ instanceIDs = []string{authz.GetInstance(ctx).InstanceID()}
+ }
+ stmt, args, err := queries.toQuery(query).Where(sq.Eq{MilestoneInstanceIDColID.identifier(): instanceIDs}).ToSql()
+ if err != nil {
+ return nil, errors.ThrowInternal(err, "QUERY-A9i5k", "Errors.Query.SQLStatement")
+ }
+ rows, err := q.client.QueryContext(ctx, stmt, args...)
+ if err != nil {
+ return nil, err
+ }
+ defer func() {
+ if closeErr := rows.Close(); closeErr != nil && err == nil {
+ err = errors.ThrowInternal(closeErr, "QUERY-CK9mI", "Errors.Query.CloseRows")
+ }
+ }()
+ milestones, err := scan(rows)
+ if err != nil {
+ return nil, err
+ }
+ if err = rows.Err(); err != nil {
+ return nil, errors.ThrowInternal(err, "QUERY-asLsI", "Errors.Internal")
+ }
+ milestones.LatestSequence, err = q.latestSequence(ctx, milestonesTable)
+ return milestones, err
+
+}
+
+func prepareMilestonesQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*Milestones, error)) {
+ return sq.Select(
+ MilestoneInstanceIDColID.identifier(),
+ MilestonePrimaryDomainColID.identifier(),
+ MilestoneReachedDateColID.identifier(),
+ MilestonePushedDateColID.identifier(),
+ MilestoneTypeColID.identifier(),
+ countColumn.identifier(),
+ ).
+ From(milestonesTable.identifier() + db.Timetravel(call.Took(ctx))).
+ PlaceholderFormat(sq.Dollar),
+ func(rows *sql.Rows) (*Milestones, error) {
+ milestones := make([]*Milestone, 0)
+ var count uint64
+ for rows.Next() {
+ m := new(Milestone)
+ reachedDate := sql.NullTime{}
+ pushedDate := sql.NullTime{}
+ primaryDomain := sql.NullString{}
+ err := rows.Scan(
+ &m.InstanceID,
+ &primaryDomain,
+ &reachedDate,
+ &pushedDate,
+ &m.Type,
+ &count,
+ )
+ if err != nil {
+ return nil, err
+ }
+ m.PrimaryDomain = primaryDomain.String
+ m.ReachedDate = reachedDate.Time
+ m.PushedDate = pushedDate.Time
+ milestones = append(milestones, m)
+ }
+ return &Milestones{
+ Milestones: milestones,
+ SearchResponse: SearchResponse{
+ Count: count,
+ },
+ }, nil
+ }
+}
diff --git a/internal/query/milestone_test.go b/internal/query/milestone_test.go
new file mode 100644
index 000000000..1c1852a59
--- /dev/null
+++ b/internal/query/milestone_test.go
@@ -0,0 +1,189 @@
+package query
+
+import (
+ "database/sql"
+ "database/sql/driver"
+ "errors"
+ "fmt"
+ "regexp"
+ "testing"
+)
+
+var (
+ expectedMilestoneQuery = regexp.QuoteMeta(`
+ SELECT projections.milestones.instance_id,
+ projections.milestones.primary_domain,
+ projections.milestones.reached_date,
+ projections.milestones.last_pushed_date,
+ projections.milestones.type,
+ COUNT(*) OVER ()
+ FROM projections.milestones AS OF SYSTEM TIME '-1 ms'
+ `)
+
+ milestoneCols = []string{
+ "instance_id",
+ "primary_domain",
+ "reached_date",
+ "last_pushed_date",
+ "type",
+ "ignore_client_ids",
+ }
+)
+
+func Test_MilestonesPrepare(t *testing.T) {
+ type want struct {
+ sqlExpectations sqlExpectation
+ err checkErr
+ }
+ tests := []struct {
+ name string
+ prepare interface{}
+ want want
+ object interface{}
+ }{
+ {
+ name: "prepareMilestonesQuery no result",
+ prepare: prepareMilestonesQuery,
+ want: want{
+ sqlExpectations: mockQueries(
+ expectedMilestoneQuery,
+ nil,
+ nil,
+ ),
+ },
+ object: &Milestones{Milestones: []*Milestone{}},
+ },
+ {
+ name: "prepareMilestonesQuery",
+ prepare: prepareMilestonesQuery,
+ want: want{
+ sqlExpectations: mockQueries(
+ expectedMilestoneQuery,
+ milestoneCols,
+ [][]driver.Value{
+ {
+ "instance-id",
+ "primary.domain",
+ testNow,
+ testNow,
+ 1,
+ 1,
+ },
+ },
+ ),
+ },
+ object: &Milestones{
+ SearchResponse: SearchResponse{
+ Count: 1,
+ },
+ Milestones: []*Milestone{
+ {
+ InstanceID: "instance-id",
+ Type: 1,
+ ReachedDate: testNow,
+ PushedDate: testNow,
+ PrimaryDomain: "primary.domain",
+ },
+ },
+ },
+ },
+ {
+ name: "prepareMilestonesQuery multiple result",
+ prepare: prepareMilestonesQuery,
+ want: want{
+ sqlExpectations: mockQueries(
+ expectedMilestoneQuery,
+ milestoneCols,
+ [][]driver.Value{
+ {
+ "instance-id",
+ "primary.domain",
+ testNow,
+ testNow,
+ 1,
+ 1,
+ },
+ {
+ "instance-id",
+ "primary.domain",
+ testNow,
+ testNow,
+ 2,
+ 2,
+ },
+ {
+ "instance-id",
+ "primary.domain",
+ testNow,
+ nil,
+ 3,
+ 3,
+ },
+ {
+ "instance-id",
+ "primary.domain",
+ nil,
+ nil,
+ 4,
+ 4,
+ },
+ },
+ ),
+ },
+ object: &Milestones{
+ SearchResponse: SearchResponse{
+ Count: 4,
+ },
+ Milestones: []*Milestone{
+ {
+ InstanceID: "instance-id",
+ Type: 1,
+ ReachedDate: testNow,
+ PushedDate: testNow,
+ PrimaryDomain: "primary.domain",
+ },
+ {
+ InstanceID: "instance-id",
+ Type: 2,
+ ReachedDate: testNow,
+ PushedDate: testNow,
+ PrimaryDomain: "primary.domain",
+ },
+ {
+ InstanceID: "instance-id",
+ Type: 3,
+ ReachedDate: testNow,
+ PrimaryDomain: "primary.domain",
+ },
+ {
+ InstanceID: "instance-id",
+ Type: 4,
+ PrimaryDomain: "primary.domain",
+ },
+ },
+ },
+ },
+ {
+ name: "prepareMilestonesQuery sql err",
+ prepare: prepareMilestonesQuery,
+ want: want{
+ sqlExpectations: mockQueryErr(
+ expectedMilestoneQuery,
+ sql.ErrConnDone,
+ ),
+ err: func(err error) (error, bool) {
+ if !errors.Is(err, sql.ErrConnDone) {
+ return fmt.Errorf("err should be sql.ErrConnDone got: %w", err), false
+ }
+ return nil, true
+ },
+ },
+ object: nil,
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err, defaultPrepareArgs...)
+ })
+ }
+}
diff --git a/internal/query/notification_policy.go b/internal/query/notification_policy.go
index e15e7c84f..64873e1ca 100644
--- a/internal/query/notification_policy.go
+++ b/internal/query/notification_policy.go
@@ -81,7 +81,8 @@ func (q *Queries) NotificationPolicyByOrg(ctx context.Context, shouldTriggerBulk
defer func() { span.EndWithError(err) }()
if shouldTriggerBulk {
- if err := projection.NotificationPolicyProjection.Trigger(ctx); err != nil {
+ ctx, err = projection.NotificationPolicyProjection.TriggerErr(ctx)
+ if err != nil {
return nil, err
}
}
@@ -112,7 +113,8 @@ func (q *Queries) DefaultNotificationPolicy(ctx context.Context, shouldTriggerBu
defer func() { span.EndWithError(err) }()
if shouldTriggerBulk {
- if err := projection.NotificationPolicyProjection.Trigger(ctx); err != nil {
+ ctx, err = projection.NotificationPolicyProjection.TriggerErr(ctx)
+ if err != nil {
return nil, err
}
}
diff --git a/internal/query/org.go b/internal/query/org.go
index d5c90f08f..40bcbe400 100644
--- a/internal/query/org.go
+++ b/internal/query/org.go
@@ -94,7 +94,7 @@ func (q *Queries) OrgByID(ctx context.Context, shouldTriggerBulk bool, id string
defer func() { span.EndWithError(err) }()
if shouldTriggerBulk {
- projection.OrgProjection.Trigger(ctx)
+ ctx = projection.OrgProjection.Trigger(ctx)
}
stmt, scan := prepareOrgQuery(ctx, q.client)
diff --git a/internal/query/org_metadata.go b/internal/query/org_metadata.go
index c22bd6a71..5ce36369e 100644
--- a/internal/query/org_metadata.go
+++ b/internal/query/org_metadata.go
@@ -82,7 +82,7 @@ func (q *Queries) GetOrgMetadataByKey(ctx context.Context, shouldTriggerBulk boo
defer func() { span.EndWithError(err) }()
if shouldTriggerBulk {
- projection.OrgMetadataProjection.Trigger(ctx)
+ ctx = projection.OrgMetadataProjection.Trigger(ctx)
}
query, scan := prepareOrgMetadataQuery(ctx, q.client)
@@ -111,7 +111,7 @@ func (q *Queries) SearchOrgMetadata(ctx context.Context, shouldTriggerBulk bool,
defer func() { span.EndWithError(err) }()
if shouldTriggerBulk {
- projection.OrgMetadataProjection.Trigger(ctx)
+ ctx = projection.OrgMetadataProjection.Trigger(ctx)
}
eq := sq.Eq{
OrgMetadataOrgIDCol.identifier(): orgID,
diff --git a/internal/query/password_age_policy.go b/internal/query/password_age_policy.go
index 2de71235a..fda59e88e 100644
--- a/internal/query/password_age_policy.go
+++ b/internal/query/password_age_policy.go
@@ -86,7 +86,7 @@ func (q *Queries) PasswordAgePolicyByOrg(ctx context.Context, shouldTriggerBulk
defer func() { span.EndWithError(err) }()
if shouldTriggerBulk {
- projection.PasswordAgeProjection.Trigger(ctx)
+ ctx = projection.PasswordAgeProjection.Trigger(ctx)
}
eq := sq.Eq{PasswordAgeColInstanceID.identifier(): authz.GetInstance(ctx).InstanceID()}
if !withOwnerRemoved {
@@ -116,7 +116,7 @@ func (q *Queries) DefaultPasswordAgePolicy(ctx context.Context, shouldTriggerBul
defer func() { span.EndWithError(err) }()
if shouldTriggerBulk {
- projection.PasswordAgeProjection.Trigger(ctx)
+ ctx = projection.PasswordAgeProjection.Trigger(ctx)
}
stmt, scan := preparePasswordAgePolicyQuery(ctx, q.client)
diff --git a/internal/query/password_complexity_policy.go b/internal/query/password_complexity_policy.go
index 4877f0159..e5f15f181 100644
--- a/internal/query/password_complexity_policy.go
+++ b/internal/query/password_complexity_policy.go
@@ -38,7 +38,7 @@ func (q *Queries) PasswordComplexityPolicyByOrg(ctx context.Context, shouldTrigg
defer func() { span.EndWithError(err) }()
if shouldTriggerBulk {
- projection.PasswordComplexityProjection.Trigger(ctx)
+ ctx = projection.PasswordComplexityProjection.Trigger(ctx)
}
eq := sq.Eq{PasswordComplexityColInstanceID.identifier(): authz.GetInstance(ctx).InstanceID()}
if !withOwnerRemoved {
@@ -68,7 +68,7 @@ func (q *Queries) DefaultPasswordComplexityPolicy(ctx context.Context, shouldTri
defer func() { span.EndWithError(err) }()
if shouldTriggerBulk {
- projection.PasswordComplexityProjection.Trigger(ctx)
+ ctx = projection.PasswordComplexityProjection.Trigger(ctx)
}
stmt, scan := preparePasswordComplexityPolicyQuery(ctx, q.client)
diff --git a/internal/query/prepare_test.go b/internal/query/prepare_test.go
index db309805c..cb69fb21e 100644
--- a/internal/query/prepare_test.go
+++ b/internal/query/prepare_test.go
@@ -14,6 +14,7 @@ import (
"github.com/DATA-DOG/go-sqlmock"
sq "github.com/Masterminds/squirrel"
"github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
)
var (
@@ -74,9 +75,9 @@ type checkErr func(error) (err error, ok bool)
type sqlExpectation func(sqlmock.Sqlmock) sqlmock.Sqlmock
-func mockQuery(stmt string, cols []string, row []driver.Value) func(m sqlmock.Sqlmock) sqlmock.Sqlmock {
+func mockQuery(stmt string, cols []string, row []driver.Value, args ...driver.Value) func(m sqlmock.Sqlmock) sqlmock.Sqlmock {
return func(m sqlmock.Sqlmock) sqlmock.Sqlmock {
- q := m.ExpectQuery(stmt)
+ q := m.ExpectQuery(stmt).WithArgs(args...)
result := sqlmock.NewRows(cols)
if len(row) > 0 {
result.AddRow(row...)
@@ -111,6 +112,15 @@ func mockQueryErr(stmt string, err error, args ...driver.Value) func(m sqlmock.S
}
}
+func execMock(t testing.TB, exp sqlExpectation, run func(db *sql.DB)) {
+ db, mock, err := sqlmock.New()
+ require.NoError(t, err)
+ defer db.Close()
+ mock = exp(mock)
+ run(db)
+ assert.NoError(t, mock.ExpectationsWereMet())
+}
+
var (
rowType = reflect.TypeOf(&sql.Row{})
rowsType = reflect.TypeOf(&sql.Rows{})
@@ -317,7 +327,9 @@ func TestValidatePrepare(t *testing.T) {
type prepareDB struct{}
-func (_ *prepareDB) Timetravel(time.Duration) string { return " AS OF SYSTEM TIME '-1 ms' " }
+const asOfSystemTime = " AS OF SYSTEM TIME '-1 ms' "
+
+func (*prepareDB) Timetravel(time.Duration) string { return asOfSystemTime }
var defaultPrepareArgs = []reflect.Value{reflect.ValueOf(context.Background()), reflect.ValueOf(new(prepareDB))}
diff --git a/internal/query/privacy_policy.go b/internal/query/privacy_policy.go
index 2874c6d22..e207c3e9b 100644
--- a/internal/query/privacy_policy.go
+++ b/internal/query/privacy_policy.go
@@ -96,7 +96,7 @@ func (q *Queries) PrivacyPolicyByOrg(ctx context.Context, shouldTriggerBulk bool
defer func() { span.EndWithError(err) }()
if shouldTriggerBulk {
- projection.PrivacyPolicyProjection.Trigger(ctx)
+ ctx = projection.PrivacyPolicyProjection.Trigger(ctx)
}
eq := sq.Eq{PrivacyColInstanceID.identifier(): authz.GetInstance(ctx).InstanceID()}
if !withOwnerRemoved {
@@ -125,7 +125,7 @@ func (q *Queries) DefaultPrivacyPolicy(ctx context.Context, shouldTriggerBulk bo
defer func() { span.EndWithError(err) }()
if shouldTriggerBulk {
- projection.PrivacyPolicyProjection.Trigger(ctx)
+ ctx = projection.PrivacyPolicyProjection.Trigger(ctx)
}
stmt, scan := preparePrivacyPolicyQuery(ctx, q.client)
diff --git a/internal/query/project.go b/internal/query/project.go
index e7df58cf5..065a3e5c5 100644
--- a/internal/query/project.go
+++ b/internal/query/project.go
@@ -105,7 +105,7 @@ func (q *Queries) ProjectByID(ctx context.Context, shouldTriggerBulk bool, id st
defer func() { span.EndWithError(err) }()
if shouldTriggerBulk {
- projection.ProjectProjection.Trigger(ctx)
+ ctx = projection.ProjectProjection.Trigger(ctx)
}
stmt, scan := prepareProjectQuery(ctx, q.client)
diff --git a/internal/query/project_grant.go b/internal/query/project_grant.go
index bbf265736..fee8ae836 100644
--- a/internal/query/project_grant.go
+++ b/internal/query/project_grant.go
@@ -116,7 +116,7 @@ func (q *Queries) ProjectGrantByID(ctx context.Context, shouldTriggerBulk bool,
defer func() { span.EndWithError(err) }()
if shouldTriggerBulk {
- projection.ProjectGrantProjection.Trigger(ctx)
+ ctx = projection.ProjectGrantProjection.Trigger(ctx)
}
stmt, scan := prepareProjectGrantQuery(ctx, q.client)
diff --git a/internal/query/project_role.go b/internal/query/project_role.go
index 03ebbf60b..7365b1247 100644
--- a/internal/query/project_role.go
+++ b/internal/query/project_role.go
@@ -88,7 +88,7 @@ func (q *Queries) SearchProjectRoles(ctx context.Context, shouldTriggerBulk bool
defer func() { span.EndWithError(err) }()
if shouldTriggerBulk {
- projection.ProjectRoleProjection.Trigger(ctx)
+ ctx = projection.ProjectRoleProjection.Trigger(ctx)
}
eq := sq.Eq{ProjectRoleColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID()}
diff --git a/internal/query/projection/assert.go b/internal/query/projection/assert.go
new file mode 100644
index 000000000..369a6da69
--- /dev/null
+++ b/internal/query/projection/assert.go
@@ -0,0 +1,14 @@
+package projection
+
+import (
+ "github.com/zitadel/zitadel/internal/errors"
+ "github.com/zitadel/zitadel/internal/eventstore"
+)
+
+func assertEvent[T eventstore.Event](event eventstore.Event) (T, error) {
+ e, ok := event.(T)
+ if !ok {
+ return e, errors.ThrowInvalidArgumentf(nil, "HANDL-1m9fS", "reduce.wrong.event.type %T", event)
+ }
+ return e, nil
+}
diff --git a/internal/query/projection/assert_test.go b/internal/query/projection/assert_test.go
new file mode 100644
index 000000000..010f0c81a
--- /dev/null
+++ b/internal/query/projection/assert_test.go
@@ -0,0 +1,52 @@
+package projection
+
+import (
+ "context"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+
+ "github.com/zitadel/zitadel/internal/eventstore"
+ "github.com/zitadel/zitadel/internal/repository/instance"
+)
+
+func Test_assertEvent(t *testing.T) {
+ type args struct {
+ event eventstore.Event
+ assertFunc func(eventstore.Event) (eventstore.Event, error)
+ }
+ type testCase struct {
+ name string
+ args args
+ wantErr assert.ErrorAssertionFunc
+ }
+ tests := []testCase{
+ {
+ name: "correct event type",
+ args: args{
+ event: instance.NewInstanceAddedEvent(context.Background(), &instance.NewAggregate("instance-id").Aggregate, "instance-name"),
+ assertFunc: func(event eventstore.Event) (eventstore.Event, error) {
+ return assertEvent[*instance.InstanceAddedEvent](event)
+ },
+ },
+ wantErr: assert.NoError,
+ }, {
+ name: "wrong event type",
+ args: args{
+ event: instance.NewInstanceRemovedEvent(context.Background(), &instance.NewAggregate("instance-id").Aggregate, "instance-name", nil),
+ assertFunc: func(event eventstore.Event) (eventstore.Event, error) {
+ return assertEvent[*instance.InstanceAddedEvent](event)
+ },
+ },
+ wantErr: assert.Error,
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ _, err := tt.args.assertFunc(tt.args.event)
+ if !tt.wantErr(t, err) {
+ return
+ }
+ })
+ }
+}
diff --git a/internal/query/projection/auth_request.go b/internal/query/projection/auth_request.go
new file mode 100644
index 000000000..b01ce175a
--- /dev/null
+++ b/internal/query/projection/auth_request.go
@@ -0,0 +1,142 @@
+package projection
+
+import (
+ "context"
+
+ "github.com/zitadel/zitadel/internal/errors"
+ "github.com/zitadel/zitadel/internal/eventstore"
+ "github.com/zitadel/zitadel/internal/eventstore/handler"
+ "github.com/zitadel/zitadel/internal/eventstore/handler/crdb"
+ "github.com/zitadel/zitadel/internal/repository/authrequest"
+ "github.com/zitadel/zitadel/internal/repository/instance"
+)
+
+const (
+ AuthRequestsProjectionTable = "projections.auth_requests"
+
+ AuthRequestColumnID = "id"
+ AuthRequestColumnCreationDate = "creation_date"
+ AuthRequestColumnChangeDate = "change_date"
+ AuthRequestColumnSequence = "sequence"
+ AuthRequestColumnResourceOwner = "resource_owner"
+ AuthRequestColumnInstanceID = "instance_id"
+ AuthRequestColumnLoginClient = "login_client"
+ AuthRequestColumnClientID = "client_id"
+ AuthRequestColumnRedirectURI = "redirect_uri"
+ AuthRequestColumnScope = "scope"
+ AuthRequestColumnPrompt = "prompt"
+ AuthRequestColumnUILocales = "ui_locales"
+ AuthRequestColumnMaxAge = "max_age"
+ AuthRequestColumnLoginHint = "login_hint"
+ AuthRequestColumnHintUserID = "hint_user_id"
+)
+
+type authRequestProjection struct {
+ crdb.StatementHandler
+}
+
+func newAuthRequestProjection(ctx context.Context, config crdb.StatementHandlerConfig) *authRequestProjection {
+ p := new(authRequestProjection)
+ config.ProjectionName = AuthRequestsProjectionTable
+ config.Reducers = p.reducers()
+ config.InitCheck = crdb.NewMultiTableCheck(
+ crdb.NewTable([]*crdb.Column{
+ crdb.NewColumn(AuthRequestColumnID, crdb.ColumnTypeText),
+ crdb.NewColumn(AuthRequestColumnCreationDate, crdb.ColumnTypeTimestamp),
+ crdb.NewColumn(AuthRequestColumnChangeDate, crdb.ColumnTypeTimestamp),
+ crdb.NewColumn(AuthRequestColumnSequence, crdb.ColumnTypeInt64),
+ crdb.NewColumn(AuthRequestColumnResourceOwner, crdb.ColumnTypeText),
+ crdb.NewColumn(AuthRequestColumnInstanceID, crdb.ColumnTypeText),
+ crdb.NewColumn(AuthRequestColumnLoginClient, crdb.ColumnTypeText),
+ crdb.NewColumn(AuthRequestColumnClientID, crdb.ColumnTypeText),
+ crdb.NewColumn(AuthRequestColumnRedirectURI, crdb.ColumnTypeText),
+ crdb.NewColumn(AuthRequestColumnScope, crdb.ColumnTypeTextArray),
+ crdb.NewColumn(AuthRequestColumnPrompt, crdb.ColumnTypeEnumArray, crdb.Nullable()),
+ crdb.NewColumn(AuthRequestColumnUILocales, crdb.ColumnTypeTextArray, crdb.Nullable()),
+ crdb.NewColumn(AuthRequestColumnMaxAge, crdb.ColumnTypeInt64, crdb.Nullable()),
+ crdb.NewColumn(AuthRequestColumnLoginHint, crdb.ColumnTypeText, crdb.Nullable()),
+ crdb.NewColumn(AuthRequestColumnHintUserID, crdb.ColumnTypeText, crdb.Nullable()),
+ },
+ crdb.NewPrimaryKey(AuthRequestColumnInstanceID, AuthRequestColumnID),
+ ),
+ )
+ p.StatementHandler = crdb.NewStatementHandler(ctx, config)
+ return p
+}
+
+func (p *authRequestProjection) reducers() []handler.AggregateReducer {
+ return []handler.AggregateReducer{
+ {
+ Aggregate: authrequest.AggregateType,
+ EventRedusers: []handler.EventReducer{
+ {
+ Event: authrequest.AddedType,
+ Reduce: p.reduceAuthRequestAdded,
+ },
+ {
+ Event: authrequest.SucceededType,
+ Reduce: p.reduceAuthRequestEnded,
+ },
+ {
+ Event: authrequest.FailedType,
+ Reduce: p.reduceAuthRequestEnded,
+ },
+ },
+ },
+ {
+ Aggregate: instance.AggregateType,
+ EventRedusers: []handler.EventReducer{
+ {
+ Event: instance.InstanceRemovedEventType,
+ Reduce: reduceInstanceRemovedHelper(AuthRequestColumnInstanceID),
+ },
+ },
+ },
+ }
+}
+
+func (p *authRequestProjection) reduceAuthRequestAdded(event eventstore.Event) (*handler.Statement, error) {
+ e, ok := event.(*authrequest.AddedEvent)
+ if !ok {
+ return nil, errors.ThrowInvalidArgumentf(nil, "HANDL-Sfwfa", "reduce.wrong.event.type %s", authrequest.AddedType)
+ }
+
+ return crdb.NewCreateStatement(
+ e,
+ []handler.Column{
+ handler.NewCol(AuthRequestColumnID, e.Aggregate().ID),
+ handler.NewCol(AuthRequestColumnInstanceID, e.Aggregate().InstanceID),
+ handler.NewCol(AuthRequestColumnCreationDate, e.CreationDate()),
+ handler.NewCol(AuthRequestColumnChangeDate, e.CreationDate()),
+ handler.NewCol(AuthRequestColumnResourceOwner, e.Aggregate().ResourceOwner),
+ handler.NewCol(AuthRequestColumnSequence, e.Sequence()),
+ handler.NewCol(AuthRequestColumnLoginClient, e.LoginClient),
+ handler.NewCol(AuthRequestColumnClientID, e.ClientID),
+ handler.NewCol(AuthRequestColumnRedirectURI, e.RedirectURI),
+ handler.NewCol(AuthRequestColumnScope, e.Scope),
+ handler.NewCol(AuthRequestColumnPrompt, e.Prompt),
+ handler.NewCol(AuthRequestColumnUILocales, e.UILocales),
+ handler.NewCol(AuthRequestColumnMaxAge, e.MaxAge),
+ handler.NewCol(AuthRequestColumnLoginHint, e.LoginHint),
+ handler.NewCol(AuthRequestColumnHintUserID, e.HintUserID),
+ },
+ ), nil
+}
+
+func (p *authRequestProjection) reduceAuthRequestEnded(event eventstore.Event) (*handler.Statement, error) {
+ switch event.(type) {
+ case *authrequest.SucceededEvent,
+ *authrequest.FailedEvent:
+ break
+ default:
+ return nil, errors.ThrowInvalidArgumentf(nil, "HANDL-ASF3h", "reduce.wrong.event.type %s", []eventstore.EventType{authrequest.SucceededType, authrequest.FailedType})
+ }
+
+ return crdb.NewDeleteStatement(
+ event,
+ []handler.Condition{
+ handler.NewCond(AuthRequestColumnID, event.Aggregate().ID),
+ handler.NewCond(AuthRequestColumnInstanceID, event.Aggregate().InstanceID),
+ },
+ ), nil
+}
diff --git a/internal/query/projection/auth_request_test.go b/internal/query/projection/auth_request_test.go
new file mode 100644
index 000000000..f0ce0651d
--- /dev/null
+++ b/internal/query/projection/auth_request_test.go
@@ -0,0 +1,134 @@
+package projection
+
+import (
+ "testing"
+ "time"
+
+ "github.com/muhlemmer/gu"
+
+ "github.com/zitadel/zitadel/internal/domain"
+ "github.com/zitadel/zitadel/internal/errors"
+ "github.com/zitadel/zitadel/internal/eventstore"
+ "github.com/zitadel/zitadel/internal/eventstore/handler"
+ "github.com/zitadel/zitadel/internal/repository/authrequest"
+)
+
+func TestAuthRequestProjection_reduces(t *testing.T) {
+ type args struct {
+ event func(t *testing.T) eventstore.Event
+ }
+ tests := []struct {
+ name string
+ args args
+ reduce func(event eventstore.Event) (*handler.Statement, error)
+ want wantReduce
+ }{
+ {
+ name: "reduceAuthRequestAdded",
+ args: args{
+ event: getEvent(testEvent(
+ authrequest.AddedType,
+ authrequest.AggregateType,
+ []byte(`{"login_client": "loginClient", "client_id":"clientId","redirect_uri": "redirectURI", "scope": ["openid"], "prompt": [1], "ui_locales": ["en","de"], "max_age": 0, "login_hint": "loginHint", "hint_user_id": "hintUserID"}`),
+ ), authrequest.AddedEventMapper),
+ },
+ reduce: (&authRequestProjection{}).reduceAuthRequestAdded,
+ want: wantReduce{
+ aggregateType: eventstore.AggregateType("auth_request"),
+ sequence: 15,
+ previousSequence: 10,
+ executer: &testExecuter{
+ executions: []execution{
+ {
+ expectedStmt: "INSERT INTO projections.auth_requests (id, instance_id, creation_date, change_date, resource_owner, sequence, login_client, client_id, redirect_uri, scope, prompt, ui_locales, max_age, login_hint, hint_user_id) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15)",
+ expectedArgs: []interface{}{
+ "agg-id",
+ "instance-id",
+ anyArg{},
+ anyArg{},
+ "ro-id",
+ uint64(15),
+ "loginClient",
+ "clientId",
+ "redirectURI",
+ []string{"openid"},
+ []domain.Prompt{domain.PromptNone},
+ []string{"en", "de"},
+ gu.Ptr(time.Duration(0)),
+ gu.Ptr("loginHint"),
+ gu.Ptr("hintUserID"),
+ },
+ },
+ },
+ },
+ },
+ },
+ {
+ name: "reduceAuthRequestFailed",
+ args: args{
+ event: getEvent(testEvent(
+ authrequest.FailedType,
+ authrequest.AggregateType,
+ []byte(`{"reason": 0}`),
+ ), authrequest.FailedEventMapper),
+ },
+ reduce: (&authRequestProjection{}).reduceAuthRequestEnded,
+ want: wantReduce{
+ aggregateType: eventstore.AggregateType("auth_request"),
+ sequence: 15,
+ previousSequence: 10,
+ executer: &testExecuter{
+ executions: []execution{
+ {
+ expectedStmt: "DELETE FROM projections.auth_requests WHERE (id = $1) AND (instance_id = $2)",
+ expectedArgs: []interface{}{
+ "agg-id",
+ "instance-id",
+ },
+ },
+ },
+ },
+ },
+ },
+ {
+ name: "reduceAuthRequestSucceeded",
+ args: args{
+ event: getEvent(testEvent(
+ authrequest.SucceededType,
+ authrequest.AggregateType,
+ nil,
+ ), authrequest.SucceededEventMapper),
+ },
+ reduce: (&authRequestProjection{}).reduceAuthRequestEnded,
+ want: wantReduce{
+ aggregateType: eventstore.AggregateType("auth_request"),
+ sequence: 15,
+ previousSequence: 10,
+ executer: &testExecuter{
+ executions: []execution{
+ {
+ expectedStmt: "DELETE FROM projections.auth_requests WHERE (id = $1) AND (instance_id = $2)",
+ expectedArgs: []interface{}{
+ "agg-id",
+ "instance-id",
+ },
+ },
+ },
+ },
+ },
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ event := baseEvent(t)
+ got, err := tt.reduce(event)
+ if !errors.IsErrorInvalidArgument(err) {
+ t.Errorf("no wrong event mapping: %v, got: %v", err, got)
+ }
+
+ event = tt.args.event(t)
+ got, err = tt.reduce(event)
+ assertReduce(t, got, err, AuthRequestsProjectionTable, tt.want)
+ })
+ }
+}
diff --git a/internal/query/projection/event_test.go b/internal/query/projection/event_test.go
index 29e32e95c..40dd2234e 100644
--- a/internal/query/projection/event_test.go
+++ b/internal/query/projection/event_test.go
@@ -14,12 +14,26 @@ func testEvent(
eventType repository.EventType,
aggregateType repository.AggregateType,
data []byte,
+) *repository.Event {
+ return timedTestEvent(eventType, aggregateType, data, time.Now())
+}
+
+func toSystemEvent(event *repository.Event) *repository.Event {
+ event.EditorService = "SYSTEM"
+ return event
+}
+
+func timedTestEvent(
+ eventType repository.EventType,
+ aggregateType repository.AggregateType,
+ data []byte,
+ creationDate time.Time,
) *repository.Event {
return &repository.Event{
Sequence: 15,
PreviousAggregateSequence: 10,
PreviousAggregateTypeSequence: 10,
- CreationDate: time.Now(),
+ CreationDate: creationDate,
Type: eventType,
AggregateType: aggregateType,
Data: data,
diff --git a/internal/query/projection/label_policy.go b/internal/query/projection/label_policy.go
index b8a84d922..e62c39b4c 100644
--- a/internal/query/projection/label_policy.go
+++ b/internal/query/projection/label_policy.go
@@ -402,10 +402,10 @@ func (p *labelPolicyProjection) reduceActivated(event eventstore.Event) (*handle
handler.NewCol(LabelPolicyDarkLogoURLCol, nil),
handler.NewCol(LabelPolicyDarkIconURLCol, nil),
},
- []handler.Condition{
- handler.NewCond(LabelPolicyIDCol, event.Aggregate().ID),
- handler.NewCond(LabelPolicyStateCol, domain.LabelPolicyStatePreview),
- handler.NewCond(LabelPolicyInstanceIDCol, event.Aggregate().InstanceID),
+ []handler.NamespacedCondition{
+ handler.NewNamespacedCondition(LabelPolicyIDCol, event.Aggregate().ID),
+ handler.NewNamespacedCondition(LabelPolicyStateCol, domain.LabelPolicyStatePreview),
+ handler.NewNamespacedCondition(LabelPolicyInstanceIDCol, event.Aggregate().InstanceID),
}), nil
}
diff --git a/internal/query/projection/label_policy_test.go b/internal/query/projection/label_policy_test.go
index f7190f6d5..88a82ea0c 100644
--- a/internal/query/projection/label_policy_test.go
+++ b/internal/query/projection/label_policy_test.go
@@ -170,7 +170,7 @@ func TestLabelPolicyProjection_reduces(t *testing.T) {
executer: &testExecuter{
executions: []execution{
{
- expectedStmt: "INSERT INTO projections.label_policies2 (change_date, sequence, state, creation_date, resource_owner, instance_id, id, is_default, hide_login_name_suffix, font_url, watermark_disabled, should_error_popup, light_primary_color, light_warn_color, light_background_color, light_font_color, light_logo_url, light_icon_url, dark_primary_color, dark_warn_color, dark_background_color, dark_font_color, dark_logo_url, dark_icon_url) SELECT $1, $2, $3, creation_date, resource_owner, instance_id, id, is_default, hide_login_name_suffix, font_url, watermark_disabled, should_error_popup, light_primary_color, light_warn_color, light_background_color, light_font_color, light_logo_url, light_icon_url, dark_primary_color, dark_warn_color, dark_background_color, dark_font_color, dark_logo_url, dark_icon_url FROM projections.label_policies2 AS copy_table WHERE copy_table.id = $4 AND copy_table.state = $5 AND copy_table.instance_id = $6 ON CONFLICT (instance_id, id, state) DO UPDATE SET (change_date, sequence, state, creation_date, resource_owner, instance_id, id, is_default, hide_login_name_suffix, font_url, watermark_disabled, should_error_popup, light_primary_color, light_warn_color, light_background_color, light_font_color, light_logo_url, light_icon_url, dark_primary_color, dark_warn_color, dark_background_color, dark_font_color, dark_logo_url, dark_icon_url) = ($1, $2, $3, EXCLUDED.creation_date, EXCLUDED.resource_owner, EXCLUDED.instance_id, EXCLUDED.id, EXCLUDED.is_default, EXCLUDED.hide_login_name_suffix, EXCLUDED.font_url, EXCLUDED.watermark_disabled, EXCLUDED.should_error_popup, EXCLUDED.light_primary_color, EXCLUDED.light_warn_color, EXCLUDED.light_background_color, EXCLUDED.light_font_color, EXCLUDED.light_logo_url, EXCLUDED.light_icon_url, EXCLUDED.dark_primary_color, EXCLUDED.dark_warn_color, EXCLUDED.dark_background_color, EXCLUDED.dark_font_color, EXCLUDED.dark_logo_url, EXCLUDED.dark_icon_url)",
+ expectedStmt: "INSERT INTO projections.label_policies2 (change_date, sequence, state, creation_date, resource_owner, instance_id, id, is_default, hide_login_name_suffix, font_url, watermark_disabled, should_error_popup, light_primary_color, light_warn_color, light_background_color, light_font_color, light_logo_url, light_icon_url, dark_primary_color, dark_warn_color, dark_background_color, dark_font_color, dark_logo_url, dark_icon_url) SELECT $1, $2, $3, creation_date, resource_owner, instance_id, id, is_default, hide_login_name_suffix, font_url, watermark_disabled, should_error_popup, light_primary_color, light_warn_color, light_background_color, light_font_color, light_logo_url, light_icon_url, dark_primary_color, dark_warn_color, dark_background_color, dark_font_color, dark_logo_url, dark_icon_url FROM projections.label_policies2 AS copy_table WHERE (copy_table.id = $4) AND (copy_table.state = $5) AND (copy_table.instance_id = $6) ON CONFLICT (instance_id, id, state) DO UPDATE SET (change_date, sequence, state, creation_date, resource_owner, instance_id, id, is_default, hide_login_name_suffix, font_url, watermark_disabled, should_error_popup, light_primary_color, light_warn_color, light_background_color, light_font_color, light_logo_url, light_icon_url, dark_primary_color, dark_warn_color, dark_background_color, dark_font_color, dark_logo_url, dark_icon_url) = ($1, $2, $3, EXCLUDED.creation_date, EXCLUDED.resource_owner, EXCLUDED.instance_id, EXCLUDED.id, EXCLUDED.is_default, EXCLUDED.hide_login_name_suffix, EXCLUDED.font_url, EXCLUDED.watermark_disabled, EXCLUDED.should_error_popup, EXCLUDED.light_primary_color, EXCLUDED.light_warn_color, EXCLUDED.light_background_color, EXCLUDED.light_font_color, EXCLUDED.light_logo_url, EXCLUDED.light_icon_url, EXCLUDED.dark_primary_color, EXCLUDED.dark_warn_color, EXCLUDED.dark_background_color, EXCLUDED.dark_font_color, EXCLUDED.dark_logo_url, EXCLUDED.dark_icon_url)",
expectedArgs: []interface{}{
anyArg{},
uint64(15),
@@ -631,7 +631,7 @@ func TestLabelPolicyProjection_reduces(t *testing.T) {
executer: &testExecuter{
executions: []execution{
{
- expectedStmt: "INSERT INTO projections.label_policies2 (change_date, sequence, state, creation_date, resource_owner, instance_id, id, is_default, hide_login_name_suffix, font_url, watermark_disabled, should_error_popup, light_primary_color, light_warn_color, light_background_color, light_font_color, light_logo_url, light_icon_url, dark_primary_color, dark_warn_color, dark_background_color, dark_font_color, dark_logo_url, dark_icon_url) SELECT $1, $2, $3, creation_date, resource_owner, instance_id, id, is_default, hide_login_name_suffix, font_url, watermark_disabled, should_error_popup, light_primary_color, light_warn_color, light_background_color, light_font_color, light_logo_url, light_icon_url, dark_primary_color, dark_warn_color, dark_background_color, dark_font_color, dark_logo_url, dark_icon_url FROM projections.label_policies2 AS copy_table WHERE copy_table.id = $4 AND copy_table.state = $5 AND copy_table.instance_id = $6 ON CONFLICT (instance_id, id, state) DO UPDATE SET (change_date, sequence, state, creation_date, resource_owner, instance_id, id, is_default, hide_login_name_suffix, font_url, watermark_disabled, should_error_popup, light_primary_color, light_warn_color, light_background_color, light_font_color, light_logo_url, light_icon_url, dark_primary_color, dark_warn_color, dark_background_color, dark_font_color, dark_logo_url, dark_icon_url) = ($1, $2, $3, EXCLUDED.creation_date, EXCLUDED.resource_owner, EXCLUDED.instance_id, EXCLUDED.id, EXCLUDED.is_default, EXCLUDED.hide_login_name_suffix, EXCLUDED.font_url, EXCLUDED.watermark_disabled, EXCLUDED.should_error_popup, EXCLUDED.light_primary_color, EXCLUDED.light_warn_color, EXCLUDED.light_background_color, EXCLUDED.light_font_color, EXCLUDED.light_logo_url, EXCLUDED.light_icon_url, EXCLUDED.dark_primary_color, EXCLUDED.dark_warn_color, EXCLUDED.dark_background_color, EXCLUDED.dark_font_color, EXCLUDED.dark_logo_url, EXCLUDED.dark_icon_url)",
+ expectedStmt: "INSERT INTO projections.label_policies2 (change_date, sequence, state, creation_date, resource_owner, instance_id, id, is_default, hide_login_name_suffix, font_url, watermark_disabled, should_error_popup, light_primary_color, light_warn_color, light_background_color, light_font_color, light_logo_url, light_icon_url, dark_primary_color, dark_warn_color, dark_background_color, dark_font_color, dark_logo_url, dark_icon_url) SELECT $1, $2, $3, creation_date, resource_owner, instance_id, id, is_default, hide_login_name_suffix, font_url, watermark_disabled, should_error_popup, light_primary_color, light_warn_color, light_background_color, light_font_color, light_logo_url, light_icon_url, dark_primary_color, dark_warn_color, dark_background_color, dark_font_color, dark_logo_url, dark_icon_url FROM projections.label_policies2 AS copy_table WHERE (copy_table.id = $4) AND (copy_table.state = $5) AND (copy_table.instance_id = $6) ON CONFLICT (instance_id, id, state) DO UPDATE SET (change_date, sequence, state, creation_date, resource_owner, instance_id, id, is_default, hide_login_name_suffix, font_url, watermark_disabled, should_error_popup, light_primary_color, light_warn_color, light_background_color, light_font_color, light_logo_url, light_icon_url, dark_primary_color, dark_warn_color, dark_background_color, dark_font_color, dark_logo_url, dark_icon_url) = ($1, $2, $3, EXCLUDED.creation_date, EXCLUDED.resource_owner, EXCLUDED.instance_id, EXCLUDED.id, EXCLUDED.is_default, EXCLUDED.hide_login_name_suffix, EXCLUDED.font_url, EXCLUDED.watermark_disabled, EXCLUDED.should_error_popup, EXCLUDED.light_primary_color, EXCLUDED.light_warn_color, EXCLUDED.light_background_color, EXCLUDED.light_font_color, EXCLUDED.light_logo_url, EXCLUDED.light_icon_url, EXCLUDED.dark_primary_color, EXCLUDED.dark_warn_color, EXCLUDED.dark_background_color, EXCLUDED.dark_font_color, EXCLUDED.dark_logo_url, EXCLUDED.dark_icon_url)",
expectedArgs: []interface{}{
anyArg{},
uint64(15),
diff --git a/internal/query/projection/login_policy.go b/internal/query/projection/login_policy.go
index 601a4531b..77fadc2ba 100644
--- a/internal/query/projection/login_policy.go
+++ b/internal/query/projection/login_policy.go
@@ -13,7 +13,7 @@ import (
)
const (
- LoginPolicyTable = "projections.login_policies4"
+ LoginPolicyTable = "projections.login_policies5"
LoginPolicyIDCol = "aggregate_id"
LoginPolicyInstanceIDCol = "instance_id"
@@ -25,6 +25,7 @@ const (
LoginPolicyAllowUsernamePasswordCol = "allow_username_password"
LoginPolicyAllowExternalIDPsCol = "allow_external_idps"
LoginPolicyForceMFACol = "force_mfa"
+ LoginPolicyForceMFALocalOnlyCol = "force_mfa_local_only"
LoginPolicy2FAsCol = "second_factors"
LoginPolicyMFAsCol = "multi_factors"
LoginPolicyPasswordlessTypeCol = "passwordless_type"
@@ -62,6 +63,7 @@ func newLoginPolicyProjection(ctx context.Context, config crdb.StatementHandlerC
crdb.NewColumn(LoginPolicyAllowUsernamePasswordCol, crdb.ColumnTypeBool),
crdb.NewColumn(LoginPolicyAllowExternalIDPsCol, crdb.ColumnTypeBool),
crdb.NewColumn(LoginPolicyForceMFACol, crdb.ColumnTypeBool),
+ crdb.NewColumn(LoginPolicyForceMFALocalOnlyCol, crdb.ColumnTypeBool, crdb.Default(false)),
crdb.NewColumn(LoginPolicy2FAsCol, crdb.ColumnTypeEnumArray, crdb.Nullable()),
crdb.NewColumn(LoginPolicyMFAsCol, crdb.ColumnTypeEnumArray, crdb.Nullable()),
crdb.NewColumn(LoginPolicyPasswordlessTypeCol, crdb.ColumnTypeEnum),
@@ -185,6 +187,7 @@ func (p *loginPolicyProjection) reduceLoginPolicyAdded(event eventstore.Event) (
handler.NewCol(LoginPolicyAllowUsernamePasswordCol, policyEvent.AllowUserNamePassword),
handler.NewCol(LoginPolicyAllowExternalIDPsCol, policyEvent.AllowExternalIDP),
handler.NewCol(LoginPolicyForceMFACol, policyEvent.ForceMFA),
+ handler.NewCol(LoginPolicyForceMFALocalOnlyCol, policyEvent.ForceMFALocalOnly),
handler.NewCol(LoginPolicyPasswordlessTypeCol, policyEvent.PasswordlessType),
handler.NewCol(LoginPolicyIsDefaultCol, isDefault),
handler.NewCol(LoginPolicyHidePWResetCol, policyEvent.HidePasswordReset),
@@ -228,6 +231,9 @@ func (p *loginPolicyProjection) reduceLoginPolicyChanged(event eventstore.Event)
if policyEvent.ForceMFA != nil {
cols = append(cols, handler.NewCol(LoginPolicyForceMFACol, *policyEvent.ForceMFA))
}
+ if policyEvent.ForceMFALocalOnly != nil {
+ cols = append(cols, handler.NewCol(LoginPolicyForceMFALocalOnlyCol, *policyEvent.ForceMFALocalOnly))
+ }
if policyEvent.PasswordlessType != nil {
cols = append(cols, handler.NewCol(LoginPolicyPasswordlessTypeCol, *policyEvent.PasswordlessType))
}
diff --git a/internal/query/projection/login_policy_test.go b/internal/query/projection/login_policy_test.go
index 8ade083c0..bd87c5c4b 100644
--- a/internal/query/projection/login_policy_test.go
+++ b/internal/query/projection/login_policy_test.go
@@ -24,7 +24,7 @@ func TestLoginPolicyProjection_reduces(t *testing.T) {
want wantReduce
}{
{
- name: "org reduceLoginPolicyAdded",
+ name: "org reduceLoginPolicyAdded without forceMFALocalOnly",
args: args{
event: getEvent(testEvent(
repository.EventType(org.LoginPolicyAddedEventType),
@@ -57,7 +57,7 @@ func TestLoginPolicyProjection_reduces(t *testing.T) {
executer: &testExecuter{
executions: []execution{
{
- expectedStmt: "INSERT INTO projections.login_policies4 (aggregate_id, instance_id, creation_date, change_date, sequence, allow_register, allow_username_password, allow_external_idps, force_mfa, passwordless_type, is_default, hide_password_reset, ignore_unknown_usernames, allow_domain_discovery, disable_login_with_email, disable_login_with_phone, default_redirect_uri, password_check_lifetime, external_login_check_lifetime, mfa_init_skip_lifetime, second_factor_check_lifetime, multi_factor_check_lifetime) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22)",
+ expectedStmt: "INSERT INTO projections.login_policies5 (aggregate_id, instance_id, creation_date, change_date, sequence, allow_register, allow_username_password, allow_external_idps, force_mfa, force_mfa_local_only, passwordless_type, is_default, hide_password_reset, ignore_unknown_usernames, allow_domain_discovery, disable_login_with_email, disable_login_with_phone, default_redirect_uri, password_check_lifetime, external_login_check_lifetime, mfa_init_skip_lifetime, second_factor_check_lifetime, multi_factor_check_lifetime) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23)",
expectedArgs: []interface{}{
"agg-id",
"instance-id",
@@ -68,6 +68,73 @@ func TestLoginPolicyProjection_reduces(t *testing.T) {
true,
false,
false,
+ false,
+ domain.PasswordlessTypeAllowed,
+ false,
+ true,
+ true,
+ true,
+ true,
+ true,
+ "https://example.com/redirect",
+ time.Millisecond * 10,
+ time.Millisecond * 10,
+ time.Millisecond * 10,
+ time.Millisecond * 10,
+ time.Millisecond * 10,
+ },
+ },
+ },
+ },
+ },
+ },
+ {
+ name: "org reduceLoginPolicyAdded",
+ args: args{
+ event: getEvent(testEvent(
+ repository.EventType(org.LoginPolicyAddedEventType),
+ org.AggregateType,
+ []byte(`{
+ "allowUsernamePassword": true,
+ "allowRegister": true,
+ "allowExternalIdp": true,
+ "forceMFA": true,
+ "forceMFALocalOnly": true,
+ "hidePasswordReset": true,
+ "ignoreUnknownUsernames": true,
+ "allowDomainDiscovery": true,
+ "disableLoginWithEmail": true,
+ "disableLoginWithPhone": true,
+ "passwordlessType": 1,
+ "defaultRedirectURI": "https://example.com/redirect",
+ "passwordCheckLifetime": 10000000,
+ "externalLoginCheckLifetime": 10000000,
+ "mfaInitSkipLifetime": 10000000,
+ "secondFactorCheckLifetime": 10000000,
+ "multiFactorCheckLifetime": 10000000
+ }`),
+ ), org.LoginPolicyAddedEventMapper),
+ },
+ reduce: (&loginPolicyProjection{}).reduceLoginPolicyAdded,
+ want: wantReduce{
+ aggregateType: eventstore.AggregateType("org"),
+ sequence: 15,
+ previousSequence: 10,
+ executer: &testExecuter{
+ executions: []execution{
+ {
+ expectedStmt: "INSERT INTO projections.login_policies5 (aggregate_id, instance_id, creation_date, change_date, sequence, allow_register, allow_username_password, allow_external_idps, force_mfa, force_mfa_local_only, passwordless_type, is_default, hide_password_reset, ignore_unknown_usernames, allow_domain_discovery, disable_login_with_email, disable_login_with_phone, default_redirect_uri, password_check_lifetime, external_login_check_lifetime, mfa_init_skip_lifetime, second_factor_check_lifetime, multi_factor_check_lifetime) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23)",
+ expectedArgs: []interface{}{
+ "agg-id",
+ "instance-id",
+ anyArg{},
+ anyArg{},
+ uint64(15),
+ true,
+ true,
+ true,
+ true,
+ true,
domain.PasswordlessTypeAllowed,
false,
true,
@@ -99,6 +166,7 @@ func TestLoginPolicyProjection_reduces(t *testing.T) {
"allowRegister": true,
"allowExternalIdp": true,
"forceMFA": true,
+ "forceMFALocalOnly": true,
"hidePasswordReset": true,
"ignoreUnknownUsernames": true,
"allowDomainDiscovery": true,
@@ -121,7 +189,7 @@ func TestLoginPolicyProjection_reduces(t *testing.T) {
executer: &testExecuter{
executions: []execution{
{
- expectedStmt: "UPDATE projections.login_policies4 SET (change_date, sequence, allow_register, allow_username_password, allow_external_idps, force_mfa, passwordless_type, hide_password_reset, ignore_unknown_usernames, allow_domain_discovery, disable_login_with_email, disable_login_with_phone, default_redirect_uri, password_check_lifetime, external_login_check_lifetime, mfa_init_skip_lifetime, second_factor_check_lifetime, multi_factor_check_lifetime) = ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18) WHERE (aggregate_id = $19) AND (instance_id = $20)",
+ expectedStmt: "UPDATE projections.login_policies5 SET (change_date, sequence, allow_register, allow_username_password, allow_external_idps, force_mfa, force_mfa_local_only, passwordless_type, hide_password_reset, ignore_unknown_usernames, allow_domain_discovery, disable_login_with_email, disable_login_with_phone, default_redirect_uri, password_check_lifetime, external_login_check_lifetime, mfa_init_skip_lifetime, second_factor_check_lifetime, multi_factor_check_lifetime) = ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19) WHERE (aggregate_id = $20) AND (instance_id = $21)",
expectedArgs: []interface{}{
anyArg{},
uint64(15),
@@ -129,6 +197,7 @@ func TestLoginPolicyProjection_reduces(t *testing.T) {
true,
true,
true,
+ true,
domain.PasswordlessTypeAllowed,
true,
true,
@@ -168,7 +237,7 @@ func TestLoginPolicyProjection_reduces(t *testing.T) {
executer: &testExecuter{
executions: []execution{
{
- expectedStmt: "UPDATE projections.login_policies4 SET (change_date, sequence, multi_factors) = ($1, $2, array_append(multi_factors, $3)) WHERE (aggregate_id = $4) AND (instance_id = $5)",
+ expectedStmt: "UPDATE projections.login_policies5 SET (change_date, sequence, multi_factors) = ($1, $2, array_append(multi_factors, $3)) WHERE (aggregate_id = $4) AND (instance_id = $5)",
expectedArgs: []interface{}{
anyArg{},
uint64(15),
@@ -200,7 +269,7 @@ func TestLoginPolicyProjection_reduces(t *testing.T) {
executer: &testExecuter{
executions: []execution{
{
- expectedStmt: "UPDATE projections.login_policies4 SET (change_date, sequence, multi_factors) = ($1, $2, array_remove(multi_factors, $3)) WHERE (aggregate_id = $4) AND (instance_id = $5)",
+ expectedStmt: "UPDATE projections.login_policies5 SET (change_date, sequence, multi_factors) = ($1, $2, array_remove(multi_factors, $3)) WHERE (aggregate_id = $4) AND (instance_id = $5)",
expectedArgs: []interface{}{
anyArg{},
uint64(15),
@@ -230,7 +299,7 @@ func TestLoginPolicyProjection_reduces(t *testing.T) {
executer: &testExecuter{
executions: []execution{
{
- expectedStmt: "DELETE FROM projections.login_policies4 WHERE (aggregate_id = $1) AND (instance_id = $2)",
+ expectedStmt: "DELETE FROM projections.login_policies5 WHERE (aggregate_id = $1) AND (instance_id = $2)",
expectedArgs: []interface{}{
"agg-id",
"instance-id",
@@ -259,7 +328,7 @@ func TestLoginPolicyProjection_reduces(t *testing.T) {
executer: &testExecuter{
executions: []execution{
{
- expectedStmt: "UPDATE projections.login_policies4 SET (change_date, sequence, second_factors) = ($1, $2, array_append(second_factors, $3)) WHERE (aggregate_id = $4) AND (instance_id = $5)",
+ expectedStmt: "UPDATE projections.login_policies5 SET (change_date, sequence, second_factors) = ($1, $2, array_append(second_factors, $3)) WHERE (aggregate_id = $4) AND (instance_id = $5)",
expectedArgs: []interface{}{
anyArg{},
uint64(15),
@@ -291,7 +360,7 @@ func TestLoginPolicyProjection_reduces(t *testing.T) {
executer: &testExecuter{
executions: []execution{
{
- expectedStmt: "UPDATE projections.login_policies4 SET (change_date, sequence, second_factors) = ($1, $2, array_remove(second_factors, $3)) WHERE (aggregate_id = $4) AND (instance_id = $5)",
+ expectedStmt: "UPDATE projections.login_policies5 SET (change_date, sequence, second_factors) = ($1, $2, array_remove(second_factors, $3)) WHERE (aggregate_id = $4) AND (instance_id = $5)",
expectedArgs: []interface{}{
anyArg{},
uint64(15),
@@ -314,8 +383,9 @@ func TestLoginPolicyProjection_reduces(t *testing.T) {
[]byte(`{
"allowUsernamePassword": true,
"allowRegister": true,
- "allowExternalIdp": false,
- "forceMFA": false,
+ "allowExternalIdp": true,
+ "forceMFA": true,
+ "forceMFALocalOnly": true,
"hidePasswordReset": true,
"ignoreUnknownUsernames": true,
"allowDomainDiscovery": true,
@@ -338,7 +408,7 @@ func TestLoginPolicyProjection_reduces(t *testing.T) {
executer: &testExecuter{
executions: []execution{
{
- expectedStmt: "INSERT INTO projections.login_policies4 (aggregate_id, instance_id, creation_date, change_date, sequence, allow_register, allow_username_password, allow_external_idps, force_mfa, passwordless_type, is_default, hide_password_reset, ignore_unknown_usernames, allow_domain_discovery, disable_login_with_email, disable_login_with_phone, default_redirect_uri, password_check_lifetime, external_login_check_lifetime, mfa_init_skip_lifetime, second_factor_check_lifetime, multi_factor_check_lifetime) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22)",
+ expectedStmt: "INSERT INTO projections.login_policies5 (aggregate_id, instance_id, creation_date, change_date, sequence, allow_register, allow_username_password, allow_external_idps, force_mfa, force_mfa_local_only, passwordless_type, is_default, hide_password_reset, ignore_unknown_usernames, allow_domain_discovery, disable_login_with_email, disable_login_with_phone, default_redirect_uri, password_check_lifetime, external_login_check_lifetime, mfa_init_skip_lifetime, second_factor_check_lifetime, multi_factor_check_lifetime) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23)",
expectedArgs: []interface{}{
"agg-id",
"instance-id",
@@ -347,8 +417,9 @@ func TestLoginPolicyProjection_reduces(t *testing.T) {
uint64(15),
true,
true,
- false,
- false,
+ true,
+ true,
+ true,
domain.PasswordlessTypeAllowed,
true,
true,
@@ -380,6 +451,7 @@ func TestLoginPolicyProjection_reduces(t *testing.T) {
"allowRegister": true,
"allowExternalIdp": true,
"forceMFA": true,
+ "forceMFALocalOnly": true,
"hidePasswordReset": true,
"ignoreUnknownUsernames": true,
"allowDomainDiscovery": true,
@@ -397,7 +469,7 @@ func TestLoginPolicyProjection_reduces(t *testing.T) {
executer: &testExecuter{
executions: []execution{
{
- expectedStmt: "UPDATE projections.login_policies4 SET (change_date, sequence, allow_register, allow_username_password, allow_external_idps, force_mfa, passwordless_type, hide_password_reset, ignore_unknown_usernames, allow_domain_discovery, disable_login_with_email, disable_login_with_phone, default_redirect_uri) = ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13) WHERE (aggregate_id = $14) AND (instance_id = $15)",
+ expectedStmt: "UPDATE projections.login_policies5 SET (change_date, sequence, allow_register, allow_username_password, allow_external_idps, force_mfa, force_mfa_local_only, passwordless_type, hide_password_reset, ignore_unknown_usernames, allow_domain_discovery, disable_login_with_email, disable_login_with_phone, default_redirect_uri) = ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14) WHERE (aggregate_id = $15) AND (instance_id = $16)",
expectedArgs: []interface{}{
anyArg{},
uint64(15),
@@ -405,6 +477,7 @@ func TestLoginPolicyProjection_reduces(t *testing.T) {
true,
true,
true,
+ true,
domain.PasswordlessTypeAllowed,
true,
true,
@@ -439,7 +512,7 @@ func TestLoginPolicyProjection_reduces(t *testing.T) {
executer: &testExecuter{
executions: []execution{
{
- expectedStmt: "UPDATE projections.login_policies4 SET (change_date, sequence, multi_factors) = ($1, $2, array_append(multi_factors, $3)) WHERE (aggregate_id = $4) AND (instance_id = $5)",
+ expectedStmt: "UPDATE projections.login_policies5 SET (change_date, sequence, multi_factors) = ($1, $2, array_append(multi_factors, $3)) WHERE (aggregate_id = $4) AND (instance_id = $5)",
expectedArgs: []interface{}{
anyArg{},
uint64(15),
@@ -471,7 +544,7 @@ func TestLoginPolicyProjection_reduces(t *testing.T) {
executer: &testExecuter{
executions: []execution{
{
- expectedStmt: "UPDATE projections.login_policies4 SET (change_date, sequence, multi_factors) = ($1, $2, array_remove(multi_factors, $3)) WHERE (aggregate_id = $4) AND (instance_id = $5)",
+ expectedStmt: "UPDATE projections.login_policies5 SET (change_date, sequence, multi_factors) = ($1, $2, array_remove(multi_factors, $3)) WHERE (aggregate_id = $4) AND (instance_id = $5)",
expectedArgs: []interface{}{
anyArg{},
uint64(15),
@@ -503,7 +576,7 @@ func TestLoginPolicyProjection_reduces(t *testing.T) {
executer: &testExecuter{
executions: []execution{
{
- expectedStmt: "UPDATE projections.login_policies4 SET (change_date, sequence, second_factors) = ($1, $2, array_append(second_factors, $3)) WHERE (aggregate_id = $4) AND (instance_id = $5)",
+ expectedStmt: "UPDATE projections.login_policies5 SET (change_date, sequence, second_factors) = ($1, $2, array_append(second_factors, $3)) WHERE (aggregate_id = $4) AND (instance_id = $5)",
expectedArgs: []interface{}{
anyArg{},
uint64(15),
@@ -535,7 +608,7 @@ func TestLoginPolicyProjection_reduces(t *testing.T) {
executer: &testExecuter{
executions: []execution{
{
- expectedStmt: "UPDATE projections.login_policies4 SET (change_date, sequence, second_factors) = ($1, $2, array_remove(second_factors, $3)) WHERE (aggregate_id = $4) AND (instance_id = $5)",
+ expectedStmt: "UPDATE projections.login_policies5 SET (change_date, sequence, second_factors) = ($1, $2, array_remove(second_factors, $3)) WHERE (aggregate_id = $4) AND (instance_id = $5)",
expectedArgs: []interface{}{
anyArg{},
uint64(15),
@@ -565,7 +638,7 @@ func TestLoginPolicyProjection_reduces(t *testing.T) {
executer: &testExecuter{
executions: []execution{
{
- expectedStmt: "UPDATE projections.login_policies4 SET (change_date, sequence, owner_removed) = ($1, $2, $3) WHERE (instance_id = $4) AND (aggregate_id = $5)",
+ expectedStmt: "UPDATE projections.login_policies5 SET (change_date, sequence, owner_removed) = ($1, $2, $3) WHERE (instance_id = $4) AND (aggregate_id = $5)",
expectedArgs: []interface{}{
anyArg{},
uint64(15),
@@ -595,7 +668,7 @@ func TestLoginPolicyProjection_reduces(t *testing.T) {
executer: &testExecuter{
executions: []execution{
{
- expectedStmt: "DELETE FROM projections.login_policies4 WHERE (instance_id = $1)",
+ expectedStmt: "DELETE FROM projections.login_policies5 WHERE (instance_id = $1)",
expectedArgs: []interface{}{
"agg-id",
},
diff --git a/internal/query/projection/milestones.go b/internal/query/projection/milestones.go
new file mode 100644
index 000000000..dbbf22a60
--- /dev/null
+++ b/internal/query/projection/milestones.go
@@ -0,0 +1,295 @@
+package projection
+
+import (
+ "context"
+ "strconv"
+ "strings"
+
+ "github.com/zitadel/zitadel/internal/eventstore"
+ "github.com/zitadel/zitadel/internal/eventstore/handler"
+ "github.com/zitadel/zitadel/internal/eventstore/handler/crdb"
+ "github.com/zitadel/zitadel/internal/repository/instance"
+ "github.com/zitadel/zitadel/internal/repository/milestone"
+ "github.com/zitadel/zitadel/internal/repository/project"
+ "github.com/zitadel/zitadel/internal/repository/user"
+)
+
+const (
+ MilestonesProjectionTable = "projections.milestones"
+
+ MilestoneColumnInstanceID = "instance_id"
+ MilestoneColumnType = "type"
+ MilestoneColumnPrimaryDomain = "primary_domain"
+ MilestoneColumnReachedDate = "reached_date"
+ MilestoneColumnPushedDate = "last_pushed_date"
+ MilestoneColumnIgnoreClientIDs = "ignore_client_ids"
+)
+
+type milestoneProjection struct {
+ crdb.StatementHandler
+}
+
+func newMilestoneProjection(ctx context.Context, config crdb.StatementHandlerConfig) *milestoneProjection {
+ p := new(milestoneProjection)
+ config.ProjectionName = MilestonesProjectionTable
+ config.Reducers = p.reducers()
+ config.InitCheck = crdb.NewMultiTableCheck(
+ crdb.NewTable([]*crdb.Column{
+ crdb.NewColumn(MilestoneColumnInstanceID, crdb.ColumnTypeText),
+ crdb.NewColumn(MilestoneColumnType, crdb.ColumnTypeEnum),
+ crdb.NewColumn(MilestoneColumnReachedDate, crdb.ColumnTypeTimestamp, crdb.Nullable()),
+ crdb.NewColumn(MilestoneColumnPushedDate, crdb.ColumnTypeTimestamp, crdb.Nullable()),
+ crdb.NewColumn(MilestoneColumnPrimaryDomain, crdb.ColumnTypeText, crdb.Nullable()),
+ crdb.NewColumn(MilestoneColumnIgnoreClientIDs, crdb.ColumnTypeTextArray, crdb.Nullable()),
+ },
+ crdb.NewPrimaryKey(MilestoneColumnInstanceID, MilestoneColumnType),
+ ),
+ )
+ p.StatementHandler = crdb.NewStatementHandler(ctx, config)
+ return p
+}
+
+func (p *milestoneProjection) reducers() []handler.AggregateReducer {
+ return []handler.AggregateReducer{
+ {
+ Aggregate: instance.AggregateType,
+ EventRedusers: []handler.EventReducer{
+ {
+ Event: instance.InstanceAddedEventType,
+ Reduce: p.reduceInstanceAdded,
+ },
+ {
+ Event: instance.InstanceDomainPrimarySetEventType,
+ Reduce: p.reduceInstanceDomainPrimarySet,
+ },
+ {
+ Event: instance.InstanceRemovedEventType,
+ Reduce: p.reduceInstanceRemoved,
+ },
+ },
+ },
+ {
+ Aggregate: project.AggregateType,
+ EventRedusers: []handler.EventReducer{
+ {
+ Event: project.ProjectAddedType,
+ Reduce: p.reduceProjectAdded,
+ },
+ {
+ Event: project.ApplicationAddedType,
+ Reduce: p.reduceApplicationAdded,
+ },
+ {
+ Event: project.OIDCConfigAddedType,
+ Reduce: p.reduceOIDCConfigAdded,
+ },
+ {
+ Event: project.APIConfigAddedType,
+ Reduce: p.reduceAPIConfigAdded,
+ },
+ },
+ },
+ {
+ Aggregate: user.AggregateType,
+ EventRedusers: []handler.EventReducer{
+ {
+ // user.UserTokenAddedType is not emitted on creation of personal access tokens
+ // PATs have no effect on milestone.AuthenticationSucceededOnApplication or milestone.AuthenticationSucceededOnInstance
+ Event: user.UserTokenAddedType,
+ Reduce: p.reduceUserTokenAdded,
+ },
+ },
+ },
+ {
+ Aggregate: milestone.AggregateType,
+ EventRedusers: []handler.EventReducer{
+ {
+ Event: milestone.PushedEventType,
+ Reduce: p.reduceMilestonePushed,
+ },
+ },
+ },
+ }
+}
+
+func (p *milestoneProjection) reduceInstanceAdded(event eventstore.Event) (*handler.Statement, error) {
+ e, err := assertEvent[*instance.InstanceAddedEvent](event)
+ if err != nil {
+ return nil, err
+ }
+ allTypes := milestone.AllTypes()
+ statements := make([]func(eventstore.Event) crdb.Exec, 0, len(allTypes))
+ for _, msType := range allTypes {
+ createColumns := []handler.Column{
+ handler.NewCol(MilestoneColumnInstanceID, e.Aggregate().InstanceID),
+ handler.NewCol(MilestoneColumnType, msType),
+ }
+ if msType == milestone.InstanceCreated {
+ createColumns = append(createColumns, handler.NewCol(MilestoneColumnReachedDate, event.CreationDate()))
+ }
+ statements = append(statements, crdb.AddCreateStatement(createColumns))
+ }
+ return crdb.NewMultiStatement(e, statements...), nil
+}
+
+func (p *milestoneProjection) reduceInstanceDomainPrimarySet(event eventstore.Event) (*handler.Statement, error) {
+ e, err := assertEvent[*instance.DomainPrimarySetEvent](event)
+ if err != nil {
+ return nil, err
+ }
+ return crdb.NewUpdateStatement(
+ e,
+ []handler.Column{
+ handler.NewCol(MilestoneColumnPrimaryDomain, e.Domain),
+ },
+ []handler.Condition{
+ handler.NewCond(MilestoneColumnInstanceID, e.Aggregate().InstanceID),
+ crdb.NewIsNullCond(MilestoneColumnPushedDate),
+ },
+ ), nil
+}
+
+func (p *milestoneProjection) reduceProjectAdded(event eventstore.Event) (*handler.Statement, error) {
+ if _, err := assertEvent[*project.ProjectAddedEvent](event); err != nil {
+ return nil, err
+ }
+ return p.reduceReachedIfUserEventFunc(milestone.ProjectCreated)(event)
+}
+
+func (p *milestoneProjection) reduceApplicationAdded(event eventstore.Event) (*handler.Statement, error) {
+ if _, err := assertEvent[*project.ApplicationAddedEvent](event); err != nil {
+ return nil, err
+ }
+ return p.reduceReachedIfUserEventFunc(milestone.ApplicationCreated)(event)
+}
+
+func (p *milestoneProjection) reduceOIDCConfigAdded(event eventstore.Event) (*handler.Statement, error) {
+ e, err := assertEvent[*project.OIDCConfigAddedEvent](event)
+ if err != nil {
+ return nil, err
+ }
+ return p.reduceAppConfigAdded(e, e.ClientID)
+}
+
+func (p *milestoneProjection) reduceAPIConfigAdded(event eventstore.Event) (*handler.Statement, error) {
+ e, err := assertEvent[*project.APIConfigAddedEvent](event)
+ if err != nil {
+ return nil, err
+ }
+ return p.reduceAppConfigAdded(e, e.ClientID)
+}
+
+func (p *milestoneProjection) reduceUserTokenAdded(event eventstore.Event) (*handler.Statement, error) {
+ e, err := assertEvent[*user.UserTokenAddedEvent](event)
+ if err != nil {
+ return nil, err
+ }
+ statements := []func(eventstore.Event) crdb.Exec{
+ crdb.AddUpdateStatement(
+ []handler.Column{
+ handler.NewCol(MilestoneColumnReachedDate, event.CreationDate()),
+ },
+ []handler.Condition{
+ handler.NewCond(MilestoneColumnInstanceID, event.Aggregate().InstanceID),
+ handler.NewCond(MilestoneColumnType, milestone.AuthenticationSucceededOnInstance),
+ crdb.NewIsNullCond(MilestoneColumnReachedDate),
+ },
+ ),
+ }
+ // We ignore authentications without app, for example JWT profile or PAT
+ if e.ApplicationID != "" {
+ statements = append(statements, crdb.AddUpdateStatement(
+ []handler.Column{
+ handler.NewCol(MilestoneColumnReachedDate, event.CreationDate()),
+ },
+ []handler.Condition{
+ handler.NewCond(MilestoneColumnInstanceID, event.Aggregate().InstanceID),
+ handler.NewCond(MilestoneColumnType, milestone.AuthenticationSucceededOnApplication),
+ crdb.Not(crdb.NewTextArrayContainsCond(MilestoneColumnIgnoreClientIDs, e.ApplicationID)),
+ crdb.NewIsNullCond(MilestoneColumnReachedDate),
+ },
+ ))
+ }
+ return crdb.NewMultiStatement(e, statements...), nil
+}
+
+func (p *milestoneProjection) reduceInstanceRemoved(event eventstore.Event) (*handler.Statement, error) {
+ if _, err := assertEvent[*instance.InstanceRemovedEvent](event); err != nil {
+ return nil, err
+ }
+ return p.reduceReachedFunc(milestone.InstanceDeleted)(event)
+}
+
+func (p *milestoneProjection) reduceMilestonePushed(event eventstore.Event) (*handler.Statement, error) {
+ e, err := assertEvent[*milestone.PushedEvent](event)
+ if err != nil {
+ return nil, err
+ }
+ if e.MilestoneType != milestone.InstanceDeleted {
+ return crdb.NewUpdateStatement(
+ event,
+ []handler.Column{
+ handler.NewCol(MilestoneColumnPushedDate, event.CreationDate()),
+ },
+ []handler.Condition{
+ handler.NewCond(MilestoneColumnInstanceID, event.Aggregate().InstanceID),
+ handler.NewCond(MilestoneColumnType, e.MilestoneType),
+ },
+ ), nil
+ }
+ return crdb.NewDeleteStatement(
+ event,
+ []handler.Condition{
+ handler.NewCond(MilestoneColumnInstanceID, event.Aggregate().InstanceID),
+ },
+ ), nil
+}
+
+func (p *milestoneProjection) reduceReachedIfUserEventFunc(msType milestone.Type) func(event eventstore.Event) (*handler.Statement, error) {
+ return func(event eventstore.Event) (*handler.Statement, error) {
+ if p.isSystemEvent(event) {
+ return crdb.NewNoOpStatement(event), nil
+ }
+ return p.reduceReachedFunc(msType)(event)
+ }
+}
+
+func (p *milestoneProjection) reduceReachedFunc(msType milestone.Type) func(event eventstore.Event) (*handler.Statement, error) {
+ return func(event eventstore.Event) (*handler.Statement, error) {
+ return crdb.NewUpdateStatement(event, []handler.Column{
+ handler.NewCol(MilestoneColumnReachedDate, event.CreationDate()),
+ },
+ []handler.Condition{
+ handler.NewCond(MilestoneColumnInstanceID, event.Aggregate().InstanceID),
+ handler.NewCond(MilestoneColumnType, msType),
+ crdb.NewIsNullCond(MilestoneColumnReachedDate),
+ }), nil
+ }
+}
+
+func (p *milestoneProjection) reduceAppConfigAdded(event eventstore.Event, clientID string) (*handler.Statement, error) {
+ if !p.isSystemEvent(event) {
+ return crdb.NewNoOpStatement(event), nil
+ }
+ return crdb.NewUpdateStatement(
+ event,
+ []handler.Column{
+ crdb.NewArrayAppendCol(MilestoneColumnIgnoreClientIDs, clientID),
+ },
+ []handler.Condition{
+ handler.NewCond(MilestoneColumnInstanceID, event.Aggregate().InstanceID),
+ handler.NewCond(MilestoneColumnType, milestone.AuthenticationSucceededOnApplication),
+ crdb.NewIsNullCond(MilestoneColumnReachedDate),
+ },
+ ), nil
+}
+
+func (p *milestoneProjection) isSystemEvent(event eventstore.Event) bool {
+ if userId, err := strconv.Atoi(event.EditorUser()); err == nil && userId > 0 {
+ return false
+ }
+ lowerEditorService := strings.ToLower(event.EditorService())
+ return lowerEditorService == "" ||
+ lowerEditorService == "system" ||
+ lowerEditorService == "system-api"
+}
diff --git a/internal/query/projection/milestones_test.go b/internal/query/projection/milestones_test.go
new file mode 100644
index 000000000..5138fc157
--- /dev/null
+++ b/internal/query/projection/milestones_test.go
@@ -0,0 +1,404 @@
+package projection
+
+import (
+ "testing"
+ "time"
+
+ "github.com/zitadel/zitadel/internal/database"
+ "github.com/zitadel/zitadel/internal/errors"
+ "github.com/zitadel/zitadel/internal/eventstore"
+ "github.com/zitadel/zitadel/internal/eventstore/handler"
+ "github.com/zitadel/zitadel/internal/eventstore/repository"
+ "github.com/zitadel/zitadel/internal/repository/instance"
+ "github.com/zitadel/zitadel/internal/repository/milestone"
+ "github.com/zitadel/zitadel/internal/repository/project"
+ "github.com/zitadel/zitadel/internal/repository/user"
+)
+
+func TestMilestonesProjection_reduces(t *testing.T) {
+ type args struct {
+ event func(t *testing.T) eventstore.Event
+ }
+ now := time.Now()
+ tests := []struct {
+ name string
+ args args
+ reduce func(event eventstore.Event) (*handler.Statement, error)
+ want wantReduce
+ }{
+ {
+ name: "reduceInstanceAdded",
+ args: args{
+ event: getEvent(timedTestEvent(
+ repository.EventType(instance.InstanceAddedEventType),
+ instance.AggregateType,
+ []byte(`{}`),
+ now,
+ ), instance.InstanceAddedEventMapper),
+ },
+ reduce: (&milestoneProjection{}).reduceInstanceAdded,
+ want: wantReduce{
+ aggregateType: eventstore.AggregateType("instance"),
+ sequence: 15,
+ previousSequence: 10,
+ executer: &testExecuter{
+ executions: []execution{
+ {
+ expectedStmt: "INSERT INTO projections.milestones (instance_id, type, reached_date) VALUES ($1, $2, $3)",
+ expectedArgs: []interface{}{
+ "instance-id",
+ milestone.InstanceCreated,
+ now,
+ },
+ },
+ {
+ expectedStmt: "INSERT INTO projections.milestones (instance_id, type) VALUES ($1, $2)",
+ expectedArgs: []interface{}{
+ "instance-id",
+ milestone.AuthenticationSucceededOnInstance,
+ },
+ },
+ {
+ expectedStmt: "INSERT INTO projections.milestones (instance_id, type) VALUES ($1, $2)",
+ expectedArgs: []interface{}{
+ "instance-id",
+ milestone.ProjectCreated,
+ },
+ },
+ {
+ expectedStmt: "INSERT INTO projections.milestones (instance_id, type) VALUES ($1, $2)",
+ expectedArgs: []interface{}{
+ "instance-id",
+ milestone.ApplicationCreated,
+ },
+ },
+ {
+ expectedStmt: "INSERT INTO projections.milestones (instance_id, type) VALUES ($1, $2)",
+ expectedArgs: []interface{}{
+ "instance-id",
+ milestone.AuthenticationSucceededOnApplication,
+ },
+ },
+ {
+ expectedStmt: "INSERT INTO projections.milestones (instance_id, type) VALUES ($1, $2)",
+ expectedArgs: []interface{}{
+ "instance-id",
+ milestone.InstanceDeleted,
+ },
+ },
+ },
+ },
+ },
+ },
+ {
+ name: "reduceInstancePrimaryDomainSet",
+ args: args{
+ event: getEvent(testEvent(
+ repository.EventType(instance.InstanceDomainPrimarySetEventType),
+ instance.AggregateType,
+ []byte(`{"domain": "my.domain"}`),
+ ), instance.DomainPrimarySetEventMapper),
+ },
+ reduce: (&milestoneProjection{}).reduceInstanceDomainPrimarySet,
+ want: wantReduce{
+ aggregateType: eventstore.AggregateType("instance"),
+ sequence: 15,
+ previousSequence: 10,
+ executer: &testExecuter{
+ executions: []execution{
+ {
+ expectedStmt: "UPDATE projections.milestones SET primary_domain = $1 WHERE (instance_id = $2) AND (last_pushed_date IS NULL)",
+ expectedArgs: []interface{}{
+ "my.domain",
+ "instance-id",
+ },
+ },
+ },
+ },
+ },
+ },
+ {
+ name: "reduceProjectAdded",
+ args: args{
+ event: getEvent(timedTestEvent(
+ repository.EventType(project.ProjectAddedType),
+ project.AggregateType,
+ []byte(`{}`),
+ now,
+ ), project.ProjectAddedEventMapper),
+ },
+ reduce: (&milestoneProjection{}).reduceProjectAdded,
+ want: wantReduce{
+ aggregateType: eventstore.AggregateType("project"),
+ sequence: 15,
+ previousSequence: 10,
+ executer: &testExecuter{
+ executions: []execution{
+ {
+ expectedStmt: "UPDATE projections.milestones SET reached_date = $1 WHERE (instance_id = $2) AND (type = $3) AND (reached_date IS NULL)",
+ expectedArgs: []interface{}{
+ now,
+ "instance-id",
+ milestone.ProjectCreated,
+ },
+ },
+ },
+ },
+ },
+ },
+ {
+ name: "reduceApplicationAdded",
+ args: args{
+ event: getEvent(timedTestEvent(
+ repository.EventType(project.ApplicationAddedType),
+ project.AggregateType,
+ []byte(`{}`),
+ now,
+ ), project.ApplicationAddedEventMapper),
+ },
+ reduce: (&milestoneProjection{}).reduceApplicationAdded,
+ want: wantReduce{
+ aggregateType: eventstore.AggregateType("project"),
+ sequence: 15,
+ previousSequence: 10,
+ executer: &testExecuter{
+ executions: []execution{
+ {
+ expectedStmt: "UPDATE projections.milestones SET reached_date = $1 WHERE (instance_id = $2) AND (type = $3) AND (reached_date IS NULL)",
+ expectedArgs: []interface{}{
+ now,
+ "instance-id",
+ milestone.ApplicationCreated,
+ },
+ },
+ },
+ },
+ },
+ },
+ {
+ name: "reduceOIDCConfigAdded user event",
+ args: args{
+ event: getEvent(testEvent(
+ repository.EventType(project.OIDCConfigAddedType),
+ project.AggregateType,
+ []byte(`{}`),
+ ), project.OIDCConfigAddedEventMapper),
+ },
+ reduce: (&milestoneProjection{}).reduceOIDCConfigAdded,
+ want: wantReduce{
+ aggregateType: eventstore.AggregateType("project"),
+ sequence: 15,
+ previousSequence: 10,
+ executer: &testExecuter{},
+ },
+ },
+ {
+ name: "reduceOIDCConfigAdded system event",
+ args: args{
+ event: getEvent(toSystemEvent(testEvent(
+ repository.EventType(project.OIDCConfigAddedType),
+ project.AggregateType,
+ []byte(`{"clientId": "client-id"}`),
+ )), project.OIDCConfigAddedEventMapper),
+ },
+ reduce: (&milestoneProjection{}).reduceOIDCConfigAdded,
+ want: wantReduce{
+ aggregateType: eventstore.AggregateType("project"),
+ sequence: 15,
+ previousSequence: 10,
+ executer: &testExecuter{
+ executions: []execution{
+ {
+ expectedStmt: "UPDATE projections.milestones SET ignore_client_ids = array_append(ignore_client_ids, $1) WHERE (instance_id = $2) AND (type = $3) AND (reached_date IS NULL)",
+ expectedArgs: []interface{}{
+ "client-id",
+ "instance-id",
+ milestone.AuthenticationSucceededOnApplication,
+ },
+ },
+ },
+ },
+ },
+ },
+ {
+ name: "reduceAPIConfigAdded user event",
+ args: args{
+ event: getEvent(testEvent(
+ repository.EventType(project.APIConfigAddedType),
+ project.AggregateType,
+ []byte(`{}`),
+ ), project.APIConfigAddedEventMapper),
+ },
+ reduce: (&milestoneProjection{}).reduceAPIConfigAdded,
+ want: wantReduce{
+ aggregateType: eventstore.AggregateType("project"),
+ sequence: 15,
+ previousSequence: 10,
+ executer: &testExecuter{},
+ },
+ },
+ {
+ name: "reduceAPIConfigAdded system event",
+ args: args{
+ event: getEvent(toSystemEvent(testEvent(
+ repository.EventType(project.APIConfigAddedType),
+ project.AggregateType,
+ []byte(`{"clientId": "client-id"}`),
+ )), project.APIConfigAddedEventMapper),
+ },
+ reduce: (&milestoneProjection{}).reduceAPIConfigAdded,
+ want: wantReduce{
+ aggregateType: eventstore.AggregateType("project"),
+ sequence: 15,
+ previousSequence: 10,
+ executer: &testExecuter{
+ executions: []execution{
+ {
+ expectedStmt: "UPDATE projections.milestones SET ignore_client_ids = array_append(ignore_client_ids, $1) WHERE (instance_id = $2) AND (type = $3) AND (reached_date IS NULL)",
+ expectedArgs: []interface{}{
+ "client-id",
+ "instance-id",
+ milestone.AuthenticationSucceededOnApplication,
+ },
+ },
+ },
+ },
+ },
+ },
+ {
+ name: "reduceUserTokenAdded",
+ args: args{
+ event: getEvent(timedTestEvent(
+ repository.EventType(user.UserTokenAddedType),
+ user.AggregateType,
+ []byte(`{"applicationId": "client-id"}`),
+ now,
+ ), user.UserTokenAddedEventMapper),
+ },
+ reduce: (&milestoneProjection{}).reduceUserTokenAdded,
+ want: wantReduce{
+ aggregateType: eventstore.AggregateType("user"),
+ sequence: 15,
+ previousSequence: 10,
+ executer: &testExecuter{
+ // TODO: This can be optimized to only use one statement with OR
+ executions: []execution{
+ {
+ expectedStmt: "UPDATE projections.milestones SET reached_date = $1 WHERE (instance_id = $2) AND (type = $3) AND (reached_date IS NULL)",
+ expectedArgs: []interface{}{
+ now,
+ "instance-id",
+ milestone.AuthenticationSucceededOnInstance,
+ },
+ },
+ {
+ expectedStmt: "UPDATE projections.milestones SET reached_date = $1 WHERE (instance_id = $2) AND (type = $3) AND (NOT (ignore_client_ids @> $4)) AND (reached_date IS NULL)",
+ expectedArgs: []interface{}{
+ now,
+ "instance-id",
+ milestone.AuthenticationSucceededOnApplication,
+ database.StringArray{"client-id"},
+ },
+ },
+ },
+ },
+ },
+ },
+ {
+ name: "reduceInstanceRemoved",
+ args: args{
+ event: getEvent(timedTestEvent(
+ repository.EventType(instance.InstanceRemovedEventType),
+ instance.AggregateType,
+ []byte(`{}`),
+ now,
+ ), instance.InstanceRemovedEventMapper),
+ },
+ reduce: (&milestoneProjection{}).reduceInstanceRemoved,
+ want: wantReduce{
+ aggregateType: eventstore.AggregateType("instance"),
+ sequence: 15,
+ previousSequence: 10,
+ executer: &testExecuter{
+ executions: []execution{
+ {
+ expectedStmt: "UPDATE projections.milestones SET reached_date = $1 WHERE (instance_id = $2) AND (type = $3) AND (reached_date IS NULL)",
+ expectedArgs: []interface{}{
+ now,
+ "instance-id",
+ milestone.InstanceDeleted,
+ },
+ },
+ },
+ },
+ },
+ },
+ {
+ name: "reduceMilestonePushed normal milestone",
+ args: args{
+ event: getEvent(timedTestEvent(
+ repository.EventType(milestone.PushedEventType),
+ milestone.AggregateType,
+ []byte(`{"type": "ProjectCreated"}`),
+ now,
+ ), milestone.PushedEventMapper),
+ },
+ reduce: (&milestoneProjection{}).reduceMilestonePushed,
+ want: wantReduce{
+ aggregateType: eventstore.AggregateType("milestone"),
+ sequence: 15,
+ previousSequence: 10,
+ executer: &testExecuter{
+ executions: []execution{
+ {
+ expectedStmt: "UPDATE projections.milestones SET last_pushed_date = $1 WHERE (instance_id = $2) AND (type = $3)",
+ expectedArgs: []interface{}{
+ now,
+ "instance-id",
+ milestone.ProjectCreated,
+ },
+ },
+ },
+ },
+ },
+ },
+ {
+ name: "reduceMilestonePushed instance deleted milestone",
+ args: args{
+ event: getEvent(testEvent(
+ repository.EventType(milestone.PushedEventType),
+ milestone.AggregateType,
+ []byte(`{"type": "InstanceDeleted"}`),
+ ), milestone.PushedEventMapper),
+ },
+ reduce: (&milestoneProjection{}).reduceMilestonePushed,
+ want: wantReduce{
+ aggregateType: eventstore.AggregateType("milestone"),
+ sequence: 15,
+ previousSequence: 10,
+ executer: &testExecuter{
+ executions: []execution{
+ {
+ expectedStmt: "DELETE FROM projections.milestones WHERE (instance_id = $1)",
+ expectedArgs: []interface{}{
+ "instance-id",
+ },
+ },
+ },
+ },
+ },
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ event := baseEvent(t)
+ got, err := tt.reduce(event)
+ if !errors.IsErrorInvalidArgument(err) {
+ t.Errorf("no wrong event mapping: %v, got: %v", err, got)
+ }
+ event = tt.args.event(t)
+ got, err = tt.reduce(event)
+ assertReduce(t, got, err, MilestonesProjectionTable, tt.want)
+ })
+ }
+}
diff --git a/internal/query/projection/projection.go b/internal/query/projection/projection.go
index 45670b8ba..e8f2bd156 100644
--- a/internal/query/projection/projection.go
+++ b/internal/query/projection/projection.go
@@ -64,8 +64,11 @@ var (
NotificationPolicyProjection *notificationPolicyProjection
NotificationsProjection interface{}
NotificationsQuotaProjection interface{}
+ TelemetryPusherProjection interface{}
DeviceAuthProjection *deviceAuthProjection
SessionProjection *sessionProjection
+ AuthRequestProjection *authRequestProjection
+ MilestoneProjection *milestoneProjection
)
type projection interface {
@@ -143,6 +146,8 @@ func Create(ctx context.Context, sqlClient *database.DB, es *eventstore.Eventsto
NotificationPolicyProjection = newNotificationPolicyProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["notification_policies"]))
DeviceAuthProjection = newDeviceAuthProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["device_auth"]))
SessionProjection = newSessionProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["sessions"]))
+ AuthRequestProjection = newAuthRequestProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["auth_requests"]))
+ MilestoneProjection = newMilestoneProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["milestones"]))
newProjectionsList()
return nil
}
@@ -191,7 +196,7 @@ func applyCustomConfig(config crdb.StatementHandlerConfig, customConfig CustomCo
// as setup and start currently create them individually, we make sure we get the right one
// will be refactored when changing to new id based projections
//
-// NotificationsProjection is not added here, because it does not statement based / has no proprietary projection table
+// Event handlers NotificationsProjection, NotificationsQuotaProjection and NotificationsProjection are not added here, because they do not reduce to database statements
func newProjectionsList() {
projections = []projection{
OrgProjection,
@@ -240,5 +245,7 @@ func newProjectionsList() {
NotificationPolicyProjection,
DeviceAuthProjection,
SessionProjection,
+ AuthRequestProjection,
+ MilestoneProjection,
}
}
diff --git a/internal/query/query.go b/internal/query/query.go
index 464cf59aa..9355265e1 100644
--- a/internal/query/query.go
+++ b/internal/query/query.go
@@ -4,6 +4,7 @@ import (
"context"
"fmt"
"net/http"
+ "regexp"
"sync"
"time"
@@ -18,9 +19,11 @@ import (
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/query/projection"
"github.com/zitadel/zitadel/internal/repository/action"
+ "github.com/zitadel/zitadel/internal/repository/authrequest"
"github.com/zitadel/zitadel/internal/repository/idpintent"
iam_repo "github.com/zitadel/zitadel/internal/repository/instance"
"github.com/zitadel/zitadel/internal/repository/keypair"
+ "github.com/zitadel/zitadel/internal/repository/oidcsession"
"github.com/zitadel/zitadel/internal/repository/org"
"github.com/zitadel/zitadel/internal/repository/project"
"github.com/zitadel/zitadel/internal/repository/session"
@@ -88,6 +91,8 @@ func StartQueries(
usergrant.RegisterEventMappers(repo.eventstore)
session.RegisterEventMappers(repo.eventstore)
idpintent.RegisterEventMappers(repo.eventstore)
+ authrequest.RegisterEventMappers(repo.eventstore)
+ oidcsession.RegisterEventMappers(repo.eventstore)
repo.idpConfigEncryption = idpConfigEncryption
repo.multifactors = domain.MultifactorConfigs{
@@ -115,3 +120,19 @@ func (q *Queries) Health(ctx context.Context) error {
type prepareDatabase interface {
Timetravel(d time.Duration) string
}
+
+// cleanStaticQueries removes whitespaces,
+// such as ` `, \t, \n, from queries to improve
+// readability in logs and errors.
+func cleanStaticQueries(qs ...*string) {
+ regex := regexp.MustCompile(`\s+`)
+ for _, q := range qs {
+ *q = regex.ReplaceAllString(*q, " ")
+ }
+}
+
+func init() {
+ cleanStaticQueries(
+ &authRequestByIDQuery,
+ )
+}
diff --git a/internal/query/query_test.go b/internal/query/query_test.go
new file mode 100644
index 000000000..fc16ee9fa
--- /dev/null
+++ b/internal/query/query_test.go
@@ -0,0 +1,17 @@
+package query
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func Test_cleanStaticQueries(t *testing.T) {
+ query := `select
+ foo,
+ bar
+from table;`
+ want := "select foo, bar from table;"
+ cleanStaticQueries(&query)
+ assert.Equal(t, want, query)
+}
diff --git a/internal/query/search_query.go b/internal/query/search_query.go
index 0f4314dd0..16d37502d 100644
--- a/internal/query/search_query.go
+++ b/internal/query/search_query.go
@@ -66,6 +66,27 @@ func (q *NotNullQuery) comp() sq.Sqlizer {
return sq.NotEq{q.Column.identifier(): nil}
}
+type IsNullQuery struct {
+ Column Column
+}
+
+func NewIsNullQuery(col Column) (*IsNullQuery, error) {
+ if col.isZero() {
+ return nil, ErrMissingColumn
+ }
+ return &IsNullQuery{
+ Column: col,
+ }, nil
+}
+
+func (q *IsNullQuery) toQuery(query sq.SelectBuilder) sq.SelectBuilder {
+ return query.Where(q.comp())
+}
+
+func (q *IsNullQuery) comp() sq.Sqlizer {
+ return sq.Eq{q.Column.identifier(): nil}
+}
+
type orQuery struct {
queries []SearchQuery
}
diff --git a/internal/query/session.go b/internal/query/session.go
index e9706410a..bcf62ba5d 100644
--- a/internal/query/session.go
+++ b/internal/query/session.go
@@ -40,6 +40,7 @@ type Session struct {
type SessionUserFactor struct {
UserID string
+ ResourceOwner string
UserCheckedAt time.Time
LoginName string
DisplayName string
@@ -141,10 +142,14 @@ var (
}
)
-func (q *Queries) SessionByID(ctx context.Context, id, sessionToken string) (_ *Session, err error) {
+func (q *Queries) SessionByID(ctx context.Context, shouldTriggerBulk bool, id, sessionToken string) (_ *Session, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
+ if shouldTriggerBulk {
+ ctx = projection.SessionProjection.Trigger(ctx)
+ }
+
query, scan := prepareSessionQuery(ctx, q.client)
stmt, args, err := query.Where(
sq.Eq{
@@ -221,6 +226,7 @@ func prepareSessionQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuil
SessionColumnUserCheckedAt.identifier(),
LoginNameNameCol.identifier(),
HumanDisplayNameCol.identifier(),
+ UserResourceOwnerCol.identifier(),
SessionColumnPasswordCheckedAt.identifier(),
SessionColumnIntentCheckedAt.identifier(),
SessionColumnPasskeyCheckedAt.identifier(),
@@ -228,7 +234,8 @@ func prepareSessionQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuil
SessionColumnToken.identifier(),
).From(sessionsTable.identifier()).
LeftJoin(join(LoginNameUserIDCol, SessionColumnUserID)).
- LeftJoin(join(HumanUserIDCol, SessionColumnUserID) + db.Timetravel(call.Took(ctx))).
+ LeftJoin(join(HumanUserIDCol, SessionColumnUserID)).
+ LeftJoin(join(UserIDCol, SessionColumnUserID) + db.Timetravel(call.Took(ctx))).
PlaceholderFormat(sq.Dollar), func(row *sql.Row) (*Session, string, error) {
session := new(Session)
@@ -237,6 +244,7 @@ func prepareSessionQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuil
userCheckedAt sql.NullTime
loginName sql.NullString
displayName sql.NullString
+ userResourceOwner sql.NullString
passwordCheckedAt sql.NullTime
intentCheckedAt sql.NullTime
passkeyCheckedAt sql.NullTime
@@ -258,6 +266,7 @@ func prepareSessionQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuil
&userCheckedAt,
&loginName,
&displayName,
+ &userResourceOwner,
&passwordCheckedAt,
&intentCheckedAt,
&passkeyCheckedAt,
@@ -277,6 +286,7 @@ func prepareSessionQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuil
session.UserFactor.UserCheckedAt = userCheckedAt.Time
session.UserFactor.LoginName = loginName.String
session.UserFactor.DisplayName = displayName.String
+ session.UserFactor.ResourceOwner = userResourceOwner.String
session.PasswordFactor.PasswordCheckedAt = passwordCheckedAt.Time
session.IntentFactor.IntentCheckedAt = intentCheckedAt.Time
session.PasskeyFactor.PasskeyCheckedAt = passkeyCheckedAt.Time
@@ -300,6 +310,7 @@ func prepareSessionsQuery(ctx context.Context, db prepareDatabase) (sq.SelectBui
SessionColumnUserCheckedAt.identifier(),
LoginNameNameCol.identifier(),
HumanDisplayNameCol.identifier(),
+ UserResourceOwnerCol.identifier(),
SessionColumnPasswordCheckedAt.identifier(),
SessionColumnIntentCheckedAt.identifier(),
SessionColumnPasskeyCheckedAt.identifier(),
@@ -307,7 +318,8 @@ func prepareSessionsQuery(ctx context.Context, db prepareDatabase) (sq.SelectBui
countColumn.identifier(),
).From(sessionsTable.identifier()).
LeftJoin(join(LoginNameUserIDCol, SessionColumnUserID)).
- LeftJoin(join(HumanUserIDCol, SessionColumnUserID) + db.Timetravel(call.Took(ctx))).
+ LeftJoin(join(HumanUserIDCol, SessionColumnUserID)).
+ LeftJoin(join(UserIDCol, SessionColumnUserID) + db.Timetravel(call.Took(ctx))).
PlaceholderFormat(sq.Dollar), func(rows *sql.Rows) (*Sessions, error) {
sessions := &Sessions{Sessions: []*Session{}}
@@ -319,6 +331,7 @@ func prepareSessionsQuery(ctx context.Context, db prepareDatabase) (sq.SelectBui
userCheckedAt sql.NullTime
loginName sql.NullString
displayName sql.NullString
+ userResourceOwner sql.NullString
passwordCheckedAt sql.NullTime
intentCheckedAt sql.NullTime
passkeyCheckedAt sql.NullTime
@@ -339,6 +352,7 @@ func prepareSessionsQuery(ctx context.Context, db prepareDatabase) (sq.SelectBui
&userCheckedAt,
&loginName,
&displayName,
+ &userResourceOwner,
&passwordCheckedAt,
&intentCheckedAt,
&passkeyCheckedAt,
@@ -354,6 +368,7 @@ func prepareSessionsQuery(ctx context.Context, db prepareDatabase) (sq.SelectBui
session.UserFactor.UserCheckedAt = userCheckedAt.Time
session.UserFactor.LoginName = loginName.String
session.UserFactor.DisplayName = displayName.String
+ session.UserFactor.ResourceOwner = userResourceOwner.String
session.PasswordFactor.PasswordCheckedAt = passwordCheckedAt.Time
session.IntentFactor.IntentCheckedAt = intentCheckedAt.Time
session.PasskeyFactor.PasskeyCheckedAt = passkeyCheckedAt.Time
diff --git a/internal/query/sessions_test.go b/internal/query/sessions_test.go
index 38662ead7..eed2ebafd 100644
--- a/internal/query/sessions_test.go
+++ b/internal/query/sessions_test.go
@@ -29,6 +29,7 @@ var (
` projections.sessions3.user_checked_at,` +
` projections.login_names2.login_name,` +
` projections.users8_humans.display_name,` +
+ ` projections.users8.resource_owner,` +
` projections.sessions3.password_checked_at,` +
` projections.sessions3.intent_checked_at,` +
` projections.sessions3.passkey_checked_at,` +
@@ -37,6 +38,7 @@ var (
` FROM projections.sessions3` +
` LEFT JOIN projections.login_names2 ON projections.sessions3.user_id = projections.login_names2.user_id AND projections.sessions3.instance_id = projections.login_names2.instance_id` +
` LEFT JOIN projections.users8_humans ON projections.sessions3.user_id = projections.users8_humans.user_id AND projections.sessions3.instance_id = projections.users8_humans.instance_id` +
+ ` LEFT JOIN projections.users8 ON projections.sessions3.user_id = projections.users8.id AND projections.sessions3.instance_id = projections.users8.instance_id` +
` AS OF SYSTEM TIME '-1 ms'`)
expectedSessionsQuery = regexp.QuoteMeta(`SELECT projections.sessions3.id,` +
` projections.sessions3.creation_date,` +
@@ -50,6 +52,7 @@ var (
` projections.sessions3.user_checked_at,` +
` projections.login_names2.login_name,` +
` projections.users8_humans.display_name,` +
+ ` projections.users8.resource_owner,` +
` projections.sessions3.password_checked_at,` +
` projections.sessions3.intent_checked_at,` +
` projections.sessions3.passkey_checked_at,` +
@@ -58,6 +61,7 @@ var (
` FROM projections.sessions3` +
` LEFT JOIN projections.login_names2 ON projections.sessions3.user_id = projections.login_names2.user_id AND projections.sessions3.instance_id = projections.login_names2.instance_id` +
` LEFT JOIN projections.users8_humans ON projections.sessions3.user_id = projections.users8_humans.user_id AND projections.sessions3.instance_id = projections.users8_humans.instance_id` +
+ ` LEFT JOIN projections.users8 ON projections.sessions3.user_id = projections.users8.id AND projections.sessions3.instance_id = projections.users8.instance_id` +
` AS OF SYSTEM TIME '-1 ms'`)
sessionCols = []string{
@@ -73,6 +77,7 @@ var (
"user_checked_at",
"login_name",
"display_name",
+ "user_resource_owner",
"password_checked_at",
"intent_checked_at",
"passkey_checked_at",
@@ -93,6 +98,7 @@ var (
"user_checked_at",
"login_name",
"display_name",
+ "user_resource_owner",
"password_checked_at",
"intent_checked_at",
"passkey_checked_at",
@@ -145,6 +151,7 @@ func Test_SessionsPrepare(t *testing.T) {
testNow,
"login-name",
"display-name",
+ "resourceOwner",
testNow,
testNow,
testNow,
@@ -172,6 +179,7 @@ func Test_SessionsPrepare(t *testing.T) {
UserCheckedAt: testNow,
LoginName: "login-name",
DisplayName: "display-name",
+ ResourceOwner: "resourceOwner",
},
PasswordFactor: SessionPasswordFactor{
PasswordCheckedAt: testNow,
@@ -210,6 +218,7 @@ func Test_SessionsPrepare(t *testing.T) {
testNow,
"login-name",
"display-name",
+ "resourceOwner",
testNow,
testNow,
testNow,
@@ -228,6 +237,7 @@ func Test_SessionsPrepare(t *testing.T) {
testNow,
"login-name2",
"display-name2",
+ "resourceOwner",
testNow,
testNow,
testNow,
@@ -255,6 +265,7 @@ func Test_SessionsPrepare(t *testing.T) {
UserCheckedAt: testNow,
LoginName: "login-name",
DisplayName: "display-name",
+ ResourceOwner: "resourceOwner",
},
PasswordFactor: SessionPasswordFactor{
PasswordCheckedAt: testNow,
@@ -283,6 +294,7 @@ func Test_SessionsPrepare(t *testing.T) {
UserCheckedAt: testNow,
LoginName: "login-name2",
DisplayName: "display-name2",
+ ResourceOwner: "resourceOwner",
},
PasswordFactor: SessionPasswordFactor{
PasswordCheckedAt: testNow,
@@ -374,6 +386,7 @@ func Test_SessionPrepare(t *testing.T) {
testNow,
"login-name",
"display-name",
+ "resourceOwner",
testNow,
testNow,
testNow,
@@ -396,6 +409,7 @@ func Test_SessionPrepare(t *testing.T) {
UserCheckedAt: testNow,
LoginName: "login-name",
DisplayName: "display-name",
+ ResourceOwner: "resourceOwner",
},
PasswordFactor: SessionPasswordFactor{
PasswordCheckedAt: testNow,
diff --git a/internal/query/user.go b/internal/query/user.go
index 1550a4678..46a683e67 100644
--- a/internal/query/user.go
+++ b/internal/query/user.go
@@ -338,8 +338,8 @@ func (q *Queries) GetUserByID(ctx context.Context, shouldTriggerBulk bool, userI
defer func() { span.EndWithError(err) }()
if shouldTriggerBulk {
- projection.UserProjection.Trigger(ctx)
- projection.LoginNameProjection.Trigger(ctx)
+ ctx = projection.UserProjection.Trigger(ctx)
+ ctx = projection.LoginNameProjection.Trigger(ctx)
}
query, scan := prepareUserQuery(ctx, q.client)
@@ -367,8 +367,8 @@ func (q *Queries) GetUser(ctx context.Context, shouldTriggerBulk bool, withOwner
defer func() { span.EndWithError(err) }()
if shouldTriggerBulk {
- projection.UserProjection.Trigger(ctx)
- projection.LoginNameProjection.Trigger(ctx)
+ ctx = projection.UserProjection.Trigger(ctx)
+ ctx = projection.LoginNameProjection.Trigger(ctx)
}
query, scan := prepareUserQuery(ctx, q.client)
@@ -467,8 +467,8 @@ func (q *Queries) GetNotifyUserByID(ctx context.Context, shouldTriggered bool, u
defer func() { span.EndWithError(err) }()
if shouldTriggered {
- projection.UserProjection.Trigger(ctx)
- projection.LoginNameProjection.Trigger(ctx)
+ ctx = projection.UserProjection.Trigger(ctx)
+ ctx = projection.LoginNameProjection.Trigger(ctx)
}
query, scan := prepareNotifyUserQuery(ctx, q.client)
@@ -496,8 +496,8 @@ func (q *Queries) GetNotifyUser(ctx context.Context, shouldTriggered bool, withO
defer func() { span.EndWithError(err) }()
if shouldTriggered {
- projection.UserProjection.Trigger(ctx)
- projection.LoginNameProjection.Trigger(ctx)
+ ctx = projection.UserProjection.Trigger(ctx)
+ ctx = projection.LoginNameProjection.Trigger(ctx)
}
query, scan := prepareNotifyUserQuery(ctx, q.client)
diff --git a/internal/query/user_auth_method.go b/internal/query/user_auth_method.go
index dde1a5a38..fc80685bc 100644
--- a/internal/query/user_auth_method.go
+++ b/internal/query/user_auth_method.go
@@ -79,6 +79,13 @@ var (
name: "count",
table: userIDPsCountTable,
}
+
+ forceMFATable = loginPolicyTable.setAlias("auth_methods_force_mfa")
+ forceMFAInstanceID = LoginPolicyColumnInstanceID.setTable(forceMFATable)
+ forceMFAOrgID = LoginPolicyColumnOrgID.setTable(forceMFATable)
+ forceMFAIsDefault = LoginPolicyColumnIsDefault.setTable(forceMFATable)
+ forceMFAForce = LoginPolicyColumnForceMFA.setTable(forceMFATable)
+ forceMFAForceLocalOnly = LoginPolicyColumnForceMFALocalOnly.setTable(forceMFATable)
)
type AuthMethods struct {
@@ -170,6 +177,36 @@ func (q *Queries) ListActiveUserAuthMethodTypes(ctx context.Context, userID stri
return userAuthMethodTypes, err
}
+func (q *Queries) ListUserAuthMethodTypesRequired(ctx context.Context, userID string, withOwnerRemoved bool) (userAuthMethodTypes []domain.UserAuthMethodType, forceMFA, forceMFALocalOnly bool, err error) {
+ ctxData := authz.GetCtxData(ctx)
+ if ctxData.UserID != userID {
+ if err := q.checkPermission(ctx, domain.PermissionUserRead, ctxData.OrgID, userID); err != nil {
+ return nil, false, false, err
+ }
+ }
+ ctx, span := tracing.NewSpan(ctx)
+ defer func() { span.EndWithError(err) }()
+
+ query, scan := prepareUserAuthMethodTypesRequiredQuery(ctx, q.client)
+ eq := sq.Eq{
+ UserIDCol.identifier(): userID,
+ UserInstanceIDCol.identifier(): authz.GetInstance(ctx).InstanceID(),
+ }
+ if !withOwnerRemoved {
+ eq[UserOwnerRemovedCol.identifier()] = false
+ }
+ stmt, args, err := query.Where(eq).ToSql()
+ if err != nil {
+ return nil, false, false, errors.ThrowInvalidArgument(err, "QUERY-E5ut4", "Errors.Query.InvalidRequest")
+ }
+
+ rows, err := q.client.QueryContext(ctx, stmt, args...)
+ if err != nil || rows.Err() != nil {
+ return nil, false, false, errors.ThrowInternal(err, "QUERY-Dun75", "Errors.Internal")
+ }
+ return scan(rows)
+}
+
func NewUserAuthMethodUserIDSearchQuery(value string) (SearchQuery, error) {
return NewTextQuery(UserAuthMethodColumnUserID, value, TextEquals)
}
@@ -311,26 +348,11 @@ func prepareUserAuthMethodsQuery(ctx context.Context, db prepareDatabase) (sq.Se
}
func prepareActiveUserAuthMethodTypesQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*AuthMethodTypes, error)) {
- authMethodsQuery, authMethodsArgs, err := sq.Select(
- "DISTINCT("+authMethodTypeTypes.identifier()+")",
- authMethodTypeUserID.identifier(),
- authMethodTypeInstanceID.identifier()).
- From(authMethodTypeTable.identifier()).
- Where(sq.Eq{authMethodTypeState.identifier(): domain.MFAStateReady}).
- ToSql()
+ authMethodsQuery, authMethodsArgs, err := prepareAuthMethodQuery()
if err != nil {
return sq.SelectBuilder{}, nil
}
- idpsQuery, _, err := sq.Select(
- userIDPsCountUserID.identifier(),
- userIDPsCountInstanceID.identifier(),
- "COUNT("+userIDPsCountUserID.identifier()+") AS "+userIDPsCountCount.name).
- From(userIDPsCountTable.identifier()).
- GroupBy(
- userIDPsCountUserID.identifier(),
- userIDPsCountInstanceID.identifier(),
- ).
- ToSql()
+ idpsQuery, err := prepareAuthMethodsIDPsQuery()
if err != nil {
return sq.SelectBuilder{}, nil
}
@@ -386,3 +408,110 @@ func prepareActiveUserAuthMethodTypesQuery(ctx context.Context, db prepareDataba
}, nil
}
}
+
+func prepareUserAuthMethodTypesRequiredQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (_ []domain.UserAuthMethodType, forceMFA, forceMFALocalOnly bool, err error)) {
+ loginPolicyQuery, err := prepareAuthMethodsForceMFAQuery()
+ if err != nil {
+ return sq.SelectBuilder{}, nil
+ }
+ authMethodsQuery, authMethodsArgs, err := prepareAuthMethodQuery()
+ if err != nil {
+ return sq.SelectBuilder{}, nil
+ }
+ idpsQuery, err := prepareAuthMethodsIDPsQuery()
+ if err != nil {
+ return sq.SelectBuilder{}, nil
+ }
+ return sq.Select(
+ NotifyPasswordSetCol.identifier(),
+ authMethodTypeTypes.identifier(),
+ userIDPsCountCount.identifier(),
+ forceMFAForce.identifier(),
+ forceMFAForceLocalOnly.identifier()).
+ From(userTable.identifier()).
+ LeftJoin(join(NotifyUserIDCol, UserIDCol)).
+ LeftJoin("("+authMethodsQuery+") AS "+authMethodTypeTable.alias+" ON "+
+ authMethodTypeUserID.identifier()+" = "+UserIDCol.identifier()+" AND "+
+ authMethodTypeInstanceID.identifier()+" = "+UserInstanceIDCol.identifier(),
+ authMethodsArgs...).
+ LeftJoin("(" + idpsQuery + ") AS " + userIDPsCountTable.alias + " ON " +
+ userIDPsCountUserID.identifier() + " = " + UserIDCol.identifier() + " AND " +
+ userIDPsCountInstanceID.identifier() + " = " + UserInstanceIDCol.identifier()).
+ LeftJoin("(" + loginPolicyQuery + ") AS " + forceMFATable.alias + " ON " +
+ "(" + forceMFAOrgID.identifier() + " = " + UserInstanceIDCol.identifier() + " OR " + forceMFAOrgID.identifier() + " = " + UserResourceOwnerCol.identifier() + ") AND " +
+ forceMFAInstanceID.identifier() + " = " + UserInstanceIDCol.identifier() + db.Timetravel(call.Took(ctx))).
+ PlaceholderFormat(sq.Dollar),
+ func(rows *sql.Rows) ([]domain.UserAuthMethodType, bool, bool, error) {
+ userAuthMethodTypes := make([]domain.UserAuthMethodType, 0)
+ var passwordSet sql.NullBool
+ var idp sql.NullInt64
+ var forceMFA sql.NullBool
+ var forceMFALocalOnly sql.NullBool
+ for rows.Next() {
+ var authMethodType sql.NullInt16
+ err := rows.Scan(
+ &passwordSet,
+ &authMethodType,
+ &idp,
+ &forceMFA,
+ &forceMFALocalOnly,
+ )
+ if err != nil {
+ return nil, false, false, err
+ }
+ if authMethodType.Valid {
+ userAuthMethodTypes = append(userAuthMethodTypes, domain.UserAuthMethodType(authMethodType.Int16))
+ }
+ }
+ if passwordSet.Valid && passwordSet.Bool {
+ userAuthMethodTypes = append(userAuthMethodTypes, domain.UserAuthMethodTypePassword)
+ }
+ if idp.Valid && idp.Int64 > 0 {
+ logging.Error("IDP", idp.Int64)
+ userAuthMethodTypes = append(userAuthMethodTypes, domain.UserAuthMethodTypeIDP)
+ }
+
+ if err := rows.Close(); err != nil {
+ return nil, false, false, errors.ThrowInternal(err, "QUERY-W4zje", "Errors.Query.CloseRows")
+ }
+
+ return userAuthMethodTypes, forceMFA.Bool, forceMFALocalOnly.Bool, nil
+ }
+}
+
+func prepareAuthMethodsIDPsQuery() (string, error) {
+ idpsQuery, _, err := sq.Select(
+ userIDPsCountUserID.identifier(),
+ userIDPsCountInstanceID.identifier(),
+ "COUNT("+userIDPsCountUserID.identifier()+") AS "+userIDPsCountCount.name).
+ From(userIDPsCountTable.identifier()).
+ GroupBy(
+ userIDPsCountUserID.identifier(),
+ userIDPsCountInstanceID.identifier(),
+ ).
+ ToSql()
+ return idpsQuery, err
+}
+
+func prepareAuthMethodQuery() (string, []interface{}, error) {
+ return sq.Select(
+ "DISTINCT("+authMethodTypeTypes.identifier()+")",
+ authMethodTypeUserID.identifier(),
+ authMethodTypeInstanceID.identifier()).
+ From(authMethodTypeTable.identifier()).
+ Where(sq.Eq{authMethodTypeState.identifier(): domain.MFAStateReady}).
+ ToSql()
+}
+
+func prepareAuthMethodsForceMFAQuery() (string, error) {
+ loginPolicyQuery, _, err := sq.Select(
+ forceMFAForce.identifier(),
+ forceMFAForceLocalOnly.identifier(),
+ forceMFAInstanceID.identifier(),
+ forceMFAOrgID.identifier(),
+ ).
+ From(forceMFATable.identifier()).
+ OrderBy(forceMFAIsDefault.identifier()).
+ ToSql()
+ return loginPolicyQuery, err
+}
diff --git a/internal/query/user_auth_method_test.go b/internal/query/user_auth_method_test.go
index da86a02aa..ec38d04e5 100644
--- a/internal/query/user_auth_method_test.go
+++ b/internal/query/user_auth_method_test.go
@@ -1,6 +1,7 @@
package query
import (
+ "context"
"database/sql"
"database/sql/driver"
"errors"
@@ -8,6 +9,8 @@ import (
"regexp"
"testing"
+ sq "github.com/Masterminds/squirrel"
+
"github.com/zitadel/zitadel/internal/domain"
)
@@ -53,6 +56,30 @@ var (
"method_type",
"idps_count",
}
+ prepareAuthMethodTypesRequiredStmt = `SELECT projections.users8_notifications.password_set,` +
+ ` auth_method_types.method_type,` +
+ ` user_idps_count.count,` +
+ ` auth_methods_force_mfa.force_mfa,` +
+ ` auth_methods_force_mfa.force_mfa_local_only` +
+ ` FROM projections.users8` +
+ ` LEFT JOIN projections.users8_notifications ON projections.users8.id = projections.users8_notifications.user_id AND projections.users8.instance_id = projections.users8_notifications.instance_id` +
+ ` LEFT JOIN (SELECT DISTINCT(auth_method_types.method_type), auth_method_types.user_id, auth_method_types.instance_id FROM projections.user_auth_methods4 AS auth_method_types` +
+ ` WHERE auth_method_types.state = $1) AS auth_method_types` +
+ ` ON auth_method_types.user_id = projections.users8.id AND auth_method_types.instance_id = projections.users8.instance_id` +
+ ` LEFT JOIN (SELECT user_idps_count.user_id, user_idps_count.instance_id, COUNT(user_idps_count.user_id) AS count FROM projections.idp_user_links3 AS user_idps_count` +
+ ` GROUP BY user_idps_count.user_id, user_idps_count.instance_id) AS user_idps_count` +
+ ` ON user_idps_count.user_id = projections.users8.id AND user_idps_count.instance_id = projections.users8.instance_id` +
+ ` LEFT JOIN (SELECT auth_methods_force_mfa.force_mfa, auth_methods_force_mfa.force_mfa_local_only, auth_methods_force_mfa.instance_id, auth_methods_force_mfa.aggregate_id FROM projections.login_policies5 AS auth_methods_force_mfa ORDER BY auth_methods_force_mfa.is_default) AS auth_methods_force_mfa` +
+ ` ON (auth_methods_force_mfa.aggregate_id = projections.users8.instance_id OR auth_methods_force_mfa.aggregate_id = projections.users8.resource_owner) AND auth_methods_force_mfa.instance_id = projections.users8.instance_id` +
+ ` AS OF SYSTEM TIME '-1 ms
+`
+ prepareAuthMethodTypesRequiredCols = []string{
+ "password_set",
+ "method_type",
+ "idps_count",
+ "force_mfa",
+ "force_mfa_local_only",
+ }
)
func Test_UserAuthMethodPrepares(t *testing.T) {
@@ -288,6 +315,136 @@ func Test_UserAuthMethodPrepares(t *testing.T) {
},
object: nil,
},
+ {
+ name: "prepareUserAuthMethodTypesRequiredQuery no result",
+ prepare: func(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*testUserAuthMethodTypesRequired, error)) {
+ builder, scan := prepareUserAuthMethodTypesRequiredQuery(ctx, db)
+ return builder, func(rows *sql.Rows) (*testUserAuthMethodTypesRequired, error) {
+ authMethods, forceMFA, forceMFALocalOnly, err := scan(rows)
+ if err != nil {
+ return nil, err
+ }
+ return &testUserAuthMethodTypesRequired{authMethods: authMethods, forceMFA: forceMFA, forceMFALocalOnly: forceMFALocalOnly}, nil
+ }
+ },
+ want: want{
+ sqlExpectations: mockQueries(
+ regexp.QuoteMeta(prepareAuthMethodTypesRequiredStmt),
+ nil,
+ nil,
+ ),
+ },
+ object: &testUserAuthMethodTypesRequired{authMethods: []domain.UserAuthMethodType{}, forceMFA: false},
+ },
+ {
+ name: "prepareUserAuthMethodTypesRequiredQuery one second factor",
+ prepare: func(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*testUserAuthMethodTypesRequired, error)) {
+ builder, scan := prepareUserAuthMethodTypesRequiredQuery(ctx, db)
+ return builder, func(rows *sql.Rows) (*testUserAuthMethodTypesRequired, error) {
+ authMethods, forceMFA, forceMFALocalOnly, err := scan(rows)
+ if err != nil {
+ return nil, err
+ }
+ return &testUserAuthMethodTypesRequired{authMethods: authMethods, forceMFA: forceMFA, forceMFALocalOnly: forceMFALocalOnly}, nil
+ }
+ },
+ want: want{
+ sqlExpectations: mockQueries(
+ regexp.QuoteMeta(prepareAuthMethodTypesRequiredStmt),
+ prepareAuthMethodTypesRequiredCols,
+ [][]driver.Value{
+ {
+ true,
+ domain.UserAuthMethodTypePasswordless,
+ 1,
+ true,
+ true,
+ },
+ },
+ ),
+ },
+ object: &testUserAuthMethodTypesRequired{
+ authMethods: []domain.UserAuthMethodType{
+ domain.UserAuthMethodTypePasswordless,
+ domain.UserAuthMethodTypePassword,
+ domain.UserAuthMethodTypeIDP,
+ },
+ forceMFA: true,
+ forceMFALocalOnly: true,
+ },
+ },
+ {
+ name: "prepareUserAuthMethodTypesRequiredQuery multiple second factors",
+ prepare: func(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*testUserAuthMethodTypesRequired, error)) {
+ builder, scan := prepareUserAuthMethodTypesRequiredQuery(ctx, db)
+ return builder, func(rows *sql.Rows) (*testUserAuthMethodTypesRequired, error) {
+ authMethods, forceMFA, forceMFALocalOnly, err := scan(rows)
+ if err != nil {
+ return nil, err
+ }
+ return &testUserAuthMethodTypesRequired{authMethods: authMethods, forceMFA: forceMFA, forceMFALocalOnly: forceMFALocalOnly}, nil
+ }
+ },
+ want: want{
+ sqlExpectations: mockQueries(
+ regexp.QuoteMeta(prepareAuthMethodTypesRequiredStmt),
+ prepareAuthMethodTypesRequiredCols,
+ [][]driver.Value{
+ {
+ true,
+ domain.UserAuthMethodTypePasswordless,
+ 1,
+ true,
+ true,
+ },
+ {
+ true,
+ domain.UserAuthMethodTypeOTP,
+ 1,
+ true,
+ true,
+ },
+ },
+ ),
+ },
+
+ object: &testUserAuthMethodTypesRequired{
+ authMethods: []domain.UserAuthMethodType{
+ domain.UserAuthMethodTypePasswordless,
+ domain.UserAuthMethodTypeOTP,
+ domain.UserAuthMethodTypePassword,
+ domain.UserAuthMethodTypeIDP,
+ },
+ forceMFA: true,
+ forceMFALocalOnly: true,
+ },
+ },
+ {
+ name: "prepareUserAuthMethodTypesRequiredQuery sql err",
+ prepare: func(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*testUserAuthMethodTypesRequired, error)) {
+ builder, scan := prepareUserAuthMethodTypesRequiredQuery(ctx, db)
+ return builder, func(rows *sql.Rows) (*testUserAuthMethodTypesRequired, error) {
+ authMethods, forceMFA, forceMFALocalOnly, err := scan(rows)
+ if err != nil {
+ return nil, err
+ }
+ return &testUserAuthMethodTypesRequired{authMethods: authMethods, forceMFA: forceMFA, forceMFALocalOnly: forceMFALocalOnly}, nil
+ }
+ },
+ want: want{
+ sqlExpectations: mockQueryErr(
+ regexp.QuoteMeta(prepareAuthMethodTypesRequiredStmt),
+ sql.ErrConnDone,
+ ),
+ err: func(err error) (error, bool) {
+ if !errors.Is(err, sql.ErrConnDone) {
+ return fmt.Errorf("err should be sql.ErrConnDone got: %w", err), false
+ }
+ return nil, true
+ },
+ },
+ object: nil,
+ },
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
@@ -295,3 +452,10 @@ func Test_UserAuthMethodPrepares(t *testing.T) {
})
}
}
+
+// testUserAuthMethodTypesRequired is required as assetPrepare is only able to return a single object from scan
+type testUserAuthMethodTypesRequired struct {
+ authMethods []domain.UserAuthMethodType
+ forceMFA bool
+ forceMFALocalOnly bool
+}
diff --git a/internal/query/user_grant.go b/internal/query/user_grant.go
index ceb8652e4..6d0eaa95b 100644
--- a/internal/query/user_grant.go
+++ b/internal/query/user_grant.go
@@ -8,8 +8,6 @@ import (
sq "github.com/Masterminds/squirrel"
- "github.com/zitadel/logging"
-
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/api/call"
"github.com/zitadel/zitadel/internal/database"
@@ -235,7 +233,7 @@ func (q *Queries) UserGrant(ctx context.Context, shouldTriggerBulk bool, withOwn
defer func() { span.EndWithError(err) }()
if shouldTriggerBulk {
- projection.UserGrantProjection.Trigger(ctx)
+ ctx = projection.UserGrantProjection.Trigger(ctx)
}
query, scan := prepareUserGrantQuery(ctx, q.client)
@@ -260,9 +258,7 @@ func (q *Queries) UserGrants(ctx context.Context, queries *UserGrantsQueries, sh
defer func() { span.EndWithError(err) }()
if shouldTriggerBulk {
- logging.OnError(
- projection.UserGrantProjection.Trigger(ctx),
- ).Debug("unable to trigger")
+ ctx = projection.UserGrantProjection.Trigger(ctx)
}
query, scan := prepareUserGrantsQuery(ctx, q.client)
diff --git a/internal/query/user_metadata.go b/internal/query/user_metadata.go
index b7c4c2736..d0a9202b8 100644
--- a/internal/query/user_metadata.go
+++ b/internal/query/user_metadata.go
@@ -82,7 +82,7 @@ func (q *Queries) GetUserMetadataByKey(ctx context.Context, shouldTriggerBulk bo
defer func() { span.EndWithError(err) }()
if shouldTriggerBulk {
- projection.UserMetadataProjection.Trigger(ctx)
+ ctx = projection.UserMetadataProjection.Trigger(ctx)
}
query, scan := prepareUserMetadataQuery(ctx, q.client)
@@ -111,7 +111,7 @@ func (q *Queries) SearchUserMetadata(ctx context.Context, shouldTriggerBulk bool
defer func() { span.EndWithError(err) }()
if shouldTriggerBulk {
- projection.UserMetadataProjection.Trigger(ctx)
+ ctx = projection.UserMetadataProjection.Trigger(ctx)
}
query, scan := prepareUserMetadataListQuery(ctx, q.client)
diff --git a/internal/query/user_password.go b/internal/query/user_password.go
index 715c9ed87..97ca99541 100644
--- a/internal/query/user_password.go
+++ b/internal/query/user_password.go
@@ -15,7 +15,7 @@ import (
type HumanPasswordReadModel struct {
*eventstore.ReadModel
- Secret *crypto.CryptoValue
+ EncodedHash string
SecretChangeRequired bool
Code *crypto.CryptoValue
@@ -26,26 +26,21 @@ type HumanPasswordReadModel struct {
UserState domain.UserState
}
-func (q *Queries) GetHumanPassword(ctx context.Context, orgID, userID string) (passwordHash []byte, algorithm string, err error) {
+func (q *Queries) GetHumanPassword(ctx context.Context, orgID, userID string) (encodedHash string, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
if userID == "" {
- return nil, "", errors.ThrowInvalidArgument(nil, "QUERY-4Mfsf", "Errors.User.UserIDMissing")
+ return "", errors.ThrowInvalidArgument(nil, "QUERY-4Mfsf", "Errors.User.UserIDMissing")
}
existingPassword, err := q.passwordReadModel(ctx, userID, orgID)
if err != nil {
- return nil, "", errors.ThrowInternal(nil, "QUERY-p1k1n2i", "Errors.User.NotFound")
+ return "", errors.ThrowInternal(nil, "QUERY-p1k1n2i", "Errors.User.NotFound")
}
if existingPassword.UserState == domain.UserStateUnspecified || existingPassword.UserState == domain.UserStateDeleted {
- return nil, "", errors.ThrowPreconditionFailed(nil, "QUERY-3n77z", "Errors.User.NotFound")
+ return "", errors.ThrowPreconditionFailed(nil, "QUERY-3n77z", "Errors.User.NotFound")
}
-
- if existingPassword.Secret != nil && existingPassword.Secret.Crypted != nil {
- return existingPassword.Secret.Crypted, existingPassword.Secret.Algorithm, nil
- }
-
- return nil, "", nil
+ return existingPassword.EncodedHash, nil
}
func (q *Queries) passwordReadModel(ctx context.Context, userID, resourceOwner string) (readModel *HumanPasswordReadModel, err error) {
@@ -77,11 +72,11 @@ func (wm *HumanPasswordReadModel) Reduce() error {
for _, event := range wm.Events {
switch e := event.(type) {
case *user.HumanAddedEvent:
- wm.Secret = e.Secret
+ wm.EncodedHash = user.SecretOrEncodedHash(e.Secret, e.EncodedHash)
wm.SecretChangeRequired = e.ChangeRequired
wm.UserState = domain.UserStateActive
case *user.HumanRegisteredEvent:
- wm.Secret = e.Secret
+ wm.EncodedHash = user.SecretOrEncodedHash(e.Secret, e.EncodedHash)
wm.SecretChangeRequired = e.ChangeRequired
wm.UserState = domain.UserStateActive
case *user.HumanInitialCodeAddedEvent:
@@ -89,7 +84,7 @@ func (wm *HumanPasswordReadModel) Reduce() error {
case *user.HumanInitializedCheckSucceededEvent:
wm.UserState = domain.UserStateActive
case *user.HumanPasswordChangedEvent:
- wm.Secret = e.Secret
+ wm.EncodedHash = user.SecretOrEncodedHash(e.Secret, e.EncodedHash)
wm.SecretChangeRequired = e.ChangeRequired
wm.Code = nil
wm.PasswordCheckFailedCount = 0
@@ -109,6 +104,8 @@ func (wm *HumanPasswordReadModel) Reduce() error {
wm.PasswordCheckFailedCount = 0
case *user.UserRemovedEvent:
wm.UserState = domain.UserStateDeleted
+ case *user.HumanPasswordHashUpdatedEvent:
+ wm.EncodedHash = e.EncodedHash
}
}
return wm.ReadModel.Reduce()
@@ -139,7 +136,8 @@ func (wm *HumanPasswordReadModel) Query() *eventstore.SearchQueryBuilder {
user.UserV1PasswordCodeAddedType,
user.UserV1EmailVerifiedType,
user.UserV1PasswordCheckFailedType,
- user.UserV1PasswordCheckSucceededType).
+ user.UserV1PasswordCheckSucceededType,
+ user.UserV1PasswordHashUpdatedType).
Builder()
if wm.ResourceOwner != "" {
diff --git a/internal/query/user_personal_access_token.go b/internal/query/user_personal_access_token.go
index 4afab7ec8..5558eed3c 100644
--- a/internal/query/user_personal_access_token.go
+++ b/internal/query/user_personal_access_token.go
@@ -90,7 +90,7 @@ func (q *Queries) PersonalAccessTokenByID(ctx context.Context, shouldTriggerBulk
defer func() { span.EndWithError(err) }()
if shouldTriggerBulk {
- projection.PersonalAccessTokenProjection.Trigger(ctx)
+ ctx = projection.PersonalAccessTokenProjection.Trigger(ctx)
}
query, scan := preparePersonalAccessTokenQuery(ctx, q.client)
diff --git a/internal/repository/authrequest/aggregate.go b/internal/repository/authrequest/aggregate.go
new file mode 100644
index 000000000..ce6d8adc2
--- /dev/null
+++ b/internal/repository/authrequest/aggregate.go
@@ -0,0 +1,26 @@
+package authrequest
+
+import (
+ "github.com/zitadel/zitadel/internal/eventstore"
+)
+
+const (
+ AggregateType = "auth_request"
+ AggregateVersion = "v1"
+)
+
+type Aggregate struct {
+ eventstore.Aggregate
+}
+
+func NewAggregate(id, instanceID string) *Aggregate {
+ return &Aggregate{
+ Aggregate: eventstore.Aggregate{
+ Type: AggregateType,
+ Version: AggregateVersion,
+ ID: id,
+ ResourceOwner: instanceID,
+ InstanceID: instanceID,
+ },
+ }
+}
diff --git a/internal/repository/authrequest/auth_request.go b/internal/repository/authrequest/auth_request.go
new file mode 100644
index 000000000..a279f23c9
--- /dev/null
+++ b/internal/repository/authrequest/auth_request.go
@@ -0,0 +1,287 @@
+package authrequest
+
+import (
+ "context"
+ "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 (
+ authRequestEventPrefix = "auth_request."
+ AddedType = authRequestEventPrefix + "added"
+ FailedType = authRequestEventPrefix + "failed"
+ CodeAddedType = authRequestEventPrefix + "code.added"
+ SessionLinkedType = authRequestEventPrefix + "session.linked"
+ CodeExchangedType = authRequestEventPrefix + "code.exchanged"
+ SucceededType = authRequestEventPrefix + "succeeded"
+)
+
+type AddedEvent struct {
+ eventstore.BaseEvent `json:"-"`
+
+ LoginClient string `json:"login_client"`
+ ClientID string `json:"client_id"`
+ RedirectURI string `json:"redirect_uri"`
+ State string `json:"state,omitempty"`
+ Nonce string `json:"nonce,omitempty"`
+ Scope []string `json:"scope,omitempty"`
+ Audience []string `json:"audience,omitempty"`
+ ResponseType domain.OIDCResponseType `json:"response_type,omitempty"`
+ CodeChallenge *domain.OIDCCodeChallenge `json:"code_challenge,omitempty"`
+ Prompt []domain.Prompt `json:"prompt,omitempty"`
+ UILocales []string `json:"ui_locales,omitempty"`
+ MaxAge *time.Duration `json:"max_age,omitempty"`
+ LoginHint *string `json:"login_hint,omitempty"`
+ HintUserID *string `json:"hint_user_id,omitempty"`
+}
+
+func (e *AddedEvent) Data() interface{} {
+ return e
+}
+
+func (e *AddedEvent) UniqueConstraints() []*eventstore.EventUniqueConstraint {
+ return nil
+}
+
+func NewAddedEvent(ctx context.Context,
+ aggregate *eventstore.Aggregate,
+ loginClient,
+ clientID,
+ redirectURI,
+ state,
+ nonce string,
+ scope,
+ audience []string,
+ responseType domain.OIDCResponseType,
+ codeChallenge *domain.OIDCCodeChallenge,
+ prompt []domain.Prompt,
+ uiLocales []string,
+ maxAge *time.Duration,
+ loginHint,
+ hintUserID *string,
+) *AddedEvent {
+ return &AddedEvent{
+ BaseEvent: *eventstore.NewBaseEventForPush(
+ ctx,
+ aggregate,
+ AddedType,
+ ),
+ LoginClient: loginClient,
+ ClientID: clientID,
+ RedirectURI: redirectURI,
+ State: state,
+ Nonce: nonce,
+ Scope: scope,
+ Audience: audience,
+ ResponseType: responseType,
+ CodeChallenge: codeChallenge,
+ Prompt: prompt,
+ UILocales: uiLocales,
+ MaxAge: maxAge,
+ LoginHint: loginHint,
+ HintUserID: hintUserID,
+ }
+}
+
+func AddedEventMapper(event *repository.Event) (eventstore.Event, error) {
+ added := &AddedEvent{
+ BaseEvent: *eventstore.BaseEventFromRepo(event),
+ }
+ err := json.Unmarshal(event.Data, added)
+ if err != nil {
+ return nil, errors.ThrowInternal(err, "AUTHR-DG4gn", "unable to unmarshal auth request added")
+ }
+
+ return added, nil
+}
+
+type SessionLinkedEvent struct {
+ eventstore.BaseEvent `json:"-"`
+
+ SessionID string `json:"session_id"`
+ UserID string `json:"user_id"`
+ AuthTime time.Time `json:"auth_time"`
+ AuthMethods []domain.UserAuthMethodType `json:"auth_methods"`
+}
+
+func (e *SessionLinkedEvent) Data() interface{} {
+ return e
+}
+
+func (e *SessionLinkedEvent) UniqueConstraints() []*eventstore.EventUniqueConstraint {
+ return nil
+}
+
+func NewSessionLinkedEvent(ctx context.Context,
+ aggregate *eventstore.Aggregate,
+ sessionID,
+ userID string,
+ authTime time.Time,
+ authMethods []domain.UserAuthMethodType,
+) *SessionLinkedEvent {
+ return &SessionLinkedEvent{
+ BaseEvent: *eventstore.NewBaseEventForPush(
+ ctx,
+ aggregate,
+ SessionLinkedType,
+ ),
+ SessionID: sessionID,
+ UserID: userID,
+ AuthTime: authTime,
+ AuthMethods: authMethods,
+ }
+}
+
+func SessionLinkedEventMapper(event *repository.Event) (eventstore.Event, error) {
+ added := &SessionLinkedEvent{
+ BaseEvent: *eventstore.BaseEventFromRepo(event),
+ }
+ err := json.Unmarshal(event.Data, added)
+ if err != nil {
+ return nil, errors.ThrowInternal(err, "AUTHR-Sfe3w", "unable to unmarshal auth request session linked")
+ }
+
+ return added, nil
+}
+
+type FailedEvent struct {
+ eventstore.BaseEvent `json:"-"`
+
+ Reason domain.OIDCErrorReason `json:"reason,omitempty"`
+}
+
+func (e *FailedEvent) Data() interface{} {
+ return e
+}
+
+func (e *FailedEvent) UniqueConstraints() []*eventstore.EventUniqueConstraint {
+ return nil
+}
+
+func NewFailedEvent(
+ ctx context.Context,
+ aggregate *eventstore.Aggregate,
+ reason domain.OIDCErrorReason,
+) *FailedEvent {
+ return &FailedEvent{
+ BaseEvent: *eventstore.NewBaseEventForPush(
+ ctx,
+ aggregate,
+ FailedType,
+ ),
+ Reason: reason,
+ }
+}
+
+func FailedEventMapper(event *repository.Event) (eventstore.Event, error) {
+ added := &FailedEvent{
+ BaseEvent: *eventstore.BaseEventFromRepo(event),
+ }
+ err := json.Unmarshal(event.Data, added)
+ if err != nil {
+ return nil, errors.ThrowInternal(err, "AUTHR-Sfe3w", "unable to unmarshal auth request session linked")
+ }
+
+ return added, nil
+}
+
+type CodeAddedEvent struct {
+ eventstore.BaseEvent `json:"-"`
+}
+
+func (e *CodeAddedEvent) Data() interface{} {
+ return e
+}
+
+func (e *CodeAddedEvent) UniqueConstraints() []*eventstore.EventUniqueConstraint {
+ return nil
+}
+
+func NewCodeAddedEvent(ctx context.Context,
+ aggregate *eventstore.Aggregate,
+) *CodeAddedEvent {
+ return &CodeAddedEvent{
+ BaseEvent: *eventstore.NewBaseEventForPush(
+ ctx,
+ aggregate,
+ CodeAddedType,
+ ),
+ }
+}
+
+func CodeAddedEventMapper(event *repository.Event) (eventstore.Event, error) {
+ added := &CodeAddedEvent{
+ BaseEvent: *eventstore.BaseEventFromRepo(event),
+ }
+ err := json.Unmarshal(event.Data, added)
+ if err != nil {
+ return nil, errors.ThrowInternal(err, "AUTHR-Sfe3w", "unable to unmarshal auth request code added")
+ }
+
+ return added, nil
+}
+
+type CodeExchangedEvent struct {
+ eventstore.BaseEvent `json:"-"`
+}
+
+func (e *CodeExchangedEvent) Data() interface{} {
+ return nil
+}
+
+func (e *CodeExchangedEvent) UniqueConstraints() []*eventstore.EventUniqueConstraint {
+ return nil
+}
+
+func NewCodeExchangedEvent(ctx context.Context,
+ aggregate *eventstore.Aggregate,
+) *CodeExchangedEvent {
+ return &CodeExchangedEvent{
+ BaseEvent: *eventstore.NewBaseEventForPush(
+ ctx,
+ aggregate,
+ CodeExchangedType,
+ ),
+ }
+}
+
+func CodeExchangedEventMapper(event *repository.Event) (eventstore.Event, error) {
+ return &CodeExchangedEvent{
+ BaseEvent: *eventstore.BaseEventFromRepo(event),
+ }, nil
+}
+
+type SucceededEvent struct {
+ eventstore.BaseEvent `json:"-"`
+}
+
+func (e *SucceededEvent) Data() interface{} {
+ return nil
+}
+
+func (e *SucceededEvent) UniqueConstraints() []*eventstore.EventUniqueConstraint {
+ return nil
+}
+
+func NewSucceededEvent(ctx context.Context,
+ aggregate *eventstore.Aggregate,
+) *SucceededEvent {
+ return &SucceededEvent{
+ BaseEvent: *eventstore.NewBaseEventForPush(
+ ctx,
+ aggregate,
+ SucceededType,
+ ),
+ }
+}
+
+func SucceededEventMapper(event *repository.Event) (eventstore.Event, error) {
+ return &SucceededEvent{
+ BaseEvent: *eventstore.BaseEventFromRepo(event),
+ }, nil
+}
diff --git a/internal/repository/authrequest/eventstore.go b/internal/repository/authrequest/eventstore.go
new file mode 100644
index 000000000..c1bfed7dc
--- /dev/null
+++ b/internal/repository/authrequest/eventstore.go
@@ -0,0 +1,12 @@
+package authrequest
+
+import "github.com/zitadel/zitadel/internal/eventstore"
+
+func RegisterEventMappers(es *eventstore.Eventstore) {
+ es.RegisterFilterEventMapper(AggregateType, AddedType, AddedEventMapper).
+ RegisterFilterEventMapper(AggregateType, SessionLinkedType, SessionLinkedEventMapper).
+ RegisterFilterEventMapper(AggregateType, CodeAddedType, CodeAddedEventMapper).
+ RegisterFilterEventMapper(AggregateType, CodeExchangedType, CodeExchangedEventMapper).
+ RegisterFilterEventMapper(AggregateType, FailedType, FailedEventMapper).
+ RegisterFilterEventMapper(AggregateType, SucceededType, SucceededEventMapper)
+}
diff --git a/internal/repository/instance/policy_login.go b/internal/repository/instance/policy_login.go
index fad06fa71..73a0b3ef4 100644
--- a/internal/repository/instance/policy_login.go
+++ b/internal/repository/instance/policy_login.go
@@ -27,6 +27,7 @@ func NewLoginPolicyAddedEvent(
allowRegister,
allowExternalIDP,
forceMFA,
+ forceMFALocalOnly,
hidePasswordReset,
ignoreUnknownUsernames,
allowDomainDiscovery,
@@ -50,6 +51,7 @@ func NewLoginPolicyAddedEvent(
allowRegister,
allowExternalIDP,
forceMFA,
+ forceMFALocalOnly,
hidePasswordReset,
ignoreUnknownUsernames,
allowDomainDiscovery,
diff --git a/internal/repository/milestone/aggregate.go b/internal/repository/milestone/aggregate.go
new file mode 100644
index 000000000..bb9ca99cb
--- /dev/null
+++ b/internal/repository/milestone/aggregate.go
@@ -0,0 +1,30 @@
+package milestone
+
+import (
+ "context"
+
+ "github.com/zitadel/zitadel/internal/api/authz"
+ "github.com/zitadel/zitadel/internal/eventstore"
+)
+
+const (
+ AggregateType = "milestone"
+ AggregateVersion = "v1"
+)
+
+type Aggregate struct {
+ eventstore.Aggregate
+}
+
+func NewAggregate(ctx context.Context, id string) *Aggregate {
+ instanceID := authz.GetInstance(ctx).InstanceID()
+ return &Aggregate{
+ Aggregate: eventstore.Aggregate{
+ Type: AggregateType,
+ Version: AggregateVersion,
+ ID: id,
+ ResourceOwner: instanceID,
+ InstanceID: instanceID,
+ },
+ }
+}
diff --git a/internal/repository/milestone/events.go b/internal/repository/milestone/events.go
new file mode 100644
index 000000000..5ec2f96f9
--- /dev/null
+++ b/internal/repository/milestone/events.go
@@ -0,0 +1,54 @@
+package milestone
+
+import (
+ "context"
+
+ "github.com/zitadel/zitadel/internal/eventstore"
+)
+
+const (
+ eventTypePrefix = eventstore.EventType("milestone.")
+ PushedEventType = eventTypePrefix + "pushed"
+)
+
+type PushedEvent struct {
+ *eventstore.BaseEvent `json:"-"`
+ MilestoneType Type `json:"type"`
+ ExternalDomain string `json:"externalDomain"`
+ PrimaryDomain string `json:"primaryDomain"`
+ Endpoints []string `json:"endpoints"`
+}
+
+func (p *PushedEvent) Data() interface{} {
+ return p
+}
+
+func (p *PushedEvent) UniqueConstraints() []*eventstore.EventUniqueConstraint {
+ return nil
+}
+
+func (p *PushedEvent) SetBaseEvent(b *eventstore.BaseEvent) {
+ p.BaseEvent = b
+}
+
+var PushedEventMapper = eventstore.GenericEventMapper[PushedEvent]
+
+func NewPushedEvent(
+ ctx context.Context,
+ aggregate *Aggregate,
+ msType Type,
+ endpoints []string,
+ externalDomain, primaryDomain string,
+) *PushedEvent {
+ return &PushedEvent{
+ BaseEvent: eventstore.NewBaseEventForPush(
+ ctx,
+ &aggregate.Aggregate,
+ PushedEventType,
+ ),
+ MilestoneType: msType,
+ Endpoints: endpoints,
+ ExternalDomain: externalDomain,
+ PrimaryDomain: primaryDomain,
+ }
+}
diff --git a/internal/repository/milestone/eventstore.go b/internal/repository/milestone/eventstore.go
new file mode 100644
index 000000000..4f82b0819
--- /dev/null
+++ b/internal/repository/milestone/eventstore.go
@@ -0,0 +1,9 @@
+package milestone
+
+import (
+ "github.com/zitadel/zitadel/internal/eventstore"
+)
+
+func RegisterEventMappers(es *eventstore.Eventstore) {
+ es.RegisterFilterEventMapper(AggregateType, PushedEventType, PushedEventMapper)
+}
diff --git a/internal/repository/milestone/type.go b/internal/repository/milestone/type.go
new file mode 100644
index 000000000..f57bb032e
--- /dev/null
+++ b/internal/repository/milestone/type.go
@@ -0,0 +1,59 @@
+//go:generate stringer -type Type
+
+package milestone
+
+import (
+ "fmt"
+ "strings"
+)
+
+type Type int
+
+const (
+ unknown Type = iota
+
+ InstanceCreated
+ AuthenticationSucceededOnInstance
+ ProjectCreated
+ ApplicationCreated
+ AuthenticationSucceededOnApplication
+ InstanceDeleted
+
+ typesCount
+)
+
+func AllTypes() []Type {
+ types := make([]Type, typesCount-1)
+ for i := Type(1); i < typesCount; i++ {
+ types[i-1] = i
+ }
+ return types
+}
+
+func (t *Type) MarshalJSON() ([]byte, error) {
+ return []byte(fmt.Sprintf(`"%s"`, t.String())), nil
+}
+
+func (t *Type) UnmarshalJSON(data []byte) error {
+ *t = typeFromString(strings.Trim(string(data), `"`))
+ return nil
+}
+
+func typeFromString(t string) Type {
+ switch t {
+ case InstanceCreated.String():
+ return InstanceCreated
+ case AuthenticationSucceededOnInstance.String():
+ return AuthenticationSucceededOnInstance
+ case ProjectCreated.String():
+ return ProjectCreated
+ case ApplicationCreated.String():
+ return ApplicationCreated
+ case AuthenticationSucceededOnApplication.String():
+ return AuthenticationSucceededOnApplication
+ case InstanceDeleted.String():
+ return InstanceDeleted
+ default:
+ return unknown
+ }
+}
diff --git a/internal/repository/milestone/type_string.go b/internal/repository/milestone/type_string.go
new file mode 100644
index 000000000..ce6a98441
--- /dev/null
+++ b/internal/repository/milestone/type_string.go
@@ -0,0 +1,30 @@
+// Code generated by "stringer -type Type"; DO NOT EDIT.
+
+package milestone
+
+import "strconv"
+
+func _() {
+ // An "invalid array index" compiler error signifies that the constant values have changed.
+ // Re-run the stringer command to generate them again.
+ var x [1]struct{}
+ _ = x[unknown-0]
+ _ = x[InstanceCreated-1]
+ _ = x[AuthenticationSucceededOnInstance-2]
+ _ = x[ProjectCreated-3]
+ _ = x[ApplicationCreated-4]
+ _ = x[AuthenticationSucceededOnApplication-5]
+ _ = x[InstanceDeleted-6]
+ _ = x[typesCount-7]
+}
+
+const _Type_name = "unknownInstanceCreatedAuthenticationSucceededOnInstanceProjectCreatedApplicationCreatedAuthenticationSucceededOnApplicationInstanceDeletedtypesCount"
+
+var _Type_index = [...]uint8{0, 7, 22, 55, 69, 87, 123, 138, 148}
+
+func (i Type) String() string {
+ if i < 0 || i >= Type(len(_Type_index)-1) {
+ return "Type(" + strconv.FormatInt(int64(i), 10) + ")"
+ }
+ return _Type_name[_Type_index[i]:_Type_index[i+1]]
+}
diff --git a/internal/repository/oidcsession/aggregate.go b/internal/repository/oidcsession/aggregate.go
new file mode 100644
index 000000000..920f6c9fc
--- /dev/null
+++ b/internal/repository/oidcsession/aggregate.go
@@ -0,0 +1,25 @@
+package oidcsession
+
+import (
+ "github.com/zitadel/zitadel/internal/eventstore"
+)
+
+const (
+ AggregateType = "oidc_session"
+ AggregateVersion = "v1"
+)
+
+type Aggregate struct {
+ eventstore.Aggregate
+}
+
+func NewAggregate(id, resourceOwner string) *Aggregate {
+ return &Aggregate{
+ Aggregate: eventstore.Aggregate{
+ Type: AggregateType,
+ Version: AggregateVersion,
+ ID: id,
+ ResourceOwner: resourceOwner,
+ },
+ }
+}
diff --git a/internal/repository/oidcsession/eventstore.go b/internal/repository/oidcsession/eventstore.go
new file mode 100644
index 000000000..091bcf5d4
--- /dev/null
+++ b/internal/repository/oidcsession/eventstore.go
@@ -0,0 +1,15 @@
+package oidcsession
+
+import (
+ "github.com/zitadel/zitadel/internal/eventstore"
+)
+
+func RegisterEventMappers(es *eventstore.Eventstore) {
+ es.RegisterFilterEventMapper(AggregateType, AddedType, eventstore.GenericEventMapper[AddedEvent]).
+ RegisterFilterEventMapper(AggregateType, AccessTokenAddedType, eventstore.GenericEventMapper[AccessTokenAddedEvent]).
+ RegisterFilterEventMapper(AggregateType, AccessTokenRevokedType, eventstore.GenericEventMapper[AccessTokenRevokedEvent]).
+ RegisterFilterEventMapper(AggregateType, RefreshTokenAddedType, eventstore.GenericEventMapper[RefreshTokenAddedEvent]).
+ RegisterFilterEventMapper(AggregateType, RefreshTokenRenewedType, eventstore.GenericEventMapper[RefreshTokenRenewedEvent]).
+ RegisterFilterEventMapper(AggregateType, RefreshTokenRevokedType, eventstore.GenericEventMapper[RefreshTokenRevokedEvent])
+
+}
diff --git a/internal/repository/oidcsession/oidc_session.go b/internal/repository/oidcsession/oidc_session.go
new file mode 100644
index 000000000..8887b6192
--- /dev/null
+++ b/internal/repository/oidcsession/oidc_session.go
@@ -0,0 +1,241 @@
+package oidcsession
+
+import (
+ "context"
+ "time"
+
+ "github.com/zitadel/zitadel/internal/domain"
+ "github.com/zitadel/zitadel/internal/eventstore"
+)
+
+const (
+ oidcSessionEventPrefix = "oidc_session."
+ AddedType = oidcSessionEventPrefix + "added"
+ AccessTokenAddedType = oidcSessionEventPrefix + "access_token.added"
+ AccessTokenRevokedType = oidcSessionEventPrefix + "access_token.revoked"
+ RefreshTokenAddedType = oidcSessionEventPrefix + "refresh_token.added"
+ RefreshTokenRenewedType = oidcSessionEventPrefix + "refresh_token.renewed"
+ RefreshTokenRevokedType = oidcSessionEventPrefix + "refresh_token.revoked"
+)
+
+type AddedEvent struct {
+ eventstore.BaseEvent `json:"-"`
+
+ UserID string `json:"userID"`
+ SessionID string `json:"sessionID"`
+ ClientID string `json:"clientID"`
+ Audience []string `json:"audience"`
+ Scope []string `json:"scope"`
+ AuthMethods []domain.UserAuthMethodType `json:"authMethods"`
+ AuthTime time.Time `json:"authTime"`
+}
+
+func (e *AddedEvent) Data() interface{} {
+ return e
+}
+
+func (e *AddedEvent) UniqueConstraints() []*eventstore.EventUniqueConstraint {
+ return nil
+}
+
+func (e *AddedEvent) SetBaseEvent(event *eventstore.BaseEvent) {
+ e.BaseEvent = *event
+}
+
+func NewAddedEvent(ctx context.Context,
+ aggregate *eventstore.Aggregate,
+ userID,
+ sessionID,
+ clientID string,
+ audience,
+ scope []string,
+ authMethods []domain.UserAuthMethodType,
+ authTime time.Time,
+) *AddedEvent {
+ return &AddedEvent{
+ BaseEvent: *eventstore.NewBaseEventForPush(
+ ctx,
+ aggregate,
+ AddedType,
+ ),
+ UserID: userID,
+ SessionID: sessionID,
+ ClientID: clientID,
+ Audience: audience,
+ Scope: scope,
+ AuthMethods: authMethods,
+ AuthTime: authTime,
+ }
+}
+
+type AccessTokenAddedEvent struct {
+ eventstore.BaseEvent `json:"-"`
+
+ ID string `json:"id"`
+ Scope []string `json:"scope"`
+ Lifetime time.Duration `json:"lifetime"`
+}
+
+func (e *AccessTokenAddedEvent) Data() interface{} {
+ return e
+}
+
+func (e *AccessTokenAddedEvent) UniqueConstraints() []*eventstore.EventUniqueConstraint {
+ return nil
+}
+
+func (e *AccessTokenAddedEvent) SetBaseEvent(event *eventstore.BaseEvent) {
+ e.BaseEvent = *event
+}
+
+func NewAccessTokenAddedEvent(
+ ctx context.Context,
+ aggregate *eventstore.Aggregate,
+ id string,
+ scope []string,
+ lifetime time.Duration,
+) *AccessTokenAddedEvent {
+ return &AccessTokenAddedEvent{
+ BaseEvent: *eventstore.NewBaseEventForPush(
+ ctx,
+ aggregate,
+ AccessTokenAddedType,
+ ),
+ ID: id,
+ Scope: scope,
+ Lifetime: lifetime,
+ }
+}
+
+type AccessTokenRevokedEvent struct {
+ eventstore.BaseEvent `json:"-"`
+}
+
+func (e *AccessTokenRevokedEvent) Data() interface{} {
+ return e
+}
+
+func (e *AccessTokenRevokedEvent) UniqueConstraints() []*eventstore.EventUniqueConstraint {
+ return nil
+}
+
+func (e *AccessTokenRevokedEvent) SetBaseEvent(event *eventstore.BaseEvent) {
+ e.BaseEvent = *event
+}
+
+func NewAccessTokenRevokedEvent(
+ ctx context.Context,
+ aggregate *eventstore.Aggregate,
+) *AccessTokenAddedEvent {
+ return &AccessTokenAddedEvent{
+ BaseEvent: *eventstore.NewBaseEventForPush(
+ ctx,
+ aggregate,
+ AccessTokenRevokedType,
+ ),
+ }
+}
+
+type RefreshTokenAddedEvent struct {
+ eventstore.BaseEvent `json:"-"`
+
+ ID string `json:"id"`
+ Lifetime time.Duration `json:"lifetime"`
+ IdleLifetime time.Duration `json:"idleLifetime"`
+}
+
+func (e *RefreshTokenAddedEvent) Data() interface{} {
+ return e
+}
+
+func (e *RefreshTokenAddedEvent) UniqueConstraints() []*eventstore.EventUniqueConstraint {
+ return nil
+}
+
+func (e *RefreshTokenAddedEvent) SetBaseEvent(event *eventstore.BaseEvent) {
+ e.BaseEvent = *event
+}
+
+func NewRefreshTokenAddedEvent(
+ ctx context.Context,
+ aggregate *eventstore.Aggregate,
+ id string,
+ lifetime,
+ idleLifetime time.Duration,
+) *RefreshTokenAddedEvent {
+ return &RefreshTokenAddedEvent{
+ BaseEvent: *eventstore.NewBaseEventForPush(
+ ctx,
+ aggregate,
+ RefreshTokenAddedType,
+ ),
+ ID: id,
+ Lifetime: lifetime,
+ IdleLifetime: idleLifetime,
+ }
+}
+
+type RefreshTokenRenewedEvent struct {
+ eventstore.BaseEvent `json:"-"`
+
+ ID string `json:"id"`
+ IdleLifetime time.Duration `json:"idleLifetime"`
+}
+
+func (e *RefreshTokenRenewedEvent) Data() interface{} {
+ return e
+}
+
+func (e *RefreshTokenRenewedEvent) UniqueConstraints() []*eventstore.EventUniqueConstraint {
+ return nil
+}
+
+func (e *RefreshTokenRenewedEvent) SetBaseEvent(event *eventstore.BaseEvent) {
+ e.BaseEvent = *event
+}
+
+func NewRefreshTokenRenewedEvent(
+ ctx context.Context,
+ aggregate *eventstore.Aggregate,
+ id string,
+ idleLifetime time.Duration,
+) *RefreshTokenRenewedEvent {
+ return &RefreshTokenRenewedEvent{
+ BaseEvent: *eventstore.NewBaseEventForPush(
+ ctx,
+ aggregate,
+ RefreshTokenRenewedType,
+ ),
+ ID: id,
+ IdleLifetime: idleLifetime,
+ }
+}
+
+type RefreshTokenRevokedEvent struct {
+ eventstore.BaseEvent `json:"-"`
+}
+
+func (e *RefreshTokenRevokedEvent) Data() interface{} {
+ return e
+}
+
+func (e *RefreshTokenRevokedEvent) UniqueConstraints() []*eventstore.EventUniqueConstraint {
+ return nil
+}
+
+func (e *RefreshTokenRevokedEvent) SetBaseEvent(event *eventstore.BaseEvent) {
+ e.BaseEvent = *event
+}
+
+func NewRefreshTokenRevokedEvent(
+ ctx context.Context,
+ aggregate *eventstore.Aggregate,
+) *RefreshTokenRevokedEvent {
+ return &RefreshTokenRevokedEvent{
+ BaseEvent: *eventstore.NewBaseEventForPush(
+ ctx,
+ aggregate,
+ RefreshTokenRevokedType,
+ ),
+ }
+}
diff --git a/internal/repository/org/policy_login.go b/internal/repository/org/policy_login.go
index 449e3e10a..95e38ca92 100644
--- a/internal/repository/org/policy_login.go
+++ b/internal/repository/org/policy_login.go
@@ -28,6 +28,7 @@ func NewLoginPolicyAddedEvent(
allowRegister,
allowExternalIDP,
forceMFA,
+ forceMFALocalOnly,
hidePasswordReset,
ignoreUnknownUsernames,
allowDomainDiscovery,
@@ -51,6 +52,7 @@ func NewLoginPolicyAddedEvent(
allowRegister,
allowExternalIDP,
forceMFA,
+ forceMFALocalOnly,
hidePasswordReset,
ignoreUnknownUsernames,
allowDomainDiscovery,
diff --git a/internal/repository/policy/login.go b/internal/repository/policy/login.go
index dfd2d0afd..790b1a571 100644
--- a/internal/repository/policy/login.go
+++ b/internal/repository/policy/login.go
@@ -24,6 +24,7 @@ type LoginPolicyAddedEvent struct {
AllowRegister bool `json:"allowRegister,omitempty"`
AllowExternalIDP bool `json:"allowExternalIdp,omitempty"`
ForceMFA bool `json:"forceMFA,omitempty"`
+ ForceMFALocalOnly bool `json:"forceMFALocalOnly,omitempty"`
HidePasswordReset bool `json:"hidePasswordReset,omitempty"`
IgnoreUnknownUsernames bool `json:"ignoreUnknownUsernames,omitempty"`
AllowDomainDiscovery bool `json:"allowDomainDiscovery,omitempty"`
@@ -52,6 +53,7 @@ func NewLoginPolicyAddedEvent(
allowRegister,
allowExternalIDP,
forceMFA,
+ forceMFALocalOnly,
hidePasswordReset,
ignoreUnknownUsernames,
allowDomainDiscovery,
@@ -71,6 +73,7 @@ func NewLoginPolicyAddedEvent(
AllowRegister: allowRegister,
AllowUserNamePassword: allowUserNamePassword,
ForceMFA: forceMFA,
+ ForceMFALocalOnly: forceMFALocalOnly,
PasswordlessType: passwordlessType,
HidePasswordReset: hidePasswordReset,
IgnoreUnknownUsernames: ignoreUnknownUsernames,
@@ -106,6 +109,7 @@ type LoginPolicyChangedEvent struct {
AllowRegister *bool `json:"allowRegister,omitempty"`
AllowExternalIDP *bool `json:"allowExternalIdp,omitempty"`
ForceMFA *bool `json:"forceMFA,omitempty"`
+ ForceMFALocalOnly *bool `json:"forceMFALocalOnly,omitempty"`
HidePasswordReset *bool `json:"hidePasswordReset,omitempty"`
IgnoreUnknownUsernames *bool `json:"ignoreUnknownUsernames,omitempty"`
AllowDomainDiscovery *bool `json:"allowDomainDiscovery,omitempty"`
@@ -170,6 +174,12 @@ func ChangeForceMFA(forceMFA bool) func(*LoginPolicyChangedEvent) {
}
}
+func ChangeForceMFALocalOnly(forceMFALocalOnly bool) func(*LoginPolicyChangedEvent) {
+ return func(e *LoginPolicyChangedEvent) {
+ e.ForceMFALocalOnly = &forceMFALocalOnly
+ }
+}
+
func ChangePasswordlessType(passwordlessType domain.PasswordlessType) func(*LoginPolicyChangedEvent) {
return func(e *LoginPolicyChangedEvent) {
e.PasswordlessType = &passwordlessType
diff --git a/internal/repository/pseudo/aggregate.go b/internal/repository/pseudo/aggregate.go
new file mode 100644
index 000000000..811f56661
--- /dev/null
+++ b/internal/repository/pseudo/aggregate.go
@@ -0,0 +1,21 @@
+package pseudo
+
+import "github.com/zitadel/zitadel/internal/eventstore"
+
+const (
+ AggregateType = "pseudo"
+ AggregateVersion = "v1"
+)
+
+type Aggregate struct {
+ eventstore.Aggregate
+}
+
+func NewAggregate() *Aggregate {
+ return &Aggregate{
+ Aggregate: eventstore.Aggregate{
+ Type: AggregateType,
+ Version: AggregateVersion,
+ },
+ }
+}
diff --git a/internal/repository/pseudo/events.go b/internal/repository/pseudo/events.go
new file mode 100644
index 000000000..0baaa9368
--- /dev/null
+++ b/internal/repository/pseudo/events.go
@@ -0,0 +1,40 @@
+// Package pseudo contains virtual events, that are not stored in the eventstore.
+package pseudo
+
+import (
+ "context"
+ "time"
+
+ "github.com/zitadel/zitadel/internal/eventstore"
+)
+
+const (
+ eventTypePrefix = eventstore.EventType("pseudo.")
+ ScheduledEventType = eventTypePrefix + "timestamp"
+)
+
+var _ eventstore.Event = (*ScheduledEvent)(nil)
+
+type ScheduledEvent struct {
+ *eventstore.BaseEvent `json:"-"`
+ Timestamp time.Time `json:"-"`
+ InstanceIDs []string `json:"-"`
+}
+
+// NewScheduledEvent returns an event that can be processed by event handlers like any other event.
+// It receives the current timestamp and an ID list of instances that are active and should be processed.
+func NewScheduledEvent(
+ ctx context.Context,
+ timestamp time.Time,
+ instanceIDs ...string,
+) *ScheduledEvent {
+ return &ScheduledEvent{
+ BaseEvent: eventstore.NewBaseEventForPush(
+ ctx,
+ &NewAggregate().Aggregate,
+ ScheduledEventType,
+ ),
+ Timestamp: timestamp,
+ InstanceIDs: instanceIDs,
+ }
+}
diff --git a/internal/repository/user/eventstore.go b/internal/repository/user/eventstore.go
index d216c910c..847736b60 100644
--- a/internal/repository/user/eventstore.go
+++ b/internal/repository/user/eventstore.go
@@ -17,6 +17,7 @@ func RegisterEventMappers(es *eventstore.Eventstore) {
RegisterFilterEventMapper(AggregateType, UserV1PasswordCodeSentType, HumanPasswordCodeSentEventMapper).
RegisterFilterEventMapper(AggregateType, UserV1PasswordCheckSucceededType, HumanPasswordCheckSucceededEventMapper).
RegisterFilterEventMapper(AggregateType, UserV1PasswordCheckFailedType, HumanPasswordCheckFailedEventMapper).
+ RegisterFilterEventMapper(AggregateType, UserV1PasswordHashUpdatedType, eventstore.GenericEventMapper[HumanPasswordHashUpdatedEvent]).
RegisterFilterEventMapper(AggregateType, UserV1EmailChangedType, HumanEmailChangedEventMapper).
RegisterFilterEventMapper(AggregateType, UserV1EmailVerifiedType, HumanEmailVerifiedEventMapper).
RegisterFilterEventMapper(AggregateType, UserV1EmailVerificationFailedType, HumanEmailVerificationFailedEventMapper).
diff --git a/internal/repository/user/human.go b/internal/repository/user/human.go
index 6184b7d07..075fd55ca 100644
--- a/internal/repository/user/human.go
+++ b/internal/repository/user/human.go
@@ -48,7 +48,10 @@ type HumanAddedEvent struct {
Region string `json:"region,omitempty"`
StreetAddress string `json:"streetAddress,omitempty"`
+ // New events only use EncodedHash. However, the secret field
+ // is preserved to handle events older than the switch to Passwap.
Secret *crypto.CryptoValue `json:"secret,omitempty"`
+ EncodedHash string `json:"encodedHash,omitempty"`
ChangeRequired bool `json:"changeRequired,omitempty"`
}
@@ -81,10 +84,10 @@ func (e *HumanAddedEvent) AddPhoneData(
}
func (e *HumanAddedEvent) AddPasswordData(
- secret *crypto.CryptoValue,
+ encoded string,
changeRequired bool,
) {
- e.Secret = secret
+ e.EncodedHash = encoded
e.ChangeRequired = changeRequired
}
@@ -149,8 +152,12 @@ type HumanRegisteredEvent struct {
PostalCode string `json:"postalCode,omitempty"`
Region string `json:"region,omitempty"`
StreetAddress string `json:"streetAddress,omitempty"`
- Secret *crypto.CryptoValue `json:"secret,omitempty"`
- ChangeRequired bool `json:"changeRequired,omitempty"`
+
+ // New events only use EncodedHash. However, the secret field
+ // is preserved to handle events older than the switch to Passwap.
+ Secret *crypto.CryptoValue `json:"secret,omitempty"` // legacy
+ EncodedHash string `json:"encodedHash,omitempty"`
+ ChangeRequired bool `json:"changeRequired,omitempty"`
}
func (e *HumanRegisteredEvent) Data() interface{} {
@@ -182,10 +189,10 @@ func (e *HumanRegisteredEvent) AddPhoneData(
}
func (e *HumanRegisteredEvent) AddPasswordData(
- secret *crypto.CryptoValue,
+ encoded string,
changeRequired bool,
) {
- e.Secret = secret
+ e.EncodedHash = encoded
e.ChangeRequired = changeRequired
}
diff --git a/internal/repository/user/human_password.go b/internal/repository/user/human_password.go
index ccd38527d..f2f40cd6c 100644
--- a/internal/repository/user/human_password.go
+++ b/internal/repository/user/human_password.go
@@ -26,7 +26,10 @@ const (
type HumanPasswordChangedEvent struct {
eventstore.BaseEvent `json:"-"`
+ // New events only use EncodedHash. However, the secret field
+ // is preserved to handle events older than the switch to Passwap.
Secret *crypto.CryptoValue `json:"secret,omitempty"`
+ EncodedHash string `json:"encodedHash,omitempty"`
ChangeRequired bool `json:"changeRequired"`
UserAgentID string `json:"userAgentID,omitempty"`
}
@@ -42,7 +45,7 @@ func (e *HumanPasswordChangedEvent) UniqueConstraints() []*eventstore.EventUniqu
func NewHumanPasswordChangedEvent(
ctx context.Context,
aggregate *eventstore.Aggregate,
- secret *crypto.CryptoValue,
+ encodeHash string,
changeRequired bool,
userAgentID string,
) *HumanPasswordChangedEvent {
@@ -52,7 +55,7 @@ func NewHumanPasswordChangedEvent(
aggregate,
HumanPasswordChangedType,
),
- Secret: secret,
+ EncodedHash: encodeHash,
ChangeRequired: changeRequired,
UserAgentID: userAgentID,
}
@@ -268,3 +271,44 @@ func HumanPasswordCheckFailedEventMapper(event *repository.Event) (eventstore.Ev
return humanAdded, nil
}
+
+type HumanPasswordHashUpdatedEvent struct {
+ eventstore.BaseEvent `json:"-"`
+ EncodedHash string `json:"encodedHash,omitempty"`
+}
+
+func (e *HumanPasswordHashUpdatedEvent) Data() interface{} {
+ return e
+}
+
+func (e *HumanPasswordHashUpdatedEvent) UniqueConstraints() []*eventstore.EventUniqueConstraint {
+ return nil
+}
+
+func (e *HumanPasswordHashUpdatedEvent) SetBaseEvent(base *eventstore.BaseEvent) {
+ e.BaseEvent = *base
+}
+
+func NewHumanPasswordHashUpdatedEvent(
+ ctx context.Context,
+ aggregate *eventstore.Aggregate,
+ encoded string,
+) *HumanPasswordHashUpdatedEvent {
+ return &HumanPasswordHashUpdatedEvent{
+ BaseEvent: *eventstore.NewBaseEventForPush(
+ ctx,
+ aggregate,
+ HumanPasswordCheckFailedType,
+ ),
+ EncodedHash: encoded,
+ }
+}
+
+// SecretOrEncodedHash returns the legacy *crypto.CryptoValue if it is not nil.
+// orherwise it will returns the encoded hash string.
+func SecretOrEncodedHash(secret *crypto.CryptoValue, encoded string) string {
+ if secret != nil {
+ return string(secret.Crypted)
+ }
+ return encoded
+}
diff --git a/internal/repository/user/v1.go b/internal/repository/user/v1.go
index 010e1a235..a2974bad9 100644
--- a/internal/repository/user/v1.go
+++ b/internal/repository/user/v1.go
@@ -15,6 +15,7 @@ const (
UserV1PasswordCodeSentType = userV1PasswordEventTypePrefix + "code.sent"
UserV1PasswordCheckSucceededType = userV1PasswordEventTypePrefix + "check.succeeded"
UserV1PasswordCheckFailedType = userV1PasswordEventTypePrefix + "check.failed"
+ UserV1PasswordHashUpdatedType = userV1PasswordEventTypePrefix + "hash.updated"
userV1EmailEventTypePrefix = userEventTypePrefix + "email."
UserV1EmailChangedType = userV1EmailEventTypePrefix + "changed"
diff --git a/internal/static/i18n/bg.yaml b/internal/static/i18n/bg.yaml
index 695007888..959c5fbbf 100644
--- a/internal/static/i18n/bg.yaml
+++ b/internal/static/i18n/bg.yaml
@@ -122,6 +122,8 @@ Errors:
Empty: Паролата е празна
Invalid: Паролата е невалидна
NotSet: Потребителят не е задал парола
+ NotChanged: Паролата не е променена
+ NotSupported: Хеш кодирането на паролата не се поддържа
PasswordComplexityPolicy:
NotFound: Политиката за парола не е намерена
MinLength: Паролата е твърде кратка
@@ -506,6 +508,15 @@ Errors:
TokenCreationFailed: Неуспешно създаване на токен
InvalidToken: Знакът за намерение е невалиден
OtherUser: Намерение, предназначено за друг потребител
+ AuthRequest:
+ AlreadyExists: Auth Request вече съществува
+ NotExisting: Auth Request не съществува
+ WrongLoginClient: Auth Request, създаден от друг клиент за влизане
+ OIDCSession:
+ RefreshTokenInvalid: Токенът за опресняване е невалиден
+ Token:
+ Invalid: Токенът е невалиден
+ Expired: Токенът е изтекъл
AggregateTypes:
action: Действие
diff --git a/internal/static/i18n/de.yaml b/internal/static/i18n/de.yaml
index 846e09dff..929ce1db6 100644
--- a/internal/static/i18n/de.yaml
+++ b/internal/static/i18n/de.yaml
@@ -120,6 +120,8 @@ Errors:
Empty: Passwort ist leer
Invalid: Passwort ungültig
NotSet: Benutzer hat kein Passwort gesetzt
+ NotChanged: Passwort nicht geändert
+ NotSupported: Passwort-Hash-Kodierung wird nicht unterstützt
PasswordComplexityPolicy:
NotFound: Passwort Policy konnte nicht gefunden werden
MinLength: Passwort ist zu kurz
@@ -488,6 +490,16 @@ Errors:
TokenCreationFailed: Tokenerstellung schlug fehl
InvalidToken: Intent Token ist ungültig
OtherUser: Intent ist für anderen Benutzer gedacht
+ AuthRequest:
+ AlreadyExists: Auth Request existiert bereits
+ NotExisting: Auth Request existiert nicht
+ WrongLoginClient: Auth Request wurde von einem anderen Login-Client erstellt
+ OIDCSession:
+ RefreshTokenInvalid: Refresh Token ist ungültig
+ Token:
+ Invalid: Token ist ungültig
+ Expired: Token ist abgelaufen
+ InvalidClient: Token wurde nicht für diesen Client ausgestellt
AggregateTypes:
action: Action
diff --git a/internal/static/i18n/en.yaml b/internal/static/i18n/en.yaml
index 5986651ea..f623dc213 100644
--- a/internal/static/i18n/en.yaml
+++ b/internal/static/i18n/en.yaml
@@ -73,8 +73,8 @@ Errors:
NotFound: Profile not found
NotChanged: Profile not changed
Empty: Profile is empty
- FirstNameEmpty: First name in profile is empty
- LastNameEmpty: Last name in profile is empty
+ FirstNameEmpty: Given name in profile is empty
+ LastNameEmpty: Family name in profile is empty
IDMissing: Profile ID is missing
Email:
NotFound: Email not found
@@ -120,6 +120,8 @@ Errors:
Empty: Password is empty
Invalid: Password is invalid
NotSet: User has not set a password
+ NotChanged: Password not changed
+ NotSupported: Password hash encoding not supported
PasswordComplexityPolicy:
NotFound: Password policy not found
MinLength: Password is too short
@@ -488,6 +490,16 @@ Errors:
TokenCreationFailed: Token creation failed
InvalidToken: Intent Token is invalid
OtherUser: Intent meant for another user
+ AuthRequest:
+ AlreadyExists: Auth Request already exists
+ NotExisting: Auth Request does not exist
+ WrongLoginClient: Auth Request created by other login client
+ OIDCSession:
+ RefreshTokenInvalid: Refresh Token is invalid
+ Token:
+ Invalid: Token is invalid
+ Expired: Token is expired
+ InvalidClient: Token was not issued for this client
AggregateTypes:
action: Action
diff --git a/internal/static/i18n/es.yaml b/internal/static/i18n/es.yaml
index 1adb10763..097bd9e7c 100644
--- a/internal/static/i18n/es.yaml
+++ b/internal/static/i18n/es.yaml
@@ -120,6 +120,8 @@ Errors:
Empty: La contraseña está vacía
Invalid: La contraseña no es válida
NotSet: El usuario no ha establecido una contraseña
+ NotChanged: Contraseña no modificada
+ NotSupported: No se admite la codificación hash de contraseña
PasswordComplexityPolicy:
NotFound: Política de contraseñas no encontrada
MinLength: La contraseña es demasiado corta
@@ -488,6 +490,16 @@ Errors:
TokenCreationFailed: Fallo en la creación del token
InvalidToken: El token de la intención no es válido
OtherUser: Destinado a otro usuario
+ AuthRequest:
+ AlreadyExists: Auth Request ya existe
+ NotExisting: Auth Request no existe
+ WrongLoginClient: Auth Request creado por otro cliente de inicio de sesión
+ OIDCSession:
+ RefreshTokenInvalid: El token de refresco no es válido
+ Token:
+ Invalid: El token no es válido
+ Expired: El token ha caducado
+ InvalidClient: El token no ha sido emitido para este cliente
AggregateTypes:
action: Acción
diff --git a/internal/static/i18n/fr.yaml b/internal/static/i18n/fr.yaml
index 5789a815c..7fba4deaf 100644
--- a/internal/static/i18n/fr.yaml
+++ b/internal/static/i18n/fr.yaml
@@ -120,6 +120,8 @@ Errors:
Empty: Le mot de passe est vide
Invalid: Le mot de passe n'est pas valide
NotSet: L'utilisateur n'a pas défini de mot de passe
+ NotChanged: Mot de passe non modifié
+ NotSupported: Encodage de hachage de mot de passe non pris en charge
PasswordComplexityPolicy:
NotFound: Politique de mot de passe non trouvée
MinLength: Le mot de passe est trop court
@@ -488,6 +490,16 @@ Errors:
TokenCreationFailed: La création du token a échoué
InvalidToken: Le jeton d'intention n'est pas valide
OtherUser: Intention destinée à un autre utilisateur
+ AuthRequest:
+ AlreadyExists: Auth Request existe déjà
+ NotExisting: Auth Request n'existe pas
+ WrongLoginClient: Auth Request créé par un autre client de connexion
+ OIDCSession:
+ RefreshTokenInvalid: Le jeton de rafraîchissement n'est pas valide
+ Token:
+ Invalid: Le jeton n'est pas valide
+ Expired: Le jeton est expiré
+ InvalidClient: Le token n'a pas été émis pour ce client
AggregateTypes:
action: Action
diff --git a/internal/static/i18n/it.yaml b/internal/static/i18n/it.yaml
index 4e51b0fc3..4981dc307 100644
--- a/internal/static/i18n/it.yaml
+++ b/internal/static/i18n/it.yaml
@@ -120,6 +120,8 @@ Errors:
Empty: La password è vuota
Invalid: La password non è valida
NotSet: L'utente non ha impostato una password
+ NotChanged: Password non modificata
+ NotSupported: Codifica hash password non supportata
PasswordComplexityPolicy:
NotFound: Impostazioni di complessità password non trovati
MinLength: La password è troppo corta
@@ -488,6 +490,16 @@ Errors:
TokenCreationFailed: creazione del token fallita
InvalidToken: Il token dell'intento non è valido
OtherUser: Intento destinato a un altro utente
+ AuthRequest:
+ AlreadyExists: Auth Request esiste già
+ NotExisting: Auth Request non esiste
+ WrongLoginClient: Auth Request creato da un altro client di accesso
+ OIDCSession:
+ RefreshTokenInvalid: Refresh Token non è valido
+ Token:
+ Invalid: Token non è valido
+ Expired: Token è scaduto
+ InvalidClient: Il token non è stato emesso per questo cliente
AggregateTypes:
action: Azione
diff --git a/internal/static/i18n/ja.yaml b/internal/static/i18n/ja.yaml
index ed6d070d1..d0ba74e27 100644
--- a/internal/static/i18n/ja.yaml
+++ b/internal/static/i18n/ja.yaml
@@ -112,6 +112,8 @@ Errors:
Empty: パスワードは空です
Invalid: 無効なパスワードです
NotSet: パスワードが未設置です
+ NotChanged: パスワードは変更されていません
+ NotSupported: パスワードハッシュエンコードはサポートされていません
PasswordComplexityPolicy:
NotFound: パスワードポリシーが見つかりません
MinLength: パスワードが短すぎます
@@ -477,6 +479,16 @@ Errors:
TokenCreationFailed: トークンの作成に失敗しました
InvalidToken: インテントのトークンが無効である
OtherUser: 他のユーザーを意図している
+ AuthRequest:
+ AlreadyExists: AuthRequestはすでに存在する
+ NotExisting: AuthRequest が存在しません
+ WrongLoginClient: 他のログインクライアントによって作成された AuthRequest
+ OIDCSession:
+ RefreshTokenInvalid: 無効なリフレッシュトークンです
+ Token:
+ Invalid: トークンが無効です
+ Expired: トークンの有効期限が切れている
+ InvalidClient: トークンが発行されていません
AggregateTypes:
action: アクション
diff --git a/internal/static/i18n/mk.yaml b/internal/static/i18n/mk.yaml
new file mode 100644
index 000000000..a02b79dc9
--- /dev/null
+++ b/internal/static/i18n/mk.yaml
@@ -0,0 +1,1200 @@
+Errors:
+ Internal: Се случи внатрешна грешка
+ NoChangesFound: Нема промени
+ OriginNotAllowed: Овој "Origin" не е дозволен
+ IDMissing: Недостасува ID
+ ResourceOwnerMissing: Недостасува Организацијата на сопственик на ресурсот
+ RemoveFailed: Не можеше да се отстрани
+ ProjectionName:
+ Invalid: Невалидно име на проекција
+ Assets:
+ EmptyKey: Клучот на активот е празен
+ Store:
+ NotInitialized: Складиштето на активите не е иницијализирано
+ NotConfigured: Складиштето на активите не е конфигурирано
+ Bucket:
+ Internal: Внатрешна грешка при креирање на кофата
+ AlreadyExists: Кофата веќе постои
+ CreateFailed: Кофата не беше креирана
+ ListFailed: Кофите не можат да се прочитаат
+ RemoveFailed: Кофата не беше избришана
+ SetPublicFailed: Не може да се постави кофата како јавна
+ Object:
+ PutFailed: Објектот не е креиран
+ GetFailed: Објектот не може да се прочита
+ NotFound: Објектот не е пронајден
+ PresignedTokenFailed: Не може да се креира потпишан токен
+ ListFailed: Листата на објекти не може да се прочита
+ RemoveFailed: Објектот не може да се отстрани
+ Limit:
+ ExceedsDefault: Лимитот го надминува стандардниот лимит
+ Language:
+ NotParsed: Јазикот не може да се парсира
+ OIDCSettings:
+ NotFound: OIDC конфигурацијата не е пронајдена
+ AlreadyExists: OIDC конфигурацијата веќе постои
+ SecretGenerator:
+ AlreadyExists: Генератор на тајни веќе постои
+ TypeMissing: Недостасува типот на генераторот на тајни
+ NotFound: Генераторот на тајни не е пронајден
+ SMSConfig:
+ NotFound: SMS конфигурацијата не е пронајдена
+ AlreadyActive: SMS конфигурацијата е веќе активна
+ AlreadyDeactivated: SMS конфигурацијата е веќе деактивирана
+ SMTPConfig:
+ NotFound: SMTP конфигурацијата не е пронајдена
+ AlreadyExists: SMTP конфигурацијата веќе постои
+ SenderAdressNotCustomDomain: Адресата на испраќачот мора да биде конфигурирана како прилагоден домен на инстанцата.
+ Notification:
+ NoDomain: Не е пронајден домен за пораката
+ User:
+ NotFound: Корисникот не е пронајден
+ AlreadyExists: Корисникот веќе постои
+ NotFoundOnOrg: Корисникот не е пронајден во избраната организација
+ NotAllowedOrg: Корисникот не е член на бараната организација
+ UserIDMissing: ID на корисник е празно
+ UserIDWrong: "Корисникот во барањето не се совпаѓа со автентицираниот корисник"
+ DomainPolicyNil: Политиката на организацијата е празна
+ EmailAsUsernameNotAllowed: Е-поштата не е дозволена како корисничко име
+ Invalid: Податоците на корисникот се невалидни
+ DomainNotAllowedAsUsername: Доменот е веќе резервиран и не може да се користи
+ AlreadyInactive: Корисникот е веќе неактивен
+ NotInactive: Корисникот не е неактивен
+ CantDeactivateInitial: Корисник со состојба почетен може само да биде избришан, а не и деактивиран
+ ShouldBeActiveOrInitial: Корисникот не е активен или почетен
+ AlreadyInitialised: Корисникот е веќе иницијализиран
+ NotInitialised: Корисникот не е сè уште иницијализиран
+ NotLocked: Корисникот не е заклучен
+ NoChanges: Не се пронајдени промени
+ InitCodeNotFound: Кодот за иницијализација не е пронајден
+ UsernameNotChanged: Корисничкото име не е променето
+ InvalidURLTemplate: Шаблонот за URL е невалиден
+ Profile:
+ NotFound: Профилот не е пронајден
+ NotChanged: Профилот не е променет
+ Empty: Профилот е празен
+ FirstNameEmpty: Името во профилот е празно
+ LastNameEmpty: Презимето во профилот е празно
+ IDMissing: ID на профилот е празно
+ Email:
+ NotFound: Е-поштата не е пронајдена
+ Invalid: Е-поштата е невалидна
+ AlreadyVerified: Е-поштата е веќе верифицирана
+ NotChanged: Е-поштата не е променета
+ Empty: Е-поштата е празна
+ IDMissing: ID на е-поштата е празно
+ Phone:
+ NotFound: Телефонскиот број не е пронајден
+ Invalid: Телефонскиот број е невалиден
+ AlreadyVerified: Телефонскиот број веќе е верифициран
+ Empty: Телефонскиот број е празен
+ NotChanged: Телефонскиот број не е променет
+ Address:
+ NotFound: Адресата не е пронајдена
+ NotChanged: Адресата не е променета
+ Machine:
+ Key:
+ NotFound: Machine key не е пронајден
+ AlreadyExisting: Machine key веќе постои
+ Secret:
+ NotExisting: Тајната не постои
+ Invalid: Тајната е невалидна
+ CouldNotGenerate: Тајната не може да биде генерирана
+ PAT:
+ NotFound: Личниот токен за пристап не е пронајден
+ NotHuman: Корисникот мора да биде личност
+ NotMachine: Корисникот мора да биде технички
+ WrongType: Не е дозволено за овој тип на корисник
+ NotAllowedToLink: Корисникот не е дозволено да се поврзе со надворешен провајдер за најава
+ Username:
+ AlreadyExists: Корисничкото име е веќе зафатено
+ Reserved: Корисничкото име е веќе зафатено
+ Empty: Корисничкото име е празно
+ Code:
+ Empty: Кодот е празен
+ NotFound: Кодот не е пронајден
+ Expired: Кодот е истечен
+ GeneratorAlgNotSupported: Неподдржан алгоритам за генерато
+ Password:
+ NotFound: Лозинката не е пронајдена
+ Empty: Лозинката е празна
+ Invalid: Невалидна лозинка
+ NotSet: Корисникот нема поставено лозинка
+ NotChanged: Лозинката не е променета
+ NotSupported: Не е поддржано хаш-кодирањето на лозинката
+ PasswordComplexityPolicy:
+ NotFound: Политиката за комплексност на лозинката не е пронајдена
+ MinLength: Лозинката е прекратка
+ MinLengthNotAllowed: Дадената минимална должина не е дозволена
+ HasLower: Лозинката мора да содржи мала буква
+ HasUpper: Лозинката мора да содржи голема буква
+ HasNumber: Лозинката мора да содржи број
+ HasSymbol: Лозинката мора да содржи симбол
+ ExternalIDP:
+ Invalid: Невалиден надворешен IDP
+ IDPConfigNotExisting: IDP не е валиден за оваа организација
+ NotAllowed: Надворешниоте IDP не е дозволен на оваа организација
+ MinimumExternalIDPNeeded: Мора да се додаде најмалку еден надворешен IDP
+ AlreadyExists: Надворешниот IDP е веќе зафатен
+ NotFound: Надворешниот IDP не е пронајден
+ MFA:
+ OTP:
+ AlreadyReady: Мултифактор OTP (Еднократна Лозинка) e веќе поставен
+ NotExisting: Мултифактор OTP (Еднократна Лозинка) не постои
+ NotReady: Мултифактор OTP (Еднократна Лозинка) не е подготвен
+ InvalidCode: Невалиден код
+ U2F:
+ NotExisting: U2F не постои
+ Passwordless:
+ NotExisting: Најава без лозинка не постои
+ WebAuthN:
+ NotFound: WebAuthN токенот не може да биде пронајден
+ BeginRegisterFailed: Почетокот на регистрацијата на WebAuthN не успеа
+ MarshalError: Грешка при претворање на податоците
+ ErrorOnParseCredential: Грешка при анализа на податоците за верификација
+ CreateCredentialFailed: Грешка при креирање на верификациски податоци
+ BeginLoginFailed: Почетокот на најавувањето на WebAuthN не успеа
+ ValidateLoginFailed: Грешка при валидација на податоците за најавување
+ CloneWarning: Креденцијалите може да бидат клонирани
+ RefreshToken:
+ Invalid: Токенот за обновување е невалиден
+ NotFound: Токенот за обновување не е пронајден
+ Instance:
+ NotFound: Инстанцата не е пронајдена
+ AlreadyExists: Инстанцата веќе постои
+ NotChanged: Инстанцата не е променета
+ Org:
+ AlreadyExists: Името на организацијата е веќе зафатено
+ Invalid: Организацијата е невалидна
+ AlreadyDeactivated: Организацијата е веќе деактивирана
+ AlreadyActive: Организацијата е веќе активна
+ Empty: Организацијата е празна
+ NotFound: Организацијата не е пронајдена
+ NotChanged: Организацијата не е променета
+ DefaultOrgNotDeletable: Стандардната организација не смее да биде избришана
+ ZitadelOrgNotDeletable: Организацијата со ZITADEL проект не смее да биде избришана
+ InvalidDomain: Невалиден домен
+ DomainMissing: Недостасува домен
+ DomainNotOnOrg: Доменот не постои во организацијата
+ DomainNotVerified: Доменот не е верифициран
+ DomainAlreadyVerified: Доменот е веќе верифициран
+ DomainVerificationTypeInvalid: Типот на верификација на доменот е невалиден
+ DomainVerificationMissing: Верификацијата на доменот сè уште не е започната
+ DomainVerificationFailed: Верификацијата на доменот не успеа
+ PrimaryDomainNotDeletable: Примарниот домен не смее да биде избришан
+ DomainNotFound: Доменот не е пронајден
+ MemberIDMissing: Недостасува ID на членот
+ MemberNotFound: Членот на организацијата не е пронајден
+ InvalidMember: Членот на организацијата е невалиден
+ UserIDMissing: Недостасува ID на корисникот
+ PolicyAlreadyExists: Политиката веќе постои
+ PolicyNotExisting: Политиката не постои
+ IdpInvalid: Валидноста на конфигурацијата за надворешни IDP е невалидна
+ IdpNotExisting: Конфигурацијата за надворешни IDP не постои
+ OIDCConfigInvalid: Валидноста на конфигурацијата за OIDC IDP е невалидна
+ IdpIsNotOIDC: Конфигурацијата за IDP не е од тип OIDC
+ Domain:
+ AlreadyExists: Доменот веќе постои
+ InvalidCharacter: Дозволени се само алфанумерички знаци, . и - се дозволени за домен
+ IDP:
+ InvalidSearchQuery: Невалидно пребарување
+ LoginPolicy:
+ NotFound: Политиката за најавување не е пронајдена
+ Invalid: Политиката за најавување е невалидна
+ RedirectURIInvalid: Невалиден стандарден URI за пренасочување
+ NotExisting: Политиката за најавување не постои
+ AlreadyExists: Политиката за најавување веќе постои
+ IdpProviderAlreadyExisting: IDP веќе постои
+ IdpProviderNotExisting: IDP не постои
+ RegistrationNotAllowed: Не е дозволена регистрација
+ UsernamePasswordNotAllowed: Не е дозволено најавување со корисничко име / лозинка
+ MFA:
+ AlreadyExists: Мултифакторот веќе постои
+ NotExisting: Мултифакторот не постои
+ Unspecified: Невалиден мултифактор
+ MailTemplate:
+ NotFound: Стандардниот шаблон за е-пошта не е пронајден
+ NotChanged: Стандардниот шаблон за е-пошта не е променет
+ AlreadyExists: Стандардниот шаблон за е-пошта веќе постои
+ Invalid: Стандардниот шаблон за е-пошта е невалиден
+ CustomMessageText:
+ NotFound: Стандардниот текст на пораката не е пронајден
+ NotChanged: Стандардниот текст на пораката не е променет
+ AlreadyExists: Стандардниот текст на пораката веќе постои
+ Invalid: Стандардниот текст на пораката е невалиден
+ PasswordComplexityPolicy:
+ NotFound: Политиката за комплексност на лозинката не е пронајдена
+ Empty: Политиката за комплексност на лозинката е празна
+ NotExisting: Политиката за комплексност на лозинката не постои
+ AlreadyExists: Политиката за комплексност на лозинката веќе постои
+ PasswordLockoutPolicy:
+ NotFound: Политиката за заклучување на лозинката не е пронајдена
+ Empty: Политиката за заклучување на лозинката е празна
+ NotExisting: Политиката за заклучување на лозинката не постои
+ AlreadyExists: Политиката за заклучување на лозинката веќе постои
+ PasswordAgePolicy:
+ NotFound: Политиката за важност на лозинката не е пронајдена
+ Empty: Политиката за важност на лозинката е празна
+ NotExisting: Политиката за важност на лозинката не постои
+ AlreadyExists: Политиката за важност на лозинката веќе постои
+ OrgIAMPolicy:
+ Empty: Политиката за IAM на организацијата е празна
+ NotExisting: Политиката за IAM на организацијата не постои
+ AlreadyExists: Политиката за IAM на организацијата веќе постои
+ NotificationPolicy:
+ NotFound: Политиката за известување не е пронајдена
+ NotChanged: Политиката за известување не е променета
+ AlreadyExists: Политиката за известување веќе постои
+ LabelPolicy:
+ NotFound: Приватната политика за ознаките не е пронајдена
+ NotChanged: Приватната политика за ознаките не е променета
+ Project:
+ ProjectIDMissing: Недостасува ID на проектот
+ AlreadyExists: Проектот веќе постои во организацијата
+ OrgNotExisting: Организацијата не постои
+ UserNotExisting: Корисникот не постои
+ CouldNotGenerateClientSecret: Не може да се генерира клиентскиот таен клуч
+ Invalid: Проектот е невалиден
+ NotActive: Проектот не е активен
+ NotInactive: Проектот не е деактивиран
+ NotFound: Проектот не е пронајден
+ UserIDMissing: Недостасува ID на корисникот
+ Member:
+ NotFound: Членот на проектот не е пронајден
+ Invalid: Членот на проектот е невалиден
+ AlreadyExists: Членот на проектот веќе постои
+ NotExisting: Членот на проектот не постои
+ MinimumOneRoleNeeded: Потребно е барем една улога да биде додадена
+ Role:
+ AlreadyExists: Улогата веќе постои
+ Invalid: Улогата е невалидна
+ NotExisting: Улогата не постои
+ IDMissing: Недостасува ID
+ App:
+ AlreadyExists: Апликацијата веќе постои
+ NotFound: Апликацијата не е пронајдена
+ Invalid: Апликацијата е невалидна
+ NotExisting: Апликацијата не постои
+ NotActive: Апликацијата не е активна
+ NotInactive: Апликацијата не е неактивна
+ OIDCConfigInvalid: OIDC конфигурацијата е невалидна
+ APIConfigInvalid: API конфигурацијата е невалидна
+ SAMLConfigInvalid: SAML конфигурацијата е невалидна
+ IsNotOIDC: Апликацијата не е тип OIDC
+ IsNotAPI: Апликацијата не е тип API
+ IsNotSAML: Апликацијата не е тип SAML
+ SAMLMetadataMissing: Недостасуваат SAML метаподатоци
+ SAMLMetadataFormat: Грешка во форматот на SAML метаподатоците
+ SAMLEntityIDAlreadyExisting: SAML EntityID веќе постои
+ OIDCAuthMethodNoSecret: Избраниот OIDC метод за автентикација не бара таен клуч
+ APIAuthMethodNoSecret: Избраниот API метод за автентикација не бара таен клуч
+ AuthMethodNoPrivateKeyJWT: Избраниот метод за автентикација не бара приватен клуч
+ ClientSecretInvalid: Клиентскиот таен клуч е невалиден
+ Key:
+ AlreadyExisting: Клучот за апликацијата веќе постои
+ NotFound: Клучот за апликацијата не е пронајден
+ RequiredFieldsMissing: Некои задолжителни полиња недостасуваат
+ Grant:
+ AlreadyExists: Овластувањето за проектот веќе постои
+ NotFound: Овластувањето не е пронајдено
+ Invalid: Овластувањето за проектот е невалидно
+ NotExisting: Овластувањето за проектот не постои
+ HasNotExistingRole: Една улога не постои на проектот
+ NotActive: Овластувањето за проектот не е активно
+ NotInactive: Овластувањето за проектот не е неактивно
+ IAM:
+ NotFound: Инстанцата не е пронајдена
+ Member:
+ RolesNotChanged: Улогите не се променети
+ MemberInvalid: Членот е невалиден
+ MemberAlreadyExisting: Членот веќе постои
+ MemberNotExisting: Членот не постои
+ IDMissing: ID недостасува
+ IAMProjectIDMissing: ID на IAM проектот недостасува
+ IamProjectAlreadySet: ID на IAM проектот веќе е поставена
+ IdpInvalid: IDP конфигурацијата е невалидна
+ IdpNotExisting: IDP конфигурацијата не постои
+ OIDCConfigInvalid: OIDC конфигурацијата е невалидна
+ IdpIsNotOIDC: IDP конфигурацијата не е од типот oidc
+ LoginPolicyInvalid: Политиката за најавување е невалидна
+ LoginPolicyNotExisting: Политиката за најавување не постои
+ IdpProviderInvalid: Доставувачот на идентитетот е невалиден
+ LoginPolicy:
+ NotFound: Стандардната политика за најавување не е пронајдена
+ NotChanged: Стандардната политика за најавување не е променета
+ NotExisting: Стандардната политика за најавување не постои
+ AlreadyExists: Стандардната политика за најавување веќе постои
+ RedirectURIInvalid: Стандардниот URI за пренасочување не е валиден
+ MFA:
+ AlreadyExists: Мултифакторот веќе постои
+ NotExisting: Мултифакторот не постои
+ Unspecified: Мултифакторот не е валиден
+ IDP:
+ AlreadyExists: Доставувачот на идентитетот веќе постои
+ NotExisting: Доставувачот на идентитетот не постои
+ Invalid: Невалиден доставувач на идентитетот
+ IDPConfig:
+ AlreadyExists: Конфигурацијата на доставувачот на идентитетот веќе постои
+ NotInactive: Конфигурацијата на доставувачот на идентитетот не е неактивна
+ NotActive: Конфигурацијата на доставувачот на идентитетот не е активна
+ LabelPolicy:
+ NotFound: Стандардната политика за приватни ознаки не е пронајдена
+ NotChanged: Стандардната политика за приватни ознаки не е променета
+ MailTemplate:
+ NotFound: Стандардниот шаблон за е-пошта не е пронајден
+ NotChanged: Стандардниот шаблон за е-пошта не е променет
+ AlreadyExists: Стандардниот шаблон за е-пошта веќе постои
+ Invalid: Стандардниот шаблон за е-пошта е невалиден
+ CustomMessageText:
+ NotFound: Стандардниот текст на пораката не е пронајден
+ NotChanged: Стандардниот текст на пораката не е променет
+ AlreadyExists: Стандардниот текст на пораката веќе постои
+ Invalid: Стандардниот текст на пораката е невалиден
+ PasswordComplexityPolicy:
+ NotFound: Стандардната политика за сложеност на лозинка не е пронајдена
+ NotExisting: Стандардната политика за сложеност на лозинка не постои
+ AlreadyExists: Стандардната политика за сложеност на лозинка веќе постои
+ Empty: Стандардната политика за сложеност на лозинка е празна
+ NotChanged: Стандардната политика за сложеност на лозинка не е променета
+ PasswordAgePolicy:
+ NotFound: Стандардната политика за важност на лозинка не е пронајдена
+ NotExisting: Стандардната политика за важност на лозинка не постои
+ AlreadyExists: Стандардната политика за важност на лозинка веќе постои
+ Empty: Стандардната политика за важност на лозинка е празна
+ NotChanged: Стандардната политика за важност на лозинка не е променета
+ PasswordLockoutPolicy:
+ NotFound: Стандардната политика за заклучување на лозинка не е пронајдена
+ NotExisting: Стандардната политика за заклучување на лозинка не постои
+ AlreadyExists: Стандардната политика за заклучување на лозинка веќе постои
+ Empty: Стандардната политика за заклучување на лозинка е празна
+ NotChanged: Стандардната политика за заклучување на лозинка не е променета
+ DomainPolicy:
+ NotFound: Политиката на IAM за организацијата не е пронајдена
+ Empty: Политиката на IAM за организацијата е празна
+ NotExisting: Политиката на IAM за организацијата не постои
+ AlreadyExists: Политиката на IAM за организацијата веќе постои
+ NotChanged: Политиката на IAM за организацијата не е променета
+ NotificationPolicy:
+ NotFound: Стандардната политика за известување не е пронајдена
+ NotChanged: Стандардната политика за известување не е променета
+ AlreadyExists: Стандардната политика за известување веќе постои
+ Policy:
+ AlreadyExists: Политиката веќе постои
+ Label:
+ Invalid:
+ PrimaryColor: Главната боја не е валидна хексадецимална вредност
+ BackgroundColor: Бојата на позадина не е валидна хексадецимална вредност
+ WarnColor: Предупредувачката боја не е валидна хексадецимална вредност
+ FontColor: Бојата на фонтот не е валидна хексадецимална вредност
+ PrimaryColorDark: Главната боја (темен режим) не е валидна хексадецимална вредност
+ BackgroundColorDark: Бојата на позадина (темен режим) не е валидна хексадецимална вредност
+ WarnColorDark: Предупредувачката боја (темен режим) не е валидна хексадецимална вредност
+ FontColorDark: Бојата на фонтот (темен режим) не е валидна хексадецимална вредност
+ UserGrant:
+ AlreadyExists: Овластувањето на корисникот веќе постои
+ NotFound: Овластувањето на корисникот не е пронајдено
+ Invalid: Овластувањето на корисникот е невалидно
+ NotChanged: Овластувањето на корисникот не е променето
+ IDMissing: ID недостасува
+ NotActive: Овластувањето на корисникот не е активно
+ NotInactive: Овластувањето на корисникот не е неактивно
+ NoPermissionForProject: Корисникот нема овластувања за овој проект
+ RoleKeyNotFound: Улогата не е пронајдена
+ Member:
+ AlreadyExists: Членот веќе постои
+ IDPConfig:
+ AlreadyExists: Конфигурацијата на IDP веќе постои
+ NotExisting: Конфигурацијата на IDP не постои
+ Changes:
+ NotFound: Нема пронајдена историја
+ AuditRetention: Историјата е надвор од задржувањето на аудитот
+ Token:
+ NotFound: Токенот не е пронајден
+ UserSession:
+ NotFound: Корисничката сесија не е пронајдена
+ Key:
+ ExpireBeforeNow: Датумот на истекување е во минатото
+ Login:
+ LoginPolicy:
+ MFA:
+ ForceAndNotConfigured: Мултифакторската проверка е конфигурирана како задолжителна, но нема конфигурирани можностите за неа. Ве молиме контактирајте го вашиот системски администратор.
+ Step:
+ Started:
+ AlreadyExists: Веќе постои започнат чекор
+ Done:
+ AlreadyExists: Веќе постои комплетиран чекор
+ CustomText:
+ AlreadyExists: Прилагоден текст веќе постои
+ Invalid: Прилагодениот текст е невалиден
+ NotFound: Прилагодениот текст не е пронајден
+ TranslationFile:
+ ReadError: Грешка при читање на преводниот документ
+ MergeError: Преводниот документ не може да се спои со прилагодените преводи
+ NotFound: Преводниот документ не постои
+ Metadata:
+ NotFound: Метаподатоците не се пронајдени
+ NoData: Листата на метаподатоци е празна
+ Invalid: Метаподатоците се невалидни
+ KeyNotExisting: Еден или повеќе клучеви не постојат
+ Action:
+ Invalid: Акцијата е невалидна
+ NotFound: Акцијата не е пронајдена
+ NotActive: Акцијата не е активна
+ NotInactive: Акцијата не е неактивна
+ MaxAllowed: Не се дозволени дополнителни активни акции
+ Flow:
+ FlowTypeMissing: FlowType не е наведен
+ Empty: Flow е веќе празен
+ WrongTriggerType: TriggerType не е валиден
+ NoChanges: Нема промени
+ ActionIDsNotExist: ActionIDs не постојат
+ Query:
+ CloseRows: SQL наредбата не може да се заврши
+ SQLStatement: SQL наредбата не може да се креира
+ InvalidRequest: Барањето е невалидно
+ Quota:
+ AlreadyExists: Веќе постои квота за оваа единица
+ NotFound: Квотата не е пронајдена за оваа единица
+ Invalid:
+ CallURL: URL-то за повик на квота е невалидно
+ Percent: Процентот на квота е помал од 1
+ Unimplemented: Квотите не се имплементирани за оваа единица
+ Amount: Износот на квотата е помал од 1
+ ResetInterval: Интервалот за ресетирање на квотата е помал од една минута
+ Noop: Неограничена квота без известувања нема ефект
+ Access:
+ Exhausted: Квотата за автентицирани барања е исцрпена
+ Execution:
+ Exhausted: Квотата за извршување во секунди е исцрпена
+ LogStore:
+ Access:
+ StorageFailed: Неуспешно зачувување на логовите за пристап во базата на податоци
+ ScanFailed: Неуспешно пребарување за автентицирани барања
+ Execution:
+ StorageFailed: Неуспешно зачувување на логовите за извршување на акции во базата на податоци
+ ScanFailed: Неуспешно пребарување за времетраењето на акции
+ Session:
+ NotExisting: Сесијата не постои
+ Terminated: Сесијата е веќе завршена
+ Token:
+ Invalid: Токенот за сесија е невалиден
+ Passkey:
+ NoChallenge: Сесија без предизвик за passkey
+ Intent:
+ IDPMissing: ID на IDP недостасува во барањето
+ SuccessURLMissing: URL за успех недостасува во барањето
+ FailureURLMissing: URL за неуспех недостасува во барањето
+ StateMissing: Параметарот State недостасува во барањето
+ NotStarted: Намерата не е започната или веќе завршена
+ NotSucceeded: Намерата не е успешна
+ TokenCreationFailed: Неуспешно креирање на токен
+ InvalidToken: Токенот за намера е невалиден
+ OtherUser: Намерата е за друг корисник
+ AuthRequest:
+ AlreadyExists: Барањето за автентикација веќе постои
+ NotExisting: Барањето за автентикација не постои
+ WrongLoginClient: Барањето за автификација беше креирано од друг клиент за најавување
+ OIDCSession:
+ RefreshTokenInvalid: Токенот за освежување е неважечки
+ Token:
+ Invalid: токенот е неважечки
+ Expired: токенот е истечен
+ InvalidClient: Токен не беше издаден на овој клиент
+
+AggregateTypes:
+ action: Акција
+ instance: Инстанца
+ key_pair: Пар на клучеви
+ org: Организација
+ project: Проект
+ user: Корисник
+ usergrant: Овластување на корисник
+ quota: Квота
+
+EventTypes:
+ user:
+ added: Додаден корисник
+ selfregistered: Корисникот се регистрираше сам
+ initialization:
+ code:
+ added: Генериран е код за иницијализација
+ sent: Кодот за иницијализација е испратен
+ check:
+ succeeded: Проверката на иницијализацијата е успешна
+ failed: Проверката на иницијализацијата е неуспешна
+ token:
+ added: Креиран е токен за пристап
+ removed: Токенот за пристап е отстранет
+ username:
+ reserved: Корисничкото име е резервирано
+ released: Корисничкото име е ослободено
+ changed: Корисничкото име е променето
+ email:
+ reserved: Е-поштата е резервирана
+ released: Е-поштата е ослободена
+ changed: Е-поштата е променета
+ verified: Е-поштата е верифицирана
+ verification:
+ failed: Верификацијата на е-поштата е неуспешна
+ code:
+ added: Генериран е код за верификација на е-пошта
+ sent: Кодот за верификација на е-пошта е испратен
+ machine:
+ added: Додаден технички корисник
+ changed: Променет технички корисник
+ key:
+ added: Клучето е додаден
+ removed: Клучот е отстранет
+ secret:
+ set: Тајната е додадена
+ removed: Тајната е отстрането
+ check:
+ succeeded: Проверката на тајната е успешна
+ failed: Проверката на тајната е неуспешна
+ human:
+ added: Додадено лице
+ selfregistered: Лицето се регистрирало само
+ avatar:
+ added: Додаден аватар
+ removed: Отстранет аватар
+ initialization:
+ code:
+ added: Генериран код за иницијализација
+ sent: Испратен код за иницијализација
+ check:
+ succeeded: Проверката на иницијализацијата е успешна
+ failed: Проверката на иницијализацијата е неуспешна
+ username:
+ reserved: Корисничкото име е резервирано
+ released: Корисничкото име е ослободено
+ email:
+ changed: Променета е-пошта
+ verified: Верифицирана е-пошта
+ verification:
+ failed: Верификацијата на е-поштата е неуспешна
+ code:
+ added: Генериран код за верификација на е-пошта
+ sent: Испратен код за верификација на е-пошта
+ password:
+ changed: Променета лозинка
+ code:
+ added: Генериран код за лозинка
+ sent: Испратен код за лозинка
+ check:
+ succeeded: Проверката на лозинката е успешна
+ failed: Проверката на лозинката е неуспешна
+ externallogin:
+ check:
+ succeeded: Надворешното најавување е успешно
+ externalidp:
+ added: Додаден надворешен IDP
+ removed: Отстранет надворешен IDP
+ cascade:
+ removed: Отстранета каскадата на надворешни IDP
+ phone:
+ changed: Променет број на телефон
+ verified: Верифициран број на телефон
+ verification:
+ failed: Верификацијата на бројот на телефонот е неуспешна
+ code:
+ added: Генериран код за број на телефон
+ sent: Испратен код за број на телефон
+ removed: Отстранет број на телефон
+ profile:
+ changed: Променет кориснички профил
+ address:
+ changed: Променета адреса на корисник
+ mfa:
+ otp:
+ added: Додаден мултифактор OTP
+ verified: Верифициран мултифактор OTP
+ removed: Отстранет мултифактор OTP
+ check:
+ succeeded: Проверката на мултифактор OTP е успешна
+ failed: Проверката на мултифактор OTP е неуспешна
+ u2f:
+ token:
+ added: Додаден мултифактор U2F токен
+ verified: Верифициран мултифактор U2F токен
+ removed: Отстранет мултифактор U2F токен
+ begin:
+ login: Започната мултифактор U2F проверка
+ check:
+ succeeded: Проверката на мултифактор U2F токен е успешна
+ failed: Проверката на мултифактор U2F токен е неуспешна
+ signcount:
+ changed: Checksum на мултифактор U2F токен е променет
+ init:
+ skipped: Прескокната иницијализација на мултифактор
+ passwordless:
+ token:
+ added: Додаден токен за најава без лозинка
+ verified: Верифициран токен за најава без лозинка
+ removed: Отстранет токен за најава без лозинка
+ begin:
+ login: Започната проверка на најава без лозинка
+ check:
+ succeeded: Проверката на најава без лозинка е успешна
+ failed: Проверката на најава без лозинка е неуспешна
+ signcount:
+ changed: Checksum на токенот за најава без лозинка е променет
+ initialization:
+ code:
+ added: Додаден код за иницијализација на најава без лозинка
+ sent: Испратен код за иницијализација на најава без лозинка
+ requested: Побаран код за иницијализација на најава без лозинка
+ check:
+ succeeded: Проверката на кодот за иницијализација на најава без лозинка е успешна
+ failed: Проверката на кодот за иницијализација на најава без лозинка е неуспешна
+ signed:
+ out: Корисникот се одјави
+ refresh:
+ token:
+ added: Креиран е токен за обновување
+ renewed: Обновен е токен за обновување
+ removed: Отстранет е токен за обновување
+ locked: Корисникот е заклучен
+ unlocked: Корисникот е отклучен
+ deactivated: Корисникот е деактивиран
+ reactivated: Корисникот е повторно активиран
+ removed: Корисникот е отстранет
+ password:
+ changed: Лозинката е променета
+ code:
+ added: Генериран е код за лозинка
+ sent: Кодот за лозинка е испратен
+ check:
+ succeeded: Проверката на лозинката е успешна
+ failed: Проверката на лозинката е неуспешна
+ phone:
+ changed: Бројот на телефонот е променет
+ verified: Бројот на телефонот е верифициран
+ verification:
+ failed: Верификацијата на бројот на телефонот е неуспешна
+ code:
+ added: Генериран е код за број на телефон
+ sent: Кодот за број на телефон е испратен
+ removed: Бројот на телефонот е отстранет
+ profile:
+ changed: Променет профил на корисникот
+ address:
+ changed: Променета адреса на корисникот
+ mfa:
+ otp:
+ added: Додаден мултифактор OTP
+ verified: Верифициран мултифактор OTP
+ removed: Отстранет мултифактор OTP
+ check:
+ succeeded: Проверката на мултифактор OTP е успешна
+ failed: Проверката на мултифактор OTP е неуспешна
+ init:
+ skipped: Прескокната иницијализација на мултифактор OTP
+ init:
+ skipped: Прескокната иницијализација на мултифактор
+ signed:
+ out: Корисникот се одјави
+ grant:
+ added: Додадено овластување
+ changed: Променето овластување
+ removed: Отстрането овластување
+ deactivated: Овластувањето е деактивирано
+ reactivated: Овластувањето е повторно активирано
+ reserved: Овластувањето е задржано
+ released: Овластувањето е ослободено
+ cascade:
+ removed: Отстрането овластување
+ changed: Променето овластување
+ metadata:
+ set: Поставени кориснички метаподатоци
+ removed: Отстранети кориснички метаподатоци
+ removed.all: Сите кориснички метаподатоци се отстранети
+ domain:
+ claimed: Доменот е преземен
+ claimed.sent: Испратено е известување за преземање на домен
+ pat:
+ added: Додаден личен токен за пристап
+ removed: Отстранет личен токен за пристап
+ org:
+ added: Додадена организација
+ changed: Променета организација
+ deactivated: Организацијата е деактивирана
+ reactivated: Организацијата е повторно активирана
+ removed: Организацијата е отстранета
+ domain:
+ added: Додаден домен
+ verification:
+ added: Додадена верификација на домен
+ failed: Верификацијата на доменот е неуспешна
+ verified: Доменот е верифициран
+ removed: Доменот е отстранет
+ primary:
+ set: Поставен примарен домен
+ reserved: Доменот е резервиран
+ released: Доменот е ослободен
+ name:
+ reserved: Името на организацијата е резервирано
+ released: Името на организацијата е ослободено
+ member:
+ added: Додаден член на организацијата
+ changed: Променет член на организацијата
+ removed: Отстранет член на организацијата
+ cascade:
+ removed: Отстранета каскада на членови на организацијата
+ iam:
+ policy:
+ added: Додадена системска политика
+ changed: Променета системска политика
+ removed: Отстранета системска политика
+ idp:
+ config:
+ added: Додадена конфигурација за IDP
+ changed: Променета конфигурација за IDP
+ removed: Отстранета конфигурација за IDP
+ deactivated: Деактивирана конфигурација за IDP
+ reactivated: Повторно активирана конфигурација за IDP
+ oidc:
+ config:
+ added: Додадена OIDC конфигурација за IDP
+ changed: Променета OIDC конфигурација за IDP
+ saml:
+ config:
+ added: Додадена SAML конфигурација за IDP
+ changed: Променета SAML конфигурација за IDP
+ jwt:
+ config:
+ added: Додадена JWT конфигурација за IDP
+ changed: Променета JWT конфигурација за IDP
+ customtext:
+ set: Поставен прилагоден текст
+ removed: Отстранет прилагоден текст
+ template:
+ removed: Отстранет шаблон за прилагоден текст
+ policy:
+ login:
+ added: Додадена политика за најавување
+ changed: Променета политика за најавување
+ removed: Отстранета политика за најавување
+ idpprovider:
+ added: Додаден провајдер на идентитет во политика за најавување
+ removed: Отстранет провајдер на идентитет од политика за најавување
+ cascade:
+ removed: Отстранет каскада на провајдери на идентитет во политика за најавување
+ secondfactor:
+ added: Додаден втор фактор во политика за најавување
+ removed: Отстранет втор фактор од политика за најавување
+ multifactor:
+ added: Додаден мултифактор во политика за најавување
+ removed: Отстранет мултифактор од политика за најавување
+ password:
+ complexity:
+ added: Додадена политика за сложеност на лозинка
+ changed: Променета политика за сложеност на лозинка
+ removed: Отстранета политика за сложеност на лозинка
+ age:
+ added: Додадена политика за важност на лозинка
+ changed: Променета политика за важност на лозинка
+ removed: Отстранета политика за важност на лозинка
+ lockout:
+ added: Додадена политика за заклучување на лозинка
+ changed: Променета политика за заклучување на лозинка
+ removed: Отстранета политика за заклучување на лозинка
+ label:
+ added: Додадена политика за ознака
+ changed: Променета политика за ознака
+ activated: Активирана политика за ознака
+ removed: Отстранета политика за ознака
+ logo:
+ added: Додаден лого во политика за ознака
+ removed: Отстрането лого од политика за ознака
+ dark:
+ added: Додадено лого (темен режим) во политика за ознака
+ removed: Отстрането лого (темен режим) од политика за ознака
+ icon:
+ added: Додадена икона во политика за ознака
+ removed: Отстранета икона од политика за ознака
+ dark:
+ added: Додадена икона (темен режим) во политика за ознака
+ removed: Отстранета икона (темен режим) од политика за ознака
+ font:
+ added: Додаден фонт во политика за ознака
+ removed: Отстранет фонт од политика за ознака
+ assets:
+ removed: Отстранети ресурси од политика за ознака
+ privacy:
+ added: Додадена политика за приватност и услови на користење
+ changed: Променета политика за приватност и услови на користење
+ removed: Отстранета политика за приватност и услови на користење
+ domain:
+ added: Додадена политика за домен
+ changed: Променета политика за домен
+ removed: Отстранета политика за домен
+ lockout:
+ added: Додадена политика за заклучување
+ changed: Променета политика за заклучување
+ removed: Отстранета политика за заклучување
+ notification:
+ added: Додадена политика за известување
+ changed: Променета политика за известување
+ removed: Отстранета политика за известување
+ flow:
+ trigger_actions:
+ set: Поставени акции
+ cascade:
+ removed: Отстранета каскада акции
+ removed: Отстранети акции
+ cleared: Исчистен Flow
+ mail:
+ template:
+ added: Додаден шаблон за е-пошта
+ changed: Променет шаблон за е-пошта
+ removed: Отстранет шаблон за е-пошта
+ text:
+ added: Додаден текст за е-пошта
+ changed: Променет текст за е-пошта
+ removed: Отстранет текст за е-пошта
+ metadata:
+ removed: Отстранети метаподатоци
+ removed.all: Отстранети сите метаподатоци
+ set: Поставени метаподатоци
+ project:
+ added: Додаден проект
+ changed: Променет проект
+ deactivated: Проектот е деактивиран
+ reactivated: Проектот е повторно активиран
+ removed: Проектот е отстранет
+ member:
+ added: Додаден член на проектот
+ changed: Променет член на проектот
+ removed: Отстранет член на проектот
+ cascade:
+ removed: Отстранетa каскада членови на проектот
+ role:
+ added: Додадена улога на проектот
+ changed: Променета улога на проектот
+ removed: Отстранета улога на проектот
+ grant:
+ added: Додаден овластување за менаџирање
+ changed: Променето овластување за менаџирање
+ removed: Отстрането овластување за менаџирање
+ deactivated: Деактивирано овластување за менаџирање
+ reactivated: Повторно активирано овластување за менаџирање
+ cascade:
+ changed: Променетa каскада овластувања за менаџирање
+ member:
+ added: Додаден член на пристапот за менаџирање
+ changed: Променет член на пристапот за менаџирање
+ removed: Отстранет член на пристапот за менаџирање
+ cascade:
+ removed: Отстранети членови на пристапот за менаџирање
+ application:
+ added: Додадена апликација
+ changed: Променета апликација
+ removed: Отстранета апликација
+ deactivated: Деактивирана апликација
+ reactivated: Повторно активирана апликација
+ oidc:
+ secret:
+ check:
+ succeeded: Проверката на OIDC клиентска тајна е успешна
+ failed: Проверката на OIDC клиентска тајна е неуспешна
+ key:
+ added: Додаден клуч за апликацијата
+ removed: Отстранет клуч за апликацијата
+ config:
+ saml:
+ added: Додадена SAML конфигурација
+ changed: Променета SAML конфигурација
+ oidc:
+ added: Додадена OIDC конфигурација
+ changed: Променета OIDC конфигурација
+ secret:
+ changed: Променета OIDC тајна
+ api:
+ added: Додадена API конфигурација
+ changed: Променета API конфигурација
+ secret:
+ changed: Променета API тајна
+ policy:
+ password:
+ complexity:
+ added: Додадена политика за сложеност на лозинка
+ changed: Променета политика за сложеност на лозинка
+ age:
+ added: Додадена политика за важност на лозинка
+ changed: Променета политика за важност на лозинка
+ lockout:
+ added: Додадена политика за заклучување на лозинка
+ changed: Променета политика за заклучување на лозинка
+ iam:
+ setup:
+ started: Започнато ZITADEL поставување
+ done: Завршено ZITADEL поставување
+ global:
+ org:
+ set: Поставена глобална организација
+ project:
+ iam:
+ set: Поставен ZITADEL проект
+ member:
+ added: Додаден ZITADEL член
+ changed: Променет ZITADEL член
+ removed: Отстранет ZITADEL член
+ cascade:
+ removed: Отстранети ZITADEL членови
+ idp:
+ config:
+ added: Додадена IDP конфигурација
+ changed: Променета IDP конфигурација
+ removed: Отстранета IDP конфигурација
+ deactivated: Деактивирана IDP конфигурација
+ reactivated: Повторно активирана IDP конфигурација
+ oidc:
+ config:
+ added: Додадена OIDC IDP конфигурација
+ changed: Променета OIDC IDP конфигурација
+ saml:
+ config:
+ added: Додадена SAML IDP конфигурација
+ changed: Променета SAML IDP конфигурација
+ jwt:
+ config:
+ added: Додадена JWT конфигурација на IDP
+ changed: Променета JWT конфигурација на IDP
+ customtext:
+ set: Текстот е поставен
+ removed: Текстот е отстранет
+ policy:
+ login:
+ added: Додадена стандардна политика за најавување
+ changed: Променета стандардна политика за најавување
+ idpprovider:
+ added: Додаден IDP во стандардната политика за најавување
+ removed: Отстранет IDP од стандардната политика за најавување
+ label:
+ added: Додадена политика за ознака
+ changed: Променета политика за ознака
+ activated: Активирана политика за ознака
+ logo:
+ added: Додаден лого на политика за ознака
+ removed: Отстрането лого од политика за ознака
+ dark:
+ added: Додадено лого (темен режим) на политика за ознака
+ removed: Отстрането лого (темен режим) од политика за ознака
+ icon:
+ added: Додадена икона на политика за ознака
+ removed: Отстранета икона од политика за ознака
+ dark:
+ added: Додадена икона (темен режим) на политика за ознака
+ removed: Отстранета икона (темен режим) од политика за ознака
+ font:
+ added: Додаден фонт во политика за ознака
+ removed: Отстранет фонт од политика за ознака
+ assets:
+ removed: Отстранети ресурси од политика за ознака
+ default:
+ language:
+ set: Поставен стандарден јазик
+ oidc:
+ settings:
+ added: Додадена OIDC конфигурација
+ changed: Променета OIDC конфигурација
+ removed: Отстранета OIDC конфигурација
+ secret:
+ generator:
+ added: Додаден генератор на тајни
+ changed: Променет генератор на тајни
+ removed: Отстранет генератор на тајни
+ smtp:
+ config:
+ added: Додадена SMTP конфигурација
+ changed: Променета SMTP конфигурација
+ password:
+ changed: Променена тајна на SMTP конфигурација
+ sms:
+ config:
+ twilio:
+ added: Додаден Twilio SMS провајдер
+ changed: Променет Twilio SMS провајдер
+ token:
+ changed: Променет токен на Twilio SMS провајдер
+ removed: Отстранет Twilio SMS провајдер
+ activated: Активиран Twilio SMS провајдер
+ deactivated: Деактивиран Twilio SMS провајдер
+ key_pair:
+ added: Додаден пар на клучеви
+ certificate:
+ added: Додаден сертификат
+ action:
+ added: Додадена акција
+ changed: Променета акција
+ deactivated: Деактивирана акција
+ reactivated: Реактивирана акција
+ removed: Отстранета акција
+ instance:
+ added: Додадена инстанца
+ changed: Променета инстанца
+ customtext:
+ removed: Отстранет прилагоден текст
+ set: Поставен прилагоден текст
+ template:
+ removed: Отстранет шаблон на прилагоден текст
+ default:
+ language:
+ set: Поставен стандарден јазик
+ org:
+ set: Поставена стандардна организација
+ domain:
+ added: Додаден домен
+ primary:
+ set: Поставен примарен домен
+ removed: Отстранет домен
+ iam:
+ console:
+ set: Поставена апликација на ZITADEL конзола
+ project:
+ set: Поставен проект на ZITADEL
+ mail:
+ template:
+ added: Додаден е-пошта шаблон
+ changed: Променет е-пошта шаблон
+ text:
+ added: Додаден е-пошта текст
+ changed: Променет е-пошта текст
+ member:
+ added: Додаден член на инстанцата
+ changed: Променет член на инстанцата
+ removed: Отстранет член на инстанцата
+ cascade:
+ removed: Отстранети членови на инстанцата
+ notification:
+ provider:
+ debug:
+ fileadded: Додаден провајдер за откривање на грешки во датотека
+ filechanged: Променет провајдер за откривање на грешки во датотека
+ fileremoved: Отстранет провајдер за откривање на грешки во датотека
+ logadded: Додаден провајдер за откривање на грешки во логови
+ logchanged: Променет провајдер за откривање на грешки во логови
+ logremoved: Отстранет провајдер за откривање на грешки во логови
+ oidc:
+ settings:
+ added: Додадени OIDC поставки
+ changed: Променети OIDC поставки
+ policy:
+ domain:
+ added: Додадена политика за домен
+ changed: Променета политика за домен
+ label:
+ activated: Активирана политика за ознаки
+ added: Додадена политика за ознаки
+ assets:
+ removed: Отстранети средства од политиката за ознаки
+ changed: Променета политика за ознаки
+ font:
+ added: Додаден фонт на политиката за ознаки
+ removed: Отстранет фонт од политиката за ознаки
+ icon:
+ added: Додадена икона на политиката за ознаки
+ removed: Отстранета икона од политиката за ознаки
+ dark:
+ added: Додадена темна икона на политиката за ознаки
+ removed: Отстранета темна икона од политиката за ознаки
+ logo:
+ added: Додаден лого на политиката за ознаки
+ removed: Отстрането лого од политиката за ознаки
+ dark:
+ added: Додадено темно лого на политиката за ознаки
+ removed: Отстрането темно лого од политиката за ознаки
+ lockout:
+ added: Додадена политика за заклучување
+ changed: Променета политика за заклучување
+ login:
+ added: Додадена политика за најавување
+ changed: Променета политика за најавување
+ idpprovider:
+ added: Додаден провајдер на идентитети во политиката за најавување
+ cascade:
+ removed: Отстранет провајдер на идентитети од политиката за најавување
+ removed: Отстранет провајдер на идентитети од политиката за најавување
+ multifactor:
+ added: Додадена мултифакторска автентикација во политиката за најавување
+ removed: Отстранета мултифакторска автентикација од политиката за најавување
+ secondfactor:
+ added: Додаден втор фактор во политиката за најавување
+ removed: Отстранет втор фактор од политиката за најавување
+ password:
+ age:
+ added: Додадена политика за возраст на лозинка
+ changed: Променета политика за возраст на лозинка
+ complexity:
+ added: Додадена политика за комплексност на лозинка
+ changed: Отстранета политика за комплексност на лозинка
+ privacy:
+ added: Додадена политика за приватност
+ changed: Променета политика за приватност
+ security:
+ set: Поставена политика за безбедност
+ removed: Отстранети инстанци
+ secret:
+ generator:
+ added: Додаден генератор на тајни
+ changed: Променет генератор на тајни
+ removed: Отстранет генератор на тајни
+ sms:
+ configtwilio:
+ activated: Активирана Twilio SMS конфигурација
+ added: Додадена Twilio SMS конфигурација
+ changed: Променета Twilio SMS конфигурација
+ deactivated: Деактивирана Twilio SMS конфигурација
+ removed: Отстранета Twilio SMS конфигурација
+ token:
+ changed: Променет токен на Twilio SMS конфигурацијата
+ smtp:
+ config:
+ added: Додадена SMTP конфигурација
+ changed: Променета SMTP конфигурација
+ password:
+ changed: Променета лозинка на SMTP конфигурацијата
+ removed: Отстранета SMTP конфигурација
+
+Application:
+ OIDC:
+ UnsupportedVersion: Вашата OIDC верзија не е поддржана
+ V1:
+ NotCompliant: Вашата конфигурација не е во согласност и се разликува од OIDC 1.0 стандардот.
+ NoRedirectUris: Мора да биде регистриран барем еден URI за пренасочување.
+ NotAllCombinationsAreAllowed: Конфигурацијата е во согласност, но не се дозволени сите можни комбинации.
+ Code:
+ RedirectUris:
+ HttpOnlyForWeb: Грант типот code дозволува само HTTP URI за пренасочување за веб апликации.
+ CustomOnlyForNative: Грант типот code дозволува само сопствени URI за пренасочување за нативни апликации (на пример appname:// ).
+ Implicit:
+ RedirectUris:
+ CustomNotAllowed: Грант типот implicit не дозволува сопствени URI за пренасочување.
+ HttpNotAllowed: Грант типот implicit не дозволува HTTP URI за пренасочување.
+ HttpLocalhostOnlyForNative: Пренасочувањето кон URI-то http://localhost е дозволено само за нативни апликации.
+ Native:
+ AuthMethodType:
+ NotNone: Нативните апликации треба да имаат authmethodtype none.
+ RedirectUris:
+ MustBeHttpLocalhost: URI-тата за пренасочување мора да почнуваат со ваш протокол, http://127.0.0.1, http://[::1] или http://localhost.
+ UserAgent:
+ AuthMethodType:
+ NotNone: Апликациите на кориснички агент треба да имаат authmethodtype none.
+ GrantType:
+ Refresh:
+ NoAuthCode: Refresh Token е дозволен само во комбинација со Authorization Code.
+
+Action:
+ Flow:
+ Type:
+ Unspecified: Неодредено
+ ExternalAuthentication: Надворешна автентикација
+ CustomiseToken: Комплемент на токенот
+ InternalAuthentication: Внатрешна автентикација
+ TriggerType:
+ Unspecified: Неодредено
+ PostAuthentication: По автентикација
+ PreCreation: Пред креирање
+ PostCreation: По креирање
+ PreUserinfoCreation: Пред креирање на кориснички информации
+ PreAccessTokenCreation: Пред креирање на токен за пристап
diff --git a/internal/static/i18n/pl.yaml b/internal/static/i18n/pl.yaml
index 9e907dcbe..3e48f4582 100644
--- a/internal/static/i18n/pl.yaml
+++ b/internal/static/i18n/pl.yaml
@@ -120,6 +120,8 @@ Errors:
Empty: Hasło jest puste
Invalid: Hasło jest nieprawidłowe
NotSet: Użytkownik nie ustawił hasła
+ NotChanged: Hasło nie zostało zmienione
+ NotSupported: Kodowanie skrótu hasła nie jest obsługiwane
PasswordComplexityPolicy:
NotFound: Polityka hasła nie znaleziona
MinLength: Hasło jest zbyt krótkie
@@ -488,6 +490,16 @@ Errors:
TokenCreationFailed: Tworzenie tokena nie powiodło się
InvalidToken: Token intencji jest nieprawidłowy
OtherUser: Intencja przeznaczona dla innego użytkownika
+ AuthRequest:
+ AlreadyExists: Auth Request już istnieje
+ NotExisting: Auth Request nie istnieje
+ WrongLoginClient: Auth Request utworzony przez innego klienta logowania
+ OIDCSession:
+ RefreshTokenInvalid: Refresh Token jest nieprawidłowy
+ Token:
+ Invalid: Token jest nieprawidłowy
+ Expired: Token wygasł
+ InvalidClient: Token nie został wydany dla tego klienta
AggregateTypes:
action: Działanie
diff --git a/internal/static/i18n/pt.yaml b/internal/static/i18n/pt.yaml
new file mode 100644
index 000000000..963191eb5
--- /dev/null
+++ b/internal/static/i18n/pt.yaml
@@ -0,0 +1,1195 @@
+Errors:
+ Internal: Ocorreu um erro interno
+ NoChangesFound: Nenhuma alteração encontrada
+ OriginNotAllowed: Esta "Origem" não é permitida
+ IDMissing: ID ausente
+ ResourceOwnerMissing: Organização proprietária de recurso ausente
+ RemoveFailed: Não foi possível remover
+ ProjectionName:
+ Invalid: Nome de projeção inválido
+ Assets:
+ EmptyKey: A chave do recurso está vazia
+ Store:
+ NotInitialized: Armazenamento de recursos não inicializado
+ NotConfigured: Armazenamento de recursos não configurado
+ Bucket:
+ Internal: Erro interno ao criar o bucket
+ AlreadyExists: O bucket já existe
+ CreateFailed: Não foi possível criar o bucket
+ ListFailed: Não foi possível ler os buckets
+ RemoveFailed: Não foi possível excluir o bucket
+ SetPublicFailed: Não foi possível definir o bucket como público
+ Object:
+ PutFailed: Não foi possível criar o objeto
+ GetFailed: Não foi possível ler o objeto
+ NotFound: Objeto não encontrado
+ PresignedTokenFailed: Não foi possível criar o token assinado
+ ListFailed: Não foi possível ler a lista de objetos
+ RemoveFailed: Não foi possível remover o objeto
+ Limit:
+ ExceedsDefault: Limite excede o limite padrão
+ Language:
+ NotParsed: Não foi possível analisar o idioma
+ OIDCSettings:
+ NotFound: Configuração OIDC não encontrada
+ AlreadyExists: Configuração OIDC já existe
+ SecretGenerator:
+ AlreadyExists: Gerador de segredos já existe
+ TypeMissing: Tipo de gerador de segredos ausente
+ NotFound: Gerador de segredos não encontrado
+ SMSConfig:
+ NotFound: Configuração de SMS não encontrada
+ AlreadyActive: Configuração de SMS já está ativa
+ AlreadyDeactivated: Configuração de SMS já está desativada
+ SMTPConfig:
+ NotFound: Configuração de SMTP não encontrada
+ AlreadyExists: Configuração de SMTP já existe
+ SenderAdressNotCustomDomain: O endereço do remetente deve ser configurado como um domínio personalizado na instância.
+ Notification:
+ NoDomain: Nenhum domínio encontrado para a mensagem
+ User:
+ NotFound: Usuário não pôde ser encontrado
+ AlreadyExists: Usuário já existe
+ NotFoundOnOrg: Usuário não pôde ser encontrado na organização escolhida
+ NotAllowedOrg: O usuário não é membro da organização requerida
+ UserIDMissing: ID do usuário ausente
+ UserIDWrong: "Usuário da solicitação não é igual ao usuário autenticado"
+ DomainPolicyNil: Política da organização está vazia
+ EmailAsUsernameNotAllowed: O email não é permitido como nome de usuário
+ Invalid: Dados do usuário são inválidos
+ DomainNotAllowedAsUsername: O domínio já está reservado e não pode ser usado
+ AlreadyInactive: Usuário já está inativo
+ NotInactive: O usuário não está inativo
+ CantDeactivateInitial: O usuário com estado inicial só pode ser excluído, não desativado
+ ShouldBeActiveOrInitial: O usuário não está ativo ou no estado inicial
+ AlreadyInitialised: O usuário já está inicializado
+ NotInitialised: O usuário ainda não está inicializado
+ NotLocked: O usuário não está bloqueado
+ NoChanges: Nenhuma alteração encontrada
+ InitCodeNotFound: Código de inicialização não encontrado
+ UsernameNotChanged: Nome de usuário não alterado
+ InvalidURLTemplate: O modelo de URL é inválido
+ Profile:
+ NotFound: Perfil não encontrado
+ NotChanged: Perfil não alterado
+ Empty: Perfil está vazio
+ FirstNameEmpty: Nome fornecido no perfil está vazio
+ LastNameEmpty: Sobrenome fornecido no perfil está vazio
+ IDMissing: ID do perfil está faltando
+ Email:
+ NotFound: Email não encontrado
+ Invalid: O email é inválido
+ AlreadyVerified: O email já foi verificado
+ NotChanged: Email não alterado
+ Empty: O email está vazio
+ IDMissing: ID do email está faltando
+ Phone:
+ NotFound: Telefone não encontrado
+ Invalid: O telefone é inválido
+ AlreadyVerified: O telefone já foi verificado
+ Empty: O telefone está vazio
+ NotChanged: Telefone não alterado
+ Address:
+ NotFound: Endereço não encontrado
+ NotChanged: Endereço não alterado
+ Machine:
+ Key:
+ NotFound: Chave de máquina não encontrada
+ AlreadyExisting: Chave de máquina já existe
+ Secret:
+ NotExisting: Segredo não existe
+ Invalid: Segredo é inválido
+ CouldNotGenerate: Não foi possível gerar o segredo
+ PAT:
+ NotFound: Token de Acesso Pessoal não encontrado
+ NotHuman: O usuário deve ser pessoal
+ NotMachine: O usuário deve ser técnico
+ WrongType: Não permitido para este tipo de usuário
+ NotAllowedToLink: O usuário não tem permissão para vincular com provedor de login externo
+ Username:
+ AlreadyExists: Nome de usuário já está em uso
+ Reserved: Nome de usuário já está em uso
+ Empty: Nome de usuário está vazio
+ Code:
+ Empty: Código está vazio
+ NotFound: Código não encontrado
+ Expired: Código expirou
+ GeneratorAlgNotSupported: Algoritmo do gerador não suportado
+ Password:
+ NotFound: Senha não encontrada
+ Empty: Senha está vazia
+ Invalid: Senha é inválida
+ NotSet: O usuário não definiu uma senha
+ PasswordComplexityPolicy:
+ NotFound: Política de complexidade de senha não encontrada
+ MinLength: A senha é muito curta
+ MinLengthNotAllowed: O comprimento mínimo fornecido não é permitido
+ HasLower: A senha deve conter letras minúsculas
+ HasUpper: A senha deve conter letras maiúsculas
+ HasNumber: A senha deve conter números
+ HasSymbol: A senha deve conter caracteres especiais
+ ExternalIDP:
+ Invalid: IDP externo inválido
+ IDPConfigNotExisting: Provedor de IDP inválido para esta organização
+ NotAllowed: IDP externo não permitido nesta organização
+ MinimumExternalIDPNeeded: Pelo menos um IDP deve ser adicionado
+ AlreadyExists: IDP externo já está em uso
+ NotFound: IDP externo não encontrado
+ MFA:
+ OTP:
+ AlreadyReady: OTP (OneTimePassword) de autenticação multifator já está configurado
+ NotExisting: OTP (OneTimePassword) de autenticação multifator não existe
+ NotReady: OTP (OneTimePassword) de autenticação multifator não está pronto
+ InvalidCode: Código inválido
+ U2F:
+ NotExisting: U2F não existe
+ Passwordless:
+ NotExisting: Autenticação sem senha não existe
+ WebAuthN:
+ NotFound: Token WebAuthN não pôde ser encontrado
+ BeginRegisterFailed: Falha ao iniciar o registro do WebAuthN
+ MarshalError: Erro ao processar os dados
+ ErrorOnParseCredential: Erro ao analisar os dados de credenciais
+ CreateCredentialFailed: Erro ao criar as credenciais
+ BeginLoginFailed: Falha ao iniciar o login do WebAuthN
+ ValidateLoginFailed: Erro ao validar as credenciais de login
+ CloneWarning: As credenciais podem ser clonadas
+ RefreshToken:
+ Invalid: Refresh Token inválido
+ NotFound: Refresh Token não encontrado
+ Instance:
+ NotFound: Instância não encontrada
+ AlreadyExists: Instância já existe
+ NotChanged: Instância não alterada
+ Org:
+ AlreadyExists: Nome da organização já está em uso
+ Invalid: Organização é inválida
+ AlreadyDeactivated: Organização já está desativada
+ AlreadyActive: Organização já está ativa
+ Empty: Organização está vazia
+ NotFound: Organização não encontrada
+ NotChanged: Organização não alterada
+ DefaultOrgNotDeletable: A organização padrão não pode ser excluída
+ ZitadelOrgNotDeletable: A organização com o projeto ZITADEL não pode ser excluída
+ InvalidDomain: Domínio inválido
+ DomainMissing: Domínio ausente
+ DomainNotOnOrg: O domínio não existe na organização
+ DomainNotVerified: O domínio não foi verificado
+ DomainAlreadyVerified: O domínio já foi verificado
+ DomainVerificationTypeInvalid: O tipo de verificação do domínio é inválido
+ DomainVerificationMissing: A verificação do domínio ainda não foi iniciada
+ DomainVerificationFailed: Falha na verificação do domínio
+ PrimaryDomainNotDeletable: O domínio principal não pode ser excluído
+ DomainNotFound: Domínio não encontrado
+ MemberIDMissing: ID do membro ausente
+ MemberNotFound: Membro da organização não encontrado
+ InvalidMember: Membro da organização é inválido
+ UserIDMissing: ID do usuário ausente
+ PolicyAlreadyExists: Política já existe
+ PolicyNotExisting: Política não existe
+ IdpInvalid: Configuração de IDP é inválida
+ IdpNotExisting: Configuração de IDP não existe
+ OIDCConfigInvalid: Configuração de IDP OIDC é inválida
+ IdpIsNotOIDC: A configuração de IDP não é do tipo OIDC
+ Domain:
+ AlreadyExists: Domínio já existe
+ InvalidCharacter: Apenas caracteres alfanuméricos, . e - são permitidos para um domínio
+ IDP:
+ InvalidSearchQuery: Consulta de pesquisa inválida
+ LoginPolicy:
+ NotFound: Política de login não encontrada
+ Invalid: Política de login é inválida
+ RedirectURIInvalid: O URI de redirecionamento padrão é inválido
+ NotExisting: Política de login não existe
+ AlreadyExists: Política de login já existe
+ IdpProviderAlreadyExisting: Provedor de identidade já existe
+ IdpProviderNotExisting: Provedor de identidade não existe
+ RegistrationNotAllowed: O registro não é permitido
+ UsernamePasswordNotAllowed: O login com nome de usuário/senha não é permitido
+ MFA:
+ AlreadyExists: Autenticação multifator já existe
+ NotExisting: Autenticação multifator não existe
+ Unspecified: Autenticação multifator inválida
+ MailTemplate:
+ NotFound: Modelo de email padrão não encontrado
+ NotChanged: Modelo de email padrão não foi alterado
+ AlreadyExists: Modelo de email padrão já existe
+ Invalid: Modelo de email padrão é inválido
+ CustomMessageText:
+ NotFound: Texto de mensagem padrão não encontrado
+ NotChanged: Texto de mensagem padrão não foi alterado
+ AlreadyExists: Texto de mensagem padrão já existe
+ Invalid: Texto de mensagem padrão é inválido
+ PasswordComplexityPolicy:
+ NotFound: Política de complexidade de senha não encontrada
+ Empty: Política de complexidade de senha está vazia
+ NotExisting: Política de complexidade de senha não existe
+ AlreadyExists: Política de complexidade de senha já existe
+ PasswordLockoutPolicy:
+ NotFound: Política de Bloqueio de Senha não encontrada
+ Empty: A Política de Bloqueio de Senha está vazia
+ NotExisting: A Política de Bloqueio de Senha não existe
+ AlreadyExists: A Política de Bloqueio de Senha já existe
+ PasswordAgePolicy:
+ NotFound: Política de Idade de Senha não encontrada
+ Empty: A Política de Idade de Senha está vazia
+ NotExisting: A Política de Idade de Senha não existe
+ AlreadyExists: A Política de Idade de Senha já existe
+ OrgIAMPolicy:
+ Empty: A Política de IAM da Organização está vazia
+ NotExisting: A Política de IAM da Organização não existe
+ AlreadyExists: A Política de IAM da Organização já existe
+ NotificationPolicy:
+ NotFound: Política de Notificação não encontrada
+ NotChanged: Política de Notificação não alterada
+ AlreadyExists: Política de Notificação já existe
+ LabelPolicy:
+ NotFound: Política de Rótulo Privado não encontrada
+ NotChanged: Política de Rótulo Privado não foi alterada
+ Project:
+ ProjectIDMissing: ID do Projeto ausente
+ AlreadyExists: Projeto já existe na organização
+ OrgNotExisting: A organização não existe
+ UserNotExisting: O usuário não existe
+ CouldNotGenerateClientSecret: Não foi possível gerar o segredo do cliente
+ Invalid: O projeto é inválido
+ NotActive: O projeto não está ativo
+ NotInactive: O projeto não está desativado
+ NotFound: Projeto não encontrado
+ UserIDMissing: ID do usuário ausente
+ Member:
+ NotFound: Membro do projeto não encontrado
+ Invalid: O membro do projeto é inválido
+ AlreadyExists: Membro do projeto já existe
+ NotExisting: O membro do projeto não existe
+ MinimumOneRoleNeeded: Pelo menos uma função deve ser adicionada
+ Role:
+ AlreadyExists: A função já existe
+ Invalid: A função é inválida
+ NotExisting: A função não existe
+ IDMissing: ID ausente
+ App:
+ AlreadyExists: O aplicativo já existe
+ NotFound: Aplicativo não encontrado
+ Invalid: Aplicativo inválido
+ NotExisting: O aplicativo não existe
+ NotActive: O aplicativo não está ativo
+ NotInactive: O aplicativo não está inativo
+ OIDCConfigInvalid: A configuração OIDC é inválida
+ APIConfigInvalid: A configuração da API é inválida
+ SAMLConfigInvalid: A configuração SAML é inválida
+ IsNotOIDC: O aplicativo não é do tipo OIDC
+ IsNotAPI: O aplicativo não é do tipo API
+ IsNotSAML: O aplicativo não é do tipo SAML
+ SAMLMetadataMissing: O metadados SAML está ausente
+ SAMLMetadataFormat: Erro de formato nos metadados SAML
+ SAMLEntityIDAlreadyExisting: O EntityID SAML já existe
+ OIDCAuthMethodNoSecret: O método de autenticação OIDC escolhido não requer um segredo
+ APIAuthMethodNoSecret: O método de autenticação da API escolhido não requer um segredo
+ AuthMethodNoPrivateKeyJWT: O método de autenticação escolhido não requer uma chave
+ ClientSecretInvalid: O segredo do cliente é inválido
+ Key:
+ AlreadyExisting: Chave do aplicativo já existente
+ NotFound: Chave do aplicativo não encontrada
+ RequiredFieldsMissing: Alguns campos obrigatórios estão faltando
+ Grant:
+ AlreadyExists: A concessão do projeto já existe
+ NotFound: Concessão não encontrada
+ Invalid: A concessão do projeto é inválida
+ NotExisting: A concessão do projeto não existe
+ HasNotExistingRole: Uma função não existe no projeto
+ NotActive: A concessão do projeto não está ativa
+ NotInactive: A concessão do projeto não está inativa
+ IAM:
+ NotFound: Instância não encontrada
+ Member:
+ RolesNotChanged: As funções não foram alteradas
+ MemberInvalid: O membro é inválido
+ MemberAlreadyExisting: O membro já existe
+ MemberNotExisting: O membro não existe
+ IDMissing: ID ausente
+ IAMProjectIDMissing: ID do projeto IAM ausente
+ IamProjectAlreadySet: O ID do projeto IAM já foi definido
+ IdpInvalid: A configuração de IDP é inválida
+ IdpNotExisting: A configuração de IDP não existe
+ OIDCConfigInvalid: A configuração de IDP OIDC é inválida
+ IdpIsNotOIDC: A configuração de IDP não é do tipo oidc
+ LoginPolicyInvalid: A Política de Login é inválida
+ LoginPolicyNotExisting: A Política de Login não existe
+ IdpProviderInvalid: O Provedor de Identidade é inválido
+ LoginPolicy:
+ NotFound: Política de Login padrão não encontrada
+ NotChanged: Política de Login padrão não foi alterada
+ NotExisting: Política de Login padrão não existe
+ AlreadyExists: Política de Login padrão já existe
+ RedirectURIInvalid: O URI de redirecionamento padrão é inválido
+ MFA:
+ AlreadyExists: A autenticação multifator já existe
+ NotExisting: A autenticação multifator não existe
+ Unspecified: A autenticação multifator é inválida
+ IDP:
+ AlreadyExists: O provedor de identidade já existe
+ NotExisting: O provedor de identidade não existe
+ Invalid: Provedor de identidade inválido
+ IDPConfig:
+ AlreadyExists: A configuração de provedor de identidade já existe
+ NotInactive: A configuração de provedor de identidade não está inativa
+ NotActive: A configuração de provedor de identidade não está ativa
+ LabelPolicy:
+ NotFound: Política de Rótulo Privado padrão não encontrada
+ NotChanged: Política de Rótulo Privado padrão não foi alterada
+ MailTemplate:
+ NotFound: Modelo de E-mail padrão não encontrado
+ NotChanged: Modelo de E-mail padrão não foi alterado
+ AlreadyExists: Modelo de E-mail padrão já existe
+ Invalid: O Modelo de E-mail padrão é inválido
+ CustomMessageText:
+ NotFound: Texto de Mensagem padrão não encontrado
+ NotChanged: Texto de Mensagem padrão não foi alterado
+ AlreadyExists: Texto de Mensagem padrão já existe
+ Invalid: O Texto de Mensagem padrão é inválido
+ PasswordComplexityPolicy:
+ NotFound: Política de Complexidade de Senha Padrão não encontrada
+ NotExisting: Política de Complexidade de Senha Padrão não existente
+ AlreadyExists: Política de Complexidade de Senha Padrão já existente
+ Empty: Política de Complexidade de Senha Padrão vazia
+ NotChanged: Política de Complexidade de Senha Padrão não foi alterada
+ PasswordAgePolicy:
+ NotFound: Política de Idade de Senha Padrão não encontrada
+ NotExisting: Política de Idade de Senha Padrão não existente
+ AlreadyExists: Política de Idade de Senha Padrão já existente
+ Empty: Política de Idade de Senha Padrão vazia
+ NotChanged: Política de Idade de Senha Padrão não foi alterada
+ PasswordLockoutPolicy:
+ NotFound: Política de Bloqueio de Senha Padrão não encontrada
+ NotExisting: Política de Bloqueio de Senha Padrão não existente
+ AlreadyExists: Política de Bloqueio de Senha Padrão já existente
+ Empty: Política de Bloqueio de Senha Padrão vazia
+ NotChanged: Política de Bloqueio de Senha Padrão não foi alterada
+ DomainPolicy:
+ NotFound: Política IAM da Organização não encontrada
+ Empty: Política IAM da Organização está vazia
+ NotExisting: Política IAM da Organização não existente
+ AlreadyExists: Política IAM da Organização já existe
+ NotChanged: Política IAM da Organização não foi alterada
+ NotificationPolicy:
+ NotFound: Política de Notificação Padrão não encontrada
+ NotChanged: Política de Notificação Padrão não foi alterada
+ AlreadyExists: Política de Notificação Padrão já existe
+ Policy:
+ AlreadyExists: Política já existe
+ Label:
+ Invalid:
+ PrimaryColor: A cor primária não é um valor hexadecimal válido
+ BackgroundColor: A cor de fundo não é um valor hexadecimal válido
+ WarnColor: A cor de aviso não é um valor hexadecimal válido
+ FontColor: A cor da fonte não é um valor hexadecimal válido
+ PrimaryColorDark: A cor primária (modo escuro) não é um valor hexadecimal válido
+ BackgroundColorDark: A cor de fundo (modo escuro) não é um valor hexadecimal válido
+ WarnColorDark: A cor de aviso (modo escuro) não é um valor hexadecimal válido
+ FontColorDark: A cor da fonte (modo escuro) não é um valor hexadecimal válido
+ UserGrant:
+ AlreadyExists: A concessão de usuário já existe
+ NotFound: A concessão de usuário não foi encontrada
+ Invalid: A concessão de usuário é inválida
+ NotChanged: A concessão de usuário não foi alterada
+ IDMissing: ID faltando
+ NotActive: A concessão de usuário não está ativa
+ NotInactive: A concessão de usuário não está desativada
+ NoPermissionForProject: O usuário não possui permissões neste projeto
+ RoleKeyNotFound: Função não encontrada
+ Member:
+ AlreadyExists: O membro já existe
+ IDPConfig:
+ AlreadyExists: Configuração de Provedor de Identidade com esse nome já existe
+ NotExisting: A Configuração do Provedor de Identidade não existe
+ Changes:
+ NotFound: Nenhum histórico encontrado
+ AuditRetention: O histórico está fora do período de retenção do registro de auditoria
+ Token:
+ NotFound: Token não encontrado
+ UserSession:
+ NotFound: Sessão do usuário não encontrada
+ Key:
+ ExpireBeforeNow: A data de expiração está no passado
+ Login:
+ LoginPolicy:
+ MFA:
+ ForceAndNotConfigured: A autenticação multifator está configurada como obrigatória, mas nenhum provedor possível está configurado. Entre em contato com o administrador do sistema.
+ Step:
+ Started:
+ AlreadyExists: A etapa já foi iniciada
+ Done:
+ AlreadyExists: A etapa já foi concluída
+ CustomText:
+ AlreadyExists: O texto personalizado já existe
+ Invalid: O texto personalizado é inválido
+ NotFound: O texto personalizado não foi encontrado
+ TranslationFile:
+ ReadError: Erro ao ler o arquivo de tradução
+ MergeError: O arquivo de tradução não pôde ser mesclado com as traduções personalizadas
+ NotFound: O arquivo de tradução não existe
+ Metadata:
+ NotFound: Metadados não encontrados
+ NoData: A lista de metadados está vazia
+ Invalid: Os metadados são inválidos
+ KeyNotExisting: Uma ou mais chaves não existem
+ Action:
+ Invalid: A ação é inválida
+ NotFound: A ação não foi encontrada
+ NotActive: A ação não está ativa
+ NotInactive: A ação não está inativa
+ MaxAllowed: Não são permitidas ações adicionais ativas
+ Flow:
+ FlowTypeMissing: O tipo de fluxo está faltando
+ Empty: O fluxo já está vazio
+ WrongTriggerType: O tipo de acionador é inválido
+ NoChanges: Sem alterações
+ ActionIDsNotExist: Os IDs de ação não existem
+ Query:
+ CloseRows: A instrução SQL não pôde ser concluída
+ SQLStatement: Não foi possível criar a instrução SQL
+ InvalidRequest: O pedido é inválido
+ Quota:
+ AlreadyExists: Cota já existe para esta unidade
+ NotFound: Cota não encontrada para esta unidade
+ Invalid:
+ CallURL: A URL de chamada da cota é inválida
+ Percent: A porcentagem da cota é menor que 1
+ Unimplemented: As cotas não são implementadas para esta unidade
+ Amount: A quantidade da cota é menor que 1
+ ResetInterval: O intervalo de reinicialização da cota é menor que um minuto
+ Noop: Uma cota ilimitada sem notificações não tem efeito
+ Access:
+ Exhausted: A cota para solicitações autenticadas está esgotada
+ Execution:
+ Exhausted: A cota para segundos de execução está esgotada
+ LogStore:
+ Access:
+ StorageFailed: Falha ao armazenar o log de acesso no banco de dados
+ ScanFailed: Falha ao consultar o uso para solicitações autenticadas
+ Execution:
+ StorageFailed: Falha ao armazenar o log de execução da ação no banco de dados
+ ScanFailed: Falha ao consultar o uso para segundos de execução da ação
+ Session:
+ NotExisting: A sessão não existe
+ Terminated: A sessão já foi encerrada
+ Token:
+ Invalid: O token da sessão é inválido
+ Passkey:
+ NoChallenge: Sessão sem desafio de senha
+ Intent:
+ IDPMissing: O ID do IDP está faltando na solicitação
+ SuccessURLMissing: A URL de sucesso está faltando na solicitação
+ FailureURLMissing: A URL de falha está faltando na solicitação
+ StateMissing: O parâmetro de estado está faltando na solicitação
+ NotStarted: A intenção não foi iniciada ou já foi encerrada
+ NotSucceeded: A intenção não teve sucesso
+ TokenCreationFailed: Falha na criação do token
+ InvalidToken: O token da intenção é inválido
+ OtherUser: Intenção destinada a outro usuário
+ AuthRequest:
+ AlreadyExists: A solicitação de autenticação já existe
+ NotExisting: A solicitação de autenticação não existe
+ WrongLoginClient: A solicitação de autenticação foi criada por outro cliente de login
+ OIDCSession:
+ RefreshTokenInvalid: O Refresh Token é inválido
+
+AggregateTypes:
+ action: Ação
+ instance: Instância
+ key_pair: Par de chaves
+ org: Organização
+ project: Projeto
+ user: Usuário
+ usergrant: Concessão de usuário
+ quota: Cota
+
+EventTypes:
+ user:
+ added: Usuário adicionado
+ selfregistered: Usuário se registrou
+ initialization:
+ code:
+ added: Código de inicialização gerado
+ sent: Código de inicialização enviado
+ check:
+ succeeded: Verificação de inicialização bem-sucedida
+ failed: Verificação de inicialização falhou
+ token:
+ added: Token de acesso criado
+ removed: Token de acesso removido
+ username:
+ reserved: Nome de usuário reservado
+ released: Nome de usuário liberado
+ changed: Nome de usuário alterado
+ email:
+ reserved: Endereço de e-mail reservado
+ released: Endereço de e-mail liberado
+ changed: Endereço de e-mail alterado
+ verified: Endereço de e-mail verificado
+ verification:
+ failed: Falha na verificação do endereço de e-mail
+ code:
+ added: Código de verificação do endereço de e-mail gerado
+ sent: Código de verificação do endereço de e-mail enviado
+ machine:
+ added: Usuário técnico adicionado
+ changed: Usuário técnico alterado
+ key:
+ added: Chave adicionada
+ removed: Chave removida
+ secret:
+ set: Segredo definido
+ removed: Segredo removido
+ check:
+ succeeded: Verificação de segredo bem-sucedida
+ failed: Verificação de segredo falhou
+ human:
+ added: Pessoa adicionada
+ selfregistered: Pessoa se registrou
+ avatar:
+ added: Avatar adicionado
+ removed: Avatar removido
+ initialization:
+ code:
+ added: Código de inicialização gerado
+ sent: Código de inicialização enviado
+ check:
+ succeeded: Verificação de inicialização bem-sucedida
+ failed: Verificação de inicialização falhou
+ username:
+ reserved: Nome de usuário reservado
+ released: Nome de usuário liberado
+ email:
+ changed: Endereço de e-mail alterado
+ verified: Endereço de e-mail verificado
+ verification:
+ failed: Falha na verificação do endereço de e-mail
+ code:
+ added: Código de verificação do endereço de e-mail gerado
+ sent: Código de verificação do endereço de e-mail enviado
+ password:
+ changed: Senha alterada
+ code:
+ added: Código de senha gerado
+ sent: Código de senha enviado
+ check:
+ succeeded: Verificação de senha bem-sucedida
+ failed: Verificação de senha falhou
+ externallogin:
+ check:
+ succeeded: Login externo bem-sucedido
+ externalidp:
+ added: IDP externo adicionado
+ removed: IDP externo removido
+ cascade:
+ removed: Cascade de IDP externo removido
+ phone:
+ changed: Número de telefone alterado
+ verified: Número de telefone verificado
+ verification:
+ failed: Falha na verificação do número de telefone
+ code:
+ added: Código de verificação do número de telefone gerado
+ sent: Código de verificação do número de telefone enviado
+ removed: Número de telefone removido
+ profile:
+ changed: Perfil do usuário alterado
+ address:
+ changed: Endereço do usuário alterado
+ mfa:
+ otp:
+ added: OTP de autenticação multifator adicionado
+ verified: OTP de autenticação multifator verificado
+ removed: OTP de autenticação multifator removido
+ check:
+ succeeded: Verificação de OTP de autenticação multifator bem-sucedida
+ failed: Verificação de OTP de autenticação multifator falhou
+ u2f:
+ token:
+ added: Token U2F de autenticação multifator adicionado
+ verified: Token U2F de autenticação multifator verificado
+ removed: Token U2F de autenticação multifator removido
+ begin:
+ login: Verificação U2F de autenticação multifator iniciada
+ check:
+ succeeded: Verificação U2F de autenticação multifator bem-sucedida
+ failed: Verificação U2F de autenticação multifator falhou
+ signcount:
+ changed: O checksum do Token U2F de autenticação multifator foi alterado
+ init:
+ skipped: Inicialização multifator pulada
+ passwordless:
+ token:
+ added: Token para login sem senha adicionado
+ verified: Token para login sem senha verificado
+ removed: Token para login sem senha removido
+ begin:
+ login: Verificação de login sem senha iniciada
+ check:
+ succeeded: Verificação de login sem senha bem-sucedida
+ failed: Verificação de login sem senha falhou
+ signcount:
+ changed: O checksum do Token para login sem senha foi alterado
+ initialization:
+ code:
+ added: Código de inicialização sem senha adicionado
+ sent: Código de inicialização sem senha enviado
+ requested: Código de inicialização sem senha solicitado
+ check:
+ succeeded: Verificação do código de inicialização sem senha realizada com sucesso
+ failed: Falha na verificação do código de inicialização sem senha
+ signed:
+ out: Usuário desconectado
+ refresh:
+ token:
+ added: Refresh Token criado
+ renewed: Refresh Token renovado
+ removed: Refresh Token removido
+ locked: Usuário bloqueado
+ unlocked: Usuário desbloqueado
+ deactivated: Usuário desativado
+ reactivated: Usuário reativado
+ removed: Usuário removido
+ password:
+ changed: Senha alterada
+ code:
+ added: Código de senha gerado
+ sent: Código de senha enviado
+ check:
+ succeeded: Verificação de senha bem-sucedida
+ failed: Verificação de senha falhou
+ phone:
+ changed: Número de telefone alterado
+ verified: Número de telefone verificado
+ verification:
+ failed: Falha na verificação do número de telefone
+ code:
+ added: Código de verificação do número de telefone gerado
+ sent: Código de verificação do número de telefone enviado
+ removed: Número de telefone removido
+ profile:
+ changed: Perfil do usuário alterado
+ address:
+ changed: Endereço do usuário alterado
+ mfa:
+ otp:
+ added: OTP de autenticação multifator adicionado
+ verified: OTP de autenticação multifator verificado
+ removed: OTP de autenticação multifator removido
+ check:
+ succeeded: Verificação de OTP de autenticação multifator bem-sucedida
+ failed: Verificação de OTP de autenticação multifator falhou
+ init:
+ skipped: Inicialização de OTP de autenticação multifator pulada
+ init:
+ skipped: Inicialização multifator pulada
+ signed:
+ out: Usuário desconectado
+ grant:
+ added: Autorização adicionada
+ changed: Autorização alterada
+ removed: Autorização removida
+ deactivated: Autorização desativada
+ reactivated: Autorização reativada
+ reserved: Autorização reservada
+ released: Autorização liberada
+ cascade:
+ removed: Autorização removida
+ changed: Autorização alterada
+ metadata:
+ set: Metadados do usuário definidos
+ removed: Metadados do usuário removidos
+ removed.all: Todos os metadados do usuário removidos
+ domain:
+ claimed: Domínio reivindicado
+ claimed.sent: Notificação de reivindicação de domínio enviada
+ pat:
+ added: Token de Acesso Pessoal adicionado
+ removed: Token de Acesso Pessoal removido
+ org:
+ added: Organização adicionada
+ changed: Organização alterada
+ deactivated: Organização desativada
+ reactivated: Organização reativada
+ removed: Organização removida
+ domain:
+ added: Domínio adicionado
+ verification:
+ added: Verificação de domínio adicionada
+ failed: Falha na verificação de domínio
+ verified: Domínio verificado
+ removed: Domínio removido
+ primary:
+ set: Domínio principal definido
+ reserved: Domínio reservado
+ released: Domínio liberado
+ name:
+ reserved: Nome da organização reservado
+ released: Nome da organização liberado
+ member:
+ added: Membro da organização adicionado
+ changed: Membro da organização alterado
+ removed: Membro da organização removido
+ cascade:
+ removed: Membro da organização removido em cascata
+ iam:
+ policy:
+ added: Política do sistema adicionada
+ changed: Política do sistema alterada
+ removed: Política do sistema removida
+ idp:
+ config:
+ added: Configuração do IDP adicionada
+ changed: Configuração do IDP alterada
+ removed: Configuração do IDP removida
+ deactivated: Configuração do IDP desativada
+ reactivated: Configuração do IDP reativada
+ oidc:
+ config:
+ added: Configuração do IDP OIDC adicionada
+ changed: Configuração do IDP OIDC alterada
+ saml:
+ config:
+ added: Configuração do IDP SAML adicionada
+ changed: Configuração do IDP SAML alterada
+ jwt:
+ config:
+ added: Configuração do IDP JWT adicionada
+ changed: Configuração do IDP JWT alterada
+ customtext:
+ set: Texto personalizado definido
+ removed: Texto personalizado removido
+ template:
+ removed: Modelo de texto personalizado removido
+ policy:
+ login:
+ added: Política de login adicionada
+ changed: Política de login alterada
+ removed: Política de login removida
+ idpprovider:
+ added: Provedor de identidade adicionado à política de login
+ removed: Provedor de identidade removido da política de login
+ cascade:
+ removed: Remoção em cascata do provedor de identidade da política de login
+ secondfactor:
+ added: Segundo fator adicionado à política de login
+ removed: Segundo fator removido da política de login
+ multifactor:
+ added: Multifator adicionado à política de login
+ removed: Multifator removido da política de login
+ password:
+ complexity:
+ added: Política de complexidade de senha adicionada
+ changed: Política de complexidade de senha alterada
+ removed: Política de complexidade de senha removida
+ age:
+ added: Política de idade da senha adicionada
+ changed: Política de idade da senha alterada
+ removed: Política de idade da senha removida
+ lockout:
+ added: Política de bloqueio de senha adicionada
+ changed: Política de bloqueio de senha alterada
+ removed: Política de bloqueio de senha removida
+ label:
+ added: Política de rótulo adicionada
+ changed: Política de rótulo alterada
+ activated: Política de rótulo ativada
+ removed: Política de rótulo removida
+ logo:
+ added: Logotipo adicionado à política de rótulo
+ removed: Logotipo removido da política de rótulo
+ dark:
+ added: Logotipo (modo escuro) adicionado à política de rótulo
+ removed: Logotipo (modo escuro) removido da política de rótulo
+ icon:
+ added: Ícone adicionado à política de rótulo
+ removed: Ícone removido da política de rótulo
+ dark:
+ added: Ícone (modo escuro) adicionado à política de rótulo
+ removed: Ícone (modo escuro) removido da política de rótulo
+ font:
+ added: Fonte adicionada à política de rótulo
+ removed: Fonte removida da política de rótulo
+ assets:
+ removed: Ativos removidos da política de rótulo
+ privacy:
+ added: Política de privacidade e TOS adicionada
+ changed: Política de privacidade e TOS alterada
+ removed: Política de privacidade e TOS removida
+ domain:
+ added: Política de domínio adicionada
+ changed: Política de domínio alterada
+ removed: Política de domínio removida
+ lockout:
+ added: Política de bloqueio adicionada
+ changed: Política de bloqueio alterada
+ removed: Política de bloqueio removida
+ notification:
+ added: Política de notificação adicionada
+ changed: Política de notificação alterada
+ removed: Política de notificação removida
+ flow:
+ trigger_actions:
+ set: Ação definida
+ cascade:
+ removed: Ações removidas em cascata
+ removed: Ações removidas
+ cleared: Fluxo limpo
+ mail:
+ template:
+ added: Modelo de e-mail adicionado
+ changed: Modelo de e-mail alterado
+ removed: Modelo de e-mail removido
+ text:
+ added: Texto de e-mail adicionado
+ changed: Texto de e-mail alterado
+ removed: Texto de e-mail removido
+ metadata:
+ removed: Metadados removidos
+ removed.all: Todos os metadados removidos
+ set: Metadados definidos
+ project:
+ added: Projeto adicionado
+ changed: Projeto alterado
+ deactivated: Projeto desativado
+ reactivated: Projeto reativado
+ removed: Projeto removido
+ member:
+ added: Membro do projeto adicionado
+ changed: Membro do projeto alterado
+ removed: Membro do projeto removido
+ cascade:
+ removed: Membro do projeto removido em cascata
+ role:
+ added: Função do projeto adicionada
+ changed: Função do projeto alterada
+ removed: Função do projeto removida
+ grant:
+ added: Acesso de gerenciamento adicionado
+ changed: Acesso de gerenciamento alterado
+ removed: Acesso de gerenciamento removido
+ deactivated: Acesso de gerenciamento desativado
+ reactivated: Acesso de gerenciamento reativado
+ cascade:
+ changed: Acesso de gerenciamento alterado
+ member:
+ added: Membro do acesso de gerenciamento adicionado
+ changed: Membro do acesso de gerenciamento alterado
+ removed: Membro do acesso de gerenciamento removido
+ cascade:
+ removed: Acesso de gerenciamento removido em cascata
+ application:
+ added: Aplicativo adicionado
+ changed: Aplicativo alterado
+ removed: Aplicativo removido
+ deactivated: Aplicativo desativado
+ reactivated: Aplicativo reativado
+ oidc:
+ secret:
+ check:
+ succeeded: Verificação de segredo do cliente OIDC bem-sucedida
+ failed: Verificação de segredo do cliente OIDC falhou
+ key:
+ added: Chave do aplicativo adicionada
+ removed: Chave do aplicativo removida
+ config:
+ saml:
+ added: Configuração SAML adicionada
+ changed: Configuração SAML alterada
+ oidc:
+ added: Configuração OIDC adicionada
+ changed: Configuração OIDC alterada
+ secret:
+ changed: Segredo OIDC alterado
+ api:
+ added: Configuração de API adicionada
+ changed: Configuração de API alterada
+ secret:
+ changed: Segredo da API alterado
+ policy:
+ password:
+ complexity:
+ added: Política de complexidade de senha adicionada
+ changed: Política de complexidade de senha alterada
+ age:
+ added: Política de idade da senha adicionada
+ changed: Política de idade da senha alterada
+ lockout:
+ added: Política de bloqueio de senha adicionada
+ changed: Política de bloqueio de senha alterada
+ iam:
+ setup:
+ started: Configuração do ZITADEL iniciada
+ done: Configuração do ZITADEL concluída
+ global:
+ org:
+ set: Organização global definida
+ project:
+ iam:
+ set: Projeto ZITADEL definido
+ member:
+ added: Membro ZITADEL adicionado
+ changed: Membro ZITADEL alterado
+ removed: Membro ZITADEL removido
+ cascade:
+ removed: Membro ZITADEL removido em cascata
+ idp:
+ config:
+ added: Configuração do IDP adicionada
+ changed: Configuração do IDP alterada
+ removed: Configuração do IDP removida
+ deactivated: Configuração do IDP desativada
+ reactivated: Configuração do IDP reativada
+ oidc:
+ config:
+ added: Configuração do IDP OIDC adicionada
+ changed: Configuração do IDP OIDC alterada
+ saml:
+ config:
+ added: Configuração do IDP SAML adicionada
+ changed: Configuração do IDP SAML alterada
+ jwt:
+ config:
+ added: Configuração JWT do provedor de identidade adicionada
+ changed: Configuração JWT do provedor de identidade removida
+ customtext:
+ set: Texto definido
+ removed: Texto removido
+ policy:
+ login:
+ added: Política de login padrão adicionada
+ changed: Política de login padrão alterada
+ idpprovider:
+ added: Provedor de identidade adicionado à política de login padrão
+ removed: Provedor de identidade removido da política de login padrão
+ label:
+ added: Política de rótulo adicionada
+ changed: Política de rótulo alterada
+ activated: Política de rótulo ativada
+ logo:
+ added: Logotipo adicionado à política de rótulo
+ removed: Logotipo removido da política de rótulo
+ dark:
+ added: Logotipo (modo escuro) adicionado à política de rótulo
+ removed: Logotipo (modo escuro) removido da política de rótulo
+ icon:
+ added: Ícone adicionado à política de rótulo
+ removed: Ícone removido da política de rótulo
+ dark:
+ added: Ícone (modo escuro) adicionado à política de rótulo
+ removed: Ícone (modo escuro) removido da política de rótulo
+ font:
+ added: Fonte adicionada à política de rótulo
+ removed: Fonte removida da política de rótulo
+ assets:
+ removed: Ativos removidos da política de rótulo
+ default:
+ language:
+ set: Idioma padrão definido
+ oidc:
+ settings:
+ added: Configuração OIDC adicionada
+ changed: Configuração OIDC alterada
+ removed: Configuração OIDC removida
+ secret:
+ generator:
+ added: Gerador de segredos adicionado
+ changed: Gerador de segredos alterado
+ removed: Gerador de segredos removido
+ smtp:
+ config:
+ added: Configuração SMTP adicionada
+ changed: Configuração SMTP alterada
+ password:
+ changed: Segredo da configuração SMTP alterado
+ sms:
+ config:
+ twilio:
+ added: Provedor de SMS Twilio adicionado
+ changed: Provedor de SMS Twilio alterado
+ token:
+ changed: Token do provedor de SMS Twilio alterado
+ removed: Provedor de SMS Twilio removido
+ activated: Provedor de SMS Twilio ativado
+ deactivated: Provedor de SMS Twilio desativado
+ key_pair:
+ added: Par de chaves adicionado
+ certificate:
+ added: Certificado adicionado
+ action:
+ added: Ação adicionada
+ changed: Ação alterada
+ deactivated: Ação desativada
+ reactivated: Ação reativada
+ removed: Ação removida
+ instance:
+ added: Instância adicionada
+ changed: Instância alterada
+ customtext:
+ removed: Texto personalizado removido
+ set: Texto personalizado definido
+ template:
+ removed: Modelo de texto personalizado removido
+ default:
+ language:
+ set: Idioma padrão definido
+ org:
+ set: Organização padrão definida
+ domain:
+ added: Domínio adicionado
+ primary:
+ set: Domínio principal definido
+ removed: Domínio removido
+ iam:
+ console:
+ set: ZITADEL Console definido
+ project:
+ set: Projeto ZITADEL definido
+ mail:
+ template:
+ added: Modelo de e-mail adicionado
+ changed: Modelo de e-mail alterado
+ text:
+ added: Texto de e-mail adicionado
+ changed: Texto de e-mail alterado
+ member:
+ added: Membro da instância adicionado
+ changed: Membro da instância alterado
+ removed: Membro da instância removido
+ cascade:
+ removed: Membro da instância removido em cascata
+ notification:
+ provider:
+ debug:
+ fileadded: Provedor de notificação de arquivo de depuração adicionado
+ filechanged: Provedor de notificação de arquivo de depuração alterado
+ fileremoved: Provedor de notificação de arquivo de depuração removido
+ logadded: Provedor de notificação de log de depuração adicionado
+ logchanged: Provedor de notificação de log de depuração alterado
+ logremoved: Provedor de notificação de log de depuração removido
+ oidc:
+ settings:
+ added: Configurações OIDC adicionadas
+ changed: Configurações OIDC alteradas
+ policy:
+ domain:
+ added: Política de domínio adicionada
+ changed: Política de domínio alterada
+ label:
+ activated: Política de rótulo ativada
+ added: Política de rótulo adicionada
+ assets:
+ removed: Ativo removido da política de rótulo
+ changed: Política de rótulo alterada
+ font:
+ added: Fonte adicionada à política de rótulo
+ removed: Fonte removida da política de rótulo
+ icon:
+ added: Ícone adicionado à política de rótulo
+ removed: Ícone removido da política de rótulo
+ dark:
+ added: Ícone adicionado à política de rótulo (modo escuro)
+ removed: Ícone removido da política de rótulo (modo escuro)
+ logo:
+ added: Logotipo adicionado à política de rótulo
+ removed: Logotipo removido da política de rótulo
+ dark:
+ added: Logotipo adicionado à política de rótulo (modo escuro)
+ removed: Logotipo removido da política de rótulo (modo escuro)
+ lockout:
+ added: Política de bloqueio adicionada
+ changed: Política de bloqueio alterada
+ login:
+ added: Política de login adicionada
+ changed: Política de login alterada
+ idpprovider:
+ added: Provedor de identidade adicionado à política de login
+ cascade:
+ removed: Remoção em cascata do provedor de identidade da política de login
+ removed: Provedor de identidade removido da política de login
+ multifactor:
+ added: Multifator adicionado à política de login
+ removed: Multifator removido da política de login
+ secondfactor:
+ added: Segundo fator adicionado à política de login
+ removed: Segundo fator removido da política de login
+ password:
+ age:
+ added: Política de idade da senha adicionada
+ changed: Política de idade da senha alterada
+ complexity:
+ added: Política de complexidade da senha adicionada
+ changed: Política de complexidade da senha removida
+ privacy:
+ added: Política de privacidade adicionada
+ changed: Política de privacidade alterada
+ security:
+ set: Política de segurança definida
+
+ removed: Instância removida
+ secret:
+ generator:
+ added: Gerador de segredos adicionado
+ changed: Gerador de segredos alterado
+ removed: Gerador de segredos removido
+ sms:
+ configtwilio:
+ activated: Configuração de SMS Twilio ativada
+ added: Configuração de SMS Twilio adicionada
+ changed: Configuração de SMS Twilio alterada
+ deactivated: Configuração de SMS Twilio desativada
+ removed: Configuração de SMS Twilio removida
+ token:
+ changed: Token da configuração de SMS Twilio alterado
+ smtp:
+ config:
+ added: Configuração SMTP adicionada
+ changed: Configuração SMTP alterada
+ password:
+ changed: Senha da configuração SMTP alterada
+ removed: Configuração SMTP removida
+
+Application:
+ OIDC:
+ UnsupportedVersion: Sua versão do OIDC não é suportada
+ V1:
+ NotCompliant: Sua configuração não está em conformidade e difere do padrão OIDC 1.0.
+ NoRedirectUris: É necessário registrar pelo menos um URI de redirecionamento.
+ NotAllCombinationsAreAllowed: A configuração está em conformidade, mas nem todas as combinações possíveis são permitidas.
+ Code:
+ RedirectUris:
+ HttpOnlyForWeb: O tipo de concessão de código permite apenas URIs de redirecionamento http para aplicativos web.
+ CustomOnlyForNative: O tipo de concessão de código permite apenas URIs de redirecionamento personalizados para aplicativos nativos (por exemplo, appname://).
+ Implicit:
+ RedirectUris:
+ CustomNotAllowed: O tipo de concessão implícita não permite URIs de redirecionamento personalizados.
+ HttpNotAllowed: O tipo de concessão implícita não permite URIs de redirecionamento http.
+ HttpLocalhostOnlyForNative: O URI de redirecionamento http://localhost só é permitido para aplicativos nativos.
+ Native:
+ AuthMethodType:
+ NotNone: Aplicativos nativos devem ter o tipo de método de autenticação nenhum.
+ RedirectUris:
+ MustBeHttpLocalhost: Os URIs de redirecionamento devem começar com seu próprio protocolo, http://127.0.0.1, http://[::1] ou http://localhost.
+ UserAgent:
+ AuthMethodType:
+ NotNone: O aplicativo de agente do usuário deve ter o tipo de método de autenticação nenhum.
+ GrantType:
+ Refresh:
+ NoAuthCode: O token de atualização só é permitido em combinação com o Código de Autorização.
+
+Action:
+ Flow:
+ Type:
+ Unspecified: Não especificado
+ ExternalAuthentication: Autenticação externa
+ CustomiseToken: Complementar Token
+ InternalAuthentication: Autenticação interna
+ TriggerType:
+ Unspecified: Não especificado
+ PostAuthentication: Pós-autenticação
+ PreCreation: Pré-criação
+ PostCreation: Póscriação
+ PreUserinfoCreation: Pré-criação de informações do usuário
+ PreAccessTokenCreation: Pré-criação de access token
diff --git a/internal/static/i18n/zh.yaml b/internal/static/i18n/zh.yaml
index 2a9f2400f..1209fd515 100644
--- a/internal/static/i18n/zh.yaml
+++ b/internal/static/i18n/zh.yaml
@@ -120,6 +120,8 @@ Errors:
Empty: 密码为空
Invalid: 密码无效
NotSet: 用户未设置密码
+ NotChanged: 密码未更改
+ NotSupported: 不支持密码哈希编码
PasswordComplexityPolicy:
NotFound: 未找到密码策略
MinLength: 密码太短
@@ -488,6 +490,16 @@ Errors:
TokenCreationFailed: 令牌创建失败
InvalidToken: 意图令牌是无效的
OtherUser: 意图是为另一个用户准备的
+ AuthRequest:
+ AlreadyExists: AuthRequest已经存在
+ NotExisting: AuthRequest不存在
+ WrongLoginClient: 其他登录客户端创建的AuthRequest
+ OIDCSession:
+ RefreshTokenInvalid: Refresh Token 无效
+ Token:
+ Invalid: 令牌无效
+ Expired: 令牌已过期
+ InvalidClient: 没有为该客户发放令牌
AggregateTypes:
action: 动作
diff --git a/internal/user/model/user_view.go b/internal/user/model/user_view.go
index 672e18aef..8cb0d4346 100644
--- a/internal/user/model/user_view.go
+++ b/internal/user/model/user_view.go
@@ -170,12 +170,12 @@ func (u *UserView) MFATypesSetupPossible(level domain.MFALevel, policy *domain.L
return types
}
-func (u *UserView) MFATypesAllowed(level domain.MFALevel, policy *domain.LoginPolicy) ([]domain.MFAType, bool) {
+func (u *UserView) MFATypesAllowed(level domain.MFALevel, policy *domain.LoginPolicy, isInternalAuthentication bool) ([]domain.MFAType, bool) {
types := make([]domain.MFAType, 0)
required := true
switch level {
default:
- required = policy.ForceMFA
+ required = domain.RequiresMFA(policy.ForceMFA, policy.ForceMFALocalOnly, isInternalAuthentication)
fallthrough
case domain.MFALevelSecondFactor:
if policy.HasSecondFactors() {
diff --git a/internal/user/repository/eventsourcing/model/password.go b/internal/user/repository/eventsourcing/model/password.go
index 1699c8c99..6c7414111 100644
--- a/internal/user/repository/eventsourcing/model/password.go
+++ b/internal/user/repository/eventsourcing/model/password.go
@@ -15,6 +15,7 @@ type Password struct {
es_models.ObjectRoot
Secret *crypto.CryptoValue `json:"secret,omitempty"`
+ EncodedHash string `json:"encodedHash,omitempty"`
ChangeRequired bool `json:"changeRequired,omitempty"`
}
diff --git a/internal/user/repository/view/model/notify_user.go b/internal/user/repository/view/model/notify_user.go
index a2ff48deb..7390bc504 100644
--- a/internal/user/repository/view/model/notify_user.go
+++ b/internal/user/repository/view/model/notify_user.go
@@ -128,6 +128,6 @@ func (u *NotifyUser) setPasswordData(event *models.Event) error {
logging.Log("MODEL-dfhw6").WithError(err).Error("could not unmarshal event data")
return caos_errs.ThrowInternal(nil, "MODEL-BHFD2", "could not unmarshal data")
}
- u.PasswordSet = password.Secret != nil
+ u.PasswordSet = password.Secret != nil || password.EncodedHash != ""
return nil
}
diff --git a/internal/user/repository/view/model/user.go b/internal/user/repository/view/model/user.go
index 3d3693aad..4c68711a0 100644
--- a/internal/user/repository/view/model/user.go
+++ b/internal/user/repository/view/model/user.go
@@ -380,7 +380,7 @@ func (u *UserView) setPasswordData(event *models.Event) error {
logging.Log("MODEL-sdw4r").WithError(err).Error("could not unmarshal event data")
return errors.ThrowInternal(nil, "MODEL-6jhsw", "could not unmarshal data")
}
- u.PasswordSet = password.Secret != nil
+ u.PasswordSet = password.Secret != nil || password.EncodedHash != ""
u.PasswordInitRequired = !u.PasswordSet
u.PasswordChangeRequired = password.ChangeRequired
u.PasswordChanged = event.CreationDate
diff --git a/openapi/handler.go b/openapi/handler.go
index db827128b..b44129018 100644
--- a/openapi/handler.go
+++ b/openapi/handler.go
@@ -1,24 +1,21 @@
package openapi
import (
+ "embed"
"net/http"
- "github.com/rakyll/statik/fs"
"github.com/rs/cors"
-
- _ "github.com/zitadel/zitadel/openapi/statik"
)
const (
HandlerPrefix = "/openapi/v2/swagger"
)
+//go:embed v2/zitadel/*
+var openapi embed.FS
+
func Start() (http.Handler, error) {
- statikFS, err := fs.NewWithNamespace("swagger")
- if err != nil {
- return nil, err
- }
handler := &http.ServeMux{}
- handler.Handle("/", cors.AllowAll().Handler(http.FileServer(statikFS)))
+ handler.Handle("/", cors.AllowAll().Handler(http.FileServer(http.FS(openapi))))
return handler, nil
}
diff --git a/openapi/statik/generate.go b/openapi/statik/generate.go
deleted file mode 100644
index 44dcfb50a..000000000
--- a/openapi/statik/generate.go
+++ /dev/null
@@ -1,3 +0,0 @@
-package statik
-
-//go:generate statik -src=../v2/zitadel -dest=.. -ns=swagger
diff --git a/pkg/grpc/protoc/v2/options.pb.go b/pkg/grpc/protoc/v2/options.pb.go
new file mode 100644
index 000000000..5b0cd2ff3
--- /dev/null
+++ b/pkg/grpc/protoc/v2/options.pb.go
@@ -0,0 +1,333 @@
+// Code generated by protoc-gen-go. DO NOT EDIT.
+// versions:
+// protoc-gen-go v1.30.0
+// protoc (unknown)
+// source: zitadel/protoc_gen_zitadel/v2/options.proto
+
+package protoc
+
+import (
+ protoreflect "google.golang.org/protobuf/reflect/protoreflect"
+ protoimpl "google.golang.org/protobuf/runtime/protoimpl"
+ descriptorpb "google.golang.org/protobuf/types/descriptorpb"
+ reflect "reflect"
+ sync "sync"
+)
+
+const (
+ // Verify that this generated code is sufficiently up-to-date.
+ _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
+ // Verify that runtime/protoimpl is sufficiently up-to-date.
+ _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
+)
+
+type Options struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ AuthOption *AuthOption `protobuf:"bytes,1,opt,name=auth_option,json=authOption,proto3" json:"auth_option,omitempty"`
+ HttpResponse *CustomHTTPResponse `protobuf:"bytes,2,opt,name=http_response,json=httpResponse,proto3" json:"http_response,omitempty"`
+}
+
+func (x *Options) Reset() {
+ *x = Options{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_zitadel_protoc_gen_zitadel_v2_options_proto_msgTypes[0]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *Options) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*Options) ProtoMessage() {}
+
+func (x *Options) ProtoReflect() protoreflect.Message {
+ mi := &file_zitadel_protoc_gen_zitadel_v2_options_proto_msgTypes[0]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use Options.ProtoReflect.Descriptor instead.
+func (*Options) Descriptor() ([]byte, []int) {
+ return file_zitadel_protoc_gen_zitadel_v2_options_proto_rawDescGZIP(), []int{0}
+}
+
+func (x *Options) GetAuthOption() *AuthOption {
+ if x != nil {
+ return x.AuthOption
+ }
+ return nil
+}
+
+func (x *Options) GetHttpResponse() *CustomHTTPResponse {
+ if x != nil {
+ return x.HttpResponse
+ }
+ return nil
+}
+
+type AuthOption struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ Permission string `protobuf:"bytes,1,opt,name=permission,proto3" json:"permission,omitempty"`
+ OrgField string `protobuf:"bytes,3,opt,name=org_field,json=orgField,proto3" json:"org_field,omitempty"`
+}
+
+func (x *AuthOption) Reset() {
+ *x = AuthOption{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_zitadel_protoc_gen_zitadel_v2_options_proto_msgTypes[1]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *AuthOption) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*AuthOption) ProtoMessage() {}
+
+func (x *AuthOption) ProtoReflect() protoreflect.Message {
+ mi := &file_zitadel_protoc_gen_zitadel_v2_options_proto_msgTypes[1]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use AuthOption.ProtoReflect.Descriptor instead.
+func (*AuthOption) Descriptor() ([]byte, []int) {
+ return file_zitadel_protoc_gen_zitadel_v2_options_proto_rawDescGZIP(), []int{1}
+}
+
+func (x *AuthOption) GetPermission() string {
+ if x != nil {
+ return x.Permission
+ }
+ return ""
+}
+
+func (x *AuthOption) GetOrgField() string {
+ if x != nil {
+ return x.OrgField
+ }
+ return ""
+}
+
+type CustomHTTPResponse struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ SuccessCode int32 `protobuf:"varint,1,opt,name=success_code,json=successCode,proto3" json:"success_code,omitempty"`
+}
+
+func (x *CustomHTTPResponse) Reset() {
+ *x = CustomHTTPResponse{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_zitadel_protoc_gen_zitadel_v2_options_proto_msgTypes[2]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *CustomHTTPResponse) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*CustomHTTPResponse) ProtoMessage() {}
+
+func (x *CustomHTTPResponse) ProtoReflect() protoreflect.Message {
+ mi := &file_zitadel_protoc_gen_zitadel_v2_options_proto_msgTypes[2]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use CustomHTTPResponse.ProtoReflect.Descriptor instead.
+func (*CustomHTTPResponse) Descriptor() ([]byte, []int) {
+ return file_zitadel_protoc_gen_zitadel_v2_options_proto_rawDescGZIP(), []int{2}
+}
+
+func (x *CustomHTTPResponse) GetSuccessCode() int32 {
+ if x != nil {
+ return x.SuccessCode
+ }
+ return 0
+}
+
+var file_zitadel_protoc_gen_zitadel_v2_options_proto_extTypes = []protoimpl.ExtensionInfo{
+ {
+ ExtendedType: (*descriptorpb.MethodOptions)(nil),
+ ExtensionType: (*Options)(nil),
+ Field: 50001,
+ Name: "zitadel.protoc_gen_zitadel.v2.options",
+ Tag: "bytes,50001,opt,name=options",
+ Filename: "zitadel/protoc_gen_zitadel/v2/options.proto",
+ },
+}
+
+// Extension fields to descriptorpb.MethodOptions.
+var (
+ // optional zitadel.protoc_gen_zitadel.v2.Options options = 50001;
+ E_Options = &file_zitadel_protoc_gen_zitadel_v2_options_proto_extTypes[0]
+)
+
+var File_zitadel_protoc_gen_zitadel_v2_options_proto protoreflect.FileDescriptor
+
+var file_zitadel_protoc_gen_zitadel_v2_options_proto_rawDesc = []byte{
+ 0x0a, 0x2b, 0x7a, 0x69, 0x74, 0x61, 0x64, 0x65, 0x6c, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63,
+ 0x5f, 0x67, 0x65, 0x6e, 0x5f, 0x7a, 0x69, 0x74, 0x61, 0x64, 0x65, 0x6c, 0x2f, 0x76, 0x32, 0x2f,
+ 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x1d, 0x7a,
+ 0x69, 0x74, 0x61, 0x64, 0x65, 0x6c, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x5f, 0x67, 0x65,
+ 0x6e, 0x5f, 0x7a, 0x69, 0x74, 0x61, 0x64, 0x65, 0x6c, 0x2e, 0x76, 0x32, 0x1a, 0x20, 0x67, 0x6f,
+ 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x64, 0x65,
+ 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x6f, 0x72, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xad,
+ 0x01, 0x0a, 0x07, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x4a, 0x0a, 0x0b, 0x61, 0x75,
+ 0x74, 0x68, 0x5f, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32,
+ 0x29, 0x2e, 0x7a, 0x69, 0x74, 0x61, 0x64, 0x65, 0x6c, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63,
+ 0x5f, 0x67, 0x65, 0x6e, 0x5f, 0x7a, 0x69, 0x74, 0x61, 0x64, 0x65, 0x6c, 0x2e, 0x76, 0x32, 0x2e,
+ 0x41, 0x75, 0x74, 0x68, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x0a, 0x61, 0x75, 0x74, 0x68,
+ 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x56, 0x0a, 0x0d, 0x68, 0x74, 0x74, 0x70, 0x5f, 0x72,
+ 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x31, 0x2e,
+ 0x7a, 0x69, 0x74, 0x61, 0x64, 0x65, 0x6c, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x5f, 0x67,
+ 0x65, 0x6e, 0x5f, 0x7a, 0x69, 0x74, 0x61, 0x64, 0x65, 0x6c, 0x2e, 0x76, 0x32, 0x2e, 0x43, 0x75,
+ 0x73, 0x74, 0x6f, 0x6d, 0x48, 0x54, 0x54, 0x50, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65,
+ 0x52, 0x0c, 0x68, 0x74, 0x74, 0x70, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x4f,
+ 0x0a, 0x0a, 0x41, 0x75, 0x74, 0x68, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x1e, 0x0a, 0x0a,
+ 0x70, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09,
+ 0x52, 0x0a, 0x70, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x1b, 0x0a, 0x09,
+ 0x6f, 0x72, 0x67, 0x5f, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52,
+ 0x08, 0x6f, 0x72, 0x67, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x4a, 0x04, 0x08, 0x02, 0x10, 0x03, 0x22,
+ 0x37, 0x0a, 0x12, 0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x48, 0x54, 0x54, 0x50, 0x52, 0x65, 0x73,
+ 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x21, 0x0a, 0x0c, 0x73, 0x75, 0x63, 0x63, 0x65, 0x73, 0x73,
+ 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0b, 0x73, 0x75, 0x63,
+ 0x63, 0x65, 0x73, 0x73, 0x43, 0x6f, 0x64, 0x65, 0x3a, 0x62, 0x0a, 0x07, 0x6f, 0x70, 0x74, 0x69,
+ 0x6f, 0x6e, 0x73, 0x12, 0x1e, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f,
+ 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x4f, 0x70, 0x74, 0x69,
+ 0x6f, 0x6e, 0x73, 0x18, 0xd1, 0x86, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x26, 0x2e, 0x7a, 0x69,
+ 0x74, 0x61, 0x64, 0x65, 0x6c, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x5f, 0x67, 0x65, 0x6e,
+ 0x5f, 0x7a, 0x69, 0x74, 0x61, 0x64, 0x65, 0x6c, 0x2e, 0x76, 0x32, 0x2e, 0x4f, 0x70, 0x74, 0x69,
+ 0x6f, 0x6e, 0x73, 0x52, 0x07, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x42, 0x36, 0x5a, 0x34,
+ 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x7a, 0x69, 0x74, 0x61, 0x64,
+ 0x65, 0x6c, 0x2f, 0x7a, 0x69, 0x74, 0x61, 0x64, 0x65, 0x6c, 0x2f, 0x70, 0x6b, 0x67, 0x2f, 0x67,
+ 0x72, 0x70, 0x63, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x2f, 0x76, 0x32, 0x3b, 0x70, 0x72,
+ 0x6f, 0x74, 0x6f, 0x63, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
+}
+
+var (
+ file_zitadel_protoc_gen_zitadel_v2_options_proto_rawDescOnce sync.Once
+ file_zitadel_protoc_gen_zitadel_v2_options_proto_rawDescData = file_zitadel_protoc_gen_zitadel_v2_options_proto_rawDesc
+)
+
+func file_zitadel_protoc_gen_zitadel_v2_options_proto_rawDescGZIP() []byte {
+ file_zitadel_protoc_gen_zitadel_v2_options_proto_rawDescOnce.Do(func() {
+ file_zitadel_protoc_gen_zitadel_v2_options_proto_rawDescData = protoimpl.X.CompressGZIP(file_zitadel_protoc_gen_zitadel_v2_options_proto_rawDescData)
+ })
+ return file_zitadel_protoc_gen_zitadel_v2_options_proto_rawDescData
+}
+
+var file_zitadel_protoc_gen_zitadel_v2_options_proto_msgTypes = make([]protoimpl.MessageInfo, 3)
+var file_zitadel_protoc_gen_zitadel_v2_options_proto_goTypes = []interface{}{
+ (*Options)(nil), // 0: zitadel.protoc_gen_zitadel.v2.Options
+ (*AuthOption)(nil), // 1: zitadel.protoc_gen_zitadel.v2.AuthOption
+ (*CustomHTTPResponse)(nil), // 2: zitadel.protoc_gen_zitadel.v2.CustomHTTPResponse
+ (*descriptorpb.MethodOptions)(nil), // 3: google.protobuf.MethodOptions
+}
+var file_zitadel_protoc_gen_zitadel_v2_options_proto_depIdxs = []int32{
+ 1, // 0: zitadel.protoc_gen_zitadel.v2.Options.auth_option:type_name -> zitadel.protoc_gen_zitadel.v2.AuthOption
+ 2, // 1: zitadel.protoc_gen_zitadel.v2.Options.http_response:type_name -> zitadel.protoc_gen_zitadel.v2.CustomHTTPResponse
+ 3, // 2: zitadel.protoc_gen_zitadel.v2.options:extendee -> google.protobuf.MethodOptions
+ 0, // 3: zitadel.protoc_gen_zitadel.v2.options:type_name -> zitadel.protoc_gen_zitadel.v2.Options
+ 4, // [4:4] is the sub-list for method output_type
+ 4, // [4:4] is the sub-list for method input_type
+ 3, // [3:4] is the sub-list for extension type_name
+ 2, // [2:3] is the sub-list for extension extendee
+ 0, // [0:2] is the sub-list for field type_name
+}
+
+func init() { file_zitadel_protoc_gen_zitadel_v2_options_proto_init() }
+func file_zitadel_protoc_gen_zitadel_v2_options_proto_init() {
+ if File_zitadel_protoc_gen_zitadel_v2_options_proto != nil {
+ return
+ }
+ if !protoimpl.UnsafeEnabled {
+ file_zitadel_protoc_gen_zitadel_v2_options_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*Options); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ file_zitadel_protoc_gen_zitadel_v2_options_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*AuthOption); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ file_zitadel_protoc_gen_zitadel_v2_options_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*CustomHTTPResponse); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ }
+ type x struct{}
+ out := protoimpl.TypeBuilder{
+ File: protoimpl.DescBuilder{
+ GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
+ RawDescriptor: file_zitadel_protoc_gen_zitadel_v2_options_proto_rawDesc,
+ NumEnums: 0,
+ NumMessages: 3,
+ NumExtensions: 1,
+ NumServices: 0,
+ },
+ GoTypes: file_zitadel_protoc_gen_zitadel_v2_options_proto_goTypes,
+ DependencyIndexes: file_zitadel_protoc_gen_zitadel_v2_options_proto_depIdxs,
+ MessageInfos: file_zitadel_protoc_gen_zitadel_v2_options_proto_msgTypes,
+ ExtensionInfos: file_zitadel_protoc_gen_zitadel_v2_options_proto_extTypes,
+ }.Build()
+ File_zitadel_protoc_gen_zitadel_v2_options_proto = out.File
+ file_zitadel_protoc_gen_zitadel_v2_options_proto_rawDesc = nil
+ file_zitadel_protoc_gen_zitadel_v2_options_proto_goTypes = nil
+ file_zitadel_protoc_gen_zitadel_v2_options_proto_depIdxs = nil
+}
diff --git a/proto/buf.yaml b/proto/buf.yaml
index 6069b76c7..483af8667 100644
--- a/proto/buf.yaml
+++ b/proto/buf.yaml
@@ -10,3 +10,28 @@ breaking:
lint:
use:
- MINIMAL
+ ignore:
+ - zitadel/action.proto
+ - zitadel/admin.proto
+ - zitadel/app.proto
+ - zitadel/auth_n_key.proto
+ - zitadel/auth.proto
+ - zitadel/change.proto
+ - zitadel/event.proto
+ - zitadel/idp.proto
+ - zitadel/instance.proto
+ - zitadel/management.proto
+ - zitadel/member.proto
+ - zitadel/message.proto
+ - zitadel/metadata.proto
+ - zitadel/object.proto
+ - zitadel/options.proto
+ - zitadel/org.proto
+ - zitadel/policy.proto
+ - zitadel/project.proto
+ - zitadel/quota.proto
+ - zitadel/settings.proto
+ - zitadel/system.proto
+ - zitadel/text.proto
+ - zitadel/user.proto
+ - zitadel/v1.proto
\ No newline at end of file
diff --git a/proto/zitadel/admin.proto b/proto/zitadel/admin.proto
index e50d3afb3..d59882770 100644
--- a/proto/zitadel/admin.proto
+++ b/proto/zitadel/admin.proto
@@ -5866,6 +5866,11 @@ message UpdateLoginPolicyRequest {
description: "defines if the user can additionally (to the login name) be identified by their verified phone number"
}
];
+ bool force_mfa_local_only = 17 [
+ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
+ description: "if activated, only local authenticated users are forced to use MFA. Authentication through IDPs won't prompt a MFA step in the login."
+ }
+ ];
}
message UpdateLoginPolicyResponse {
diff --git a/proto/zitadel/auth.proto b/proto/zitadel/auth.proto
index a8e0b38d2..2e291e13c 100644
--- a/proto/zitadel/auth.proto
+++ b/proto/zitadel/auth.proto
@@ -361,7 +361,7 @@ service AuthService {
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
tags: "User Profile";
summary: "Get My Profile";
- description: "Returns the profile information of the authenticated user, this includes first name, last name, etc."
+ description: "Returns the profile information of the authenticated user, this includes given name, family name, etc."
};
}
@@ -378,7 +378,7 @@ service AuthService {
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
tags: "User Profile";
summary: "Update My Profile";
- description: "Change the profile information of the authenticated user. This includes information like first name, last name, language, etc."
+ description: "Change the profile information of the authenticated user. This includes information like given name, family name, language, etc."
};
}
@@ -820,7 +820,7 @@ service AuthService {
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
tags: "User Authorizations/Grants"
- summary: "List My Authorizations/Grants";
+ summary: "List My Organizations";
description: "Returns a list of the organizations where the authenticated user has any authorizations/user grants. The request is made in the context of the requested project. This request can be used in multi-tenancy applications to show the user a tenant switcher."
};
}
@@ -852,7 +852,7 @@ service AuthService {
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
tags: "User Authorizations/Grants"
- summary: "List My Project Permissions";
+ summary: "List My Project Roles";
description: "Returns a list of roles for the authenticated user and for the requesting project (based on the token)."
};
}
@@ -869,8 +869,8 @@ service AuthService {
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
tags: "User Memberships"
- summary: "List My ZITADEL Permissions";
- description: "Show all the permissions my user has in ZITADEL (ZITADEL Manager)."
+ summary: "List My ZITADEL Manager Roles";
+ description: "Show all the management roles my user has in ZITADEL (ZITADEL Manager)."
};
}
diff --git a/proto/zitadel/management.proto b/proto/zitadel/management.proto
index b0d5bc97f..4f434e6fe 100644
--- a/proto/zitadel/management.proto
+++ b/proto/zitadel/management.proto
@@ -1629,7 +1629,7 @@ service ManagementService {
};
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
- summary: "Create Key for machine user";
+ summary: "Delete Key for machine user";
description: "Delete a specific key from a user. The user will not be able to authenticate with that key afterward."
tags: "Users";
tags: "User Machine";
@@ -7406,8 +7406,14 @@ message ImportHumanUserRequest {
description: "Use this to import hashed passwords from another system."
}
};
- string value = 1;
- string algorithm = 2;
+ string value = 1 [
+ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
+ example: "\"$2y$12$hXUrnqdq1RIIYZ2HPytIIe5lXdIvbhqrTvdPsSF7o.jFh817Z6lwm\"";
+ description: "Encoded hash of a password in Modular Crypt Format: https://passlib.readthedocs.io/en/stable/modular_crypt_format.html"
+ }
+ ];
+ reserved 2; // was algortithm, which is actually obtained from the encoded hash
+ reserved "algortithm";
}
message IDP {
string config_id = 1 [
@@ -8935,8 +8941,8 @@ message AddOIDCAppRequest {
];
repeated string additional_origins = 16 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
- example: "[\"https://console.zitadel.ch/auth/callback\"]";
- description: "Additional origins (other than the redirect_uris) from where the API can be used";
+ example: "[\"scheme://localhost:8080\"]";
+ description: "Additional origins (other than the redirect_uris) from where the API can be used, provided string has to be an origin (scheme://hostname[:port]) without path, query or fragment";
}
];
bool skip_native_app_success_page = 17 [
@@ -9110,8 +9116,8 @@ message UpdateOIDCAppConfigRequest {
];
repeated string additional_origins = 15 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
- example: "[\"https://console.zitadel.ch/auth/callback\"]";
- description: "Additional origins (other than the redirect_uris) from where the API can be used";
+ example: "[\"scheme://localhost:8080\"]";
+ description: "Additional origins (other than the redirect_uris) from where the API can be used, provided string has to be an origin (scheme://hostname[:port]) without path, query or fragment";
}
];
bool skip_native_app_success_page = 16 [
@@ -9648,6 +9654,11 @@ message AddCustomLoginPolicyRequest {
description: "defines if the user can additionally (to the login name) be identified by their verified phone number"
}
];
+ bool force_mfa_local_only = 20 [
+ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
+ description: "if activated, only local authenticated users are forced to use MFA. Authentication through IDPs won't prompt a MFA step in the login."
+ }
+ ];
}
message AddCustomLoginPolicyResponse {
@@ -9692,6 +9703,11 @@ message UpdateCustomLoginPolicyRequest {
description: "defines if the user can additionally (to the login name) be identified by their verified phone number"
}
];
+ bool force_mfa_local_only = 17 [
+ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
+ description: "if activated, only local authenticated users are forced to use MFA. Authentication through IDPs won't prompt a MFA step in the login."
+ }
+ ];
}
message UpdateCustomLoginPolicyResponse {
diff --git a/proto/zitadel/oidc/v2alpha/authorization.proto b/proto/zitadel/oidc/v2alpha/authorization.proto
new file mode 100644
index 000000000..69a14a3d0
--- /dev/null
+++ b/proto/zitadel/oidc/v2alpha/authorization.proto
@@ -0,0 +1,117 @@
+syntax = "proto3";
+
+package zitadel.oidc.v2alpha;
+
+import "google/protobuf/duration.proto";
+import "google/protobuf/timestamp.proto";
+import "protoc-gen-openapiv2/options/annotations.proto";
+
+option go_package = "github.com/zitadel/zitadel/pkg/grpc/oidc/v2alpha;oidc";
+
+message AuthRequest{
+ option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_schema) = {
+ external_docs: {
+ url: "https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest";
+ description: "Find out more about OIDC Auth Request parameters";
+ }
+ };
+
+ string id = 1 [
+ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
+ description: "ID of the authorization request";
+ }
+ ];
+
+ google.protobuf.Timestamp creation_date = 2 [
+ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
+ description: "Time when the auth request was created";
+ }
+ ];
+
+ string client_id = 3 [
+ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
+ description: "OIDC client ID of the application that created the auth request";
+ }
+ ];
+
+ repeated string scope = 4 [
+ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
+ description: "Requested scopes by the application, which the user must consent to.";
+ }
+ ];
+
+ string redirect_uri = 5 [
+ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
+ description: "Base URI that points back to the application";
+ }
+ ];
+
+ repeated Prompt prompt = 6 [
+ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
+ description: "Prompts that must be displayed to the user";
+ }
+ ];
+
+ repeated string ui_locales = 7 [
+ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
+ description: "End-User's preferred languages and scripts for the user interface, represented as a list of BCP47 [RFC5646] language tag values, ordered by preference. For instance, the value [fr-CA, fr, en] represents a preference for French as spoken in Canada, then French (without a region designation), followed by English (without a region designation). An error SHOULD NOT result if some or all of the requested locales are not supported.";
+ }
+ ];
+
+ optional string login_hint = 8 [
+ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
+ description: "Login hint can be set by the application with a user identifier such as an email or phone number.";
+ }
+ ];
+
+ optional google.protobuf.Duration max_age = 9 [
+ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
+ description: "Specifies the allowable elapsed time in seconds since the last time the End-User was actively authenticated. If the elapsed time is greater than this value, or the field is present with 0 duration, the user must be re-authenticated.";
+ }
+ ];
+
+ optional string hint_user_id = 10 [
+ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
+ description: "User ID taken from a ID Token Hint if it was present and valid.";
+ }
+ ];
+}
+
+enum Prompt {
+ PROMPT_UNSPECIFIED = 0;
+ PROMPT_NONE = 1;
+ PROMPT_LOGIN = 2;
+ PROMPT_CONSENT = 3;
+ PROMPT_SELECT_ACCOUNT = 4;
+ PROMPT_CREATE = 5;
+}
+
+message AuthorizationError {
+ ErrorReason error = 1;
+ optional string error_description = 2;
+ optional string error_uri = 3;
+}
+
+enum ErrorReason {
+ ERROR_REASON_UNSPECIFIED = 0;
+
+ // Error states from https://datatracker.ietf.org/doc/html/rfc6749#section-4.2.2.1
+ ERROR_REASON_INVALID_REQUEST = 1;
+ ERROR_REASON_UNAUTHORIZED_CLIENT = 2;
+ ERROR_REASON_ACCESS_DENIED = 3;
+ ERROR_REASON_UNSUPPORTED_RESPONSE_TYPE = 4;
+ ERROR_REASON_INVALID_SCOPE = 5;
+ ERROR_REASON_SERVER_ERROR = 6;
+ ERROR_REASON_TEMPORARY_UNAVAILABLE = 7;
+
+ // Error states from https://openid.net/specs/openid-connect-core-1_0.html#AuthError
+ ERROR_REASON_INTERACTION_REQUIRED = 8;
+ ERROR_REASON_LOGIN_REQUIRED = 9;
+ ERROR_REASON_ACCOUNT_SELECTION_REQUIRED = 10;
+ ERROR_REASON_CONSENT_REQUIRED = 11;
+ ERROR_REASON_INVALID_REQUEST_URI = 12;
+ ERROR_REASON_INVALID_REQUEST_OBJECT = 13;
+ ERROR_REASON_REQUEST_NOT_SUPPORTED = 14;
+ ERROR_REASON_REQUEST_URI_NOT_SUPPORTED = 15;
+ ERROR_REASON_REGISTRATION_NOT_SUPPORTED = 16;
+}
\ No newline at end of file
diff --git a/proto/zitadel/oidc/v2alpha/oidc_service.proto b/proto/zitadel/oidc/v2alpha/oidc_service.proto
new file mode 100644
index 000000000..94ba36413
--- /dev/null
+++ b/proto/zitadel/oidc/v2alpha/oidc_service.proto
@@ -0,0 +1,190 @@
+syntax = "proto3";
+
+package zitadel.oidc.v2alpha;
+
+import "zitadel/object/v2alpha/object.proto";
+import "zitadel/protoc_gen_zitadel/v2/options.proto";
+import "zitadel/oidc/v2alpha/authorization.proto";
+import "google/api/annotations.proto";
+import "google/api/field_behavior.proto";
+import "protoc-gen-openapiv2/options/annotations.proto";
+import "validate/validate.proto";
+
+option go_package = "github.com/zitadel/zitadel/pkg/grpc/oidc/v2alpha;oidc";
+
+option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = {
+ info: {
+ title: "OIDC Service";
+ version: "2.0-alpha";
+ description: "Get OIDC Auth Request details and create callback URLs. This project is in alpha state. It can AND will continue breaking until the services provide the same functionality as the current login.";
+ contact:{
+ name: "ZITADEL"
+ url: "https://zitadel.com"
+ email: "hi@zitadel.com"
+ }
+ license: {
+ name: "Apache 2.0",
+ url: "https://github.com/zitadel/zitadel/blob/main/LICENSE";
+ };
+ };
+ schemes: HTTPS;
+ schemes: HTTP;
+
+ consumes: "application/json";
+ consumes: "application/grpc";
+
+ produces: "application/json";
+ produces: "application/grpc";
+
+ consumes: "application/grpc-web+proto";
+ produces: "application/grpc-web+proto";
+
+ host: "$ZITADEL_DOMAIN";
+ base_path: "/";
+
+ external_docs: {
+ description: "Detailed information about ZITADEL",
+ url: "https://zitadel.com/docs"
+ }
+
+ responses: {
+ key: "403";
+ value: {
+ description: "Returned when the user does not have permission to access the resource.";
+ schema: {
+ json_schema: {
+ ref: "#/definitions/rpcStatus";
+ }
+ }
+ }
+ }
+ responses: {
+ key: "404";
+ value: {
+ description: "Returned when the resource does not exist.";
+ schema: {
+ json_schema: {
+ ref: "#/definitions/rpcStatus";
+ }
+ }
+ }
+ }
+};
+
+service OIDCService {
+ rpc GetAuthRequest (GetAuthRequestRequest) returns (GetAuthRequestResponse) {
+ option (google.api.http) = {
+ get: "/v2alpha/oidc/auth_requests/{auth_request_id}"
+ };
+
+ option (zitadel.protoc_gen_zitadel.v2.options) = {
+ auth_option: {
+ permission: "authenticated"
+ }
+ };
+
+ option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
+ summary: "Get OIDC Auth Request details";
+ description: "Get OIDC Auth Request details by ID, obtained from the redirect URL. Returns details that are parsed from the application's Auth Request."
+ responses: {
+ key: "200"
+ value: {
+ description: "OK";
+ }
+ };
+ };
+ }
+
+ rpc CreateCallback (CreateCallbackRequest) returns (CreateCallbackResponse) {
+ option (google.api.http) = {
+ post: "/v2alpha/oidc/auth_requests/{auth_request_id}"
+ body: "*"
+ };
+
+ option (zitadel.protoc_gen_zitadel.v2.options) = {
+ auth_option: {
+ permission: "authenticated"
+ }
+ };
+
+ option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
+ summary: "Finalize an Auth Request and get the callback URL.";
+ description: "Finalize an Auth Request and get the callback URL for success or failure. The user must be redirected to the URL in order to inform the application about the success or failure. On success, the URL contains details for the application to obtain the tokens. This method can only be called once for an Auth request."
+ responses: {
+ key: "200"
+ value: {
+ description: "OK";
+ }
+ };
+ };
+ }
+}
+
+message GetAuthRequestRequest {
+ string auth_request_id = 1 [
+ (validate.rules).string = {min_len: 1, max_len: 200},
+ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
+ min_length: 1;
+ max_length: 200;
+ description: "ID of the Auth Request, as obtained from the redirect URL.";
+ example: "\"163840776835432705\"";
+ }
+ ];
+}
+
+message GetAuthRequestResponse {
+ AuthRequest auth_request = 1;
+}
+
+message CreateCallbackRequest {
+ string auth_request_id = 1 [
+ (validate.rules).string = {min_len: 1, max_len: 200},
+ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
+ description: "Set this field when the authorization flow failed. It creates a callback URL to the application, with the error details set.";
+ ref: "https://openid.net/specs/openid-connect-core-1_0.html#AuthError";
+ }
+ ];
+
+ oneof callback_kind {
+ option (validate.required) = true;
+ Session session = 2;
+ AuthorizationError error = 3 [
+ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
+ description: "Set this field when the authorization flow failed. It creates a callback URL to the application, with the error details set.";
+ ref: "https://openid.net/specs/openid-connect-core-1_0.html#AuthError";
+ }
+ ];
+ }
+}
+
+message Session {
+ string session_id = 1 [
+ (validate.rules).string = {min_len: 1, max_len: 200},
+ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
+ min_length: 1;
+ max_length: 200;
+ description: "ID of the session, used to login the user. Connects the session to the Auth Request.";
+ example: "\"163840776835432705\"";
+ }
+ ];
+
+ string session_token = 2 [
+ (validate.rules).string = {min_len: 1, max_len: 200},
+ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
+ min_length: 1;
+ max_length: 200;
+ description: "Token to verify the session is valid";
+ }
+ ];
+}
+
+message CreateCallbackResponse {
+ zitadel.object.v2alpha.Details details = 1;
+ string callback_url = 2 [
+ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
+ description: "Callback URL where the user should be redirected, using a \"302 FOUND\" status. Contains details for the application to obtain the tokens on success, or error details on failure. Note that this field must be treated as credentials, as the contained code can be used to obtain tokens on behalve of the user.";
+ example: "\"https://client.example.org/cb?code=SplxlOBeZQQYbYS6WxSbIA&state=af0ifjsldkj\""
+ }
+ ];
+}
+
diff --git a/proto/zitadel/options.proto b/proto/zitadel/options.proto
index b71082858..e3dc45f2a 100644
--- a/proto/zitadel/options.proto
+++ b/proto/zitadel/options.proto
@@ -6,7 +6,6 @@ import "google/protobuf/descriptor.proto";
option go_package = "github.com/zitadel/zitadel/internal/protoc/protoc-gen-authoption/authoption";
-
extend google.protobuf.MethodOptions {
AuthOption auth_option = 50000;
}
diff --git a/proto/zitadel/policy.proto b/proto/zitadel/policy.proto
index c6cbc7a46..49dbe34dc 100644
--- a/proto/zitadel/policy.proto
+++ b/proto/zitadel/policy.proto
@@ -239,6 +239,11 @@ message LoginPolicy {
description: "defines if the user can additionally (to the login name) be identified by their verified phone number"
}
];
+ bool force_mfa_local_only = 22 [
+ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
+ description: "if activated, only local authenticated users are forced to use MFA. Authentication through IDPs won't prompt a MFA step in the login."
+ }
+ ];
}
enum SecondFactorType {
diff --git a/proto/zitadel/session/v2alpha/session.proto b/proto/zitadel/session/v2alpha/session.proto
index b3e11a443..e6e69ca50 100644
--- a/proto/zitadel/session/v2alpha/session.proto
+++ b/proto/zitadel/session/v2alpha/session.proto
@@ -74,6 +74,11 @@ message UserFactor {
description: "\"display name of the checked user\"";
}
];
+ string organisation_id = 5 [
+ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
+ description: "\"organisation id of the checked user\"";
+ }
+ ];
}
message PasswordFactor {
diff --git a/proto/zitadel/settings/v2alpha/login_settings.proto b/proto/zitadel/settings/v2alpha/login_settings.proto
index d84423c3c..9d4b37237 100644
--- a/proto/zitadel/settings/v2alpha/login_settings.proto
+++ b/proto/zitadel/settings/v2alpha/login_settings.proto
@@ -104,6 +104,11 @@ message LoginSettings {
description: "resource_owner_type returns if the settings is managed on the organization or on the instance";
}
];
+ bool force_mfa_local_only = 22 [
+ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
+ description: "if activated, only local authenticated users are forced to use MFA. Authentication through IDPs won't prompt a MFA step in the login."
+ }
+ ];
}
enum SecondFactorType {
diff --git a/proto/zitadel/user/v2alpha/password.proto b/proto/zitadel/user/v2alpha/password.proto
index 38089b0b0..cb5415b12 100644
--- a/proto/zitadel/user/v2alpha/password.proto
+++ b/proto/zitadel/user/v2alpha/password.proto
@@ -32,17 +32,7 @@ message HashedPassword {
max_length: 200;
}
];
- string algorithm = 2 [
- (validate.rules).string = {min_len: 1, max_len: 200, const: "bcrypt"},
- (google.api.field_behavior) = REQUIRED,
- (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
- example: "\"bcrypt\"";
- description: "\"algorithm used for the hash. currently only bcrypt is supported\"";
- min_length: 1,
- max_length: 200;
- }
- ];
- bool change_required = 3;
+ bool change_required = 2;
}
message SendPasswordResetLink {
diff --git a/proto/zitadel/user/v2alpha/user_service.proto b/proto/zitadel/user/v2alpha/user_service.proto
index 1534a4d74..50562bcf5 100644
--- a/proto/zitadel/user/v2alpha/user_service.proto
+++ b/proto/zitadel/user/v2alpha/user_service.proto
@@ -430,8 +430,8 @@ service UserService {
};
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
- summary: "Request a code to reset a password";
- description: "Request a code to reset a password";
+ summary: "Change password";
+ description: "Change the password of a user with either a verification code or the current password.";
responses: {
key: "200"
value: {
@@ -574,7 +574,7 @@ message RegisterPasskeyRequest{
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
min_length: 1;
max_length: 200;
- example: "\"d654e6ba-70a3-48ef-a95d-37c8d8a7901a\"";
+ example: "\"163840776835432705\"";
}
];
optional PasskeyRegistrationCode code = 2 [
@@ -598,7 +598,7 @@ message RegisterPasskeyResponse{
zitadel.object.v2alpha.Details details = 1;
string passkey_id = 2 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
- example: "\"fabde5c8-c13f-481d-a90b-5e59a001a076\""
+ example: "\"163840776835432705\""
}
];
google.protobuf.Struct public_key_credential_creation_options = 3 [
@@ -616,7 +616,7 @@ message VerifyPasskeyRegistrationRequest{
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
min_length: 1;
max_length: 200;
- example: "\"d654e6ba-70a3-48ef-a95d-37c8d8a7901a\"";
+ example: "\"163840776835432705\"";
}
];
string passkey_id = 2 [
@@ -625,7 +625,7 @@ message VerifyPasskeyRegistrationRequest{
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
min_length: 1;
max_length: 200;
- example: "\"fabde5c8-c13f-481d-a90b-5e59a001a076\"";
+ example: "\"163840776835432705\"";
}
];
google.protobuf.Struct public_key_credential = 3 [
@@ -660,7 +660,7 @@ message RegisterU2FRequest{
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
min_length: 1;
max_length: 200;
- example: "\"d654e6ba-70a3-48ef-a95d-37c8d8a7901a\"";
+ example: "\"163840776835432705\"";
}
];
string domain = 2 [
@@ -674,7 +674,7 @@ message RegisterU2FResponse{
zitadel.object.v2alpha.Details details = 1;
string u2f_id = 2 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
- example: "\"fabde5c8-c13f-481d-a90b-5e59a001a076\""
+ example: "\"163840776835432705\""
}
];
google.protobuf.Struct public_key_credential_creation_options = 3 [
@@ -692,7 +692,7 @@ message VerifyU2FRegistrationRequest{
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
min_length: 1;
max_length: 200;
- example: "\"d654e6ba-70a3-48ef-a95d-37c8d8a7901a\"";
+ example: "\"163840776835432705\"";
}
];
string u2f_id = 2 [
@@ -701,7 +701,7 @@ message VerifyU2FRegistrationRequest{
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
min_length: 1;
max_length: 200;
- example: "\"fabde5c8-c13f-481d-a90b-5e59a001a076\"";
+ example: "\"163840776835432705\"";
}
];
google.protobuf.Struct public_key_credential = 3 [
@@ -736,7 +736,7 @@ message RegisterTOTPRequest {
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
min_length: 1;
max_length: 200;
- example: "\"d654e6ba-70a3-48ef-a95d-37c8d8a7901a\"";
+ example: "\"163840776835432705\"";
}
];
}
@@ -762,7 +762,7 @@ message VerifyTOTPRegistrationRequest {
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
min_length: 1;
max_length: 200;
- example: "\"d654e6ba-70a3-48ef-a95d-37c8d8a7901a\"";
+ example: "\"163840776835432705\"";
}
];
string code = 2 [
@@ -786,7 +786,7 @@ message CreatePasskeyRegistrationLinkRequest{
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
min_length: 1;
max_length: 200;
- example: "\"d654e6ba-70a3-48ef-a95d-37c8d8a7901a\"";
+ example: "\"163840776835432705\"";
}
];
// if no medium is specified, an email is sent with the default url
@@ -813,7 +813,7 @@ message StartIdentityProviderFlowRequest{
description: "ID for existing identity provider"
min_length: 1;
max_length: 200;
- example: "\"d654e6ba-70a3-48ef-a95d-37c8d8a7901a\"";
+ example: "\"163840776835432705\"";
}
];
string success_url = 2 [
@@ -855,7 +855,7 @@ message RetrieveIdentityProviderInformationRequest{
description: "ID of the intent, previously returned on the success response of the IDP callback"
min_length: 1;
max_length: 200;
- example: "\"d654e6ba-70a3-48ef-a95d-37c8d8a7901a\"";
+ example: "\"163840776835432705\"";
}
];
string token = 2 [
diff --git a/yarn.lock b/yarn.lock
deleted file mode 100644
index fb57ccd13..000000000
--- a/yarn.lock
+++ /dev/null
@@ -1,4 +0,0 @@
-# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
-# yarn lockfile v1
-
-
|