From 314379da0f53c44c7b504aea6ca232c44e84ffc8 Mon Sep 17 00:00:00 2001 From: Thomas Faust Date: Sat, 15 Mar 2025 18:18:05 +0100 Subject: [PATCH 01/96] feat(zitadel-proto): add CJS and ESM support; export in Zitadel api structure; add build configuration --- packages/zitadel-proto/README.md | 83 +++++++++++++++++++++--- packages/zitadel-proto/index.ts | 5 ++ packages/zitadel-proto/package.json | 28 ++++++-- packages/zitadel-proto/test/cjs-test.cjs | 28 ++++++++ packages/zitadel-proto/test/esm-test.mjs | 28 ++++++++ packages/zitadel-proto/tsup.config.ts | 13 ++++ packages/zitadel-proto/v1.ts | 53 +++++++++++++++ packages/zitadel-proto/v2.ts | 49 ++++++++++++++ packages/zitadel-proto/v3alpha.ts | 9 +++ 9 files changed, 282 insertions(+), 14 deletions(-) create mode 100644 packages/zitadel-proto/index.ts create mode 100644 packages/zitadel-proto/test/cjs-test.cjs create mode 100644 packages/zitadel-proto/test/esm-test.mjs create mode 100644 packages/zitadel-proto/tsup.config.ts create mode 100644 packages/zitadel-proto/v1.ts create mode 100644 packages/zitadel-proto/v2.ts create mode 100644 packages/zitadel-proto/v3alpha.ts diff --git a/packages/zitadel-proto/README.md b/packages/zitadel-proto/README.md index bf8a064c12..32bf3236c7 100644 --- a/packages/zitadel-proto/README.md +++ b/packages/zitadel-proto/README.md @@ -8,22 +8,87 @@ To install the package, use npm or yarn: ```sh npm install @zitadel/proto -``` - -or - -```sh +# or yarn add @zitadel/proto +# or +pnpm add @zitadel/proto ``` ## Usage -To use the proto definitions in your project, import the generated code: +This package supports both ESM and CommonJS imports. The API is organized into version-specific namespaces: `v1`, `v2`, and `v3alpha`. -```ts -import { Organization } from "@zitadel/proto/zitadel/org/v2/org_pb"; +### ESM (ECMAScript Modules) -const org: Organization | null = await getDefaultOrg(); +```typescript +// Import the entire package +import * as zitadel from "@zitadel/proto"; + +// Use the version-specific namespaces +const userRequest = new zitadel.v1.user.GetUserRequest(); + +// Or import specific versions +import { v2 } from "@zitadel/proto"; +const userServiceRequest = new v2.user_service.GetUserRequest(); +``` + +### CommonJS + +```typescript +// Import the entire package +const zitadel = require("@zitadel/proto"); + +// Use the version-specific namespaces +const userRequest = new zitadel.v1.user.GetUserRequest(); +``` + +## API Structure + +The package is organized into version-specific namespaces: + +- `v1`: Contains the original ZITADEL API +- `v2`: Contains the newer version of the API with improved organization +- `v3alpha`: Contains the alpha version of the upcoming API + +## Package Structure + +The package is organized as follows: + +- `index.ts`: Main entry point that exports the version-specific APIs +- `v1.ts`: Exports all v1 API modules +- `v2.ts`: Exports all v2 API modules +- `v3alpha.ts`: Exports all v3alpha API modules +- `zitadel/`: Contains the generated proto files + +## Development + +### Generating the proto files + +The proto files are generated from the ZITADEL API definitions using [buf](https://buf.build/). + +```sh +pnpm generate +``` + +### Building the package + +```sh +pnpm build +``` + +### Testing + +To test both ESM and CommonJS imports: + +```sh +pnpm test +``` + +Or test them individually: + +```bash +pnpm test:cjs # Test CommonJS imports +pnpm test:esm # Test ESM imports ``` ## Documentation diff --git a/packages/zitadel-proto/index.ts b/packages/zitadel-proto/index.ts new file mode 100644 index 0000000000..6b7b927f11 --- /dev/null +++ b/packages/zitadel-proto/index.ts @@ -0,0 +1,5 @@ +import * as v1 from "./v1.js"; +import * as v2 from "./v2.js"; +import * as v3alpha from "./v3alpha.js"; + +export { v1, v2, v3alpha }; diff --git a/packages/zitadel-proto/package.json b/packages/zitadel-proto/package.json index dcfa9b3f22..5a1b2fee1d 100644 --- a/packages/zitadel-proto/package.json +++ b/packages/zitadel-proto/package.json @@ -5,22 +5,40 @@ "publishConfig": { "access": "public" }, - "type": "module", + "main": "./dist/index.js", + "module": "./dist/index.mjs", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.mjs", + "require": "./dist/index.js" + } + }, "files": [ + "index.ts", + "v1.ts", + "v2.ts", + "v3alpha.ts", "zitadel/**", "validate/**", "google/**", - "protoc-gen-openapiv2/**" + "dist/**" ], "sideEffects": false, "scripts": { "generate": "buf generate https://github.com/zitadel/zitadel.git#tag=v2.71.1 --path ./proto/zitadel", - "clean": "rm -rf zitadel .turbo node_modules google protoc-gen-openapiv2 validate" + "clean": "rm -rf zitadel .turbo node_modules google protoc-gen-openapiv2 validate", + "build": "tsup", + "test:cjs": "node test/cjs-test.cjs", + "test:esm": "node test/esm-test.mjs", + "test": "pnpm build && pnpm test:cjs && pnpm test:esm" }, "dependencies": { "@bufbuild/protobuf": "^2.2.2" }, "devDependencies": { - "@bufbuild/buf": "^1.47.2" + "@bufbuild/buf": "^1.47.2", + "tsup": "^8.0.0" } -} +} \ No newline at end of file diff --git a/packages/zitadel-proto/test/cjs-test.cjs b/packages/zitadel-proto/test/cjs-test.cjs new file mode 100644 index 0000000000..87033cfd3b --- /dev/null +++ b/packages/zitadel-proto/test/cjs-test.cjs @@ -0,0 +1,28 @@ +// CommonJS import test +const zitadel = require("@zitadel/proto"); + +// Check if the import worked by accessing some properties +console.log("CommonJS import test:"); +console.log("- Has v1 API:", !!zitadel.v1); +console.log("- Has v2 API:", !!zitadel.v2); +console.log("- Has v3alpha API:", !!zitadel.v3alpha); + +// Test v1 API +console.log("- v1.user module:", !!zitadel.v1.user); +console.log("- v1.management module:", !!zitadel.v1.management); + +// Test v2 API +console.log("- v2.user module:", !!zitadel.v2.user); +console.log("- v2.user_service module:", !!zitadel.v2.user_service); + +// Test v3alpha API +console.log("- v3alpha.user module:", !!zitadel.v3alpha.user); +console.log("- v3alpha.user_service module:", !!zitadel.v3alpha.user_service); + +// Test successful if we can access these modules +if (zitadel.v1 && zitadel.v2 && zitadel.v3alpha) { + console.log("✅ CommonJS import test passed!"); +} else { + console.error("❌ CommonJS import test failed!"); + process.exit(1); +} diff --git a/packages/zitadel-proto/test/esm-test.mjs b/packages/zitadel-proto/test/esm-test.mjs new file mode 100644 index 0000000000..aafaf01df3 --- /dev/null +++ b/packages/zitadel-proto/test/esm-test.mjs @@ -0,0 +1,28 @@ +// ESM import test +import * as zitadel from "@zitadel/proto"; + +// Check if the import worked by accessing some properties +console.log("ESM import test:"); +console.log("- Has v1 API:", !!zitadel.v1); +console.log("- Has v2 API:", !!zitadel.v2); +console.log("- Has v3alpha API:", !!zitadel.v3alpha); + +// Test v1 API +console.log("- v1.user module:", !!zitadel.v1.user); +console.log("- v1.management module:", !!zitadel.v1.management); + +// Test v2 API +console.log("- v2.user module:", !!zitadel.v2.user); +console.log("- v2.user_service module:", !!zitadel.v2.user_service); + +// Test v3alpha API +console.log("- v3alpha.user module:", !!zitadel.v3alpha.user); +console.log("- v3alpha.user_service module:", !!zitadel.v3alpha.user_service); + +// Test successful if we can access these modules +if (zitadel.v1 && zitadel.v2 && zitadel.v3alpha) { + console.log("✅ ESM import test passed!"); +} else { + console.error("❌ ESM import test failed!"); + process.exit(1); +} diff --git a/packages/zitadel-proto/tsup.config.ts b/packages/zitadel-proto/tsup.config.ts new file mode 100644 index 0000000000..1fa9c664a7 --- /dev/null +++ b/packages/zitadel-proto/tsup.config.ts @@ -0,0 +1,13 @@ +import { defineConfig } from "tsup"; + +export default defineConfig({ + entry: ["index.ts"], + dts: true, + clean: true, + minify: false, + splitting: false, + sourcemap: true, + format: ["esm", "cjs"], + platform: "neutral", + target: "node16", +}); diff --git a/packages/zitadel-proto/v1.ts b/packages/zitadel-proto/v1.ts new file mode 100644 index 0000000000..bea11ca384 --- /dev/null +++ b/packages/zitadel-proto/v1.ts @@ -0,0 +1,53 @@ +import * as action from "./zitadel/action_pb.js"; +import * as admin from "./zitadel/admin_pb.js"; +import * as app from "./zitadel/app_pb.js"; +import * as auth_n_key from "./zitadel/auth_n_key_pb.js"; +import * as auth from "./zitadel/auth_pb.js"; +import * as change from "./zitadel/change_pb.js"; +import * as event from "./zitadel/event_pb.js"; +import * as feature from "./zitadel/feature_pb.js"; +import * as idp from "./zitadel/idp_pb.js"; +import * as instance from "./zitadel/instance_pb.js"; +import * as management from "./zitadel/management_pb.js"; +import * as member from "./zitadel/member_pb.js"; +import * as message from "./zitadel/message_pb.js"; +import * as metadata from "./zitadel/metadata_pb.js"; +import * as object from "./zitadel/object_pb.js"; +import * as options from "./zitadel/options_pb.js"; +import * as org from "./zitadel/org_pb.js"; +import * as policy from "./zitadel/policy_pb.js"; +import * as project from "./zitadel/project_pb.js"; +import * as quota from "./zitadel/quota_pb.js"; +import * as settings from "./zitadel/settings_pb.js"; +import * as system from "./zitadel/system_pb.js"; +import * as text from "./zitadel/text_pb.js"; +import * as user from "./zitadel/user_pb.js"; +import * as v1 from "./zitadel/v1_pb.js"; + +export { + action, + admin, + app, + auth, + auth_n_key, + change, + event, + feature, + idp, + instance, + management, + member, + message, + metadata, + object, + options, + org, + policy, + project, + quota, + settings, + system, + text, + user, + v1, +}; diff --git a/packages/zitadel-proto/v2.ts b/packages/zitadel-proto/v2.ts new file mode 100644 index 0000000000..532db0d82e --- /dev/null +++ b/packages/zitadel-proto/v2.ts @@ -0,0 +1,49 @@ +import * as feature from "./zitadel/feature/v2/feature_pb.js"; +import * as feature_service from "./zitadel/feature/v2/feature_service_pb.js"; +import * as idp from "./zitadel/idp/v2/idp_pb.js"; +import * as idp_service from "./zitadel/idp/v2/idp_service_pb.js"; +import * as object from "./zitadel/object/v2/object_pb.js"; +import * as oidc_authorization from "./zitadel/oidc/v2/authorization_pb.js"; +import * as oidc_service from "./zitadel/oidc/v2/oidc_service_pb.js"; +import * as org from "./zitadel/org/v2/org_pb.js"; +import * as org_service from "./zitadel/org/v2/org_service_pb.js"; +import * as saml_authorization from "./zitadel/saml/v2/authorization_pb.js"; +import * as saml_service from "./zitadel/saml/v2/saml_service_pb.js"; +import * as session from "./zitadel/session/v2/session_pb.js"; +import * as session_service from "./zitadel/session/v2/session_service_pb.js"; +import * as settings from "./zitadel/settings/v2/settings_pb.js"; +import * as settings_service from "./zitadel/settings/v2/settings_service_pb.js"; +import * as user_auth from "./zitadel/user/v2/auth_pb.js"; +import * as user_email from "./zitadel/user/v2/email_pb.js"; +import * as user_idp from "./zitadel/user/v2/idp_pb.js"; +import * as user_password from "./zitadel/user/v2/password_pb.js"; +import * as user_phone from "./zitadel/user/v2/phone_pb.js"; +import * as user_query from "./zitadel/user/v2/query_pb.js"; +import * as user from "./zitadel/user/v2/user_pb.js"; +import * as user_service from "./zitadel/user/v2/user_service_pb.js"; + +export { + feature, + feature_service, + idp, + idp_service, + object, + oidc_authorization, + oidc_service, + org, + org_service, + saml_authorization, + saml_service, + session, + session_service, + settings, + settings_service, + user, + user_auth, + user_email, + user_idp, + user_password, + user_phone, + user_query, + user_service, +}; diff --git a/packages/zitadel-proto/v3alpha.ts b/packages/zitadel-proto/v3alpha.ts new file mode 100644 index 0000000000..e6b2c33464 --- /dev/null +++ b/packages/zitadel-proto/v3alpha.ts @@ -0,0 +1,9 @@ +import * as user_authenticator from "./zitadel/resources/user/v3alpha/authenticator_pb.js"; +import * as user_communication from "./zitadel/resources/user/v3alpha/communication_pb.js"; +import * as user_query from "./zitadel/resources/user/v3alpha/query_pb.js"; +import * as user from "./zitadel/resources/user/v3alpha/user_pb.js"; +import * as user_service from "./zitadel/resources/user/v3alpha/user_service_pb.js"; +import * as user_schema from "./zitadel/resources/userschema/v3alpha/user_schema_pb.js"; +import * as user_schema_service from "./zitadel/resources/userschema/v3alpha/user_schema_service_pb.js"; + +export { user, user_authenticator, user_communication, user_query, user_schema, user_schema_service, user_service }; From 2c1939744b3b7d58ff97ee6eb0fee37f342f039c Mon Sep 17 00:00:00 2001 From: Thomas Faust Date: Sun, 16 Mar 2025 07:16:53 +0100 Subject: [PATCH 02/96] refactor(zitadel-proto): restructure package files and update entry points --- packages/zitadel-proto/package.json | 7 +-- packages/zitadel-proto/{ => src}/index.ts | 0 packages/zitadel-proto/src/v1.ts | 53 +++++++++++++++++++++++ packages/zitadel-proto/src/v2.ts | 49 +++++++++++++++++++++ packages/zitadel-proto/src/v3alpha.ts | 9 ++++ packages/zitadel-proto/tsup.config.ts | 3 +- packages/zitadel-proto/v1.ts | 53 ----------------------- packages/zitadel-proto/v2.ts | 49 --------------------- packages/zitadel-proto/v3alpha.ts | 9 ---- 9 files changed, 115 insertions(+), 117 deletions(-) rename packages/zitadel-proto/{ => src}/index.ts (100%) create mode 100644 packages/zitadel-proto/src/v1.ts create mode 100644 packages/zitadel-proto/src/v2.ts create mode 100644 packages/zitadel-proto/src/v3alpha.ts delete mode 100644 packages/zitadel-proto/v1.ts delete mode 100644 packages/zitadel-proto/v2.ts delete mode 100644 packages/zitadel-proto/v3alpha.ts diff --git a/packages/zitadel-proto/package.json b/packages/zitadel-proto/package.json index 5a1b2fee1d..cc3d37fdb6 100644 --- a/packages/zitadel-proto/package.json +++ b/packages/zitadel-proto/package.json @@ -16,10 +16,7 @@ } }, "files": [ - "index.ts", - "v1.ts", - "v2.ts", - "v3alpha.ts", + "src/**", "zitadel/**", "validate/**", "google/**", @@ -32,7 +29,7 @@ "build": "tsup", "test:cjs": "node test/cjs-test.cjs", "test:esm": "node test/esm-test.mjs", - "test": "pnpm build && pnpm test:cjs && pnpm test:esm" + "test": "pnpm test:cjs && pnpm test:esm" }, "dependencies": { "@bufbuild/protobuf": "^2.2.2" diff --git a/packages/zitadel-proto/index.ts b/packages/zitadel-proto/src/index.ts similarity index 100% rename from packages/zitadel-proto/index.ts rename to packages/zitadel-proto/src/index.ts diff --git a/packages/zitadel-proto/src/v1.ts b/packages/zitadel-proto/src/v1.ts new file mode 100644 index 0000000000..887df5ceb9 --- /dev/null +++ b/packages/zitadel-proto/src/v1.ts @@ -0,0 +1,53 @@ +import * as action from "../zitadel/action_pb.js"; +import * as admin from "../zitadel/admin_pb.js"; +import * as app from "../zitadel/app_pb.js"; +import * as auth_n_key from "../zitadel/auth_n_key_pb.js"; +import * as auth from "../zitadel/auth_pb.js"; +import * as change from "../zitadel/change_pb.js"; +import * as event from "../zitadel/event_pb.js"; +import * as feature from "../zitadel/feature_pb.js"; +import * as idp from "../zitadel/idp_pb.js"; +import * as instance from "../zitadel/instance_pb.js"; +import * as management from "../zitadel/management_pb.js"; +import * as member from "../zitadel/member_pb.js"; +import * as message from "../zitadel/message_pb.js"; +import * as metadata from "../zitadel/metadata_pb.js"; +import * as object from "../zitadel/object_pb.js"; +import * as options from "../zitadel/options_pb.js"; +import * as org from "../zitadel/org_pb.js"; +import * as policy from "../zitadel/policy_pb.js"; +import * as project from "../zitadel/project_pb.js"; +import * as quota from "../zitadel/quota_pb.js"; +import * as settings from "../zitadel/settings_pb.js"; +import * as system from "../zitadel/system_pb.js"; +import * as text from "../zitadel/text_pb.js"; +import * as user from "../zitadel/user_pb.js"; +import * as v1 from "../zitadel/v1_pb.js"; + +export { + action, + admin, + app, + auth, + auth_n_key, + change, + event, + feature, + idp, + instance, + management, + member, + message, + metadata, + object, + options, + org, + policy, + project, + quota, + settings, + system, + text, + user, + v1, +}; diff --git a/packages/zitadel-proto/src/v2.ts b/packages/zitadel-proto/src/v2.ts new file mode 100644 index 0000000000..e2167c4581 --- /dev/null +++ b/packages/zitadel-proto/src/v2.ts @@ -0,0 +1,49 @@ +import * as feature from "../zitadel/feature/v2/feature_pb.js"; +import * as feature_service from "../zitadel/feature/v2/feature_service_pb.js"; +import * as idp from "../zitadel/idp/v2/idp_pb.js"; +import * as idp_service from "../zitadel/idp/v2/idp_service_pb.js"; +import * as object from "../zitadel/object/v2/object_pb.js"; +import * as oidc_authorization from "../zitadel/oidc/v2/authorization_pb.js"; +import * as oidc_service from "../zitadel/oidc/v2/oidc_service_pb.js"; +import * as org from "../zitadel/org/v2/org_pb.js"; +import * as org_service from "../zitadel/org/v2/org_service_pb.js"; +import * as saml_authorization from "../zitadel/saml/v2/authorization_pb.js"; +import * as saml_service from "../zitadel/saml/v2/saml_service_pb.js"; +import * as session from "../zitadel/session/v2/session_pb.js"; +import * as session_service from "../zitadel/session/v2/session_service_pb.js"; +import * as settings from "../zitadel/settings/v2/settings_pb.js"; +import * as settings_service from "../zitadel/settings/v2/settings_service_pb.js"; +import * as user_auth from "../zitadel/user/v2/auth_pb.js"; +import * as user_email from "../zitadel/user/v2/email_pb.js"; +import * as user_idp from "../zitadel/user/v2/idp_pb.js"; +import * as user_password from "../zitadel/user/v2/password_pb.js"; +import * as user_phone from "../zitadel/user/v2/phone_pb.js"; +import * as user_query from "../zitadel/user/v2/query_pb.js"; +import * as user from "../zitadel/user/v2/user_pb.js"; +import * as user_service from "../zitadel/user/v2/user_service_pb.js"; + +export { + feature, + feature_service, + idp, + idp_service, + object, + oidc_authorization, + oidc_service, + org, + org_service, + saml_authorization, + saml_service, + session, + session_service, + settings, + settings_service, + user, + user_auth, + user_email, + user_idp, + user_password, + user_phone, + user_query, + user_service, +}; diff --git a/packages/zitadel-proto/src/v3alpha.ts b/packages/zitadel-proto/src/v3alpha.ts new file mode 100644 index 0000000000..9d787bdc5b --- /dev/null +++ b/packages/zitadel-proto/src/v3alpha.ts @@ -0,0 +1,9 @@ +import * as user_authenticator from "../zitadel/resources/user/v3alpha/authenticator_pb.js"; +import * as user_communication from "../zitadel/resources/user/v3alpha/communication_pb.js"; +import * as user_query from "../zitadel/resources/user/v3alpha/query_pb.js"; +import * as user from "../zitadel/resources/user/v3alpha/user_pb.js"; +import * as user_service from "../zitadel/resources/user/v3alpha/user_service_pb.js"; +import * as user_schema from "../zitadel/resources/userschema/v3alpha/user_schema_pb.js"; +import * as user_schema_service from "../zitadel/resources/userschema/v3alpha/user_schema_service_pb.js"; + +export { user, user_authenticator, user_communication, user_query, user_schema, user_schema_service, user_service }; diff --git a/packages/zitadel-proto/tsup.config.ts b/packages/zitadel-proto/tsup.config.ts index 1fa9c664a7..13a78084c6 100644 --- a/packages/zitadel-proto/tsup.config.ts +++ b/packages/zitadel-proto/tsup.config.ts @@ -1,11 +1,12 @@ import { defineConfig } from "tsup"; export default defineConfig({ - entry: ["index.ts"], + entry: ["src/index.ts", "src/v1.ts", "src/v2.ts", "src/v3alpha.ts"], dts: true, clean: true, minify: false, splitting: false, + treeshake: false, sourcemap: true, format: ["esm", "cjs"], platform: "neutral", diff --git a/packages/zitadel-proto/v1.ts b/packages/zitadel-proto/v1.ts deleted file mode 100644 index bea11ca384..0000000000 --- a/packages/zitadel-proto/v1.ts +++ /dev/null @@ -1,53 +0,0 @@ -import * as action from "./zitadel/action_pb.js"; -import * as admin from "./zitadel/admin_pb.js"; -import * as app from "./zitadel/app_pb.js"; -import * as auth_n_key from "./zitadel/auth_n_key_pb.js"; -import * as auth from "./zitadel/auth_pb.js"; -import * as change from "./zitadel/change_pb.js"; -import * as event from "./zitadel/event_pb.js"; -import * as feature from "./zitadel/feature_pb.js"; -import * as idp from "./zitadel/idp_pb.js"; -import * as instance from "./zitadel/instance_pb.js"; -import * as management from "./zitadel/management_pb.js"; -import * as member from "./zitadel/member_pb.js"; -import * as message from "./zitadel/message_pb.js"; -import * as metadata from "./zitadel/metadata_pb.js"; -import * as object from "./zitadel/object_pb.js"; -import * as options from "./zitadel/options_pb.js"; -import * as org from "./zitadel/org_pb.js"; -import * as policy from "./zitadel/policy_pb.js"; -import * as project from "./zitadel/project_pb.js"; -import * as quota from "./zitadel/quota_pb.js"; -import * as settings from "./zitadel/settings_pb.js"; -import * as system from "./zitadel/system_pb.js"; -import * as text from "./zitadel/text_pb.js"; -import * as user from "./zitadel/user_pb.js"; -import * as v1 from "./zitadel/v1_pb.js"; - -export { - action, - admin, - app, - auth, - auth_n_key, - change, - event, - feature, - idp, - instance, - management, - member, - message, - metadata, - object, - options, - org, - policy, - project, - quota, - settings, - system, - text, - user, - v1, -}; diff --git a/packages/zitadel-proto/v2.ts b/packages/zitadel-proto/v2.ts deleted file mode 100644 index 532db0d82e..0000000000 --- a/packages/zitadel-proto/v2.ts +++ /dev/null @@ -1,49 +0,0 @@ -import * as feature from "./zitadel/feature/v2/feature_pb.js"; -import * as feature_service from "./zitadel/feature/v2/feature_service_pb.js"; -import * as idp from "./zitadel/idp/v2/idp_pb.js"; -import * as idp_service from "./zitadel/idp/v2/idp_service_pb.js"; -import * as object from "./zitadel/object/v2/object_pb.js"; -import * as oidc_authorization from "./zitadel/oidc/v2/authorization_pb.js"; -import * as oidc_service from "./zitadel/oidc/v2/oidc_service_pb.js"; -import * as org from "./zitadel/org/v2/org_pb.js"; -import * as org_service from "./zitadel/org/v2/org_service_pb.js"; -import * as saml_authorization from "./zitadel/saml/v2/authorization_pb.js"; -import * as saml_service from "./zitadel/saml/v2/saml_service_pb.js"; -import * as session from "./zitadel/session/v2/session_pb.js"; -import * as session_service from "./zitadel/session/v2/session_service_pb.js"; -import * as settings from "./zitadel/settings/v2/settings_pb.js"; -import * as settings_service from "./zitadel/settings/v2/settings_service_pb.js"; -import * as user_auth from "./zitadel/user/v2/auth_pb.js"; -import * as user_email from "./zitadel/user/v2/email_pb.js"; -import * as user_idp from "./zitadel/user/v2/idp_pb.js"; -import * as user_password from "./zitadel/user/v2/password_pb.js"; -import * as user_phone from "./zitadel/user/v2/phone_pb.js"; -import * as user_query from "./zitadel/user/v2/query_pb.js"; -import * as user from "./zitadel/user/v2/user_pb.js"; -import * as user_service from "./zitadel/user/v2/user_service_pb.js"; - -export { - feature, - feature_service, - idp, - idp_service, - object, - oidc_authorization, - oidc_service, - org, - org_service, - saml_authorization, - saml_service, - session, - session_service, - settings, - settings_service, - user, - user_auth, - user_email, - user_idp, - user_password, - user_phone, - user_query, - user_service, -}; diff --git a/packages/zitadel-proto/v3alpha.ts b/packages/zitadel-proto/v3alpha.ts deleted file mode 100644 index e6b2c33464..0000000000 --- a/packages/zitadel-proto/v3alpha.ts +++ /dev/null @@ -1,9 +0,0 @@ -import * as user_authenticator from "./zitadel/resources/user/v3alpha/authenticator_pb.js"; -import * as user_communication from "./zitadel/resources/user/v3alpha/communication_pb.js"; -import * as user_query from "./zitadel/resources/user/v3alpha/query_pb.js"; -import * as user from "./zitadel/resources/user/v3alpha/user_pb.js"; -import * as user_service from "./zitadel/resources/user/v3alpha/user_service_pb.js"; -import * as user_schema from "./zitadel/resources/userschema/v3alpha/user_schema_pb.js"; -import * as user_schema_service from "./zitadel/resources/userschema/v3alpha/user_schema_service_pb.js"; - -export { user, user_authenticator, user_communication, user_query, user_schema, user_schema_service, user_service }; From eae0ce5680c1781721e1d08649e502ee20292aa1 Mon Sep 17 00:00:00 2001 From: Thomas Faust Date: Thu, 20 Mar 2025 07:22:43 +0100 Subject: [PATCH 03/96] fix(zitadel-proto): make build dependent on generate --- packages/zitadel-proto/turbo.json | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/packages/zitadel-proto/turbo.json b/packages/zitadel-proto/turbo.json index bffd614f62..86d4f1e86c 100644 --- a/packages/zitadel-proto/turbo.json +++ b/packages/zitadel-proto/turbo.json @@ -1,9 +1,22 @@ { - "extends": ["//"], + "extends": [ + "//" + ], "tasks": { + "build": { + "dependsOn": [ + "generate" + ], + "outputs": [ + "dist/**" + ], + "cache": false + }, "generate": { - "outputs": ["zitadel/**"], + "outputs": [ + "zitadel/**" + ], "cache": true } } -} +} \ No newline at end of file From fbef66800ea6a32e82050dc72c7859e2e59eb1f8 Mon Sep 17 00:00:00 2001 From: Thomas Faust Date: Thu, 20 Mar 2025 07:23:57 +0100 Subject: [PATCH 04/96] feat(zitadel-proto): export api versions and zitadel *.{js,d.ts} for legacy imports --- packages/zitadel-proto/package.json | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/packages/zitadel-proto/package.json b/packages/zitadel-proto/package.json index cc3d37fdb6..95ddccdc09 100644 --- a/packages/zitadel-proto/package.json +++ b/packages/zitadel-proto/package.json @@ -13,6 +13,31 @@ "types": "./dist/index.d.ts", "import": "./dist/index.mjs", "require": "./dist/index.js" + }, + "./v1": { + "types": "./dist/v1.d.ts", + "import": "./dist/v1.mjs", + "require": "./dist/v1.js" + }, + "./v2": { + "types": "./dist/v2.d.ts", + "import": "./dist/v2.mjs", + "require": "./dist/v2.js" + }, + "./v3alpha": { + "types": "./dist/v3alpha.d.ts", + "import": "./dist/v3alpha.mjs", + "require": "./dist/v3alpha.js" + }, + "./zitadel/*": { + "types": "./zitadel/*.d.ts", + "import": "./zitadel/*.js", + "require": "./zitadel/*.js" + }, + "./zitadel/*.js": { + "types": "./zitadel/*.d.ts", + "import": "./zitadel/*.js", + "require": "./zitadel/*.js" } }, "files": [ From 347b41823881899c8148c724f952d6c750970720 Mon Sep 17 00:00:00 2001 From: Thomas Faust Date: Thu, 20 Mar 2025 07:54:36 +0100 Subject: [PATCH 05/96] feat(zitadel-proto): add legacy test and update tsconfig for compatibility --- package.json | 2 +- packages/zitadel-proto/package.json | 7 +- packages/zitadel-proto/test/legacy-test.ts | 15 +++ packages/zitadel-proto/tsconfig.json | 5 + pnpm-lock.yaml | 146 +++++++++++++++++++-- 5 files changed, 158 insertions(+), 17 deletions(-) create mode 100644 packages/zitadel-proto/test/legacy-test.ts create mode 100644 packages/zitadel-proto/tsconfig.json diff --git a/package.json b/package.json index a824e47571..57326f73cd 100644 --- a/package.json +++ b/package.json @@ -55,4 +55,4 @@ "vite-tsconfig-paths": "^5.1.2", "vitest": "^2.1.4" } -} +} \ No newline at end of file diff --git a/packages/zitadel-proto/package.json b/packages/zitadel-proto/package.json index 95ddccdc09..fc2b15a37d 100644 --- a/packages/zitadel-proto/package.json +++ b/packages/zitadel-proto/package.json @@ -54,13 +54,16 @@ "build": "tsup", "test:cjs": "node test/cjs-test.cjs", "test:esm": "node test/esm-test.mjs", - "test": "pnpm test:cjs && pnpm test:esm" + "test:legacy": "ts-node test/legacy-test.ts", + "test": "pnpm test:cjs && pnpm test:esm && pnpm test:legacy" }, "dependencies": { "@bufbuild/protobuf": "^2.2.2" }, "devDependencies": { "@bufbuild/buf": "^1.47.2", - "tsup": "^8.0.0" + "tsup": "^8.0.0", + "ts-node": "^10.9.2", + "@zitadel/tsconfig": "workspace:*" } } \ No newline at end of file diff --git a/packages/zitadel-proto/test/legacy-test.ts b/packages/zitadel-proto/test/legacy-test.ts new file mode 100644 index 0000000000..ddf6def6b1 --- /dev/null +++ b/packages/zitadel-proto/test/legacy-test.ts @@ -0,0 +1,15 @@ +import { OrganizationSchema as OrgSchema1 } from "@zitadel/proto/zitadel/org/v2/org_pb"; +// FYI Reparsing as ES module because module syntax was detected. This incurs a performance overhead. +import { OrganizationSchema as OrgSchema2 } from "@zitadel/proto/zitadel/org/v2/org_pb.js"; + +console.log("Legacy import test:"); +console.log("- Generated zitadel/org/v2/org_pb import (discouraged):", !!OrgSchema1); +console.log("- Generated zitadel/org/v2/org_pb.js import (recommended):", !!OrgSchema2); + +// Test successful if we can access these modules and they are the same type +if (OrgSchema1 && OrgSchema2 && OrgSchema1 === OrgSchema2) { + console.log("✅ Legacy import test passed!"); +} else { + console.error("❌ Legacy import test failed!"); + process.exit(1); +} diff --git a/packages/zitadel-proto/tsconfig.json b/packages/zitadel-proto/tsconfig.json new file mode 100644 index 0000000000..bdc2eb95bd --- /dev/null +++ b/packages/zitadel-proto/tsconfig.json @@ -0,0 +1,5 @@ +{ + "extends": "@zitadel/tsconfig/tsup.json", + "include": ["./src/**/*", "./zitadel/**/*"], + "exclude": ["dist", "build", "node_modules"] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b28520f9e2..738a8bacb1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -82,7 +82,7 @@ importers: version: 2.1.3(react@19.0.0) '@tailwindcss/forms': specifier: 0.5.7 - version: 0.5.7(tailwindcss@3.4.14) + version: 0.5.7(tailwindcss@3.4.14(ts-node@10.9.2(@types/node@20.17.17)(typescript@5.6.3))) '@vercel/analytics': specifier: ^1.2.2 version: 1.3.1(next@15.2.0-canary.33(@babel/core@7.26.0)(@playwright/test@1.48.2)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.80.7))(react@19.0.0) @@ -227,7 +227,7 @@ importers: version: 2.0.8 tailwindcss: specifier: 3.4.14 - version: 3.4.14 + version: 3.4.14(ts-node@10.9.2(@types/node@20.17.17)(typescript@5.6.3)) ts-proto: specifier: ^2.2.7 version: 2.2.7 @@ -298,15 +298,24 @@ importers: '@bufbuild/buf': specifier: ^1.47.2 version: 1.47.2 + '@zitadel/tsconfig': + specifier: workspace:* + version: link:../zitadel-tsconfig + ts-node: + specifier: ^10.9.2 + version: 10.9.2(@types/node@20.17.17)(typescript@5.6.3) + tsup: + specifier: ^8.0.0 + version: 8.3.5(jiti@1.21.6)(postcss@8.4.49)(typescript@5.6.3)(yaml@2.5.0) packages/zitadel-tailwind-config: devDependencies: '@tailwindcss/forms': specifier: 0.5.3 - version: 0.5.3(tailwindcss@3.4.14) + version: 0.5.3(tailwindcss@3.4.14(ts-node@10.9.2(@types/node@20.17.17)(typescript@5.6.3))) tailwindcss: specifier: ^3.4.14 - version: 3.4.14 + version: 3.4.14(ts-node@10.9.2(@types/node@20.17.17)(typescript@5.6.3)) packages/zitadel-tsconfig: {} @@ -593,6 +602,10 @@ packages: peerDependencies: '@bufbuild/protobuf': ^2.2.0 + '@cspotcode/source-map-support@0.8.1': + resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} + engines: {node: '>=12'} + '@cypress/request@3.0.6': resolution: {integrity: sha512-fi0eVdCOtKu5Ed6+E8mYxUF6ZTFJDZvHogCBelM0xVXmrDEkyM22gRArQzq1YcHPm1V47Vf/iAD+WgVdUlJCGg==} engines: {node: '>= 6'} @@ -1123,6 +1136,9 @@ packages: '@jridgewell/trace-mapping@0.3.25': resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} + '@jridgewell/trace-mapping@0.3.9': + resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} + '@js-sdsl/ordered-map@4.4.2': resolution: {integrity: sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==} @@ -1530,6 +1546,18 @@ packages: '@types/react-dom': optional: true + '@tsconfig/node10@1.0.11': + resolution: {integrity: sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==} + + '@tsconfig/node12@1.0.11': + resolution: {integrity: sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==} + + '@tsconfig/node14@1.0.3': + resolution: {integrity: sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==} + + '@tsconfig/node16@1.0.4': + resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==} + '@types/aria-query@5.0.4': resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==} @@ -1729,6 +1757,10 @@ packages: peerDependencies: acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + acorn-walk@8.3.4: + resolution: {integrity: sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==} + engines: {node: '>=0.4.0'} + acorn@8.12.1: resolution: {integrity: sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==} engines: {node: '>=0.4.0'} @@ -1799,6 +1831,9 @@ packages: engines: {node: '>=10'} deprecated: This package is no longer supported. + arg@4.1.3: + resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==} + arg@5.0.2: resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==} @@ -2148,6 +2183,9 @@ packages: core-util-is@1.0.2: resolution: {integrity: sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==} + create-require@1.1.1: + resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} + cross-spawn@5.1.0: resolution: {integrity: sha512-pTgQJ5KC0d2hcY8eyL1IzlBPYjTkyH72XRZPnLyKus2mBfNjQs3klqbJU2VILqZryAZUt9JOb3h/mWMy23/f5A==} @@ -2284,6 +2322,10 @@ packages: didyoumean@1.2.2: resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==} + diff@4.0.2: + resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==} + engines: {node: '>=0.3.1'} + dir-glob@3.0.1: resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} engines: {node: '>=8'} @@ -3359,6 +3401,9 @@ packages: resolution: {integrity: sha512-G0yBotnlWVonPClw+tq+xi4K7DZC9n96HjGTBDdHkstAVsDkfZhi1sTvZypXLpyQTbISBkDtK0E5XlUqDsShQg==} engines: {node: '>=18'} + make-error@1.3.6: + resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} + map-stream@0.1.0: resolution: {integrity: sha512-CkYQrPYZfWnu/DAmVCpTSX/xHpKZ80eKh2lAkyA6AJTef6bW+6JpbQZN5rofum7da+SyN1bi5ctTm+lTfcCW3g==} @@ -4467,6 +4512,20 @@ packages: ts-interface-checker@0.1.13: resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} + ts-node@10.9.2: + resolution: {integrity: sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==} + hasBin: true + peerDependencies: + '@swc/core': '>=1.2.50' + '@swc/wasm': '>=1.2.50' + '@types/node': '*' + typescript: '>=2.7' + peerDependenciesMeta: + '@swc/core': + optional: true + '@swc/wasm': + optional: true + ts-poet@6.9.0: resolution: {integrity: sha512-roe6W6MeZmCjRmppyfOURklO5tQFQ6Sg7swURKkwYJvV7dbGCrK28um5+51iW3twdPRKtwarqFAVMU6G1mvnuQ==} @@ -4649,6 +4708,9 @@ packages: resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} hasBin: true + v8-compile-cache-lib@3.0.1: + resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==} + verror@1.10.0: resolution: {integrity: sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==} engines: {'0': node >=0.6.0} @@ -4862,6 +4924,10 @@ packages: yauzl@2.10.0: resolution: {integrity: sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==} + yn@3.1.1: + resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==} + engines: {node: '>=6'} + yocto-queue@0.1.0: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} @@ -5230,6 +5296,10 @@ snapshots: dependencies: '@bufbuild/protobuf': 2.2.2 + '@cspotcode/source-map-support@0.8.1': + dependencies: + '@jridgewell/trace-mapping': 0.3.9 + '@cypress/request@3.0.6': dependencies: aws-sign2: 0.7.0 @@ -5634,6 +5704,11 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.0 + '@jridgewell/trace-mapping@0.3.9': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.0 + '@js-sdsl/ordered-map@4.4.2': {} '@manypkg/find-root@1.1.0': @@ -5938,15 +6013,15 @@ snapshots: '@swc/counter': 0.1.3 tslib: 2.8.1 - '@tailwindcss/forms@0.5.3(tailwindcss@3.4.14)': + '@tailwindcss/forms@0.5.3(tailwindcss@3.4.14(ts-node@10.9.2(@types/node@20.17.17)(typescript@5.6.3)))': dependencies: mini-svg-data-uri: 1.4.4 - tailwindcss: 3.4.14 + tailwindcss: 3.4.14(ts-node@10.9.2(@types/node@20.17.17)(typescript@5.6.3)) - '@tailwindcss/forms@0.5.7(tailwindcss@3.4.14)': + '@tailwindcss/forms@0.5.7(tailwindcss@3.4.14(ts-node@10.9.2(@types/node@20.17.17)(typescript@5.6.3)))': dependencies: mini-svg-data-uri: 1.4.4 - tailwindcss: 3.4.14 + tailwindcss: 3.4.14(ts-node@10.9.2(@types/node@20.17.17)(typescript@5.6.3)) '@tanstack/react-virtual@3.10.6(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': dependencies: @@ -5987,6 +6062,14 @@ snapshots: '@types/react': 19.0.2 '@types/react-dom': 19.0.2(@types/react@19.0.2) + '@tsconfig/node10@1.0.11': {} + + '@tsconfig/node12@1.0.11': {} + + '@tsconfig/node14@1.0.3': {} + + '@tsconfig/node16@1.0.4': {} + '@types/aria-query@5.0.4': {} '@types/babel__core@7.20.5': @@ -6222,6 +6305,10 @@ snapshots: dependencies: acorn: 8.12.1 + acorn-walk@8.3.4: + dependencies: + acorn: 8.12.1 + acorn@8.12.1: {} agent-base@6.0.2: @@ -6286,6 +6373,8 @@ snapshots: delegates: 1.0.0 readable-stream: 3.6.2 + arg@4.1.3: {} + arg@5.0.2: {} argparse@1.0.10: @@ -6648,6 +6737,8 @@ snapshots: core-util-is@1.0.2: {} + create-require@1.1.1: {} + cross-spawn@5.1.0: dependencies: lru-cache: 4.1.5 @@ -6840,6 +6931,8 @@ snapshots: didyoumean@1.2.2: {} + diff@4.0.2: {} + dir-glob@3.0.1: dependencies: path-type: 4.0.0 @@ -7114,7 +7207,7 @@ snapshots: debug: 4.3.7(supports-color@5.5.0) enhanced-resolve: 5.17.1 eslint: 8.57.1 - eslint-module-utils: 2.8.2(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.1) + eslint-module-utils: 2.8.2(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@8.57.1))(eslint@8.57.1) fast-glob: 3.3.2 get-tsconfig: 4.8.0 is-bun-module: 1.1.0 @@ -7127,7 +7220,7 @@ snapshots: - eslint-import-resolver-webpack - supports-color - eslint-module-utils@2.8.2(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.1): + eslint-module-utils@2.8.2(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@8.57.1))(eslint@8.57.1): dependencies: debug: 3.2.7(supports-color@8.1.1) optionalDependencies: @@ -7148,7 +7241,7 @@ snapshots: doctrine: 2.1.0 eslint: 8.57.1 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.8.2(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.1) + eslint-module-utils: 2.8.2(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@8.57.1))(eslint@8.57.1) hasown: 2.0.2 is-core-module: 2.15.1 is-glob: 4.0.3 @@ -8152,6 +8245,8 @@ snapshots: make-dir@5.0.0: {} + make-error@1.3.6: {} + map-stream@0.1.0: {} meow@13.2.0: {} @@ -8509,12 +8604,13 @@ snapshots: camelcase-css: 2.0.1 postcss: 8.4.49 - postcss-load-config@4.0.2(postcss@8.4.49): + postcss-load-config@4.0.2(postcss@8.4.49)(ts-node@10.9.2(@types/node@20.17.17)(typescript@5.6.3)): dependencies: lilconfig: 3.1.2 yaml: 2.5.0 optionalDependencies: postcss: 8.4.49 + ts-node: 10.9.2(@types/node@20.17.17)(typescript@5.6.3) postcss-load-config@6.0.1(jiti@1.21.6)(postcss@8.4.49)(yaml@2.5.0): dependencies: @@ -9094,7 +9190,7 @@ snapshots: tabbable@6.2.0: {} - tailwindcss@3.4.14: + tailwindcss@3.4.14(ts-node@10.9.2(@types/node@20.17.17)(typescript@5.6.3)): dependencies: '@alloc/quick-lru': 5.2.0 arg: 5.0.2 @@ -9113,7 +9209,7 @@ snapshots: postcss: 8.4.49 postcss-import: 15.1.0(postcss@8.4.49) postcss-js: 4.0.1(postcss@8.4.49) - postcss-load-config: 4.0.2(postcss@8.4.49) + postcss-load-config: 4.0.2(postcss@8.4.49)(ts-node@10.9.2(@types/node@20.17.17)(typescript@5.6.3)) postcss-nested: 6.2.0(postcss@8.4.49) postcss-selector-parser: 6.1.2 resolve: 1.22.8 @@ -9211,6 +9307,24 @@ snapshots: ts-interface-checker@0.1.13: {} + ts-node@10.9.2(@types/node@20.17.17)(typescript@5.6.3): + dependencies: + '@cspotcode/source-map-support': 0.8.1 + '@tsconfig/node10': 1.0.11 + '@tsconfig/node12': 1.0.11 + '@tsconfig/node14': 1.0.3 + '@tsconfig/node16': 1.0.4 + '@types/node': 20.17.17 + acorn: 8.12.1 + acorn-walk: 8.3.4 + arg: 4.1.3 + create-require: 1.1.1 + diff: 4.0.2 + make-error: 1.3.6 + typescript: 5.6.3 + v8-compile-cache-lib: 3.0.1 + yn: 3.1.1 + ts-poet@6.9.0: dependencies: dprint-node: 1.0.8 @@ -9394,6 +9508,8 @@ snapshots: uuid@8.3.2: {} + v8-compile-cache-lib@3.0.1: {} + verror@1.10.0: dependencies: assert-plus: 1.0.0 @@ -9632,4 +9748,6 @@ snapshots: buffer-crc32: 0.2.13 fd-slicer: 1.1.0 + yn@3.1.1: {} + yocto-queue@0.1.0: {} From a023c5c57bc7cf2099ab1ea530a1c23f37206414 Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Tue, 22 Apr 2025 14:59:26 +0200 Subject: [PATCH 06/96] v2 utils, v3 protos --- packages/zitadel-client/src/v2.ts | 29 ++++++++++++++++------------- packages/zitadel-proto/package.json | 4 ++-- 2 files changed, 18 insertions(+), 15 deletions(-) diff --git a/packages/zitadel-client/src/v2.ts b/packages/zitadel-client/src/v2.ts index 49cf901734..28b7cd1721 100644 --- a/packages/zitadel-client/src/v2.ts +++ b/packages/zitadel-client/src/v2.ts @@ -1,4 +1,4 @@ -import { create } from "@bufbuild/protobuf"; +import { MessageInitShape } from "@bufbuild/protobuf"; import { FeatureService } from "@zitadel/proto/zitadel/feature/v2/feature_service_pb.js"; import { IdentityProviderService } from "@zitadel/proto/zitadel/idp/v2/idp_service_pb.js"; import { RequestContextSchema } from "@zitadel/proto/zitadel/object/v2/object_pb.js"; @@ -11,17 +11,20 @@ import { UserService } from "@zitadel/proto/zitadel/user/v2/user_service_pb.js"; import { createClientFor } from "./helpers.js"; -export const createUserServiceClient = createClientFor(UserService); -export const createSettingsServiceClient = createClientFor(SettingsService); -export const createSessionServiceClient = createClientFor(SessionService); -export const createOIDCServiceClient = createClientFor(OIDCService); -export const createSAMLServiceClient = createClientFor(SAMLService); -export const createOrganizationServiceClient = createClientFor(OrganizationService); -export const createFeatureServiceClient = createClientFor(FeatureService); -export const createIdpServiceClient = createClientFor(IdentityProviderService); +export const createUserServiceClient: ReturnType> = createClientFor(UserService); +export const createSettingsServiceClient: ReturnType> = + createClientFor(SettingsService); +export const createSessionServiceClient: ReturnType> = + createClientFor(SessionService); +export const createOIDCServiceClient: ReturnType> = createClientFor(OIDCService); +export const createSAMLServiceClient: ReturnType> = createClientFor(SAMLService); +export const createOrganizationServiceClient: ReturnType> = + createClientFor(OrganizationService); +export const createFeatureServiceClient: ReturnType> = + createClientFor(FeatureService); +export const createIdpServiceClient: ReturnType> = + createClientFor(IdentityProviderService); -export function makeReqCtx(orgId: string | undefined) { - return create(RequestContextSchema, { - resourceOwner: orgId ? { case: "orgId", value: orgId } : { case: "instance", value: true }, - }); +export function makeReqCtx(orgId: string | undefined): MessageInitShape { + return { resourceOwner: orgId ? { case: "orgId", value: orgId } : { case: "instance", value: true } }; } diff --git a/packages/zitadel-proto/package.json b/packages/zitadel-proto/package.json index b738a0a63b..75f487b618 100644 --- a/packages/zitadel-proto/package.json +++ b/packages/zitadel-proto/package.json @@ -49,7 +49,7 @@ ], "sideEffects": false, "scripts": { - "generate": "buf generate https://github.com/zitadel/zitadel.git#tag=v2.71.7 --path ./proto/zitadel", + "generate": "buf generate https://github.com/zitadel/zitadel.git#tag=v3.0.0-rc.1 --path ./proto/zitadel", "clean": "rm -rf zitadel .turbo node_modules google protoc-gen-openapiv2 validate", "build": "tsup", "test:cjs": "node test/cjs-test.cjs", @@ -66,4 +66,4 @@ "ts-node": "^10.9.2", "@zitadel/tsconfig": "workspace:*" } -} \ No newline at end of file +} From ec5ee7c7971cddae90d395f41dd37d9fdf6d2190 Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Tue, 22 Apr 2025 15:07:41 +0200 Subject: [PATCH 07/96] fix implicit error type --- apps/login/src/lib/self.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/login/src/lib/self.ts b/apps/login/src/lib/self.ts index d1971c19b1..130bcd66bc 100644 --- a/apps/login/src/lib/self.ts +++ b/apps/login/src/lib/self.ts @@ -1,5 +1,6 @@ "use server"; +import { ConnectError } from "@zitadel/client"; import { createServerTransport } from "@zitadel/client/node"; import { createUserServiceClient } from "@zitadel/client/v2"; import { headers } from "next/headers"; @@ -54,7 +55,7 @@ export async function setMyPassword({ }, {}, ) - .catch((error) => { + .catch((error: ConnectError) => { console.log(error); if (error.code === 7) { return { error: "Session is not valid." }; From 33eb6ca51d73c6485e12d642ddd5d6feee043257 Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Tue, 22 Apr 2025 15:34:58 +0200 Subject: [PATCH 08/96] typing --- packages/zitadel-client/src/v1.ts | 11 +++++++---- packages/zitadel-client/src/v3alpha.ts | 5 ++--- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/packages/zitadel-client/src/v1.ts b/packages/zitadel-client/src/v1.ts index d04180cf88..d54ce619e7 100644 --- a/packages/zitadel-client/src/v1.ts +++ b/packages/zitadel-client/src/v1.ts @@ -5,7 +5,10 @@ import { AuthService } from "@zitadel/proto/zitadel/auth_pb.js"; import { ManagementService } from "@zitadel/proto/zitadel/management_pb.js"; import { SystemService } from "@zitadel/proto/zitadel/system_pb.js"; -export const createAdminServiceClient = createClientFor(AdminService); -export const createAuthServiceClient = createClientFor(AuthService); -export const createManagementServiceClient = createClientFor(ManagementService); -export const createSystemServiceClient = createClientFor(SystemService); +export const createAdminServiceClient: ReturnType> = + createClientFor(AdminService); +export const createAuthServiceClient: ReturnType> = createClientFor(AuthService); +export const createManagementServiceClient: ReturnType> = + createClientFor(ManagementService); +export const createSystemServiceClient: ReturnType> = + createClientFor(SystemService); diff --git a/packages/zitadel-client/src/v3alpha.ts b/packages/zitadel-client/src/v3alpha.ts index a5cc533ade..81b25c746c 100644 --- a/packages/zitadel-client/src/v3alpha.ts +++ b/packages/zitadel-client/src/v3alpha.ts @@ -1,6 +1,5 @@ -import { ZITADELUsers } from "@zitadel/proto/zitadel/resources/user/v3alpha/user_service_pb.js"; import { ZITADELUserSchemas } from "@zitadel/proto/zitadel/resources/userschema/v3alpha/user_schema_service_pb.js"; import { createClientFor } from "./helpers.js"; -export const createUserSchemaServiceClient = createClientFor(ZITADELUserSchemas); -export const createUserServiceClient = createClientFor(ZITADELUsers); +export const createUserSchemaServiceClient: ReturnType> = + createClientFor(ZITADELUserSchemas); From 940f6a6982c9032e8dd33b953fc18418de0cf79f Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Tue, 22 Apr 2025 15:38:49 +0200 Subject: [PATCH 09/96] changesets --- .changeset/pretty-insects-attend.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/pretty-insects-attend.md diff --git a/.changeset/pretty-insects-attend.md b/.changeset/pretty-insects-attend.md new file mode 100644 index 0000000000..be8380bd91 --- /dev/null +++ b/.changeset/pretty-insects-attend.md @@ -0,0 +1,6 @@ +--- +"@zitadel/client": minor +"@zitadel/proto": minor +--- + +CJS and ESM support for @zitadel/proto From e2718483cc8ccd52dd5316acdf554a51d4922d8f Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Mon, 28 Apr 2025 14:57:08 +0200 Subject: [PATCH 10/96] device page, code form --- apps/login/locales/en.json | 12 +++ apps/login/src/app/(login)/device/page.tsx | 86 +++++++++++++++++ apps/login/src/components/app-avatar.tsx | 48 ++++++++++ apps/login/src/components/avatar.tsx | 2 +- .../login/src/components/device-code-form.tsx | 93 +++++++++++++++++++ apps/login/src/components/dynamic-theme.tsx | 21 +++-- apps/login/src/lib/server/oidc.ts | 15 +++ apps/login/src/lib/zitadel.ts | 14 +++ 8 files changed, 283 insertions(+), 8 deletions(-) create mode 100644 apps/login/src/app/(login)/device/page.tsx create mode 100644 apps/login/src/components/app-avatar.tsx create mode 100644 apps/login/src/components/device-code-form.tsx create mode 100644 apps/login/src/lib/server/oidc.ts diff --git a/apps/login/locales/en.json b/apps/login/locales/en.json index 36776ccbd9..cb5011f59a 100644 --- a/apps/login/locales/en.json +++ b/apps/login/locales/en.json @@ -187,6 +187,18 @@ "allSetup": "You have already setup an authenticator!", "linkWithIDP": "or link with an Identity Provider" }, + "device": { + "usercode": { + "title": "Device code", + "description": "Enter the code provided in the verification email.", + "submit": "Continue" + }, + "request": { + "title": "would like to connect:", + "description": "By clicking Allow, you allow this app and Zitadel to use your information in accordance with their respective terms of service and privacy policies. You can revoke this access at any time.", + "submit": "Allow" + } + }, "error": { "unknownContext": "Could not get the context of the user. Make sure to enter the username first or provide a loginName as searchParam.", "sessionExpired": "Your current session has expired. Please login again.", diff --git a/apps/login/src/app/(login)/device/page.tsx b/apps/login/src/app/(login)/device/page.tsx new file mode 100644 index 0000000000..3c533271c0 --- /dev/null +++ b/apps/login/src/app/(login)/device/page.tsx @@ -0,0 +1,86 @@ +import { DeviceCodeForm } from "@/components/device-code-form"; +import { DynamicTheme } from "@/components/dynamic-theme"; +import { getServiceUrlFromHeaders } from "@/lib/service"; +import { + getBrandingSettings, + getDefaultOrg, + getDeviceAuthorizationRequest, +} from "@/lib/zitadel"; +import { DeviceAuthorizationRequest } from "@zitadel/proto/zitadel/oidc/v2/authorization_pb"; +import { Organization } from "@zitadel/proto/zitadel/org/v2/org_pb"; +import { getLocale, getTranslations } from "next-intl/server"; +import { headers } from "next/headers"; + +export default async function Page(props: { + searchParams: Promise>; +}) { + const searchParams = await props.searchParams; + const locale = getLocale(); + const t = await getTranslations({ locale, namespace: "device" }); + + const loginName = searchParams?.loginName; + const userCode = searchParams?.user_code; + const organization = searchParams?.organization; + + const _headers = await headers(); + const { serviceUrl } = getServiceUrlFromHeaders(_headers); + + let defaultOrganization; + if (!organization) { + const org: Organization | null = await getDefaultOrg({ + serviceUrl, + }); + if (org) { + defaultOrganization = org.id; + } + } + + let deviceAuthRequest: DeviceAuthorizationRequest | null = null; + if (userCode) { + const deviceAuthorizationRequestResponse = + await getDeviceAuthorizationRequest({ + serviceUrl, + userCode, + }); + + if (deviceAuthorizationRequestResponse.deviceAuthorizationRequest) { + deviceAuthRequest = + deviceAuthorizationRequestResponse.deviceAuthorizationRequest; + } + } + + const branding = await getBrandingSettings({ + serviceUrl, + organization: organization ?? defaultOrganization, + }); + + return ( + +
+ {!userCode && ( + <> +

{t("usercode.title")}

+

{t("usercode.description")}

+ + + )} + + {deviceAuthRequest && ( +
+

+ {deviceAuthRequest.appName} +
+ {t("request.title")} +

+

+ {t("request.description")} +

+ {/* {JSON.stringify(deviceAuthRequest)} */} +
+ )} +
+
+ ); +} diff --git a/apps/login/src/components/app-avatar.tsx b/apps/login/src/components/app-avatar.tsx new file mode 100644 index 0000000000..defe388438 --- /dev/null +++ b/apps/login/src/components/app-avatar.tsx @@ -0,0 +1,48 @@ +import { ColorShade, getColorHash } from "@/helpers/colors"; +import { useTheme } from "next-themes"; +import Image from "next/image"; +import { getInitials } from "./avatar"; + +interface AvatarProps { + appName: string; + imageUrl?: string; + shadow?: boolean; +} + +export function AppAvatar({ appName, imageUrl, shadow }: AvatarProps) { + const { resolvedTheme } = useTheme(); + const credentials = getInitials(appName, appName); + + const color: ColorShade = getColorHash(appName); + + const avatarStyleDark = { + backgroundColor: color[900], + color: color[200], + }; + + const avatarStyleLight = { + backgroundColor: color[200], + color: color[900], + }; + + return ( +
+ {imageUrl ? ( + avatar + ) : ( + {credentials} + )} +
+ ); +} diff --git a/apps/login/src/components/avatar.tsx b/apps/login/src/components/avatar.tsx index 3f340e09b7..2300659875 100644 --- a/apps/login/src/components/avatar.tsx +++ b/apps/login/src/components/avatar.tsx @@ -12,7 +12,7 @@ interface AvatarProps { shadow?: boolean; } -function getInitials(name: string, loginName: string) { +export function getInitials(name: string, loginName: string) { let credentials = ""; if (name) { const split = name.split(" "); diff --git a/apps/login/src/components/device-code-form.tsx b/apps/login/src/components/device-code-form.tsx new file mode 100644 index 0000000000..5747e52f3c --- /dev/null +++ b/apps/login/src/components/device-code-form.tsx @@ -0,0 +1,93 @@ +"use client"; + +import { Alert } from "@/components/alert"; +import { getDeviceAuthorizationRequest } from "@/lib/server/oidc"; +import { useTranslations } from "next-intl"; +import { useRouter } from "next/navigation"; +import { useState } from "react"; +import { useForm } from "react-hook-form"; +import { BackButton } from "./back-button"; +import { Button, ButtonVariants } from "./button"; +import { TextInput } from "./input"; +import { Spinner } from "./spinner"; + +type Inputs = { + userCode: string; +}; + +export function DeviceCodeForm() { + const t = useTranslations("verify"); + + const router = useRouter(); + + const { register, handleSubmit, formState } = useForm({ + mode: "onBlur", + defaultValues: { + userCode: "", + }, + }); + + const [error, setError] = useState(""); + + const [loading, setLoading] = useState(false); + + async function submitCodeAndContinue(value: Inputs): Promise { + setLoading(true); + + const response = await getDeviceAuthorizationRequest(value.userCode) + .catch(() => { + setError("Could not verify user"); + return; + }) + .finally(() => { + setLoading(false); + }); + + if (response && "error" in response && response?.error) { + setError(response.error); + return; + } + + if (response && "redirect" in response && response?.redirect) { + return router.push(response?.redirect); + } + } + + return ( + <> +
+
+ +
+ + {error && ( +
+ {error} +
+ )} + +
+ + + +
+
+ + ); +} diff --git a/apps/login/src/components/dynamic-theme.tsx b/apps/login/src/components/dynamic-theme.tsx index 7d0fecb558..d50bc082ea 100644 --- a/apps/login/src/components/dynamic-theme.tsx +++ b/apps/login/src/components/dynamic-theme.tsx @@ -3,27 +3,34 @@ import { Logo } from "@/components/logo"; import { BrandingSettings } from "@zitadel/proto/zitadel/settings/v2/branding_settings_pb"; import { ReactNode } from "react"; +import { AppAvatar } from "./app-avatar"; import { ThemeWrapper } from "./theme-wrapper"; export function DynamicTheme({ branding, children, + appName, }: { children: ReactNode; branding?: BrandingSettings; + appName?: string; }) { return (
-
+
{branding && ( - + <> + + + {appName && } + )}
diff --git a/apps/login/src/lib/server/oidc.ts b/apps/login/src/lib/server/oidc.ts new file mode 100644 index 0000000000..4ae01b4a47 --- /dev/null +++ b/apps/login/src/lib/server/oidc.ts @@ -0,0 +1,15 @@ +"use server"; + +import { getDeviceAuthorizationRequest as zitadelGetDeviceAuthorizationRequest } from "@/lib/zitadel"; +import { headers } from "next/headers"; +import { getServiceUrlFromHeaders } from "../service"; + +export async function getDeviceAuthorizationRequest(userCode: string) { + const _headers = await headers(); + const { serviceUrl } = getServiceUrlFromHeaders(_headers); + + return zitadelGetDeviceAuthorizationRequest({ + serviceUrl, + userCode, + }); +} diff --git a/apps/login/src/lib/zitadel.ts b/apps/login/src/lib/zitadel.ts index 0511eaaf0d..aee182dc41 100644 --- a/apps/login/src/lib/zitadel.ts +++ b/apps/login/src/lib/zitadel.ts @@ -929,6 +929,20 @@ export async function getAuthRequest({ }); } +export async function getDeviceAuthorizationRequest({ + serviceUrl, + userCode, +}: { + serviceUrl: string; + userCode: string; +}) { + const oidcService = await createServiceForHost(OIDCService, serviceUrl); + + return oidcService.getDeviceAuthorizationRequest({ + userCode, + }); +} + export async function createCallback({ serviceUrl, req, From 6eb72c9a974c2561bee0dfe24d33b1afe409f496 Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Tue, 29 Apr 2025 09:20:33 +0200 Subject: [PATCH 11/96] use main --- packages/zitadel-proto/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/zitadel-proto/package.json b/packages/zitadel-proto/package.json index 5c0eac42f4..e4926ffcda 100644 --- a/packages/zitadel-proto/package.json +++ b/packages/zitadel-proto/package.json @@ -49,7 +49,7 @@ ], "sideEffects": false, "scripts": { - "generate": "buf generate https://github.com/zitadel/zitadel.git#tag=v3.0.0-rc.1 --path ./proto/zitadel", + "generate": "buf generate https://github.com/zitadel/zitadel.git --path ./proto/zitadel", "clean": "rm -rf zitadel .turbo node_modules google protoc-gen-openapiv2 validate", "build": "tsup", "test:cjs": "node test/cjs-test.cjs", From d4bb9a5331b8da18aca188a61ae49358d7bb9753 Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Tue, 29 Apr 2025 09:45:06 +0200 Subject: [PATCH 12/96] Revert "Merge pull request #398 from tafaust/feat/395-add-cjs-support" This reverts commit 9692297706fa6a174e873a671bdb5aa6c2143682, reversing changes made to 830c2795e98f42a358681062ab3439fd6600843f. --- .changeset/pretty-insects-attend.md | 6 - apps/login/src/lib/self.ts | 3 +- package.json | 2 +- packages/zitadel-client/src/v1.ts | 11 +- packages/zitadel-client/src/v2.ts | 29 ++--- packages/zitadel-client/src/v3alpha.ts | 5 +- packages/zitadel-proto/README.md | 83 ++----------- packages/zitadel-proto/package.json | 51 +------- packages/zitadel-proto/src/index.ts | 5 - packages/zitadel-proto/src/v1.ts | 53 --------- packages/zitadel-proto/src/v2.ts | 49 -------- packages/zitadel-proto/src/v3alpha.ts | 9 -- packages/zitadel-proto/test/cjs-test.cjs | 28 ----- packages/zitadel-proto/test/esm-test.mjs | 28 ----- packages/zitadel-proto/test/legacy-test.ts | 15 --- packages/zitadel-proto/tsconfig.json | 5 - packages/zitadel-proto/tsup.config.ts | 14 --- packages/zitadel-proto/turbo.json | 19 +-- pnpm-lock.yaml | 132 ++------------------- 19 files changed, 45 insertions(+), 502 deletions(-) delete mode 100644 .changeset/pretty-insects-attend.md delete mode 100644 packages/zitadel-proto/src/index.ts delete mode 100644 packages/zitadel-proto/src/v1.ts delete mode 100644 packages/zitadel-proto/src/v2.ts delete mode 100644 packages/zitadel-proto/src/v3alpha.ts delete mode 100644 packages/zitadel-proto/test/cjs-test.cjs delete mode 100644 packages/zitadel-proto/test/esm-test.mjs delete mode 100644 packages/zitadel-proto/test/legacy-test.ts delete mode 100644 packages/zitadel-proto/tsconfig.json delete mode 100644 packages/zitadel-proto/tsup.config.ts diff --git a/.changeset/pretty-insects-attend.md b/.changeset/pretty-insects-attend.md deleted file mode 100644 index be8380bd91..0000000000 --- a/.changeset/pretty-insects-attend.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -"@zitadel/client": minor -"@zitadel/proto": minor ---- - -CJS and ESM support for @zitadel/proto diff --git a/apps/login/src/lib/self.ts b/apps/login/src/lib/self.ts index 130bcd66bc..d1971c19b1 100644 --- a/apps/login/src/lib/self.ts +++ b/apps/login/src/lib/self.ts @@ -1,6 +1,5 @@ "use server"; -import { ConnectError } from "@zitadel/client"; import { createServerTransport } from "@zitadel/client/node"; import { createUserServiceClient } from "@zitadel/client/v2"; import { headers } from "next/headers"; @@ -55,7 +54,7 @@ export async function setMyPassword({ }, {}, ) - .catch((error: ConnectError) => { + .catch((error) => { console.log(error); if (error.code === 7) { return { error: "Session is not valid." }; diff --git a/package.json b/package.json index 2233db0b2e..c96663b83d 100644 --- a/package.json +++ b/package.json @@ -59,4 +59,4 @@ "vite-tsconfig-paths": "^5.1.4", "vitest": "^3.1.2" } -} \ No newline at end of file +} diff --git a/packages/zitadel-client/src/v1.ts b/packages/zitadel-client/src/v1.ts index d54ce619e7..d04180cf88 100644 --- a/packages/zitadel-client/src/v1.ts +++ b/packages/zitadel-client/src/v1.ts @@ -5,10 +5,7 @@ import { AuthService } from "@zitadel/proto/zitadel/auth_pb.js"; import { ManagementService } from "@zitadel/proto/zitadel/management_pb.js"; import { SystemService } from "@zitadel/proto/zitadel/system_pb.js"; -export const createAdminServiceClient: ReturnType> = - createClientFor(AdminService); -export const createAuthServiceClient: ReturnType> = createClientFor(AuthService); -export const createManagementServiceClient: ReturnType> = - createClientFor(ManagementService); -export const createSystemServiceClient: ReturnType> = - createClientFor(SystemService); +export const createAdminServiceClient = createClientFor(AdminService); +export const createAuthServiceClient = createClientFor(AuthService); +export const createManagementServiceClient = createClientFor(ManagementService); +export const createSystemServiceClient = createClientFor(SystemService); diff --git a/packages/zitadel-client/src/v2.ts b/packages/zitadel-client/src/v2.ts index 28b7cd1721..49cf901734 100644 --- a/packages/zitadel-client/src/v2.ts +++ b/packages/zitadel-client/src/v2.ts @@ -1,4 +1,4 @@ -import { MessageInitShape } from "@bufbuild/protobuf"; +import { create } from "@bufbuild/protobuf"; import { FeatureService } from "@zitadel/proto/zitadel/feature/v2/feature_service_pb.js"; import { IdentityProviderService } from "@zitadel/proto/zitadel/idp/v2/idp_service_pb.js"; import { RequestContextSchema } from "@zitadel/proto/zitadel/object/v2/object_pb.js"; @@ -11,20 +11,17 @@ import { UserService } from "@zitadel/proto/zitadel/user/v2/user_service_pb.js"; import { createClientFor } from "./helpers.js"; -export const createUserServiceClient: ReturnType> = createClientFor(UserService); -export const createSettingsServiceClient: ReturnType> = - createClientFor(SettingsService); -export const createSessionServiceClient: ReturnType> = - createClientFor(SessionService); -export const createOIDCServiceClient: ReturnType> = createClientFor(OIDCService); -export const createSAMLServiceClient: ReturnType> = createClientFor(SAMLService); -export const createOrganizationServiceClient: ReturnType> = - createClientFor(OrganizationService); -export const createFeatureServiceClient: ReturnType> = - createClientFor(FeatureService); -export const createIdpServiceClient: ReturnType> = - createClientFor(IdentityProviderService); +export const createUserServiceClient = createClientFor(UserService); +export const createSettingsServiceClient = createClientFor(SettingsService); +export const createSessionServiceClient = createClientFor(SessionService); +export const createOIDCServiceClient = createClientFor(OIDCService); +export const createSAMLServiceClient = createClientFor(SAMLService); +export const createOrganizationServiceClient = createClientFor(OrganizationService); +export const createFeatureServiceClient = createClientFor(FeatureService); +export const createIdpServiceClient = createClientFor(IdentityProviderService); -export function makeReqCtx(orgId: string | undefined): MessageInitShape { - return { resourceOwner: orgId ? { case: "orgId", value: orgId } : { case: "instance", value: true } }; +export function makeReqCtx(orgId: string | undefined) { + return create(RequestContextSchema, { + resourceOwner: orgId ? { case: "orgId", value: orgId } : { case: "instance", value: true }, + }); } diff --git a/packages/zitadel-client/src/v3alpha.ts b/packages/zitadel-client/src/v3alpha.ts index 81b25c746c..a5cc533ade 100644 --- a/packages/zitadel-client/src/v3alpha.ts +++ b/packages/zitadel-client/src/v3alpha.ts @@ -1,5 +1,6 @@ +import { ZITADELUsers } from "@zitadel/proto/zitadel/resources/user/v3alpha/user_service_pb.js"; import { ZITADELUserSchemas } from "@zitadel/proto/zitadel/resources/userschema/v3alpha/user_schema_service_pb.js"; import { createClientFor } from "./helpers.js"; -export const createUserSchemaServiceClient: ReturnType> = - createClientFor(ZITADELUserSchemas); +export const createUserSchemaServiceClient = createClientFor(ZITADELUserSchemas); +export const createUserServiceClient = createClientFor(ZITADELUsers); diff --git a/packages/zitadel-proto/README.md b/packages/zitadel-proto/README.md index 32bf3236c7..bf8a064c12 100644 --- a/packages/zitadel-proto/README.md +++ b/packages/zitadel-proto/README.md @@ -8,87 +8,22 @@ To install the package, use npm or yarn: ```sh npm install @zitadel/proto -# or +``` + +or + +```sh yarn add @zitadel/proto -# or -pnpm add @zitadel/proto ``` ## Usage -This package supports both ESM and CommonJS imports. The API is organized into version-specific namespaces: `v1`, `v2`, and `v3alpha`. +To use the proto definitions in your project, import the generated code: -### ESM (ECMAScript Modules) +```ts +import { Organization } from "@zitadel/proto/zitadel/org/v2/org_pb"; -```typescript -// Import the entire package -import * as zitadel from "@zitadel/proto"; - -// Use the version-specific namespaces -const userRequest = new zitadel.v1.user.GetUserRequest(); - -// Or import specific versions -import { v2 } from "@zitadel/proto"; -const userServiceRequest = new v2.user_service.GetUserRequest(); -``` - -### CommonJS - -```typescript -// Import the entire package -const zitadel = require("@zitadel/proto"); - -// Use the version-specific namespaces -const userRequest = new zitadel.v1.user.GetUserRequest(); -``` - -## API Structure - -The package is organized into version-specific namespaces: - -- `v1`: Contains the original ZITADEL API -- `v2`: Contains the newer version of the API with improved organization -- `v3alpha`: Contains the alpha version of the upcoming API - -## Package Structure - -The package is organized as follows: - -- `index.ts`: Main entry point that exports the version-specific APIs -- `v1.ts`: Exports all v1 API modules -- `v2.ts`: Exports all v2 API modules -- `v3alpha.ts`: Exports all v3alpha API modules -- `zitadel/`: Contains the generated proto files - -## Development - -### Generating the proto files - -The proto files are generated from the ZITADEL API definitions using [buf](https://buf.build/). - -```sh -pnpm generate -``` - -### Building the package - -```sh -pnpm build -``` - -### Testing - -To test both ESM and CommonJS imports: - -```sh -pnpm test -``` - -Or test them individually: - -```bash -pnpm test:cjs # Test CommonJS imports -pnpm test:esm # Test ESM imports +const org: Organization | null = await getDefaultOrg(); ``` ## Documentation diff --git a/packages/zitadel-proto/package.json b/packages/zitadel-proto/package.json index e4926ffcda..bafcb089f7 100644 --- a/packages/zitadel-proto/package.json +++ b/packages/zitadel-proto/package.json @@ -5,65 +5,22 @@ "publishConfig": { "access": "public" }, - "main": "./dist/index.js", - "module": "./dist/index.mjs", - "types": "./dist/index.d.ts", - "exports": { - ".": { - "types": "./dist/index.d.ts", - "import": "./dist/index.mjs", - "require": "./dist/index.js" - }, - "./v1": { - "types": "./dist/v1.d.ts", - "import": "./dist/v1.mjs", - "require": "./dist/v1.js" - }, - "./v2": { - "types": "./dist/v2.d.ts", - "import": "./dist/v2.mjs", - "require": "./dist/v2.js" - }, - "./v3alpha": { - "types": "./dist/v3alpha.d.ts", - "import": "./dist/v3alpha.mjs", - "require": "./dist/v3alpha.js" - }, - "./zitadel/*": { - "types": "./zitadel/*.d.ts", - "import": "./zitadel/*.js", - "require": "./zitadel/*.js" - }, - "./zitadel/*.js": { - "types": "./zitadel/*.d.ts", - "import": "./zitadel/*.js", - "require": "./zitadel/*.js" - } - }, + "type": "module", "files": [ - "src/**", "zitadel/**", "validate/**", "google/**", - "dist/**" + "protoc-gen-openapiv2/**" ], "sideEffects": false, "scripts": { "generate": "buf generate https://github.com/zitadel/zitadel.git --path ./proto/zitadel", - "clean": "rm -rf zitadel .turbo node_modules google protoc-gen-openapiv2 validate", - "build": "tsup", - "test:cjs": "node test/cjs-test.cjs", - "test:esm": "node test/esm-test.mjs", - "test:legacy": "ts-node test/legacy-test.ts", - "test": "pnpm test:cjs && pnpm test:esm && pnpm test:legacy" + "clean": "rm -rf zitadel .turbo node_modules google protoc-gen-openapiv2 validate" }, "dependencies": { "@bufbuild/protobuf": "^2.2.2" }, "devDependencies": { - "@bufbuild/buf": "^1.53.0", - "tsup": "^8.0.0", - "ts-node": "^10.9.2", - "@zitadel/tsconfig": "workspace:*" + "@bufbuild/buf": "^1.53.0" } } diff --git a/packages/zitadel-proto/src/index.ts b/packages/zitadel-proto/src/index.ts deleted file mode 100644 index 6b7b927f11..0000000000 --- a/packages/zitadel-proto/src/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -import * as v1 from "./v1.js"; -import * as v2 from "./v2.js"; -import * as v3alpha from "./v3alpha.js"; - -export { v1, v2, v3alpha }; diff --git a/packages/zitadel-proto/src/v1.ts b/packages/zitadel-proto/src/v1.ts deleted file mode 100644 index 887df5ceb9..0000000000 --- a/packages/zitadel-proto/src/v1.ts +++ /dev/null @@ -1,53 +0,0 @@ -import * as action from "../zitadel/action_pb.js"; -import * as admin from "../zitadel/admin_pb.js"; -import * as app from "../zitadel/app_pb.js"; -import * as auth_n_key from "../zitadel/auth_n_key_pb.js"; -import * as auth from "../zitadel/auth_pb.js"; -import * as change from "../zitadel/change_pb.js"; -import * as event from "../zitadel/event_pb.js"; -import * as feature from "../zitadel/feature_pb.js"; -import * as idp from "../zitadel/idp_pb.js"; -import * as instance from "../zitadel/instance_pb.js"; -import * as management from "../zitadel/management_pb.js"; -import * as member from "../zitadel/member_pb.js"; -import * as message from "../zitadel/message_pb.js"; -import * as metadata from "../zitadel/metadata_pb.js"; -import * as object from "../zitadel/object_pb.js"; -import * as options from "../zitadel/options_pb.js"; -import * as org from "../zitadel/org_pb.js"; -import * as policy from "../zitadel/policy_pb.js"; -import * as project from "../zitadel/project_pb.js"; -import * as quota from "../zitadel/quota_pb.js"; -import * as settings from "../zitadel/settings_pb.js"; -import * as system from "../zitadel/system_pb.js"; -import * as text from "../zitadel/text_pb.js"; -import * as user from "../zitadel/user_pb.js"; -import * as v1 from "../zitadel/v1_pb.js"; - -export { - action, - admin, - app, - auth, - auth_n_key, - change, - event, - feature, - idp, - instance, - management, - member, - message, - metadata, - object, - options, - org, - policy, - project, - quota, - settings, - system, - text, - user, - v1, -}; diff --git a/packages/zitadel-proto/src/v2.ts b/packages/zitadel-proto/src/v2.ts deleted file mode 100644 index e2167c4581..0000000000 --- a/packages/zitadel-proto/src/v2.ts +++ /dev/null @@ -1,49 +0,0 @@ -import * as feature from "../zitadel/feature/v2/feature_pb.js"; -import * as feature_service from "../zitadel/feature/v2/feature_service_pb.js"; -import * as idp from "../zitadel/idp/v2/idp_pb.js"; -import * as idp_service from "../zitadel/idp/v2/idp_service_pb.js"; -import * as object from "../zitadel/object/v2/object_pb.js"; -import * as oidc_authorization from "../zitadel/oidc/v2/authorization_pb.js"; -import * as oidc_service from "../zitadel/oidc/v2/oidc_service_pb.js"; -import * as org from "../zitadel/org/v2/org_pb.js"; -import * as org_service from "../zitadel/org/v2/org_service_pb.js"; -import * as saml_authorization from "../zitadel/saml/v2/authorization_pb.js"; -import * as saml_service from "../zitadel/saml/v2/saml_service_pb.js"; -import * as session from "../zitadel/session/v2/session_pb.js"; -import * as session_service from "../zitadel/session/v2/session_service_pb.js"; -import * as settings from "../zitadel/settings/v2/settings_pb.js"; -import * as settings_service from "../zitadel/settings/v2/settings_service_pb.js"; -import * as user_auth from "../zitadel/user/v2/auth_pb.js"; -import * as user_email from "../zitadel/user/v2/email_pb.js"; -import * as user_idp from "../zitadel/user/v2/idp_pb.js"; -import * as user_password from "../zitadel/user/v2/password_pb.js"; -import * as user_phone from "../zitadel/user/v2/phone_pb.js"; -import * as user_query from "../zitadel/user/v2/query_pb.js"; -import * as user from "../zitadel/user/v2/user_pb.js"; -import * as user_service from "../zitadel/user/v2/user_service_pb.js"; - -export { - feature, - feature_service, - idp, - idp_service, - object, - oidc_authorization, - oidc_service, - org, - org_service, - saml_authorization, - saml_service, - session, - session_service, - settings, - settings_service, - user, - user_auth, - user_email, - user_idp, - user_password, - user_phone, - user_query, - user_service, -}; diff --git a/packages/zitadel-proto/src/v3alpha.ts b/packages/zitadel-proto/src/v3alpha.ts deleted file mode 100644 index 9d787bdc5b..0000000000 --- a/packages/zitadel-proto/src/v3alpha.ts +++ /dev/null @@ -1,9 +0,0 @@ -import * as user_authenticator from "../zitadel/resources/user/v3alpha/authenticator_pb.js"; -import * as user_communication from "../zitadel/resources/user/v3alpha/communication_pb.js"; -import * as user_query from "../zitadel/resources/user/v3alpha/query_pb.js"; -import * as user from "../zitadel/resources/user/v3alpha/user_pb.js"; -import * as user_service from "../zitadel/resources/user/v3alpha/user_service_pb.js"; -import * as user_schema from "../zitadel/resources/userschema/v3alpha/user_schema_pb.js"; -import * as user_schema_service from "../zitadel/resources/userschema/v3alpha/user_schema_service_pb.js"; - -export { user, user_authenticator, user_communication, user_query, user_schema, user_schema_service, user_service }; diff --git a/packages/zitadel-proto/test/cjs-test.cjs b/packages/zitadel-proto/test/cjs-test.cjs deleted file mode 100644 index 87033cfd3b..0000000000 --- a/packages/zitadel-proto/test/cjs-test.cjs +++ /dev/null @@ -1,28 +0,0 @@ -// CommonJS import test -const zitadel = require("@zitadel/proto"); - -// Check if the import worked by accessing some properties -console.log("CommonJS import test:"); -console.log("- Has v1 API:", !!zitadel.v1); -console.log("- Has v2 API:", !!zitadel.v2); -console.log("- Has v3alpha API:", !!zitadel.v3alpha); - -// Test v1 API -console.log("- v1.user module:", !!zitadel.v1.user); -console.log("- v1.management module:", !!zitadel.v1.management); - -// Test v2 API -console.log("- v2.user module:", !!zitadel.v2.user); -console.log("- v2.user_service module:", !!zitadel.v2.user_service); - -// Test v3alpha API -console.log("- v3alpha.user module:", !!zitadel.v3alpha.user); -console.log("- v3alpha.user_service module:", !!zitadel.v3alpha.user_service); - -// Test successful if we can access these modules -if (zitadel.v1 && zitadel.v2 && zitadel.v3alpha) { - console.log("✅ CommonJS import test passed!"); -} else { - console.error("❌ CommonJS import test failed!"); - process.exit(1); -} diff --git a/packages/zitadel-proto/test/esm-test.mjs b/packages/zitadel-proto/test/esm-test.mjs deleted file mode 100644 index aafaf01df3..0000000000 --- a/packages/zitadel-proto/test/esm-test.mjs +++ /dev/null @@ -1,28 +0,0 @@ -// ESM import test -import * as zitadel from "@zitadel/proto"; - -// Check if the import worked by accessing some properties -console.log("ESM import test:"); -console.log("- Has v1 API:", !!zitadel.v1); -console.log("- Has v2 API:", !!zitadel.v2); -console.log("- Has v3alpha API:", !!zitadel.v3alpha); - -// Test v1 API -console.log("- v1.user module:", !!zitadel.v1.user); -console.log("- v1.management module:", !!zitadel.v1.management); - -// Test v2 API -console.log("- v2.user module:", !!zitadel.v2.user); -console.log("- v2.user_service module:", !!zitadel.v2.user_service); - -// Test v3alpha API -console.log("- v3alpha.user module:", !!zitadel.v3alpha.user); -console.log("- v3alpha.user_service module:", !!zitadel.v3alpha.user_service); - -// Test successful if we can access these modules -if (zitadel.v1 && zitadel.v2 && zitadel.v3alpha) { - console.log("✅ ESM import test passed!"); -} else { - console.error("❌ ESM import test failed!"); - process.exit(1); -} diff --git a/packages/zitadel-proto/test/legacy-test.ts b/packages/zitadel-proto/test/legacy-test.ts deleted file mode 100644 index ddf6def6b1..0000000000 --- a/packages/zitadel-proto/test/legacy-test.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { OrganizationSchema as OrgSchema1 } from "@zitadel/proto/zitadel/org/v2/org_pb"; -// FYI Reparsing as ES module because module syntax was detected. This incurs a performance overhead. -import { OrganizationSchema as OrgSchema2 } from "@zitadel/proto/zitadel/org/v2/org_pb.js"; - -console.log("Legacy import test:"); -console.log("- Generated zitadel/org/v2/org_pb import (discouraged):", !!OrgSchema1); -console.log("- Generated zitadel/org/v2/org_pb.js import (recommended):", !!OrgSchema2); - -// Test successful if we can access these modules and they are the same type -if (OrgSchema1 && OrgSchema2 && OrgSchema1 === OrgSchema2) { - console.log("✅ Legacy import test passed!"); -} else { - console.error("❌ Legacy import test failed!"); - process.exit(1); -} diff --git a/packages/zitadel-proto/tsconfig.json b/packages/zitadel-proto/tsconfig.json deleted file mode 100644 index bdc2eb95bd..0000000000 --- a/packages/zitadel-proto/tsconfig.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "extends": "@zitadel/tsconfig/tsup.json", - "include": ["./src/**/*", "./zitadel/**/*"], - "exclude": ["dist", "build", "node_modules"] -} diff --git a/packages/zitadel-proto/tsup.config.ts b/packages/zitadel-proto/tsup.config.ts deleted file mode 100644 index 13a78084c6..0000000000 --- a/packages/zitadel-proto/tsup.config.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { defineConfig } from "tsup"; - -export default defineConfig({ - entry: ["src/index.ts", "src/v1.ts", "src/v2.ts", "src/v3alpha.ts"], - dts: true, - clean: true, - minify: false, - splitting: false, - treeshake: false, - sourcemap: true, - format: ["esm", "cjs"], - platform: "neutral", - target: "node16", -}); diff --git a/packages/zitadel-proto/turbo.json b/packages/zitadel-proto/turbo.json index 86d4f1e86c..bffd614f62 100644 --- a/packages/zitadel-proto/turbo.json +++ b/packages/zitadel-proto/turbo.json @@ -1,22 +1,9 @@ { - "extends": [ - "//" - ], + "extends": ["//"], "tasks": { - "build": { - "dependsOn": [ - "generate" - ], - "outputs": [ - "dist/**" - ], - "cache": false - }, "generate": { - "outputs": [ - "zitadel/**" - ], + "outputs": ["zitadel/**"], "cache": true } } -} \ No newline at end of file +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f46ff5fcac..46a448c2f1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -82,7 +82,7 @@ importers: version: 2.1.3(react@19.1.0) '@tailwindcss/forms': specifier: 0.5.7 - version: 0.5.7(tailwindcss@3.4.14(ts-node@10.9.2(@types/node@22.14.1)(typescript@5.8.3))) + version: 0.5.7(tailwindcss@3.4.14) '@vercel/analytics': specifier: ^1.2.2 version: 1.3.1(next@15.4.0-canary.3(@babel/core@7.26.10)(@playwright/test@1.52.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.87.0))(react@19.1.0) @@ -230,7 +230,7 @@ importers: version: 2.0.11 tailwindcss: specifier: 3.4.14 - version: 3.4.14(ts-node@10.9.2(@types/node@22.14.1)(typescript@5.8.3)) + version: 3.4.14 ts-proto: specifier: ^2.7.0 version: 2.7.0 @@ -301,15 +301,6 @@ importers: '@bufbuild/buf': specifier: ^1.53.0 version: 1.53.0 - '@zitadel/tsconfig': - specifier: workspace:* - version: link:../zitadel-tsconfig - ts-node: - specifier: ^10.9.2 - version: 10.9.2(@types/node@22.14.1)(typescript@5.8.3) - tsup: - specifier: ^8.0.0 - version: 8.4.0(jiti@1.21.6)(postcss@8.5.3)(typescript@5.8.3)(yaml@2.7.1) packages/zitadel-tailwind-config: devDependencies: @@ -563,10 +554,6 @@ packages: peerDependencies: '@bufbuild/protobuf': ^2.2.0 - '@cspotcode/source-map-support@0.8.1': - resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} - engines: {node: '>=12'} - '@csstools/color-helpers@5.0.2': resolution: {integrity: sha512-JqWH1vsgdGcw2RR6VliXXdA0/59LttzlU8UlRT/iUUsEeWfYq8I+K0yhihEUTTHLRm1EXvpsCx3083EU15ecsA==} engines: {node: '>=18'} @@ -998,9 +985,6 @@ packages: '@jridgewell/trace-mapping@0.3.25': resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} - '@jridgewell/trace-mapping@0.3.9': - resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} - '@js-sdsl/ordered-map@4.4.2': resolution: {integrity: sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==} @@ -1415,18 +1399,6 @@ packages: '@types/react-dom': optional: true - '@tsconfig/node10@1.0.11': - resolution: {integrity: sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==} - - '@tsconfig/node12@1.0.11': - resolution: {integrity: sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==} - - '@tsconfig/node14@1.0.3': - resolution: {integrity: sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==} - - '@tsconfig/node16@1.0.4': - resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==} - '@types/aria-query@5.0.4': resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==} @@ -1626,10 +1598,6 @@ packages: peerDependencies: acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 - acorn-walk@8.3.4: - resolution: {integrity: sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==} - engines: {node: '>=0.4.0'} - acorn@8.12.1: resolution: {integrity: sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==} engines: {node: '>=0.4.0'} @@ -1700,9 +1668,6 @@ packages: engines: {node: '>=10'} deprecated: This package is no longer supported. - arg@4.1.3: - resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==} - arg@5.0.2: resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==} @@ -2055,9 +2020,6 @@ packages: core-util-is@1.0.2: resolution: {integrity: sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==} - create-require@1.1.1: - resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} - cross-spawn@7.0.3: resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} engines: {node: '>= 8'} @@ -2208,10 +2170,6 @@ packages: didyoumean@1.2.2: resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==} - diff@4.0.2: - resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==} - engines: {node: '>=0.3.1'} - dir-glob@3.0.1: resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} engines: {node: '>=8'} @@ -3325,9 +3283,6 @@ packages: resolution: {integrity: sha512-G0yBotnlWVonPClw+tq+xi4K7DZC9n96HjGTBDdHkstAVsDkfZhi1sTvZypXLpyQTbISBkDtK0E5XlUqDsShQg==} engines: {node: '>=18'} - make-error@1.3.6: - resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} - map-stream@0.1.0: resolution: {integrity: sha512-CkYQrPYZfWnu/DAmVCpTSX/xHpKZ80eKh2lAkyA6AJTef6bW+6JpbQZN5rofum7da+SyN1bi5ctTm+lTfcCW3g==} @@ -4453,20 +4408,6 @@ packages: ts-interface-checker@0.1.13: resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} - ts-node@10.9.2: - resolution: {integrity: sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==} - hasBin: true - peerDependencies: - '@swc/core': '>=1.2.50' - '@swc/wasm': '>=1.2.50' - '@types/node': '*' - typescript: '>=2.7' - peerDependenciesMeta: - '@swc/core': - optional: true - '@swc/wasm': - optional: true - ts-poet@6.11.0: resolution: {integrity: sha512-r5AGF8vvb+GjBsnqiTqbLhN1/U2FJt6BI+k0dfCrkKzWvUhNlwMmq9nDHuucHs45LomgHjZPvYj96dD3JawjJA==} @@ -4640,9 +4581,6 @@ packages: resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} hasBin: true - v8-compile-cache-lib@3.0.1: - resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==} - verror@1.10.0: resolution: {integrity: sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==} engines: {'0': node >=0.6.0} @@ -4861,10 +4799,6 @@ packages: yauzl@2.10.0: resolution: {integrity: sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==} - yn@3.1.1: - resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==} - engines: {node: '>=6'} - yocto-queue@0.1.0: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} @@ -5210,10 +5144,6 @@ snapshots: dependencies: '@bufbuild/protobuf': 2.2.2 - '@cspotcode/source-map-support@0.8.1': - dependencies: - '@jridgewell/trace-mapping': 0.3.9 - '@csstools/color-helpers@5.0.2': {} '@csstools/css-calc@2.1.3(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3)': @@ -5575,11 +5505,6 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.0 - '@jridgewell/trace-mapping@0.3.9': - dependencies: - '@jridgewell/resolve-uri': 3.1.2 - '@jridgewell/sourcemap-codec': 1.5.0 - '@js-sdsl/ordered-map@4.4.2': {} '@manypkg/find-root@1.1.0': @@ -5891,10 +5816,10 @@ snapshots: mini-svg-data-uri: 1.4.4 tailwindcss: 4.1.4 - '@tailwindcss/forms@0.5.7(tailwindcss@3.4.14(ts-node@10.9.2(@types/node@22.14.1)(typescript@5.8.3)))': + '@tailwindcss/forms@0.5.7(tailwindcss@3.4.14)': dependencies: mini-svg-data-uri: 1.4.4 - tailwindcss: 3.4.14(ts-node@10.9.2(@types/node@22.14.1)(typescript@5.8.3)) + tailwindcss: 3.4.14 '@tanstack/react-virtual@3.10.6(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': dependencies: @@ -5935,14 +5860,6 @@ snapshots: '@types/react': 19.1.2 '@types/react-dom': 19.1.2(@types/react@19.1.2) - '@tsconfig/node10@1.0.11': {} - - '@tsconfig/node12@1.0.11': {} - - '@tsconfig/node14@1.0.3': {} - - '@tsconfig/node16@1.0.4': {} - '@types/aria-query@5.0.4': {} '@types/babel__core@7.20.5': @@ -6178,10 +6095,6 @@ snapshots: dependencies: acorn: 8.12.1 - acorn-walk@8.3.4: - dependencies: - acorn: 8.12.1 - acorn@8.12.1: {} agent-base@6.0.2: @@ -6242,8 +6155,6 @@ snapshots: delegates: 1.0.0 readable-stream: 3.6.2 - arg@4.1.3: {} - arg@5.0.2: {} argparse@1.0.10: @@ -6609,8 +6520,6 @@ snapshots: core-util-is@1.0.2: {} - create-require@1.1.1: {} - cross-spawn@7.0.3: dependencies: path-key: 3.1.1 @@ -6811,8 +6720,6 @@ snapshots: didyoumean@1.2.2: {} - diff@4.0.2: {} - dir-glob@3.0.1: dependencies: path-type: 4.0.0 @@ -8153,8 +8060,6 @@ snapshots: make-dir@5.0.0: {} - make-error@1.3.6: {} - map-stream@0.1.0: {} math-intrinsics@1.1.0: {} @@ -8516,13 +8421,12 @@ snapshots: camelcase-css: 2.0.1 postcss: 8.5.3 - postcss-load-config@4.0.2(postcss@8.5.3)(ts-node@10.9.2(@types/node@22.14.1)(typescript@5.8.3)): + postcss-load-config@4.0.2(postcss@8.5.3): dependencies: lilconfig: 3.1.3 yaml: 2.7.1 optionalDependencies: postcss: 8.5.3 - ts-node: 10.9.2(@types/node@22.14.1)(typescript@5.8.3) postcss-load-config@6.0.1(jiti@1.21.6)(postcss@8.5.3)(yaml@2.7.1): dependencies: @@ -9127,7 +9031,7 @@ snapshots: tabbable@6.2.0: {} - tailwindcss@3.4.14(ts-node@10.9.2(@types/node@22.14.1)(typescript@5.8.3)): + tailwindcss@3.4.14: dependencies: '@alloc/quick-lru': 5.2.0 arg: 5.0.2 @@ -9146,7 +9050,7 @@ snapshots: postcss: 8.5.3 postcss-import: 15.1.0(postcss@8.5.3) postcss-js: 4.0.1(postcss@8.5.3) - postcss-load-config: 4.0.2(postcss@8.5.3)(ts-node@10.9.2(@types/node@22.14.1)(typescript@5.8.3)) + postcss-load-config: 4.0.2(postcss@8.5.3) postcss-nested: 6.2.0(postcss@8.5.3) postcss-selector-parser: 6.1.2 resolve: 1.22.8 @@ -9246,24 +9150,6 @@ snapshots: ts-interface-checker@0.1.13: {} - ts-node@10.9.2(@types/node@22.14.1)(typescript@5.8.3): - dependencies: - '@cspotcode/source-map-support': 0.8.1 - '@tsconfig/node10': 1.0.11 - '@tsconfig/node12': 1.0.11 - '@tsconfig/node14': 1.0.3 - '@tsconfig/node16': 1.0.4 - '@types/node': 22.14.1 - acorn: 8.12.1 - acorn-walk: 8.3.4 - arg: 4.1.3 - create-require: 1.1.1 - diff: 4.0.2 - make-error: 1.3.6 - typescript: 5.8.3 - v8-compile-cache-lib: 3.0.1 - yn: 3.1.1 - ts-poet@6.11.0: dependencies: dprint-node: 1.0.8 @@ -9439,8 +9325,6 @@ snapshots: uuid@8.3.2: {} - v8-compile-cache-lib@3.0.1: {} - verror@1.10.0: dependencies: assert-plus: 1.0.0 @@ -9686,6 +9570,4 @@ snapshots: buffer-crc32: 0.2.13 fd-slicer: 1.1.0 - yn@3.1.1: {} - yocto-queue@0.1.0: {} From 62ad3888ee25d2fb61fdda1fecad9473a09b353e Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Tue, 29 Apr 2025 09:49:13 +0200 Subject: [PATCH 13/96] version --- .changeset/red-owls-battle.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/red-owls-battle.md diff --git a/.changeset/red-owls-battle.md b/.changeset/red-owls-battle.md new file mode 100644 index 0000000000..46694b8ef2 --- /dev/null +++ b/.changeset/red-owls-battle.md @@ -0,0 +1,6 @@ +--- +"@zitadel/client": minor +"@zitadel/proto": minor +--- + +revert CJS support From 93b6f9a4a2d52c5f13e0f7039121120889a3290e Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Tue, 29 Apr 2025 10:09:57 +0200 Subject: [PATCH 14/96] Version packages 1.2.0 --- .changeset/red-owls-battle.md | 6 ------ packages/zitadel-client/CHANGELOG.md | 11 +++++++++++ packages/zitadel-client/package.json | 2 +- packages/zitadel-proto/CHANGELOG.md | 6 ++++++ packages/zitadel-proto/package.json | 2 +- packages/zitadel-proto/turbo.json | 2 +- 6 files changed, 20 insertions(+), 9 deletions(-) delete mode 100644 .changeset/red-owls-battle.md diff --git a/.changeset/red-owls-battle.md b/.changeset/red-owls-battle.md deleted file mode 100644 index 46694b8ef2..0000000000 --- a/.changeset/red-owls-battle.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -"@zitadel/client": minor -"@zitadel/proto": minor ---- - -revert CJS support diff --git a/packages/zitadel-client/CHANGELOG.md b/packages/zitadel-client/CHANGELOG.md index 3010b8feb9..5af51d2bfb 100644 --- a/packages/zitadel-client/CHANGELOG.md +++ b/packages/zitadel-client/CHANGELOG.md @@ -1,5 +1,16 @@ # @zitadel/client +## 1.1.0 + +### Minor Changes + +- 62ad388: revert CJS support + +### Patch Changes + +- Updated dependencies [62ad388] + - @zitadel/proto@1.1.0 + ## 1.0.7 ### Patch Changes diff --git a/packages/zitadel-client/package.json b/packages/zitadel-client/package.json index e5634923c7..a47e3e56e9 100644 --- a/packages/zitadel-client/package.json +++ b/packages/zitadel-client/package.json @@ -1,6 +1,6 @@ { "name": "@zitadel/client", - "version": "1.0.7", + "version": "1.2.0", "license": "MIT", "publishConfig": { "access": "public" diff --git a/packages/zitadel-proto/CHANGELOG.md b/packages/zitadel-proto/CHANGELOG.md index b15f2d2e23..11a7e81a77 100644 --- a/packages/zitadel-proto/CHANGELOG.md +++ b/packages/zitadel-proto/CHANGELOG.md @@ -1,5 +1,11 @@ # @zitadel/proto +## 1.1.0 + +### Minor Changes + +- 62ad388: revert CJS support + ## 1.0.4 ### Patch Changes diff --git a/packages/zitadel-proto/package.json b/packages/zitadel-proto/package.json index bafcb089f7..61ef296616 100644 --- a/packages/zitadel-proto/package.json +++ b/packages/zitadel-proto/package.json @@ -1,6 +1,6 @@ { "name": "@zitadel/proto", - "version": "1.0.4", + "version": "1.2.0", "license": "MIT", "publishConfig": { "access": "public" diff --git a/packages/zitadel-proto/turbo.json b/packages/zitadel-proto/turbo.json index bffd614f62..2d24f0349b 100644 --- a/packages/zitadel-proto/turbo.json +++ b/packages/zitadel-proto/turbo.json @@ -3,7 +3,7 @@ "tasks": { "generate": { "outputs": ["zitadel/**"], - "cache": true + "cache": false } } } From d8d3a032ec2db73f1401893b35aed98ba322b010 Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Tue, 29 Apr 2025 10:22:48 +0200 Subject: [PATCH 15/96] changelog --- packages/zitadel-client/CHANGELOG.md | 9 +++++---- packages/zitadel-proto/CHANGELOG.md | 8 +++++++- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/packages/zitadel-client/CHANGELOG.md b/packages/zitadel-client/CHANGELOG.md index 5af51d2bfb..f1107bcc5a 100644 --- a/packages/zitadel-client/CHANGELOG.md +++ b/packages/zitadel-client/CHANGELOG.md @@ -1,15 +1,16 @@ # @zitadel/client -## 1.1.0 +## 1.2.0 ### Minor Changes - 62ad388: revert CJS support -### Patch Changes +## 1.1.0 -- Updated dependencies [62ad388] - - @zitadel/proto@1.1.0 +### Minor Changes + +- 9692297: add CJS and ESM support ## 1.0.7 diff --git a/packages/zitadel-proto/CHANGELOG.md b/packages/zitadel-proto/CHANGELOG.md index 11a7e81a77..c3964e2b29 100644 --- a/packages/zitadel-proto/CHANGELOG.md +++ b/packages/zitadel-proto/CHANGELOG.md @@ -1,10 +1,16 @@ # @zitadel/proto +## 1.2.0 + +### Minor Changes + +- 62ad388: revert CJS support + ## 1.1.0 ### Minor Changes -- 62ad388: revert CJS support +- 9692297: add CJS and ESM support ## 1.0.4 From 7a25dce936a0bd699d2287ba82d00634e2ee8631 Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Wed, 30 Apr 2025 09:14:11 +0200 Subject: [PATCH 16/96] fix: override csp for allowed iframe --- apps/login/next.config.mjs | 5 ++- .../app/(login)/authenticator/set/page.tsx | 11 ++--- apps/login/src/app/login/route.ts | 44 +++++++++++++++---- apps/login/src/lib/csp.ts | 2 + apps/login/src/lib/zitadel.ts | 15 +++++++ apps/login/src/middleware.ts | 18 ++++++++ 6 files changed, 76 insertions(+), 19 deletions(-) create mode 100644 apps/login/src/lib/csp.ts diff --git a/apps/login/next.config.mjs b/apps/login/next.config.mjs index 00fa1e19c4..2795854114 100755 --- a/apps/login/next.config.mjs +++ b/apps/login/next.config.mjs @@ -1,4 +1,5 @@ import createNextIntlPlugin from "next-intl/plugin"; +import { DEFAULT_CSP } from "./src/lib/csp"; const withNextIntl = createNextIntlPlugin(); @@ -29,9 +30,9 @@ const secureHeaders = [ // script-src va.vercel-scripts.com for analytics/vercel scripts { key: "Content-Security-Policy", - value: - "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://va.vercel-scripts.com; connect-src 'self'; child-src; style-src 'self' 'unsafe-inline'; font-src 'self'; object-src 'none'; img-src 'self' https://vercel.com;", + value: `${DEFAULT_CSP} frame-ancestors 'none'`, }, + { key: "X-Frame-Options", value: "deny" }, ]; const imageRemotePatterns = [ diff --git a/apps/login/src/app/(login)/authenticator/set/page.tsx b/apps/login/src/app/(login)/authenticator/set/page.tsx index 8240023c2d..8904eff963 100644 --- a/apps/login/src/app/(login)/authenticator/set/page.tsx +++ b/apps/login/src/app/(login)/authenticator/set/page.tsx @@ -33,8 +33,8 @@ export default async function Page(props: { const { serviceUrl } = getServiceUrlFromHeaders(_headers); const sessionWithData = sessionId - ? await loadSessionById(serviceUrl, sessionId, organization) - : await loadSessionByLoginname(serviceUrl, loginName, organization); + ? await loadSessionById(sessionId, organization) + : await loadSessionByLoginname(loginName, organization); async function getAuthMethodsAndUser( serviceUrl: string, @@ -67,7 +67,6 @@ export default async function Page(props: { } async function loadSessionByLoginname( - host: string, loginName?: string, organization?: string, ) { @@ -82,11 +81,7 @@ export default async function Page(props: { }); } - async function loadSessionById( - host: string, - sessionId: string, - organization?: string, - ) { + async function loadSessionById(sessionId: string, organization?: string) { const recent = await getSessionCookieById({ sessionId, organization }); return getSession({ serviceUrl, diff --git a/apps/login/src/app/login/route.ts b/apps/login/src/app/login/route.ts index e3834e5a27..b3da6f863e 100644 --- a/apps/login/src/app/login/route.ts +++ b/apps/login/src/app/login/route.ts @@ -1,4 +1,5 @@ import { getAllSessions } from "@/lib/cookies"; +import { DEFAULT_CSP } from "@/lib/csp"; import { idpTypeToSlug } from "@/lib/idp"; import { loginWithOIDCandSession } from "@/lib/oidc"; import { loginWithSAMLandSession } from "@/lib/saml"; @@ -12,6 +13,7 @@ import { getAuthRequest, getOrgsByDomain, getSAMLRequest, + getSecuritySettings, listSessions, startIdentityProviderFlow, } from "@/lib/zitadel"; @@ -293,17 +295,32 @@ export async function GET(request: NextRequest) { * This means that the user should not be prompted to enter their password again. * Instead, the server attempts to silently authenticate the user using an existing session or other authentication mechanisms that do not require user interaction **/ + const securitySettings = await getSecuritySettings({ + serviceUrl, + }); + const selectedSession = await findValidSession({ serviceUrl, sessions, authRequest, }); - if (!selectedSession || !selectedSession.id) { - return NextResponse.json( - { error: "No active session found" }, - { status: 400 }, + const noSessionResponse = NextResponse.json( + { error: "No active session found" }, + { status: 400 }, + ); + + if (securitySettings?.embeddedIframe?.enabled) { + securitySettings.embeddedIframe.allowedOrigins; + noSessionResponse.headers.set( + "Content-Security-Policy", + `${DEFAULT_CSP} frame-ancestors ${securitySettings.embeddedIframe.allowedOrigins.join(" ")};`, ); + noSessionResponse.headers.delete("X-Frame-Options"); + } + + if (!selectedSession || !selectedSession.id) { + return noSessionResponse; } const cookie = sessionCookies.find( @@ -311,10 +328,7 @@ export async function GET(request: NextRequest) { ); if (!cookie || !cookie.id || !cookie.token) { - return NextResponse.json( - { error: "No active session found" }, - { status: 400 }, - ); + return noSessionResponse; } const session = { @@ -332,7 +346,19 @@ export async function GET(request: NextRequest) { }, }), }); - return NextResponse.redirect(callbackUrl); + + const callbackResponse = NextResponse.redirect(callbackUrl); + + if (securitySettings?.embeddedIframe?.enabled) { + securitySettings.embeddedIframe.allowedOrigins; + callbackResponse.headers.set( + "Content-Security-Policy", + `${DEFAULT_CSP} frame-ancestors ${securitySettings.embeddedIframe.allowedOrigins.join(" ")};`, + ); + callbackResponse.headers.delete("X-Frame-Options"); + } + + return callbackResponse; } else { // check for loginHint, userId hint and valid sessions let selectedSession = await findValidSession({ diff --git a/apps/login/src/lib/csp.ts b/apps/login/src/lib/csp.ts new file mode 100644 index 0000000000..5cc1e254f3 --- /dev/null +++ b/apps/login/src/lib/csp.ts @@ -0,0 +1,2 @@ +export const DEFAULT_CSP = + "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://va.vercel-scripts.com; connect-src 'self'; child-src; style-src 'self' 'unsafe-inline'; font-src 'self'; object-src 'none'; img-src 'self' https://vercel.com;"; diff --git a/apps/login/src/lib/zitadel.ts b/apps/login/src/lib/zitadel.ts index 0511eaaf0d..a5abc0dcb1 100644 --- a/apps/login/src/lib/zitadel.ts +++ b/apps/login/src/lib/zitadel.ts @@ -92,6 +92,21 @@ export async function getLoginSettings({ return useCache ? cacheWrapper(callback) : callback; } +export async function getSecuritySettings({ + serviceUrl, +}: { + serviceUrl: string; +}) { + const settingsService: Client = + await createServiceForHost(SettingsService, serviceUrl); + + const callback = settingsService + .getSecuritySettings({}) + .then((resp) => (resp.settings ? resp.settings : undefined)); + + return useCache ? cacheWrapper(callback) : callback; +} + export async function getLockoutSettings({ serviceUrl, orgId, diff --git a/apps/login/src/middleware.ts b/apps/login/src/middleware.ts index 8d4080cddf..0572fe43b6 100644 --- a/apps/login/src/middleware.ts +++ b/apps/login/src/middleware.ts @@ -1,6 +1,8 @@ import { headers } from "next/headers"; import { NextRequest, NextResponse } from "next/server"; +import { DEFAULT_CSP } from "./lib/csp"; import { getServiceUrlFromHeaders } from "./lib/service"; +import { getSecuritySettings } from "./lib/zitadel"; export const config = { matcher: [ @@ -22,6 +24,8 @@ export async function middleware(request: NextRequest) { const { serviceUrl } = getServiceUrlFromHeaders(_headers); + const securitySettings = await getSecuritySettings({ serviceUrl }); + const instanceHost = `${serviceUrl}` .replace("https://", "") .replace("http://", ""); @@ -39,6 +43,20 @@ export async function middleware(request: NextRequest) { responseHeaders.set("Access-Control-Allow-Origin", "*"); responseHeaders.set("Access-Control-Allow-Headers", "*"); + responseHeaders.set( + "Content-Security-Policy", + `${DEFAULT_CSP} frame-ancestors 'none'`, + ); + + if (securitySettings?.embeddedIframe?.enabled) { + securitySettings.embeddedIframe.allowedOrigins; + responseHeaders.set( + "Content-Security-Policy", + `${DEFAULT_CSP} frame-ancestors ${securitySettings.embeddedIframe.allowedOrigins.join(" ")};`, + ); + responseHeaders.delete("X-Frame-Options"); + } + request.nextUrl.href = `${serviceUrl}${request.nextUrl.pathname}${request.nextUrl.search}`; return NextResponse.rewrite(request.nextUrl, { request: { From 231ecdc5c54f3872cbcc4e1eb270943d224010cd Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Wed, 30 Apr 2025 09:18:01 +0200 Subject: [PATCH 17/96] change import --- apps/login/next.config.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/login/next.config.mjs b/apps/login/next.config.mjs index 2795854114..7fb0f5b65d 100755 --- a/apps/login/next.config.mjs +++ b/apps/login/next.config.mjs @@ -1,5 +1,5 @@ import createNextIntlPlugin from "next-intl/plugin"; -import { DEFAULT_CSP } from "./src/lib/csp"; +import { DEFAULT_CSP } from "src/lib/csp"; const withNextIntl = createNextIntlPlugin(); From a31c17f5fa4c424bab251d92f2db869617c9e20e Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Wed, 30 Apr 2025 09:20:26 +0200 Subject: [PATCH 18/96] @ ns --- apps/login/next.config.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/login/next.config.mjs b/apps/login/next.config.mjs index 7fb0f5b65d..0820c0c555 100755 --- a/apps/login/next.config.mjs +++ b/apps/login/next.config.mjs @@ -1,5 +1,5 @@ import createNextIntlPlugin from "next-intl/plugin"; -import { DEFAULT_CSP } from "src/lib/csp"; +import { DEFAULT_CSP } from "@/lib/csp"; const withNextIntl = createNextIntlPlugin(); From a690b254c4858e3419f14968a621936ecc7cebed Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Wed, 30 Apr 2025 09:23:56 +0200 Subject: [PATCH 19/96] import --- apps/login/next.config.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/login/next.config.mjs b/apps/login/next.config.mjs index 0820c0c555..2795854114 100755 --- a/apps/login/next.config.mjs +++ b/apps/login/next.config.mjs @@ -1,5 +1,5 @@ import createNextIntlPlugin from "next-intl/plugin"; -import { DEFAULT_CSP } from "@/lib/csp"; +import { DEFAULT_CSP } from "./src/lib/csp"; const withNextIntl = createNextIntlPlugin(); From 77e9f6f2e90505236a2dbdd9125f6ba8aa06ce6e Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Wed, 30 Apr 2025 09:41:00 +0200 Subject: [PATCH 20/96] override cookie sameSite settings --- apps/login/src/lib/cookies.ts | 60 ++++++++++++++------- apps/login/src/lib/server/cookie.ts | 79 ++++++++++++++++++---------- apps/login/src/lib/server/session.ts | 11 +++- 3 files changed, 99 insertions(+), 51 deletions(-) diff --git a/apps/login/src/lib/cookies.ts b/apps/login/src/lib/cookies.ts index cf762b904f..76f5580a16 100644 --- a/apps/login/src/lib/cookies.ts +++ b/apps/login/src/lib/cookies.ts @@ -20,7 +20,10 @@ export type Cookie = { type SessionCookie = Cookie & T; -async function setSessionHttpOnlyCookie(sessions: SessionCookie[]) { +async function setSessionHttpOnlyCookie( + sessions: SessionCookie[], + sameSite: boolean | "lax" | "strict" | "none" = true, +) { const cookiesList = await cookies(); return cookiesList.set({ @@ -28,6 +31,7 @@ async function setSessionHttpOnlyCookie(sessions: SessionCookie[]) { value: JSON.stringify(sessions), httpOnly: true, path: "/", + sameSite, }); } @@ -42,10 +46,15 @@ export async function setLanguageCookie(language: string) { }); } -export async function addSessionToCookie( - session: SessionCookie, - cleanup: boolean = false, -): Promise { +export async function addSessionToCookie({ + session, + cleanup, + sameSite, +}: { + session: SessionCookie; + cleanup?: boolean; + sameSite?: boolean | "lax" | "strict" | "none" | undefined; +}): Promise { const cookiesList = await cookies(); const stringifiedCookie = cookiesList.get("sessions"); @@ -79,17 +88,23 @@ export async function addSessionToCookie( ? timestampDate(timestampFromMs(Number(session.expirationTs))) > now : true, ); - return setSessionHttpOnlyCookie(filteredSessions); + return setSessionHttpOnlyCookie(filteredSessions, sameSite); } else { - return setSessionHttpOnlyCookie(currentSessions); + return setSessionHttpOnlyCookie(currentSessions, sameSite); } } -export async function updateSessionCookie( - id: string, - session: SessionCookie, - cleanup: boolean = false, -): Promise { +export async function updateSessionCookie({ + id, + session, + cleanup, + sameSite, +}: { + id: string; + session: SessionCookie; + cleanup?: boolean; + sameSite?: boolean | "lax" | "strict" | "none" | undefined; +}): Promise { const cookiesList = await cookies(); const stringifiedCookie = cookiesList.get("sessions"); @@ -108,19 +123,24 @@ export async function updateSessionCookie( ? timestampDate(timestampFromMs(Number(session.expirationTs))) > now : true, ); - return setSessionHttpOnlyCookie(filteredSessions); + return setSessionHttpOnlyCookie(filteredSessions, sameSite); } else { - return setSessionHttpOnlyCookie(sessions); + return setSessionHttpOnlyCookie(sessions, sameSite); } } else { throw "updateSessionCookie: session id now found"; } } -export async function removeSessionFromCookie( - session: SessionCookie, - cleanup: boolean = false, -): Promise { +export async function removeSessionFromCookie({ + session, + cleanup, + sameSite, +}: { + session: SessionCookie; + cleanup?: boolean; + sameSite?: boolean | "lax" | "strict" | "none" | undefined; +}): Promise { const cookiesList = await cookies(); const stringifiedCookie = cookiesList.get("sessions"); @@ -136,9 +156,9 @@ export async function removeSessionFromCookie( ? timestampDate(timestampFromMs(Number(session.expirationTs))) > now : true, ); - return setSessionHttpOnlyCookie(filteredSessions); + return setSessionHttpOnlyCookie(filteredSessions, sameSite); } else { - return setSessionHttpOnlyCookie(reducedSessions); + return setSessionHttpOnlyCookie(reducedSessions, sameSite); } } diff --git a/apps/login/src/lib/server/cookie.ts b/apps/login/src/lib/server/cookie.ts index d54a4047b1..7cc86e9337 100644 --- a/apps/login/src/lib/server/cookie.ts +++ b/apps/login/src/lib/server/cookie.ts @@ -4,6 +4,7 @@ import { addSessionToCookie, updateSessionCookie } from "@/lib/cookies"; import { createSessionForUserIdAndIdpIntent, createSessionFromChecks, + getSecuritySettings, getSession, setSession, } from "@/lib/zitadel"; @@ -65,7 +66,7 @@ export async function createSessionAndUpdateCookie(command: { serviceUrl, sessionId: createdSession.sessionId, sessionToken: createdSession.sessionToken, - }).then((response) => { + }).then(async (response) => { if (response?.session && response.session?.factors?.user?.loginName) { const sessionCookie: CustomCookieData = { id: createdSession.sessionId, @@ -91,9 +92,14 @@ export async function createSessionAndUpdateCookie(command: { response.session.factors.user.organizationId; } - return addSessionToCookie(sessionCookie).then(() => { - return response.session as Session; - }); + const securitySettings = await getSecuritySettings({ serviceUrl }); + const sameSite = securitySettings?.embeddedIframe?.enabled + ? "none" + : true; + + await addSessionToCookie({ session: sessionCookie, sameSite }); + + return response.session as Session; } else { throw "could not get session or session does not have loginName"; } @@ -167,7 +173,10 @@ export async function createSessionForIdpAndUpdateCookie( sessionCookie.organization = session.factors.user.organizationId; } - return addSessionToCookie(sessionCookie).then(() => { + const securitySettings = await getSecuritySettings({ serviceUrl }); + const sameSite = securitySettings?.embeddedIframe?.enabled ? "none" : true; + + return addSessionToCookie({ session: sessionCookie, sameSite }).then(() => { return session as Session; }); } @@ -217,32 +226,44 @@ export async function setSessionAndUpdateCookie( serviceUrl, sessionId: sessionCookie.id, sessionToken: sessionCookie.token, - }).then((response) => { - if (response?.session && response.session.factors?.user?.loginName) { - const { session } = response; - const newCookie: CustomCookieData = { - id: sessionCookie.id, - token: updatedSession.sessionToken, - creationTs: sessionCookie.creationTs, - expirationTs: sessionCookie.expirationTs, - // just overwrite the changeDate with the new one - changeTs: updatedSession.details?.changeDate - ? `${timestampMs(updatedSession.details.changeDate)}` - : "", - loginName: session.factors?.user?.loginName ?? "", - organization: session.factors?.user?.organizationId ?? "", - }; - - if (sessionCookie.requestId) { - newCookie.requestId = sessionCookie.requestId; - } - - return updateSessionCookie(sessionCookie.id, newCookie).then(() => { - return { challenges: updatedSession.challenges, ...session }; - }); - } else { + }).then(async (response) => { + if ( + !response?.session || + !response.session.factors?.user?.loginName + ) { throw "could not get session or session does not have loginName"; } + + const { session } = response; + const newCookie: CustomCookieData = { + id: sessionCookie.id, + token: updatedSession.sessionToken, + creationTs: sessionCookie.creationTs, + expirationTs: sessionCookie.expirationTs, + // just overwrite the changeDate with the new one + changeTs: updatedSession.details?.changeDate + ? `${timestampMs(updatedSession.details.changeDate)}` + : "", + loginName: session.factors?.user?.loginName ?? "", + organization: session.factors?.user?.organizationId ?? "", + }; + + if (sessionCookie.requestId) { + newCookie.requestId = sessionCookie.requestId; + } + + const securitySettings = await getSecuritySettings({ serviceUrl }); + const sameSite = securitySettings?.embeddedIframe?.enabled + ? "none" + : true; + + return updateSessionCookie({ + id: sessionCookie.id, + session: newCookie, + sameSite, + }).then(() => { + return { challenges: updatedSession.challenges, ...session }; + }); }); } else { throw "Session not be set"; diff --git a/apps/login/src/lib/server/session.ts b/apps/login/src/lib/server/session.ts index 66688bf415..3ff3d14017 100644 --- a/apps/login/src/lib/server/session.ts +++ b/apps/login/src/lib/server/session.ts @@ -4,6 +4,7 @@ import { setSessionAndUpdateCookie } from "@/lib/server/cookie"; import { deleteSession, getLoginSettings, + getSecuritySettings, humanMFAInitSkipped, listAuthenticationMethodTypes, } from "@/lib/zitadel"; @@ -209,8 +210,11 @@ export async function clearSession(options: ClearSessionOptions) { sessionToken: session.token, }); + const securitySettings = await getSecuritySettings({ serviceUrl }); + const sameSite = securitySettings?.embeddedIframe?.enabled ? "none" : true; + if (deletedSession) { - return removeSessionFromCookie(session); + return removeSessionFromCookie({ session, sameSite }); } } @@ -230,9 +234,12 @@ export async function cleanupSession({ sessionId }: CleanupSessionCommand) { sessionToken: sessionCookie.token, }); + const securitySettings = await getSecuritySettings({ serviceUrl }); + const sameSite = securitySettings?.embeddedIframe?.enabled ? "none" : true; + if (!deleteResponse) { throw new Error("Could not delete session"); } - return removeSessionFromCookie(sessionCookie); + return removeSessionFromCookie({ session: sessionCookie, sameSite }); } From 43dff470bd41ef797a3726368b421057d2f06ef8 Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Wed, 30 Apr 2025 09:47:51 +0200 Subject: [PATCH 21/96] csp import --- apps/login/constants/csp.js | 2 ++ apps/login/next.config.mjs | 2 +- apps/login/src/app/login/route.ts | 2 +- apps/login/src/lib/csp.ts | 2 -- apps/login/src/middleware.ts | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) create mode 100644 apps/login/constants/csp.js delete mode 100644 apps/login/src/lib/csp.ts diff --git a/apps/login/constants/csp.js b/apps/login/constants/csp.js new file mode 100644 index 0000000000..21dc869a53 --- /dev/null +++ b/apps/login/constants/csp.js @@ -0,0 +1,2 @@ +export const DEFAULT_CSP = + "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://va.vercel-scripts.com; connect-src 'self'; child-src; style-src 'self' 'unsafe-inline'; font-src 'self'; object-src 'none'; img-src 'self' https://vercel.com;"; diff --git a/apps/login/next.config.mjs b/apps/login/next.config.mjs index 2795854114..edf5e54595 100755 --- a/apps/login/next.config.mjs +++ b/apps/login/next.config.mjs @@ -1,5 +1,5 @@ import createNextIntlPlugin from "next-intl/plugin"; -import { DEFAULT_CSP } from "./src/lib/csp"; +import { DEFAULT_CSP } from "./constants/csp.js"; const withNextIntl = createNextIntlPlugin(); diff --git a/apps/login/src/app/login/route.ts b/apps/login/src/app/login/route.ts index b3da6f863e..fb2f5e5f49 100644 --- a/apps/login/src/app/login/route.ts +++ b/apps/login/src/app/login/route.ts @@ -1,5 +1,4 @@ import { getAllSessions } from "@/lib/cookies"; -import { DEFAULT_CSP } from "@/lib/csp"; import { idpTypeToSlug } from "@/lib/idp"; import { loginWithOIDCandSession } from "@/lib/oidc"; import { loginWithSAMLandSession } from "@/lib/saml"; @@ -27,6 +26,7 @@ import { CreateResponseRequestSchema } from "@zitadel/proto/zitadel/saml/v2/saml import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb"; import { headers } from "next/headers"; import { NextRequest, NextResponse } from "next/server"; +import { DEFAULT_CSP } from "../../../constants/csp"; export const dynamic = "force-dynamic"; export const revalidate = false; diff --git a/apps/login/src/lib/csp.ts b/apps/login/src/lib/csp.ts deleted file mode 100644 index 5cc1e254f3..0000000000 --- a/apps/login/src/lib/csp.ts +++ /dev/null @@ -1,2 +0,0 @@ -export const DEFAULT_CSP = - "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://va.vercel-scripts.com; connect-src 'self'; child-src; style-src 'self' 'unsafe-inline'; font-src 'self'; object-src 'none'; img-src 'self' https://vercel.com;"; diff --git a/apps/login/src/middleware.ts b/apps/login/src/middleware.ts index 0572fe43b6..0621b2f5f2 100644 --- a/apps/login/src/middleware.ts +++ b/apps/login/src/middleware.ts @@ -1,6 +1,6 @@ import { headers } from "next/headers"; import { NextRequest, NextResponse } from "next/server"; -import { DEFAULT_CSP } from "./lib/csp"; +import { DEFAULT_CSP } from "../constants/csp"; import { getServiceUrlFromHeaders } from "./lib/service"; import { getSecuritySettings } from "./lib/zitadel"; From 0568aed6e0bfc443c53b139453036eae3f009c02 Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Wed, 30 Apr 2025 09:52:03 +0200 Subject: [PATCH 22/96] lint --- apps/login/constants/csp.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/login/constants/csp.js b/apps/login/constants/csp.js index 21dc869a53..5cc1e254f3 100644 --- a/apps/login/constants/csp.js +++ b/apps/login/constants/csp.js @@ -1,2 +1,2 @@ export const DEFAULT_CSP = - "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://va.vercel-scripts.com; connect-src 'self'; child-src; style-src 'self' 'unsafe-inline'; font-src 'self'; object-src 'none'; img-src 'self' https://vercel.com;"; + "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://va.vercel-scripts.com; connect-src 'self'; child-src; style-src 'self' 'unsafe-inline'; font-src 'self'; object-src 'none'; img-src 'self' https://vercel.com;"; From a6cf9a6db688aa23875a706716de9e6ecae62a27 Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Wed, 30 Apr 2025 09:58:29 +0200 Subject: [PATCH 23/96] empty security settings as default for integration tests --- .../initial-stubs/zitadel.settings.v2.SettingsService.json | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/apps/login/mock/initial-stubs/zitadel.settings.v2.SettingsService.json b/apps/login/mock/initial-stubs/zitadel.settings.v2.SettingsService.json index 07e9980f9b..3da4ae999f 100644 --- a/apps/login/mock/initial-stubs/zitadel.settings.v2.SettingsService.json +++ b/apps/login/mock/initial-stubs/zitadel.settings.v2.SettingsService.json @@ -6,6 +6,13 @@ "data": {} } }, + { + "service": "zitadel.settings.v2.SettingsService", + "method": "GetSecuritySettings", + "out": { + "data": {} + } + }, { "service": "zitadel.settings.v2.SettingsService", "method": "GetLegalAndSupportSettings", From 449e632766c7cb7a8dd95bdafe2c318b7c530ed2 Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Wed, 30 Apr 2025 10:05:24 +0200 Subject: [PATCH 24/96] remove fallback --- apps/login/src/middleware.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/apps/login/src/middleware.ts b/apps/login/src/middleware.ts index 0621b2f5f2..6184bee182 100644 --- a/apps/login/src/middleware.ts +++ b/apps/login/src/middleware.ts @@ -43,11 +43,6 @@ export async function middleware(request: NextRequest) { responseHeaders.set("Access-Control-Allow-Origin", "*"); responseHeaders.set("Access-Control-Allow-Headers", "*"); - responseHeaders.set( - "Content-Security-Policy", - `${DEFAULT_CSP} frame-ancestors 'none'`, - ); - if (securitySettings?.embeddedIframe?.enabled) { securitySettings.embeddedIframe.allowedOrigins; responseHeaders.set( From 9d43c3f6b8c925e28c7c3dfa3a4462f1f0448293 Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Wed, 30 Apr 2025 10:56:20 +0200 Subject: [PATCH 25/96] log policy, csp --- apps/login/src/middleware.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/login/src/middleware.ts b/apps/login/src/middleware.ts index 6184bee182..dd888f02e8 100644 --- a/apps/login/src/middleware.ts +++ b/apps/login/src/middleware.ts @@ -26,6 +26,8 @@ export async function middleware(request: NextRequest) { const securitySettings = await getSecuritySettings({ serviceUrl }); + console.log(securitySettings, DEFAULT_CSP); + const instanceHost = `${serviceUrl}` .replace("https://", "") .replace("http://", ""); From 06e69ace5e0c608ee92469d7ba7445bc0760118e Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Wed, 30 Apr 2025 11:08:21 +0200 Subject: [PATCH 26/96] log --- apps/login/src/middleware.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/login/src/middleware.ts b/apps/login/src/middleware.ts index dd888f02e8..4ae8e2a47c 100644 --- a/apps/login/src/middleware.ts +++ b/apps/login/src/middleware.ts @@ -24,9 +24,11 @@ export async function middleware(request: NextRequest) { const { serviceUrl } = getServiceUrlFromHeaders(_headers); + console.log("defaultCSP", DEFAULT_CSP); + const securitySettings = await getSecuritySettings({ serviceUrl }); - console.log(securitySettings, DEFAULT_CSP); + console.log("securitySettings", securitySettings); const instanceHost = `${serviceUrl}` .replace("https://", "") From 4cca720f052f19323bb6ef1118726046d205b7d1 Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Wed, 30 Apr 2025 13:20:03 +0200 Subject: [PATCH 27/96] route handler for middleware --- apps/login/src/app/security/route.ts | 27 +++++++++++++++++++++++++++ apps/login/src/middleware.ts | 14 ++++++++++++-- 2 files changed, 39 insertions(+), 2 deletions(-) create mode 100644 apps/login/src/app/security/route.ts diff --git a/apps/login/src/app/security/route.ts b/apps/login/src/app/security/route.ts new file mode 100644 index 0000000000..e89a609e52 --- /dev/null +++ b/apps/login/src/app/security/route.ts @@ -0,0 +1,27 @@ +import { createServiceForHost, getServiceUrlFromHeaders } from "@/lib/service"; +import { Client } from "@zitadel/client"; +import { SettingsService } from "@zitadel/proto/zitadel/settings/v2/settings_service_pb"; +import { headers } from "next/headers"; +import { NextRequest, NextResponse } from "next/server"; + +export async function GET(request: NextRequest) { + const _headers = await headers(); + const { serviceUrl } = getServiceUrlFromHeaders(_headers); + + const settingsService: Client = + await createServiceForHost(SettingsService, serviceUrl); + + const settings = settingsService + .getSecuritySettings({}) + .then((resp) => (resp.settings ? resp.settings : undefined)); + + const response = NextResponse.json({ settings }, { status: 200 }); + + // Add Cache-Control header to cache the response for up to 1 hour + response.headers.set( + "Cache-Control", + "public, max-age=3600, stale-while-revalidate=86400", + ); + + return response; +} diff --git a/apps/login/src/middleware.ts b/apps/login/src/middleware.ts index 4ae8e2a47c..22dc143790 100644 --- a/apps/login/src/middleware.ts +++ b/apps/login/src/middleware.ts @@ -2,7 +2,6 @@ import { headers } from "next/headers"; import { NextRequest, NextResponse } from "next/server"; import { DEFAULT_CSP } from "../constants/csp"; import { getServiceUrlFromHeaders } from "./lib/service"; -import { getSecuritySettings } from "./lib/zitadel"; export const config = { matcher: [ @@ -26,8 +25,19 @@ export async function middleware(request: NextRequest) { console.log("defaultCSP", DEFAULT_CSP); - const securitySettings = await getSecuritySettings({ serviceUrl }); + // Call the /security route handler + // TODO check this on cloud run deployment + const securityResponse = await fetch(`${request.nextUrl.origin}/security`); + if (!securityResponse.ok) { + console.error( + "Failed to fetch security settings:", + securityResponse.statusText, + ); + return NextResponse.next(); // Fallback if the request fails + } + + const { settings: securitySettings } = await securityResponse.json(); console.log("securitySettings", securitySettings); const instanceHost = `${serviceUrl}` From 65da744d9a202ac1ad5b6f6778ed69799015ee69 Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Wed, 30 Apr 2025 13:27:50 +0200 Subject: [PATCH 28/96] fix zlib export --- apps/login/src/app/(login)/accounts/page.tsx | 2 +- .../app/(login)/authenticator/set/page.tsx | 2 +- .../(login)/idp/[provider]/failure/page.tsx | 2 +- .../(login)/idp/[provider]/success/page.tsx | 2 +- apps/login/src/app/(login)/idp/page.tsx | 2 +- apps/login/src/app/(login)/invite/page.tsx | 2 +- .../src/app/(login)/invite/success/page.tsx | 2 +- apps/login/src/app/(login)/loginname/page.tsx | 2 +- apps/login/src/app/(login)/mfa/page.tsx | 2 +- apps/login/src/app/(login)/mfa/set/page.tsx | 2 +- .../src/app/(login)/otp/[method]/page.tsx | 2 +- .../src/app/(login)/otp/[method]/set/page.tsx | 2 +- apps/login/src/app/(login)/passkey/page.tsx | 2 +- .../src/app/(login)/passkey/set/page.tsx | 2 +- .../src/app/(login)/password/change/page.tsx | 2 +- apps/login/src/app/(login)/password/page.tsx | 2 +- .../src/app/(login)/password/set/page.tsx | 2 +- apps/login/src/app/(login)/register/page.tsx | 2 +- .../app/(login)/register/password/page.tsx | 2 +- apps/login/src/app/(login)/signedin/page.tsx | 2 +- apps/login/src/app/(login)/u2f/page.tsx | 2 +- apps/login/src/app/(login)/u2f/set/page.tsx | 2 +- apps/login/src/app/(login)/verify/page.tsx | 2 +- apps/login/src/app/security/route.ts | 3 +- apps/login/src/lib/service-url.ts | 58 +++++++++++++++++++ apps/login/src/lib/service.ts | 44 -------------- apps/login/src/middleware.ts | 2 +- 27 files changed, 84 insertions(+), 69 deletions(-) create mode 100644 apps/login/src/lib/service-url.ts diff --git a/apps/login/src/app/(login)/accounts/page.tsx b/apps/login/src/app/(login)/accounts/page.tsx index 6a57f05cee..32c88d17bf 100644 --- a/apps/login/src/app/(login)/accounts/page.tsx +++ b/apps/login/src/app/(login)/accounts/page.tsx @@ -1,7 +1,7 @@ import { DynamicTheme } from "@/components/dynamic-theme"; import { SessionsList } from "@/components/sessions-list"; import { getAllSessionCookieIds } from "@/lib/cookies"; -import { getServiceUrlFromHeaders } from "@/lib/service"; +import { getServiceUrlFromHeaders } from "@/lib/service-url"; import { getBrandingSettings, getDefaultOrg, diff --git a/apps/login/src/app/(login)/authenticator/set/page.tsx b/apps/login/src/app/(login)/authenticator/set/page.tsx index 8904eff963..3e1b49eed0 100644 --- a/apps/login/src/app/(login)/authenticator/set/page.tsx +++ b/apps/login/src/app/(login)/authenticator/set/page.tsx @@ -5,7 +5,7 @@ import { DynamicTheme } from "@/components/dynamic-theme"; import { SignInWithIdp } from "@/components/sign-in-with-idp"; import { UserAvatar } from "@/components/user-avatar"; import { getSessionCookieById } from "@/lib/cookies"; -import { getServiceUrlFromHeaders } from "@/lib/service"; +import { getServiceUrlFromHeaders } from "@/lib/service-url"; import { loadMostRecentSession } from "@/lib/session"; import { getActiveIdentityProviders, diff --git a/apps/login/src/app/(login)/idp/[provider]/failure/page.tsx b/apps/login/src/app/(login)/idp/[provider]/failure/page.tsx index 82c7224a92..de6ad858d9 100644 --- a/apps/login/src/app/(login)/idp/[provider]/failure/page.tsx +++ b/apps/login/src/app/(login)/idp/[provider]/failure/page.tsx @@ -2,7 +2,7 @@ import { Alert, AlertType } from "@/components/alert"; import { ChooseAuthenticatorToLogin } from "@/components/choose-authenticator-to-login"; import { DynamicTheme } from "@/components/dynamic-theme"; import { UserAvatar } from "@/components/user-avatar"; -import { getServiceUrlFromHeaders } from "@/lib/service"; +import { getServiceUrlFromHeaders } from "@/lib/service-url"; import { getBrandingSettings, getLoginSettings, diff --git a/apps/login/src/app/(login)/idp/[provider]/success/page.tsx b/apps/login/src/app/(login)/idp/[provider]/success/page.tsx index 00fd9593c0..2c9724f13a 100644 --- a/apps/login/src/app/(login)/idp/[provider]/success/page.tsx +++ b/apps/login/src/app/(login)/idp/[provider]/success/page.tsx @@ -4,7 +4,7 @@ import { linkingFailed } from "@/components/idps/pages/linking-failed"; import { linkingSuccess } from "@/components/idps/pages/linking-success"; import { loginFailed } from "@/components/idps/pages/login-failed"; import { loginSuccess } from "@/components/idps/pages/login-success"; -import { getServiceUrlFromHeaders } from "@/lib/service"; +import { getServiceUrlFromHeaders } from "@/lib/service-url"; import { addHuman, addIDPLink, diff --git a/apps/login/src/app/(login)/idp/page.tsx b/apps/login/src/app/(login)/idp/page.tsx index e8c63512d2..db492cb79c 100644 --- a/apps/login/src/app/(login)/idp/page.tsx +++ b/apps/login/src/app/(login)/idp/page.tsx @@ -1,6 +1,6 @@ import { DynamicTheme } from "@/components/dynamic-theme"; import { SignInWithIdp } from "@/components/sign-in-with-idp"; -import { getServiceUrlFromHeaders } from "@/lib/service"; +import { getServiceUrlFromHeaders } from "@/lib/service-url"; import { getActiveIdentityProviders, getBrandingSettings } from "@/lib/zitadel"; import { getLocale, getTranslations } from "next-intl/server"; import { headers } from "next/headers"; diff --git a/apps/login/src/app/(login)/invite/page.tsx b/apps/login/src/app/(login)/invite/page.tsx index ed29624d0e..11e9d732ee 100644 --- a/apps/login/src/app/(login)/invite/page.tsx +++ b/apps/login/src/app/(login)/invite/page.tsx @@ -1,7 +1,7 @@ import { Alert, AlertType } from "@/components/alert"; import { DynamicTheme } from "@/components/dynamic-theme"; import { InviteForm } from "@/components/invite-form"; -import { getServiceUrlFromHeaders } from "@/lib/service"; +import { getServiceUrlFromHeaders } from "@/lib/service-url"; import { getBrandingSettings, getDefaultOrg, diff --git a/apps/login/src/app/(login)/invite/success/page.tsx b/apps/login/src/app/(login)/invite/success/page.tsx index 8a2cfb96e9..1b12a5b903 100644 --- a/apps/login/src/app/(login)/invite/success/page.tsx +++ b/apps/login/src/app/(login)/invite/success/page.tsx @@ -2,7 +2,7 @@ import { Alert, AlertType } from "@/components/alert"; import { Button, ButtonVariants } from "@/components/button"; import { DynamicTheme } from "@/components/dynamic-theme"; import { UserAvatar } from "@/components/user-avatar"; -import { getServiceUrlFromHeaders } from "@/lib/service"; +import { getServiceUrlFromHeaders } from "@/lib/service-url"; import { getBrandingSettings, getDefaultOrg, getUserByID } from "@/lib/zitadel"; import { HumanUser, User } from "@zitadel/proto/zitadel/user/v2/user_pb"; import { getLocale, getTranslations } from "next-intl/server"; diff --git a/apps/login/src/app/(login)/loginname/page.tsx b/apps/login/src/app/(login)/loginname/page.tsx index e3406b5fee..79372729c4 100644 --- a/apps/login/src/app/(login)/loginname/page.tsx +++ b/apps/login/src/app/(login)/loginname/page.tsx @@ -1,7 +1,7 @@ import { DynamicTheme } from "@/components/dynamic-theme"; import { SignInWithIdp } from "@/components/sign-in-with-idp"; import { UsernameForm } from "@/components/username-form"; -import { getServiceUrlFromHeaders } from "@/lib/service"; +import { getServiceUrlFromHeaders } from "@/lib/service-url"; import { getActiveIdentityProviders, getBrandingSettings, diff --git a/apps/login/src/app/(login)/mfa/page.tsx b/apps/login/src/app/(login)/mfa/page.tsx index 2838457b30..c65d6d3058 100644 --- a/apps/login/src/app/(login)/mfa/page.tsx +++ b/apps/login/src/app/(login)/mfa/page.tsx @@ -4,7 +4,7 @@ import { ChooseSecondFactor } from "@/components/choose-second-factor"; import { DynamicTheme } from "@/components/dynamic-theme"; import { UserAvatar } from "@/components/user-avatar"; import { getSessionCookieById } from "@/lib/cookies"; -import { getServiceUrlFromHeaders } from "@/lib/service"; +import { getServiceUrlFromHeaders } from "@/lib/service-url"; import { loadMostRecentSession } from "@/lib/session"; import { getBrandingSettings, diff --git a/apps/login/src/app/(login)/mfa/set/page.tsx b/apps/login/src/app/(login)/mfa/set/page.tsx index c198ed9f2c..c7f2fa6599 100644 --- a/apps/login/src/app/(login)/mfa/set/page.tsx +++ b/apps/login/src/app/(login)/mfa/set/page.tsx @@ -4,7 +4,7 @@ import { ChooseSecondFactorToSetup } from "@/components/choose-second-factor-to- import { DynamicTheme } from "@/components/dynamic-theme"; import { UserAvatar } from "@/components/user-avatar"; import { getSessionCookieById } from "@/lib/cookies"; -import { getServiceUrlFromHeaders } from "@/lib/service"; +import { getServiceUrlFromHeaders } from "@/lib/service-url"; import { loadMostRecentSession } from "@/lib/session"; import { getBrandingSettings, diff --git a/apps/login/src/app/(login)/otp/[method]/page.tsx b/apps/login/src/app/(login)/otp/[method]/page.tsx index f566f2b0e6..ee58420c42 100644 --- a/apps/login/src/app/(login)/otp/[method]/page.tsx +++ b/apps/login/src/app/(login)/otp/[method]/page.tsx @@ -3,7 +3,7 @@ import { DynamicTheme } from "@/components/dynamic-theme"; import { LoginOTP } from "@/components/login-otp"; import { UserAvatar } from "@/components/user-avatar"; import { getSessionCookieById } from "@/lib/cookies"; -import { getServiceUrlFromHeaders } from "@/lib/service"; +import { getServiceUrlFromHeaders } from "@/lib/service-url"; import { loadMostRecentSession } from "@/lib/session"; import { getBrandingSettings, diff --git a/apps/login/src/app/(login)/otp/[method]/set/page.tsx b/apps/login/src/app/(login)/otp/[method]/set/page.tsx index b16dd177a9..d3fc4c89f7 100644 --- a/apps/login/src/app/(login)/otp/[method]/set/page.tsx +++ b/apps/login/src/app/(login)/otp/[method]/set/page.tsx @@ -4,7 +4,7 @@ import { Button, ButtonVariants } from "@/components/button"; import { DynamicTheme } from "@/components/dynamic-theme"; import { TotpRegister } from "@/components/totp-register"; import { UserAvatar } from "@/components/user-avatar"; -import { getServiceUrlFromHeaders } from "@/lib/service"; +import { getServiceUrlFromHeaders } from "@/lib/service-url"; import { loadMostRecentSession } from "@/lib/session"; import { addOTPEmail, diff --git a/apps/login/src/app/(login)/passkey/page.tsx b/apps/login/src/app/(login)/passkey/page.tsx index cef7032a54..e24585e7e0 100644 --- a/apps/login/src/app/(login)/passkey/page.tsx +++ b/apps/login/src/app/(login)/passkey/page.tsx @@ -3,7 +3,7 @@ import { DynamicTheme } from "@/components/dynamic-theme"; import { LoginPasskey } from "@/components/login-passkey"; import { UserAvatar } from "@/components/user-avatar"; import { getSessionCookieById } from "@/lib/cookies"; -import { getServiceUrlFromHeaders } from "@/lib/service"; +import { getServiceUrlFromHeaders } from "@/lib/service-url"; import { loadMostRecentSession } from "@/lib/session"; import { getBrandingSettings, getSession } from "@/lib/zitadel"; import { getLocale, getTranslations } from "next-intl/server"; diff --git a/apps/login/src/app/(login)/passkey/set/page.tsx b/apps/login/src/app/(login)/passkey/set/page.tsx index b9745bdd68..dad3749fd7 100644 --- a/apps/login/src/app/(login)/passkey/set/page.tsx +++ b/apps/login/src/app/(login)/passkey/set/page.tsx @@ -2,7 +2,7 @@ import { Alert, AlertType } from "@/components/alert"; import { DynamicTheme } from "@/components/dynamic-theme"; import { RegisterPasskey } from "@/components/register-passkey"; import { UserAvatar } from "@/components/user-avatar"; -import { getServiceUrlFromHeaders } from "@/lib/service"; +import { getServiceUrlFromHeaders } from "@/lib/service-url"; import { loadMostRecentSession } from "@/lib/session"; import { getBrandingSettings } from "@/lib/zitadel"; import { getLocale, getTranslations } from "next-intl/server"; diff --git a/apps/login/src/app/(login)/password/change/page.tsx b/apps/login/src/app/(login)/password/change/page.tsx index f0ab94d315..05f8cd6a10 100644 --- a/apps/login/src/app/(login)/password/change/page.tsx +++ b/apps/login/src/app/(login)/password/change/page.tsx @@ -2,7 +2,7 @@ import { Alert } from "@/components/alert"; import { ChangePasswordForm } from "@/components/change-password-form"; import { DynamicTheme } from "@/components/dynamic-theme"; import { UserAvatar } from "@/components/user-avatar"; -import { getServiceUrlFromHeaders } from "@/lib/service"; +import { getServiceUrlFromHeaders } from "@/lib/service-url"; import { loadMostRecentSession } from "@/lib/session"; import { getBrandingSettings, diff --git a/apps/login/src/app/(login)/password/page.tsx b/apps/login/src/app/(login)/password/page.tsx index 64f2709009..506454a275 100644 --- a/apps/login/src/app/(login)/password/page.tsx +++ b/apps/login/src/app/(login)/password/page.tsx @@ -2,7 +2,7 @@ import { Alert } from "@/components/alert"; import { DynamicTheme } from "@/components/dynamic-theme"; import { PasswordForm } from "@/components/password-form"; import { UserAvatar } from "@/components/user-avatar"; -import { getServiceUrlFromHeaders } from "@/lib/service"; +import { getServiceUrlFromHeaders } from "@/lib/service-url"; import { loadMostRecentSession } from "@/lib/session"; import { getBrandingSettings, diff --git a/apps/login/src/app/(login)/password/set/page.tsx b/apps/login/src/app/(login)/password/set/page.tsx index 2205f5e0a5..26e065438c 100644 --- a/apps/login/src/app/(login)/password/set/page.tsx +++ b/apps/login/src/app/(login)/password/set/page.tsx @@ -2,7 +2,7 @@ import { Alert, AlertType } from "@/components/alert"; import { DynamicTheme } from "@/components/dynamic-theme"; import { SetPasswordForm } from "@/components/set-password-form"; import { UserAvatar } from "@/components/user-avatar"; -import { getServiceUrlFromHeaders } from "@/lib/service"; +import { getServiceUrlFromHeaders } from "@/lib/service-url"; import { loadMostRecentSession } from "@/lib/session"; import { getBrandingSettings, diff --git a/apps/login/src/app/(login)/register/page.tsx b/apps/login/src/app/(login)/register/page.tsx index 7a69755f4d..e50511edb1 100644 --- a/apps/login/src/app/(login)/register/page.tsx +++ b/apps/login/src/app/(login)/register/page.tsx @@ -1,6 +1,6 @@ import { DynamicTheme } from "@/components/dynamic-theme"; import { RegisterForm } from "@/components/register-form"; -import { getServiceUrlFromHeaders } from "@/lib/service"; +import { getServiceUrlFromHeaders } from "@/lib/service-url"; import { getBrandingSettings, getDefaultOrg, diff --git a/apps/login/src/app/(login)/register/password/page.tsx b/apps/login/src/app/(login)/register/password/page.tsx index 8a2c0e1649..ee6fa03e59 100644 --- a/apps/login/src/app/(login)/register/password/page.tsx +++ b/apps/login/src/app/(login)/register/password/page.tsx @@ -1,6 +1,6 @@ import { DynamicTheme } from "@/components/dynamic-theme"; import { SetRegisterPasswordForm } from "@/components/set-register-password-form"; -import { getServiceUrlFromHeaders } from "@/lib/service"; +import { getServiceUrlFromHeaders } from "@/lib/service-url"; import { getBrandingSettings, getDefaultOrg, diff --git a/apps/login/src/app/(login)/signedin/page.tsx b/apps/login/src/app/(login)/signedin/page.tsx index 8c5c5486ac..230890bf88 100644 --- a/apps/login/src/app/(login)/signedin/page.tsx +++ b/apps/login/src/app/(login)/signedin/page.tsx @@ -3,7 +3,7 @@ import { DynamicTheme } from "@/components/dynamic-theme"; import { SelfServiceMenu } from "@/components/self-service-menu"; import { UserAvatar } from "@/components/user-avatar"; import { getMostRecentCookieWithLoginname } from "@/lib/cookies"; -import { getServiceUrlFromHeaders } from "@/lib/service"; +import { getServiceUrlFromHeaders } from "@/lib/service-url"; import { createCallback, createResponse, diff --git a/apps/login/src/app/(login)/u2f/page.tsx b/apps/login/src/app/(login)/u2f/page.tsx index bf8bd7ce37..b16dc88f4b 100644 --- a/apps/login/src/app/(login)/u2f/page.tsx +++ b/apps/login/src/app/(login)/u2f/page.tsx @@ -3,7 +3,7 @@ import { DynamicTheme } from "@/components/dynamic-theme"; import { LoginPasskey } from "@/components/login-passkey"; import { UserAvatar } from "@/components/user-avatar"; import { getSessionCookieById } from "@/lib/cookies"; -import { getServiceUrlFromHeaders } from "@/lib/service"; +import { getServiceUrlFromHeaders } from "@/lib/service-url"; import { loadMostRecentSession } from "@/lib/session"; import { getBrandingSettings, getSession } from "@/lib/zitadel"; import { getLocale, getTranslations } from "next-intl/server"; diff --git a/apps/login/src/app/(login)/u2f/set/page.tsx b/apps/login/src/app/(login)/u2f/set/page.tsx index 0119502ffd..b31b87c202 100644 --- a/apps/login/src/app/(login)/u2f/set/page.tsx +++ b/apps/login/src/app/(login)/u2f/set/page.tsx @@ -2,7 +2,7 @@ import { Alert } from "@/components/alert"; import { DynamicTheme } from "@/components/dynamic-theme"; import { RegisterU2f } from "@/components/register-u2f"; import { UserAvatar } from "@/components/user-avatar"; -import { getServiceUrlFromHeaders } from "@/lib/service"; +import { getServiceUrlFromHeaders } from "@/lib/service-url"; import { loadMostRecentSession } from "@/lib/session"; import { getBrandingSettings } from "@/lib/zitadel"; import { getLocale, getTranslations } from "next-intl/server"; diff --git a/apps/login/src/app/(login)/verify/page.tsx b/apps/login/src/app/(login)/verify/page.tsx index 1464d5185a..198a46a5fe 100644 --- a/apps/login/src/app/(login)/verify/page.tsx +++ b/apps/login/src/app/(login)/verify/page.tsx @@ -4,7 +4,7 @@ import { UserAvatar } from "@/components/user-avatar"; import { VerifyForm } from "@/components/verify-form"; import { VerifyRedirectButton } from "@/components/verify-redirect-button"; import { sendEmailCode } from "@/lib/server/verify"; -import { getServiceUrlFromHeaders } from "@/lib/service"; +import { getServiceUrlFromHeaders } from "@/lib/service-url"; import { loadMostRecentSession } from "@/lib/session"; import { getBrandingSettings, diff --git a/apps/login/src/app/security/route.ts b/apps/login/src/app/security/route.ts index e89a609e52..d47daaa40e 100644 --- a/apps/login/src/app/security/route.ts +++ b/apps/login/src/app/security/route.ts @@ -1,4 +1,5 @@ -import { createServiceForHost, getServiceUrlFromHeaders } from "@/lib/service"; +import { createServiceForHost } from "@/lib/service"; +import { getServiceUrlFromHeaders } from "@/lib/service-url"; import { Client } from "@zitadel/client"; import { SettingsService } from "@zitadel/proto/zitadel/settings/v2/settings_service_pb"; import { headers } from "next/headers"; diff --git a/apps/login/src/lib/service-url.ts b/apps/login/src/lib/service-url.ts new file mode 100644 index 0000000000..e74ee1f333 --- /dev/null +++ b/apps/login/src/lib/service-url.ts @@ -0,0 +1,58 @@ +import { ReadonlyHeaders } from "next/dist/server/web/spec-extension/adapters/headers"; +import { NextRequest } from "next/server"; + +/** + * Extracts the service url and region from the headers if used in a multitenant context (host, x-zitadel-forward-host header) + * or falls back to the ZITADEL_API_URL for a self hosting deployment + * or falls back to the host header for a self hosting deployment using custom domains + * @param headers + * @returns the service url and region from the headers + * @throws if the service url could not be determined + * + */ +export function getServiceUrlFromHeaders(headers: ReadonlyHeaders): { + serviceUrl: string; +} { + let instanceUrl; + + const forwardedHost = headers.get("x-zitadel-forward-host"); + // use the forwarded host if available (multitenant), otherwise fall back to the host of the deployment itself + if (forwardedHost) { + instanceUrl = forwardedHost; + instanceUrl = instanceUrl.startsWith("http://") + ? instanceUrl + : `https://${instanceUrl}`; + } else if (process.env.ZITADEL_API_URL) { + instanceUrl = process.env.ZITADEL_API_URL; + } else { + const host = headers.get("host"); + + if (host) { + const [hostname, port] = host.split(":"); + if (hostname !== "localhost") { + instanceUrl = host.startsWith("http") ? host : `https://${host}`; + } + } + } + + if (!instanceUrl) { + throw new Error("Service URL could not be determined"); + } + + return { + serviceUrl: instanceUrl, + }; +} + +export function constructUrl(request: NextRequest, path: string) { + const forwardedProto = request.headers.get("x-forwarded-proto") + ? `${request.headers.get("x-forwarded-proto")}:` + : request.nextUrl.protocol; + + const forwardedHost = + request.headers.get("x-zitadel-forward-host") ?? + request.headers.get("x-forwarded-host") ?? + request.headers.get("host"); + const basePath = process.env.NEXT_PUBLIC_BASE_PATH || ""; + return new URL(`${basePath}${path}`, `${forwardedProto}//${forwardedHost}`); +} diff --git a/apps/login/src/lib/service.ts b/apps/login/src/lib/service.ts index ec01c619f5..97143cf003 100644 --- a/apps/login/src/lib/service.ts +++ b/apps/login/src/lib/service.ts @@ -7,7 +7,6 @@ import { SAMLService } from "@zitadel/proto/zitadel/saml/v2/saml_service_pb"; import { SessionService } from "@zitadel/proto/zitadel/session/v2/session_service_pb"; import { SettingsService } from "@zitadel/proto/zitadel/settings/v2/settings_service_pb"; import { UserService } from "@zitadel/proto/zitadel/user/v2/user_service_pb"; -import { ReadonlyHeaders } from "next/dist/server/web/spec-extension/adapters/headers"; import { NextRequest } from "next/server"; import { systemAPIToken } from "./api"; @@ -67,49 +66,6 @@ export async function createServiceForHost( return createClientFor(service)(transport); } -/** - * Extracts the service url and region from the headers if used in a multitenant context (host, x-zitadel-forward-host header) - * or falls back to the ZITADEL_API_URL for a self hosting deployment - * or falls back to the host header for a self hosting deployment using custom domains - * @param headers - * @returns the service url and region from the headers - * @throws if the service url could not be determined - * - */ -export function getServiceUrlFromHeaders(headers: ReadonlyHeaders): { - serviceUrl: string; -} { - let instanceUrl; - - const forwardedHost = headers.get("x-zitadel-forward-host"); - // use the forwarded host if available (multitenant), otherwise fall back to the host of the deployment itself - if (forwardedHost) { - instanceUrl = forwardedHost; - instanceUrl = instanceUrl.startsWith("http://") - ? instanceUrl - : `https://${instanceUrl}`; - } else if (process.env.ZITADEL_API_URL) { - instanceUrl = process.env.ZITADEL_API_URL; - } else { - const host = headers.get("host"); - - if (host) { - const [hostname, port] = host.split(":"); - if (hostname !== "localhost") { - instanceUrl = host.startsWith("http") ? host : `https://${host}`; - } - } - } - - if (!instanceUrl) { - throw new Error("Service URL could not be determined"); - } - - return { - serviceUrl: instanceUrl, - }; -} - export function constructUrl(request: NextRequest, path: string) { const forwardedProto = request.headers.get("x-forwarded-proto") ? `${request.headers.get("x-forwarded-proto")}:` diff --git a/apps/login/src/middleware.ts b/apps/login/src/middleware.ts index 22dc143790..fc02d859c6 100644 --- a/apps/login/src/middleware.ts +++ b/apps/login/src/middleware.ts @@ -1,7 +1,7 @@ import { headers } from "next/headers"; import { NextRequest, NextResponse } from "next/server"; import { DEFAULT_CSP } from "../constants/csp"; -import { getServiceUrlFromHeaders } from "./lib/service"; +import { getServiceUrlFromHeaders } from "./lib/service-url"; export const config = { matcher: [ From 54cb3b086e1e04006113db73041dad5575c136a8 Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Wed, 30 Apr 2025 13:31:05 +0200 Subject: [PATCH 29/96] fix imports --- apps/login/src/lib/server/cookie.ts | 2 +- apps/login/src/lib/server/idp.ts | 2 +- apps/login/src/lib/server/invite.ts | 2 +- apps/login/src/lib/server/loginname.ts | 2 +- apps/login/src/lib/server/otp.ts | 2 +- apps/login/src/lib/server/passkeys.ts | 2 +- apps/login/src/lib/server/password.ts | 2 +- apps/login/src/lib/server/register.ts | 2 +- apps/login/src/lib/server/session.ts | 2 +- apps/login/src/lib/server/u2f.ts | 2 +- apps/login/src/lib/server/verify.ts | 2 +- 11 files changed, 11 insertions(+), 11 deletions(-) diff --git a/apps/login/src/lib/server/cookie.ts b/apps/login/src/lib/server/cookie.ts index 7cc86e9337..88e0b48290 100644 --- a/apps/login/src/lib/server/cookie.ts +++ b/apps/login/src/lib/server/cookie.ts @@ -21,7 +21,7 @@ import { import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb"; import { Checks } from "@zitadel/proto/zitadel/session/v2/session_service_pb"; import { headers } from "next/headers"; -import { getServiceUrlFromHeaders } from "../service"; +import { getServiceUrlFromHeaders } from "../service-url"; type CustomCookieData = { id: string; diff --git a/apps/login/src/lib/server/idp.ts b/apps/login/src/lib/server/idp.ts index e6861a60c4..5cac537690 100644 --- a/apps/login/src/lib/server/idp.ts +++ b/apps/login/src/lib/server/idp.ts @@ -8,7 +8,7 @@ import { import { headers } from "next/headers"; import { redirect } from "next/navigation"; import { getNextUrl } from "../client"; -import { getServiceUrlFromHeaders } from "../service"; +import { getServiceUrlFromHeaders } from "../service-url"; import { checkEmailVerification } from "../verify-helper"; import { createSessionForIdpAndUpdateCookie } from "./cookie"; diff --git a/apps/login/src/lib/server/invite.ts b/apps/login/src/lib/server/invite.ts index 9821336099..c0fc63fef5 100644 --- a/apps/login/src/lib/server/invite.ts +++ b/apps/login/src/lib/server/invite.ts @@ -3,7 +3,7 @@ import { addHumanUser, createInviteCode } from "@/lib/zitadel"; import { Factors } from "@zitadel/proto/zitadel/session/v2/session_pb"; import { headers } from "next/headers"; -import { getServiceUrlFromHeaders } from "../service"; +import { getServiceUrlFromHeaders } from "../service-url"; type InviteUserCommand = { email: string; diff --git a/apps/login/src/lib/server/loginname.ts b/apps/login/src/lib/server/loginname.ts index 78aa455483..2ea6004fdc 100644 --- a/apps/login/src/lib/server/loginname.ts +++ b/apps/login/src/lib/server/loginname.ts @@ -8,7 +8,7 @@ import { idpTypeToIdentityProviderType, idpTypeToSlug } from "../idp"; import { PasskeysType } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb"; import { UserState } from "@zitadel/proto/zitadel/user/v2/user_pb"; -import { getServiceUrlFromHeaders } from "../service"; +import { getServiceUrlFromHeaders } from "../service-url"; import { checkInvite } from "../verify-helper"; import { getActiveIdentityProviders, diff --git a/apps/login/src/lib/server/otp.ts b/apps/login/src/lib/server/otp.ts index 77def53af6..f3d4a1536a 100644 --- a/apps/login/src/lib/server/otp.ts +++ b/apps/login/src/lib/server/otp.ts @@ -13,7 +13,7 @@ import { getSessionCookieById, getSessionCookieByLoginName, } from "../cookies"; -import { getServiceUrlFromHeaders } from "../service"; +import { getServiceUrlFromHeaders } from "../service-url"; import { getLoginSettings } from "../zitadel"; export type SetOTPCommand = { diff --git a/apps/login/src/lib/server/passkeys.ts b/apps/login/src/lib/server/passkeys.ts index bbed8e9175..73d12043b0 100644 --- a/apps/login/src/lib/server/passkeys.ts +++ b/apps/login/src/lib/server/passkeys.ts @@ -22,7 +22,7 @@ import { getSessionCookieById, getSessionCookieByLoginName, } from "../cookies"; -import { getServiceUrlFromHeaders } from "../service"; +import { getServiceUrlFromHeaders } from "../service-url"; import { checkEmailVerification } from "../verify-helper"; import { setSessionAndUpdateCookie } from "./cookie"; diff --git a/apps/login/src/lib/server/password.ts b/apps/login/src/lib/server/password.ts index 3fff193851..9e256c71d9 100644 --- a/apps/login/src/lib/server/password.ts +++ b/apps/login/src/lib/server/password.ts @@ -32,7 +32,7 @@ import { import { headers } from "next/headers"; import { getNextUrl } from "../client"; import { getSessionCookieById, getSessionCookieByLoginName } from "../cookies"; -import { getServiceUrlFromHeaders } from "../service"; +import { getServiceUrlFromHeaders } from "../service-url"; import { checkEmailVerification, checkMFAFactors, diff --git a/apps/login/src/lib/server/register.ts b/apps/login/src/lib/server/register.ts index 59f6d00171..25bea33527 100644 --- a/apps/login/src/lib/server/register.ts +++ b/apps/login/src/lib/server/register.ts @@ -10,7 +10,7 @@ import { } from "@zitadel/proto/zitadel/session/v2/session_service_pb"; import { headers } from "next/headers"; import { getNextUrl } from "../client"; -import { getServiceUrlFromHeaders } from "../service"; +import { getServiceUrlFromHeaders } from "../service-url"; import { checkEmailVerification } from "../verify-helper"; type RegisterUserCommand = { diff --git a/apps/login/src/lib/server/session.ts b/apps/login/src/lib/server/session.ts index 3ff3d14017..0535a27c35 100644 --- a/apps/login/src/lib/server/session.ts +++ b/apps/login/src/lib/server/session.ts @@ -20,7 +20,7 @@ import { getSessionCookieByLoginName, removeSessionFromCookie, } from "../cookies"; -import { getServiceUrlFromHeaders } from "../service"; +import { getServiceUrlFromHeaders } from "../service-url"; export async function skipMFAAndContinueWithNextUrl({ userId, diff --git a/apps/login/src/lib/server/u2f.ts b/apps/login/src/lib/server/u2f.ts index a13be1176a..3fe5194336 100644 --- a/apps/login/src/lib/server/u2f.ts +++ b/apps/login/src/lib/server/u2f.ts @@ -6,7 +6,7 @@ import { VerifyU2FRegistrationRequestSchema } from "@zitadel/proto/zitadel/user/ import { headers } from "next/headers"; import { userAgent } from "next/server"; import { getSessionCookieById } from "../cookies"; -import { getServiceUrlFromHeaders } from "../service"; +import { getServiceUrlFromHeaders } from "../service-url"; type RegisterU2FCommand = { sessionId: string; diff --git a/apps/login/src/lib/server/verify.ts b/apps/login/src/lib/server/verify.ts index 513aa03d40..e7c9f5e715 100644 --- a/apps/login/src/lib/server/verify.ts +++ b/apps/login/src/lib/server/verify.ts @@ -19,7 +19,7 @@ import { User } from "@zitadel/proto/zitadel/user/v2/user_pb"; import { headers } from "next/headers"; import { getNextUrl } from "../client"; import { getSessionCookieByLoginName } from "../cookies"; -import { getServiceUrlFromHeaders } from "../service"; +import { getServiceUrlFromHeaders } from "../service-url"; import { loadMostRecentSession } from "../session"; import { checkMFAFactors } from "../verify-helper"; import { createSessionAndUpdateCookie } from "./cookie"; From 74dee578c012ef01e7004fe92d7a0268084e06f2 Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Wed, 30 Apr 2025 13:34:14 +0200 Subject: [PATCH 30/96] import --- apps/login/src/app/login/route.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/login/src/app/login/route.ts b/apps/login/src/app/login/route.ts index fb2f5e5f49..00998d225d 100644 --- a/apps/login/src/app/login/route.ts +++ b/apps/login/src/app/login/route.ts @@ -3,7 +3,7 @@ import { idpTypeToSlug } from "@/lib/idp"; import { loginWithOIDCandSession } from "@/lib/oidc"; import { loginWithSAMLandSession } from "@/lib/saml"; import { sendLoginname, SendLoginnameCommand } from "@/lib/server/loginname"; -import { constructUrl, getServiceUrlFromHeaders } from "@/lib/service"; +import { constructUrl, getServiceUrlFromHeaders } from "@/lib/service-url"; import { findValidSession } from "@/lib/session"; import { createCallback, From 79043c2f34371fe30e7258893c16d3fce5ede292 Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Wed, 30 Apr 2025 13:39:02 +0200 Subject: [PATCH 31/96] eliminate duplicate constructurl --- apps/login/src/lib/oidc.ts | 2 +- apps/login/src/lib/saml.ts | 2 +- apps/login/src/lib/self.ts | 2 +- apps/login/src/lib/service.ts | 14 -------------- 4 files changed, 3 insertions(+), 17 deletions(-) diff --git a/apps/login/src/lib/oidc.ts b/apps/login/src/lib/oidc.ts index c1038d90c4..7efab0b254 100644 --- a/apps/login/src/lib/oidc.ts +++ b/apps/login/src/lib/oidc.ts @@ -8,7 +8,7 @@ import { } from "@zitadel/proto/zitadel/oidc/v2/oidc_service_pb"; import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb"; import { NextRequest, NextResponse } from "next/server"; -import { constructUrl } from "./service"; +import { constructUrl } from "./service-url"; import { isSessionValid } from "./session"; type LoginWithOIDCandSession = { diff --git a/apps/login/src/lib/saml.ts b/apps/login/src/lib/saml.ts index 9b12e48d25..c2664599b5 100644 --- a/apps/login/src/lib/saml.ts +++ b/apps/login/src/lib/saml.ts @@ -5,7 +5,7 @@ import { create } from "@zitadel/client"; import { CreateResponseRequestSchema } from "@zitadel/proto/zitadel/saml/v2/saml_service_pb"; import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb"; import { NextRequest, NextResponse } from "next/server"; -import { constructUrl } from "./service"; +import { constructUrl } from "./service-url"; import { isSessionValid } from "./session"; type LoginWithSAMLandSession = { diff --git a/apps/login/src/lib/self.ts b/apps/login/src/lib/self.ts index d1971c19b1..7375f4f114 100644 --- a/apps/login/src/lib/self.ts +++ b/apps/login/src/lib/self.ts @@ -4,7 +4,7 @@ import { createServerTransport } from "@zitadel/client/node"; import { createUserServiceClient } from "@zitadel/client/v2"; import { headers } from "next/headers"; import { getSessionCookieById } from "./cookies"; -import { getServiceUrlFromHeaders } from "./service"; +import { getServiceUrlFromHeaders } from "./service-url"; import { getSession } from "./zitadel"; const transport = async (serviceUrl: string, token: string) => { diff --git a/apps/login/src/lib/service.ts b/apps/login/src/lib/service.ts index 97143cf003..0fbb083b05 100644 --- a/apps/login/src/lib/service.ts +++ b/apps/login/src/lib/service.ts @@ -7,7 +7,6 @@ import { SAMLService } from "@zitadel/proto/zitadel/saml/v2/saml_service_pb"; import { SessionService } from "@zitadel/proto/zitadel/session/v2/session_service_pb"; import { SettingsService } from "@zitadel/proto/zitadel/settings/v2/settings_service_pb"; import { UserService } from "@zitadel/proto/zitadel/user/v2/user_service_pb"; -import { NextRequest } from "next/server"; import { systemAPIToken } from "./api"; type ServiceClass = @@ -65,16 +64,3 @@ export async function createServiceForHost( return createClientFor(service)(transport); } - -export function constructUrl(request: NextRequest, path: string) { - const forwardedProto = request.headers.get("x-forwarded-proto") - ? `${request.headers.get("x-forwarded-proto")}:` - : request.nextUrl.protocol; - - const forwardedHost = - request.headers.get("x-zitadel-forward-host") ?? - request.headers.get("x-forwarded-host") ?? - request.headers.get("host"); - const basePath = process.env.NEXT_PUBLIC_BASE_PATH || ""; - return new URL(`${basePath}${path}`, `${forwardedProto}//${forwardedHost}`); -} From b49c2be471638411e83915e145a3c9e4042e3d92 Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Wed, 30 Apr 2025 13:45:15 +0200 Subject: [PATCH 32/96] await response --- apps/login/src/app/security/route.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/login/src/app/security/route.ts b/apps/login/src/app/security/route.ts index d47daaa40e..704e59c8e8 100644 --- a/apps/login/src/app/security/route.ts +++ b/apps/login/src/app/security/route.ts @@ -12,7 +12,7 @@ export async function GET(request: NextRequest) { const settingsService: Client = await createServiceForHost(SettingsService, serviceUrl); - const settings = settingsService + const settings = await settingsService .getSecuritySettings({}) .then((resp) => (resp.settings ? resp.settings : undefined)); From 08d34b87a892ff0b0dc5f372f11c452689132f96 Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Wed, 30 Apr 2025 14:21:23 +0200 Subject: [PATCH 33/96] cleanup --- apps/login/src/app/security/route.ts | 4 ++-- apps/login/src/middleware.ts | 4 +--- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/apps/login/src/app/security/route.ts b/apps/login/src/app/security/route.ts index 704e59c8e8..4a2b6d4854 100644 --- a/apps/login/src/app/security/route.ts +++ b/apps/login/src/app/security/route.ts @@ -3,9 +3,9 @@ import { getServiceUrlFromHeaders } from "@/lib/service-url"; import { Client } from "@zitadel/client"; import { SettingsService } from "@zitadel/proto/zitadel/settings/v2/settings_service_pb"; import { headers } from "next/headers"; -import { NextRequest, NextResponse } from "next/server"; +import { NextResponse } from "next/server"; -export async function GET(request: NextRequest) { +export async function GET() { const _headers = await headers(); const { serviceUrl } = getServiceUrlFromHeaders(_headers); diff --git a/apps/login/src/middleware.ts b/apps/login/src/middleware.ts index fc02d859c6..4d66d0ab39 100644 --- a/apps/login/src/middleware.ts +++ b/apps/login/src/middleware.ts @@ -23,8 +23,6 @@ export async function middleware(request: NextRequest) { const { serviceUrl } = getServiceUrlFromHeaders(_headers); - console.log("defaultCSP", DEFAULT_CSP); - // Call the /security route handler // TODO check this on cloud run deployment const securityResponse = await fetch(`${request.nextUrl.origin}/security`); @@ -38,7 +36,6 @@ export async function middleware(request: NextRequest) { } const { settings: securitySettings } = await securityResponse.json(); - console.log("securitySettings", securitySettings); const instanceHost = `${serviceUrl}` .replace("https://", "") @@ -67,6 +64,7 @@ export async function middleware(request: NextRequest) { } request.nextUrl.href = `${serviceUrl}${request.nextUrl.pathname}${request.nextUrl.search}`; + return NextResponse.rewrite(request.nextUrl, { request: { headers: requestHeaders, From 833669792b607ed679c71a63d0d438326b92aaa9 Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Wed, 30 Apr 2025 14:38:53 +0200 Subject: [PATCH 34/96] also run on qa --- .github/workflows/docker.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 9485e82977..58a48f9882 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -4,6 +4,7 @@ on: push: branches: - main + - qa workflow_dispatch: permissions: From ed37eaff80afff4acd99a0a9eb28218d95dd903c Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Fri, 2 May 2025 09:01:42 +0200 Subject: [PATCH 35/96] authorizeOrDenyDeviceAuthorization --- apps/login/src/lib/zitadel.ts | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/apps/login/src/lib/zitadel.ts b/apps/login/src/lib/zitadel.ts index aee182dc41..da690c10e2 100644 --- a/apps/login/src/lib/zitadel.ts +++ b/apps/login/src/lib/zitadel.ts @@ -943,6 +943,31 @@ export async function getDeviceAuthorizationRequest({ }); } +export async function authorizeOrDenyDeviceAuthorization({ + serviceUrl, + deviceAuthorizationId, + session, +}: { + serviceUrl: string; + deviceAuthorizationId: string; + session?: { sessionId: string; sessionToken: string }; +}) { + const oidcService = await createServiceForHost(OIDCService, serviceUrl); + + return oidcService.authorizeOrDenyDeviceAuthorization({ + deviceAuthorizationId, + decision: session + ? { + case: "session", + value: session, + } + : { + case: "deny", + value: {}, + }, + }); +} + export async function createCallback({ serviceUrl, req, From 5274c2bd7d1f33965b0e45473ad8cdaa649880ef Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Fri, 2 May 2025 13:52:58 +0200 Subject: [PATCH 36/96] device code request --- .../src/app/(login)/device/consent/page.tsx | 67 ++++++++++ apps/login/src/app/(login)/device/page.tsx | 42 +----- apps/login/src/app/login/route.ts | 18 ++- apps/login/src/components/consent.tsx | 12 ++ .../login/src/components/device-code-form.tsx | 20 +-- apps/login/src/lib/device.ts | 123 ++++++++++++++++++ apps/login/src/lib/oidc.ts | 6 +- apps/login/src/lib/saml.ts | 6 +- 8 files changed, 237 insertions(+), 57 deletions(-) create mode 100644 apps/login/src/app/(login)/device/consent/page.tsx create mode 100644 apps/login/src/components/consent.tsx create mode 100644 apps/login/src/lib/device.ts diff --git a/apps/login/src/app/(login)/device/consent/page.tsx b/apps/login/src/app/(login)/device/consent/page.tsx new file mode 100644 index 0000000000..ee4312b955 --- /dev/null +++ b/apps/login/src/app/(login)/device/consent/page.tsx @@ -0,0 +1,67 @@ +import { ConsentScreen } from "@/components/consent"; +import { DynamicTheme } from "@/components/dynamic-theme"; +import { getServiceUrlFromHeaders } from "@/lib/service"; +import { + getBrandingSettings, + getDefaultOrg, + getDeviceAuthorizationRequest, +} from "@/lib/zitadel"; +import { Organization } from "@zitadel/proto/zitadel/org/v2/org_pb"; +import { getLocale, getTranslations } from "next-intl/server"; +import { headers } from "next/headers"; + +export default async function Page(props: { + searchParams: Promise>; +}) { + const searchParams = await props.searchParams; + const locale = getLocale(); + const t = await getTranslations({ locale, namespace: "device" }); + + const userCode = searchParams?.user_code; + const requestId = searchParams?.requestId; + const organization = searchParams?.organization; + + if (!userCode || !requestId) { + return
{t("error.no_user_code")}
; + } + + const _headers = await headers(); + const { serviceUrl } = getServiceUrlFromHeaders(_headers); + + const { deviceAuthorizationRequest } = await getDeviceAuthorizationRequest({ + serviceUrl, + userCode, + }); + + let defaultOrganization; + if (!organization) { + const org: Organization | null = await getDefaultOrg({ + serviceUrl, + }); + if (org) { + defaultOrganization = org.id; + } + } + + const branding = await getBrandingSettings({ + serviceUrl, + organization: organization ?? defaultOrganization, + }); + + return ( + +
+ {!userCode && ( + <> +

{t("usercode.title")}

+

{t("usercode.description")}

+ + + )} +
+
+ ); +} diff --git a/apps/login/src/app/(login)/device/page.tsx b/apps/login/src/app/(login)/device/page.tsx index 3c533271c0..bde104b631 100644 --- a/apps/login/src/app/(login)/device/page.tsx +++ b/apps/login/src/app/(login)/device/page.tsx @@ -1,12 +1,7 @@ import { DeviceCodeForm } from "@/components/device-code-form"; import { DynamicTheme } from "@/components/dynamic-theme"; import { getServiceUrlFromHeaders } from "@/lib/service"; -import { - getBrandingSettings, - getDefaultOrg, - getDeviceAuthorizationRequest, -} from "@/lib/zitadel"; -import { DeviceAuthorizationRequest } from "@zitadel/proto/zitadel/oidc/v2/authorization_pb"; +import { getBrandingSettings, getDefaultOrg } from "@/lib/zitadel"; import { Organization } from "@zitadel/proto/zitadel/org/v2/org_pb"; import { getLocale, getTranslations } from "next-intl/server"; import { headers } from "next/headers"; @@ -18,7 +13,6 @@ export default async function Page(props: { const locale = getLocale(); const t = await getTranslations({ locale, namespace: "device" }); - const loginName = searchParams?.loginName; const userCode = searchParams?.user_code; const organization = searchParams?.organization; @@ -35,51 +29,21 @@ export default async function Page(props: { } } - let deviceAuthRequest: DeviceAuthorizationRequest | null = null; - if (userCode) { - const deviceAuthorizationRequestResponse = - await getDeviceAuthorizationRequest({ - serviceUrl, - userCode, - }); - - if (deviceAuthorizationRequestResponse.deviceAuthorizationRequest) { - deviceAuthRequest = - deviceAuthorizationRequestResponse.deviceAuthorizationRequest; - } - } - const branding = await getBrandingSettings({ serviceUrl, organization: organization ?? defaultOrganization, }); return ( - +
{!userCode && ( <>

{t("usercode.title")}

{t("usercode.description")}

- + )} - - {deviceAuthRequest && ( -
-

- {deviceAuthRequest.appName} -
- {t("request.title")} -

-

- {t("request.description")} -

- {/* {JSON.stringify(deviceAuthRequest)} */} -
- )}
); diff --git a/apps/login/src/app/login/route.ts b/apps/login/src/app/login/route.ts index e3834e5a27..4e57030470 100644 --- a/apps/login/src/app/login/route.ts +++ b/apps/login/src/app/login/route.ts @@ -1,7 +1,8 @@ import { getAllSessions } from "@/lib/cookies"; +import { loginWithDeviceAndSession } from "@/lib/device"; import { idpTypeToSlug } from "@/lib/idp"; -import { loginWithOIDCandSession } from "@/lib/oidc"; -import { loginWithSAMLandSession } from "@/lib/saml"; +import { loginWithOIDCAndSession } from "@/lib/oidc"; +import { loginWithSAMLAndSession } from "@/lib/saml"; import { sendLoginname, SendLoginnameCommand } from "@/lib/server/loginname"; import { constructUrl, getServiceUrlFromHeaders } from "@/lib/service"; import { findValidSession } from "@/lib/session"; @@ -107,7 +108,7 @@ export async function GET(request: NextRequest) { if (requestId && sessionId) { if (requestId.startsWith("oidc_")) { // this finishes the login process for OIDC - return loginWithOIDCandSession({ + return loginWithOIDCAndSession({ serviceUrl, authRequest: requestId.replace("oidc_", ""), sessionId, @@ -117,7 +118,7 @@ export async function GET(request: NextRequest) { }); } else if (requestId.startsWith("saml_")) { // this finishes the login process for SAML - return loginWithSAMLandSession({ + return loginWithSAMLAndSession({ serviceUrl, samlRequest: requestId.replace("saml_", ""), sessionId, @@ -125,6 +126,15 @@ export async function GET(request: NextRequest) { sessionCookies, request, }); + } else if (requestId.startsWith("device_")) { + return loginWithDeviceAndSession({ + serviceUrl, + deviceRequest: requestId.replace("device_", ""), + sessionId, + sessions, + sessionCookies, + request, + }); } } diff --git a/apps/login/src/components/consent.tsx b/apps/login/src/components/consent.tsx new file mode 100644 index 0000000000..315aaded13 --- /dev/null +++ b/apps/login/src/components/consent.tsx @@ -0,0 +1,12 @@ +export function ConsentScreen({ scope }: { scope?: string[] }) { + return ( +
+

Consent

+

Please confirm your consent.

+
+ + +
+
+ ); +} diff --git a/apps/login/src/components/device-code-form.tsx b/apps/login/src/components/device-code-form.tsx index 5747e52f3c..faa77c3cdd 100644 --- a/apps/login/src/components/device-code-form.tsx +++ b/apps/login/src/components/device-code-form.tsx @@ -15,7 +15,7 @@ type Inputs = { userCode: string; }; -export function DeviceCodeForm() { +export function DeviceCodeForm({ userCode }: { userCode?: string }) { const t = useTranslations("verify"); const router = useRouter(); @@ -23,7 +23,7 @@ export function DeviceCodeForm() { const { register, handleSubmit, formState } = useForm({ mode: "onBlur", defaultValues: { - userCode: "", + userCode: userCode || "", }, }); @@ -36,21 +36,25 @@ export function DeviceCodeForm() { const response = await getDeviceAuthorizationRequest(value.userCode) .catch(() => { - setError("Could not verify user"); + setError("Could not complete the request"); return; }) .finally(() => { setLoading(false); }); - if (response && "error" in response && response?.error) { - setError(response.error); + if (!response || !response.deviceAuthorizationRequest?.id) { + setError("Could not complete the request"); return; } - if (response && "redirect" in response && response?.redirect) { - return router.push(response?.redirect); - } + return router.push( + `/device/consent?` + + new URLSearchParams({ + requestId: `device_${response.deviceAuthorizationRequest.id}`, + user_code: value.userCode, + }).toString(), + ); } return ( diff --git a/apps/login/src/lib/device.ts b/apps/login/src/lib/device.ts new file mode 100644 index 0000000000..cc42c6cafa --- /dev/null +++ b/apps/login/src/lib/device.ts @@ -0,0 +1,123 @@ +import { Cookie } from "@/lib/cookies"; +import { sendLoginname, SendLoginnameCommand } from "@/lib/server/loginname"; +import { + authorizeOrDenyDeviceAuthorization, + getLoginSettings, +} from "@/lib/zitadel"; +import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb"; +import { NextRequest, NextResponse } from "next/server"; +import { constructUrl } from "./service"; +import { isSessionValid } from "./session"; + +type LoginWithOIDCandSession = { + serviceUrl: string; + deviceRequest: string; + sessionId: string; + sessions: Session[]; + sessionCookies: Cookie[]; + request: NextRequest; +}; +export async function loginWithDeviceAndSession({ + serviceUrl, + deviceRequest, + sessionId, + sessions, + sessionCookies, + request, +}: LoginWithOIDCandSession) { + console.log( + `Login with session: ${sessionId} and deviceRequest: ${deviceRequest}`, + ); + + const selectedSession = sessions.find((s) => s.id === sessionId); + + if (selectedSession && selectedSession.id) { + console.log(`Found session ${selectedSession.id}`); + + const isValid = await isSessionValid({ + serviceUrl, + session: selectedSession, + }); + + console.log("Session is valid:", isValid); + + if (!isValid && selectedSession.factors?.user) { + // if the session is not valid anymore, we need to redirect the user to re-authenticate / + // TODO: handle IDP intent direcly if available + const command: SendLoginnameCommand = { + loginName: selectedSession.factors.user?.loginName, + organization: selectedSession.factors?.user?.organizationId, + requestId: `device_${deviceRequest}`, + }; + + const res = await sendLoginname(command); + + if (res && "redirect" in res && res?.redirect) { + const absoluteUrl = constructUrl(request, res.redirect); + return NextResponse.redirect(absoluteUrl.toString()); + } + } + + const cookie = sessionCookies.find( + (cookie) => cookie.id === selectedSession?.id, + ); + + if (cookie && cookie.id && cookie.token) { + const session = { + sessionId: cookie?.id, + sessionToken: cookie?.token, + }; + + // works not with _rsc request + try { + const authResponse = await authorizeOrDenyDeviceAuthorization({ + serviceUrl, + deviceAuthorizationId: deviceRequest, + session, + }); + if (!authResponse) { + return NextResponse.json( + { error: "An error occurred!" }, + { status: 500 }, + ); + } + } catch (error: unknown) { + // handle already handled gracefully as these could come up if old emails with requestId are used (reset password, register emails etc.) + console.error(error); + if ( + error && + typeof error === "object" && + "code" in error && + error?.code === 9 + ) { + const loginSettings = await getLoginSettings({ + serviceUrl, + organization: selectedSession.factors?.user?.organizationId, + }); + + if (loginSettings?.defaultRedirectUri) { + return NextResponse.redirect(loginSettings.defaultRedirectUri); + } + + const signedinUrl = constructUrl(request, "/signedin"); + + if (selectedSession.factors?.user?.loginName) { + signedinUrl.searchParams.set( + "loginName", + selectedSession.factors?.user?.loginName, + ); + } + if (selectedSession.factors?.user?.organizationId) { + signedinUrl.searchParams.set( + "organization", + selectedSession.factors?.user?.organizationId, + ); + } + return NextResponse.redirect(signedinUrl); + } else { + return NextResponse.json({ error }, { status: 500 }); + } + } + } + } +} diff --git a/apps/login/src/lib/oidc.ts b/apps/login/src/lib/oidc.ts index c1038d90c4..09f6e0354e 100644 --- a/apps/login/src/lib/oidc.ts +++ b/apps/login/src/lib/oidc.ts @@ -11,7 +11,7 @@ import { NextRequest, NextResponse } from "next/server"; import { constructUrl } from "./service"; import { isSessionValid } from "./session"; -type LoginWithOIDCandSession = { +type LoginWithOIDCAndSession = { serviceUrl: string; authRequest: string; sessionId: string; @@ -19,14 +19,14 @@ type LoginWithOIDCandSession = { sessionCookies: Cookie[]; request: NextRequest; }; -export async function loginWithOIDCandSession({ +export async function loginWithOIDCAndSession({ serviceUrl, authRequest, sessionId, sessions, sessionCookies, request, -}: LoginWithOIDCandSession) { +}: LoginWithOIDCAndSession) { console.log( `Login with session: ${sessionId} and authRequest: ${authRequest}`, ); diff --git a/apps/login/src/lib/saml.ts b/apps/login/src/lib/saml.ts index 9b12e48d25..7d294c908a 100644 --- a/apps/login/src/lib/saml.ts +++ b/apps/login/src/lib/saml.ts @@ -8,7 +8,7 @@ import { NextRequest, NextResponse } from "next/server"; import { constructUrl } from "./service"; import { isSessionValid } from "./session"; -type LoginWithSAMLandSession = { +type LoginWithSAMLAndSession = { serviceUrl: string; samlRequest: string; sessionId: string; @@ -17,14 +17,14 @@ type LoginWithSAMLandSession = { request: NextRequest; }; -export async function loginWithSAMLandSession({ +export async function loginWithSAMLAndSession({ serviceUrl, samlRequest, sessionId, sessions, sessionCookies, request, -}: LoginWithSAMLandSession) { +}: LoginWithSAMLAndSession) { console.log( `Login with session: ${sessionId} and samlRequest: ${samlRequest}`, ); From 6270cf9522add45c6d90c1cd37a5e3e8ff2a8039 Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Fri, 2 May 2025 15:08:41 +0200 Subject: [PATCH 37/96] device code flow --- apps/login/locales/en.json | 2 +- apps/login/src/app/(login)/signedin/page.tsx | 23 +++++++- apps/login/src/app/login/route.ts | 5 +- .../login/src/components/device-code-form.tsx | 2 +- apps/login/src/lib/client.ts | 52 +++++++++++++++---- 5 files changed, 70 insertions(+), 14 deletions(-) diff --git a/apps/login/locales/en.json b/apps/login/locales/en.json index cb5011f59a..806b91dbff 100644 --- a/apps/login/locales/en.json +++ b/apps/login/locales/en.json @@ -190,7 +190,7 @@ "device": { "usercode": { "title": "Device code", - "description": "Enter the code provided in the verification email.", + "description": "Enter the code.", "submit": "Continue" }, "request": { diff --git a/apps/login/src/app/(login)/signedin/page.tsx b/apps/login/src/app/(login)/signedin/page.tsx index 8c5c5486ac..271b8ea5ac 100644 --- a/apps/login/src/app/(login)/signedin/page.tsx +++ b/apps/login/src/app/(login)/signedin/page.tsx @@ -5,9 +5,11 @@ import { UserAvatar } from "@/components/user-avatar"; import { getMostRecentCookieWithLoginname } from "@/lib/cookies"; import { getServiceUrlFromHeaders } from "@/lib/service"; import { + authorizeOrDenyDeviceAuthorization, createCallback, createResponse, getBrandingSettings, + getDeviceAuthorizationRequest, getLoginSettings, getSession, } from "@/lib/zitadel"; @@ -24,7 +26,6 @@ import { redirect } from "next/navigation"; async function loadSession( serviceUrl: string, - loginName: string, requestId?: string, ) { @@ -62,6 +63,26 @@ async function loadSession( }).then(({ url }) => { return redirect(url); }); + } else if (requestId && requestId.startsWith("device_")) { + const userCode = requestId.replace("device_", ""); + + const { deviceAuthorizationRequest } = await getDeviceAuthorizationRequest({ + serviceUrl, + userCode, + }); + + if (!deviceAuthorizationRequest) { + throw new Error("Device authorization request not found"); + } + + return authorizeOrDenyDeviceAuthorization({ + serviceUrl, + deviceAuthorizationId: deviceAuthorizationRequest?.id, + session: { + sessionId: recent.id, + sessionToken: recent.token, + }, + }); } return getSession({ diff --git a/apps/login/src/app/login/route.ts b/apps/login/src/app/login/route.ts index 4e57030470..365f83a225 100644 --- a/apps/login/src/app/login/route.ts +++ b/apps/login/src/app/login/route.ts @@ -127,6 +127,7 @@ export async function GET(request: NextRequest) { request, }); } else if (requestId.startsWith("device_")) { + // this finishes the login process for Device Authorization return loginWithDeviceAndSession({ serviceUrl, deviceRequest: requestId.replace("device_", ""), @@ -509,7 +510,9 @@ export async function GET(request: NextRequest) { requestId: `saml_${samlRequest.id}`, }); } - } else { + } + // Device Authorization does not need to start here as it is handled on the /device endpoint + else { return NextResponse.json( { error: "No authRequest nor samlRequest provided" }, { status: 500 }, diff --git a/apps/login/src/components/device-code-form.tsx b/apps/login/src/components/device-code-form.tsx index faa77c3cdd..8adb8c3386 100644 --- a/apps/login/src/components/device-code-form.tsx +++ b/apps/login/src/components/device-code-form.tsx @@ -51,7 +51,7 @@ export function DeviceCodeForm({ userCode }: { userCode?: string }) { return router.push( `/device/consent?` + new URLSearchParams({ - requestId: `device_${response.deviceAuthorizationRequest.id}`, + requestId: `device_${userCode}`, user_code: value.userCode, }).toString(), ); diff --git a/apps/login/src/lib/client.ts b/apps/login/src/lib/client.ts index 953d66e7ee..df04986ccc 100644 --- a/apps/login/src/lib/client.ts +++ b/apps/login/src/lib/client.ts @@ -5,6 +5,28 @@ type FinishFlowCommand = } | { loginName: string }; +function goToSignedInPage( + props: + | { sessionId: string; organization?: string } + | { organization?: string; loginName: string }, +) { + const params = new URLSearchParams({}); + + if ("loginName" in props && props.loginName) { + params.append("loginName", props.loginName); + } + + if ("sessionId" in props && props.sessionId) { + params.append("sessionId", props.sessionId); + } + + if (props.organization) { + params.append("organization", props.organization); + } + + return `/signedin?` + params; +} + /** * for client: redirects user back to an OIDC or SAML application or to a success page when using requestId, check if a default redirect and redirect to it, or just redirect to a success page with the loginName * @param command @@ -14,7 +36,25 @@ export async function getNextUrl( command: FinishFlowCommand & { organization?: string }, defaultRedirectUri?: string, ): Promise { - if ("sessionId" in command && "requestId" in command) { + // finish Device Authorization Flow + if ( + "requestId" in command && + command.requestId.startsWith("device_") && + ("loginName" in command || "sessionId" in command) + ) { + return goToSignedInPage({ + ...command, + organization: command.organization, + }); + } + + // finish SAML or OIDC flow + if ( + "sessionId" in command && + "requestId" in command && + (command.requestId.startsWith("saml_") || + command.requestId.startsWith("oidc_")) + ) { const params = new URLSearchParams({ sessionId: command.sessionId, requestId: command.requestId, @@ -31,13 +71,5 @@ export async function getNextUrl( return defaultRedirectUri; } - const params = new URLSearchParams({ - loginName: command.loginName, - }); - - if (command.organization) { - params.append("organization", command.organization); - } - - return `/signedin?` + params; + return goToSignedInPage(command); } From 54fd748b12988206e4ed6e9b513bea174622a890 Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Fri, 2 May 2025 17:20:28 +0200 Subject: [PATCH 38/96] dc --- apps/login/locales/en.json | 5 ++ .../src/app/(login)/device/consent/page.tsx | 25 +++++--- apps/login/src/app/(login)/signedin/page.tsx | 30 ++++------ apps/login/src/components/consent.tsx | 60 ++++++++++++++++--- .../login/src/components/device-code-form.tsx | 2 +- 5 files changed, 90 insertions(+), 32 deletions(-) diff --git a/apps/login/locales/en.json b/apps/login/locales/en.json index 806b91dbff..1b52353c87 100644 --- a/apps/login/locales/en.json +++ b/apps/login/locales/en.json @@ -197,6 +197,11 @@ "title": "would like to connect:", "description": "By clicking Allow, you allow this app and Zitadel to use your information in accordance with their respective terms of service and privacy policies. You can revoke this access at any time.", "submit": "Allow" + }, + "scope": { + "email": "Access your email address.", + "profile": "Access your full profile information.", + "offline_access": "Allow offline access to your account." } }, "error": { diff --git a/apps/login/src/app/(login)/device/consent/page.tsx b/apps/login/src/app/(login)/device/consent/page.tsx index ee4312b955..1c33d6c831 100644 --- a/apps/login/src/app/(login)/device/consent/page.tsx +++ b/apps/login/src/app/(login)/device/consent/page.tsx @@ -33,6 +33,8 @@ export default async function Page(props: { userCode, }); + console.log(deviceAuthorizationRequest); + let defaultOrganization; if (!organization) { const org: Organization | null = await getDefaultOrg({ @@ -48,19 +50,28 @@ export default async function Page(props: { organization: organization ?? defaultOrganization, }); + const params = new URLSearchParams(); + + if (requestId) { + params.append("requestId", requestId); + } + + if (organization) { + params.append("organization", organization); + } + return (
- {!userCode && ( - <> -

{t("usercode.title")}

-

{t("usercode.description")}

- - - )} +

{t("usercode.title")}

+

{t("usercode.description")}

+
); diff --git a/apps/login/src/app/(login)/signedin/page.tsx b/apps/login/src/app/(login)/signedin/page.tsx index 271b8ea5ac..42a10dabd4 100644 --- a/apps/login/src/app/(login)/signedin/page.tsx +++ b/apps/login/src/app/(login)/signedin/page.tsx @@ -9,7 +9,6 @@ import { createCallback, createResponse, getBrandingSettings, - getDeviceAuthorizationRequest, getLoginSettings, getSession, } from "@/lib/zitadel"; @@ -64,24 +63,17 @@ async function loadSession( return redirect(url); }); } else if (requestId && requestId.startsWith("device_")) { - const userCode = requestId.replace("device_", ""); - - const { deviceAuthorizationRequest } = await getDeviceAuthorizationRequest({ - serviceUrl, - userCode, - }); - - if (!deviceAuthorizationRequest) { - throw new Error("Device authorization request not found"); - } + const session = { + sessionId: recent.id, + sessionToken: recent.token, + }; return authorizeOrDenyDeviceAuthorization({ serviceUrl, - deviceAuthorizationId: deviceAuthorizationRequest?.id, - session: { - sessionId: recent.id, - sessionToken: recent.token, - }, + deviceAuthorizationId: requestId.replace("device_", ""), + session, + }).then(() => { + return session; }); } @@ -105,7 +97,11 @@ export default async function Page(props: { searchParams: Promise }) { const { serviceUrl } = getServiceUrlFromHeaders(_headers); const { loginName, requestId, organization } = searchParams; - const sessionFactors = await loadSession(serviceUrl, loginName, requestId); + // const sessionFactors = await loadSession(serviceUrl, loginName, requestId); + + const sessionFactors = sessionId + ? await loadSessionById(serviceUrl, sessionId, organization) + : await loadSessionByLoginname(serviceUrl, loginName, organization); const branding = await getBrandingSettings({ serviceUrl, diff --git a/apps/login/src/components/consent.tsx b/apps/login/src/components/consent.tsx index 315aaded13..5bf3747f73 100644 --- a/apps/login/src/components/consent.tsx +++ b/apps/login/src/components/consent.tsx @@ -1,11 +1,57 @@ -export function ConsentScreen({ scope }: { scope?: string[] }) { +import { useTranslations } from "next-intl"; +import Link from "next/link"; +import { Button, ButtonVariants } from "./button"; + +export function ConsentScreen({ + scope, + nextUrl, +}: { + scope?: string[]; + nextUrl: string; +}) { + const t = useTranslations(); + return ( -
-

Consent

-

Please confirm your consent.

-
- - +
+
    + {scope?.map((s) => { + const translationKey = `device.scope.${s}`; + const description = t(translationKey, null); + + // Check if the key itself is returned and provide a fallback + const resolvedDescription = + description === translationKey + ? "No description available." + : description; + + return ( +
  • + {s} + {resolvedDescription} +
  • + ); + })} +
+ +
+ + + + + +
); diff --git a/apps/login/src/components/device-code-form.tsx b/apps/login/src/components/device-code-form.tsx index 8adb8c3386..faa77c3cdd 100644 --- a/apps/login/src/components/device-code-form.tsx +++ b/apps/login/src/components/device-code-form.tsx @@ -51,7 +51,7 @@ export function DeviceCodeForm({ userCode }: { userCode?: string }) { return router.push( `/device/consent?` + new URLSearchParams({ - requestId: `device_${userCode}`, + requestId: `device_${response.deviceAuthorizationRequest.id}`, user_code: value.userCode, }).toString(), ); From 781462d9f1bfe6ecdbd47bbb2a8883bcc38d3e3f Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Mon, 5 May 2025 09:39:50 +0200 Subject: [PATCH 39/96] fix signedin session --- apps/login/locales/en.json | 2 +- .../src/app/(login)/device/consent/page.tsx | 6 ++-- apps/login/src/app/(login)/signedin/page.tsx | 30 +++++++++++++++++-- apps/login/src/components/consent.tsx | 10 ++++--- 4 files changed, 38 insertions(+), 10 deletions(-) diff --git a/apps/login/locales/en.json b/apps/login/locales/en.json index 1b52353c87..c604615152 100644 --- a/apps/login/locales/en.json +++ b/apps/login/locales/en.json @@ -194,7 +194,7 @@ "submit": "Continue" }, "request": { - "title": "would like to connect:", + "title": "{appName} would like to connect:", "description": "By clicking Allow, you allow this app and Zitadel to use your information in accordance with their respective terms of service and privacy policies. You can revoke this access at any time.", "submit": "Allow" }, diff --git a/apps/login/src/app/(login)/device/consent/page.tsx b/apps/login/src/app/(login)/device/consent/page.tsx index 1c33d6c831..283d46d155 100644 --- a/apps/login/src/app/(login)/device/consent/page.tsx +++ b/apps/login/src/app/(login)/device/consent/page.tsx @@ -66,8 +66,10 @@ export default async function Page(props: { appName={deviceAuthorizationRequest?.appName} >
-

{t("usercode.title")}

-

{t("usercode.description")}

+

+ {t("request.title", { appName: deviceAuthorizationRequest?.appName })} +

+ { + if (response?.session) { + return response.session; + } + }); +} + export default async function Page(props: { searchParams: Promise }) { const searchParams = await props.searchParams; const locale = getLocale(); @@ -96,12 +117,15 @@ export default async function Page(props: { searchParams: Promise }) { const _headers = await headers(); const { serviceUrl } = getServiceUrlFromHeaders(_headers); - const { loginName, requestId, organization } = searchParams; + const { loginName, requestId, organization, sessionId } = searchParams; // const sessionFactors = await loadSession(serviceUrl, loginName, requestId); const sessionFactors = sessionId ? await loadSessionById(serviceUrl, sessionId, organization) - : await loadSessionByLoginname(serviceUrl, loginName, organization); + : await loadMostRecentSession({ + serviceUrl, + sessionParams: { loginName, organization }, + }); const branding = await getBrandingSettings({ serviceUrl, diff --git a/apps/login/src/components/consent.tsx b/apps/login/src/components/consent.tsx index 5bf3747f73..7422b657d6 100644 --- a/apps/login/src/components/consent.tsx +++ b/apps/login/src/components/consent.tsx @@ -12,7 +12,7 @@ export function ConsentScreen({ const t = useTranslations(); return ( -
+
    {scope?.map((s) => { const translationKey = `device.scope.${s}`; @@ -20,9 +20,7 @@ export function ConsentScreen({ // Check if the key itself is returned and provide a fallback const resolvedDescription = - description === translationKey - ? "No description available." - : description; + description === translationKey ? "" : description; return (
  • +

    + {t("device.request.description")} +

    +
    @@ -51,7 +92,7 @@ export function ConsentScreen({ className="self-end" variant={ButtonVariants.Primary} > - continue + {t("device.request.submit")}
    diff --git a/apps/login/src/lib/server/oidc.ts b/apps/login/src/lib/server/oidc.ts index 4ae01b4a47..3bd0e53df4 100644 --- a/apps/login/src/lib/server/oidc.ts +++ b/apps/login/src/lib/server/oidc.ts @@ -1,6 +1,9 @@ "use server"; -import { getDeviceAuthorizationRequest as zitadelGetDeviceAuthorizationRequest } from "@/lib/zitadel"; +import { + authorizeOrDenyDeviceAuthorization, + getDeviceAuthorizationRequest as zitadelGetDeviceAuthorizationRequest, +} from "@/lib/zitadel"; import { headers } from "next/headers"; import { getServiceUrlFromHeaders } from "../service"; @@ -13,3 +16,14 @@ export async function getDeviceAuthorizationRequest(userCode: string) { userCode, }); } + +export async function denyDeviceAuthorization(deviceAuthorizationId: string) { + const _headers = await headers(); + const { serviceUrl } = getServiceUrlFromHeaders(_headers); + + // without the session, device auth request is denied + return authorizeOrDenyDeviceAuthorization({ + serviceUrl, + deviceAuthorizationId, + }); +} From a4e08b5419e136774310e5f6405a7a1f1f352b31 Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Tue, 6 May 2025 14:41:44 +0200 Subject: [PATCH 43/96] i18n --- apps/login/locales/de.json | 1 + apps/login/locales/en.json | 7 +- apps/login/locales/es.json | 1 + apps/login/locales/it.json | 1 + apps/login/locales/pl.json | 1 + apps/login/locales/ru.json | 1 + apps/login/locales/zh.json | 1 + .../src/app/(login)/device/consent/page.tsx | 5 +- apps/login/src/app/(login)/signedin/page.tsx | 80 +------------------ apps/login/src/components/consent.tsx | 8 +- apps/login/src/lib/client.ts | 9 ++- 11 files changed, 23 insertions(+), 92 deletions(-) diff --git a/apps/login/locales/de.json b/apps/login/locales/de.json index befd14e7be..452620c697 100644 --- a/apps/login/locales/de.json +++ b/apps/login/locales/de.json @@ -200,6 +200,7 @@ "deny": "Ablehnen" }, "scope": { + "openid": "Überprüfen Sie Ihre Identität.", "email": "Zugriff auf Ihre E-Mail-Adresse.", "profile": "Zugriff auf Ihre vollständigen Profilinformationen.", "offline_access": "Erlauben Sie den Offline-Zugriff auf Ihr Konto." diff --git a/apps/login/locales/en.json b/apps/login/locales/en.json index 7aecf0294b..27b0880615 100644 --- a/apps/login/locales/en.json +++ b/apps/login/locales/en.json @@ -190,7 +190,7 @@ "device": { "usercode": { "title": "Device code", - "description": "Enter the code.", + "description": "Enter the code displayed on your app or device.", "submit": "Continue" }, "request": { @@ -200,8 +200,9 @@ "deny": "Deny" }, "scope": { - "email": "Access your email address.", - "profile": "Access your full profile information.", + "openid": "Verify your identity.", + "email": "Access to your email address.", + "profile": "Access to your full profile information.", "offline_access": "Allow offline access to your account." } }, diff --git a/apps/login/locales/es.json b/apps/login/locales/es.json index 450aadd867..e27ef1c25d 100644 --- a/apps/login/locales/es.json +++ b/apps/login/locales/es.json @@ -200,6 +200,7 @@ "deny": "Denegar" }, "scope": { + "openid": "Verifica tu identidad.", "email": "Accede a tu dirección de correo electrónico.", "profile": "Accede a la información completa de tu perfil.", "offline_access": "Permitir acceso sin conexión a tu cuenta." diff --git a/apps/login/locales/it.json b/apps/login/locales/it.json index 558ac51a23..1c7a4d495d 100644 --- a/apps/login/locales/it.json +++ b/apps/login/locales/it.json @@ -200,6 +200,7 @@ "deny": "Nega" }, "scope": { + "openid": "Verifica la tua identità.", "email": "Accedi al tuo indirizzo email.", "profile": "Accedi alle informazioni complete del tuo profilo.", "offline_access": "Consenti l'accesso offline al tuo account." diff --git a/apps/login/locales/pl.json b/apps/login/locales/pl.json index 49683303db..a82efd9807 100644 --- a/apps/login/locales/pl.json +++ b/apps/login/locales/pl.json @@ -200,6 +200,7 @@ "deny": "Odmów" }, "scope": { + "openid": "Zweryfikuj swoją tożsamość.", "email": "Uzyskaj dostęp do swojego adresu e-mail.", "profile": "Uzyskaj dostęp do pełnych informacji o swoim profilu.", "offline_access": "Zezwól na dostęp offline do swojego konta." diff --git a/apps/login/locales/ru.json b/apps/login/locales/ru.json index 149bea5a88..f3ac0dfa43 100644 --- a/apps/login/locales/ru.json +++ b/apps/login/locales/ru.json @@ -200,6 +200,7 @@ "deny": "Запретить" }, "scope": { + "openid": "Проверка вашей личности.", "email": "Доступ к вашему адресу электронной почты.", "profile": "Доступ к полной информации вашего профиля.", "offline_access": "Разрешить офлайн-доступ к вашему аккаунту." diff --git a/apps/login/locales/zh.json b/apps/login/locales/zh.json index 13a91ac77e..72c4bc4735 100644 --- a/apps/login/locales/zh.json +++ b/apps/login/locales/zh.json @@ -200,6 +200,7 @@ "deny": "拒绝" }, "scope": { + "openid": "验证您的身份。", "email": "访问您的电子邮件地址。", "profile": "访问您的完整个人资料信息。", "offline_access": "允许离线访问您的账户。" diff --git a/apps/login/src/app/(login)/device/consent/page.tsx b/apps/login/src/app/(login)/device/consent/page.tsx index c190451d48..64754d5ab8 100644 --- a/apps/login/src/app/(login)/device/consent/page.tsx +++ b/apps/login/src/app/(login)/device/consent/page.tsx @@ -63,10 +63,7 @@ export default async function Page(props: { } return ( - +

    {t("request.title", { appName: deviceAuthorizationRequest?.appName })} diff --git a/apps/login/src/app/(login)/signedin/page.tsx b/apps/login/src/app/(login)/signedin/page.tsx index 4fbaf9b937..09c96b56d4 100644 --- a/apps/login/src/app/(login)/signedin/page.tsx +++ b/apps/login/src/app/(login)/signedin/page.tsx @@ -2,95 +2,17 @@ import { Alert, AlertType } from "@/components/alert"; import { Button, ButtonVariants } from "@/components/button"; import { DynamicTheme } from "@/components/dynamic-theme"; import { UserAvatar } from "@/components/user-avatar"; -import { - getMostRecentCookieWithLoginname, - getSessionCookieById, -} from "@/lib/cookies"; +import { getSessionCookieById } from "@/lib/cookies"; import { getServiceUrlFromHeaders } from "@/lib/service"; import { loadMostRecentSession } from "@/lib/session"; import { - authorizeOrDenyDeviceAuthorization, - createCallback, - createResponse, getBrandingSettings, getLoginSettings, getSession, } from "@/lib/zitadel"; -import { create } from "@zitadel/client"; -import { - CreateCallbackRequestSchema, - SessionSchema, -} from "@zitadel/proto/zitadel/oidc/v2/oidc_service_pb"; -import { CreateResponseRequestSchema } from "@zitadel/proto/zitadel/saml/v2/saml_service_pb"; import { getLocale, getTranslations } from "next-intl/server"; import { headers } from "next/headers"; import Link from "next/link"; -import { redirect } from "next/navigation"; - -async function loadSession( - serviceUrl: string, - loginName: string, - requestId?: string, -) { - const recent = await getMostRecentCookieWithLoginname({ loginName }); - - if (requestId && requestId.startsWith("oidc_")) { - return createCallback({ - serviceUrl, - req: create(CreateCallbackRequestSchema, { - authRequestId: requestId, - callbackKind: { - case: "session", - value: create(SessionSchema, { - sessionId: recent.id, - sessionToken: recent.token, - }), - }, - }), - }).then(({ callbackUrl }) => { - return redirect(callbackUrl); - }); - } else if (requestId && requestId.startsWith("saml_")) { - return createResponse({ - serviceUrl, - req: create(CreateResponseRequestSchema, { - samlRequestId: requestId.replace("saml_", ""), - responseKind: { - case: "session", - value: { - sessionId: recent.id, - sessionToken: recent.token, - }, - }, - }), - }).then(({ url }) => { - return redirect(url); - }); - } else if (requestId && requestId.startsWith("device_")) { - const session = { - sessionId: recent.id, - sessionToken: recent.token, - }; - - return authorizeOrDenyDeviceAuthorization({ - serviceUrl, - deviceAuthorizationId: requestId.replace("device_", ""), - session, - }).then(() => { - return session; - }); - } - - return getSession({ - serviceUrl, - sessionId: recent.id, - sessionToken: recent.token, - }).then((response) => { - if (response?.session) { - return response.session; - } - }); -} async function loadSessionById( serviceUrl: string, diff --git a/apps/login/src/components/consent.tsx b/apps/login/src/components/consent.tsx index 82266a71dc..15b106e62e 100644 --- a/apps/login/src/components/consent.tsx +++ b/apps/login/src/components/consent.tsx @@ -1,3 +1,5 @@ +"use client"; + import { denyDeviceAuthorization } from "@/lib/server/oidc"; import { useTranslations } from "next-intl"; import Link from "next/link"; @@ -32,11 +34,9 @@ export function ConsentScreen({ setLoading(false); }); - if (response && "redirect" in response && response.redirect) { + if (response) { return router.push("/device"); } - - return response; } return ( @@ -77,7 +77,7 @@ export function ConsentScreen({ onClick={() => { denyDeviceAuth(); }} - variant={ButtonVariants.Destructive} + variant={ButtonVariants.Secondary} data-testid="deny-button" > {loading && } diff --git a/apps/login/src/lib/client.ts b/apps/login/src/lib/client.ts index df04986ccc..a59af90b77 100644 --- a/apps/login/src/lib/client.ts +++ b/apps/login/src/lib/client.ts @@ -7,8 +7,8 @@ type FinishFlowCommand = function goToSignedInPage( props: - | { sessionId: string; organization?: string } - | { organization?: string; loginName: string }, + | { sessionId: string; organization?: string; requestId?: string } + | { organization?: string; loginName: string; requestId?: string }, ) { const params = new URLSearchParams({}); @@ -24,6 +24,11 @@ function goToSignedInPage( params.append("organization", props.organization); } + // required to show conditional UI for device flow + if (props.requestId) { + params.append("requestId", props.requestId); + } + return `/signedin?` + params; } From 4f8aca1434c1373a92dd3f5f76759521d8d87bac Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Tue, 6 May 2025 14:49:14 +0200 Subject: [PATCH 44/96] i18n improvement --- apps/login/locales/de.json | 2 +- apps/login/locales/en.json | 2 +- apps/login/locales/es.json | 2 +- apps/login/locales/it.json | 2 +- apps/login/src/app/(login)/device/consent/page.tsx | 1 + apps/login/src/components/consent.tsx | 4 +++- 6 files changed, 8 insertions(+), 5 deletions(-) diff --git a/apps/login/locales/de.json b/apps/login/locales/de.json index 452620c697..25f2ad4b38 100644 --- a/apps/login/locales/de.json +++ b/apps/login/locales/de.json @@ -195,7 +195,7 @@ }, "request": { "title": "{appName} möchte eine Verbindung herstellen:", - "description": "Durch Klicken auf Zulassen erlauben Sie dieser App und Zitadel, Ihre Informationen gemäß ihren jeweiligen Nutzungsbedingungen und Datenschutzrichtlinien zu verwenden. Sie können diesen Zugriff jederzeit widerrufen.", + "description": "Durch Klicken auf Zulassen erlauben Sie {appName} und Zitadel, Ihre Informationen gemäß ihren jeweiligen Nutzungsbedingungen und Datenschutzrichtlinien zu verwenden. Sie können diesen Zugriff jederzeit widerrufen.", "submit": "Zulassen", "deny": "Ablehnen" }, diff --git a/apps/login/locales/en.json b/apps/login/locales/en.json index 27b0880615..efcb5a8503 100644 --- a/apps/login/locales/en.json +++ b/apps/login/locales/en.json @@ -195,7 +195,7 @@ }, "request": { "title": "{appName} would like to connect:", - "description": "By clicking Allow, you allow this app and Zitadel to use your information in accordance with their respective terms of service and privacy policies. You can revoke this access at any time.", + "description": "By clicking Allow, you allow {appName} and Zitadel to use your information in accordance with their respective terms of service and privacy policies. You can revoke this access at any time.", "submit": "Allow", "deny": "Deny" }, diff --git a/apps/login/locales/es.json b/apps/login/locales/es.json index e27ef1c25d..9a9f63f5cd 100644 --- a/apps/login/locales/es.json +++ b/apps/login/locales/es.json @@ -195,7 +195,7 @@ }, "request": { "title": "{appName} desea conectarse:", - "description": "Al hacer clic en Permitir, autorizas a esta aplicación y a Zitadel a usar tu información de acuerdo con sus respectivos términos de servicio y políticas de privacidad. Puedes revocar este acceso en cualquier momento.", + "description": "Al hacer clic en Permitir, autorizas a {appName} y a Zitadel a usar tu información de acuerdo con sus respectivos términos de servicio y políticas de privacidad. Puedes revocar este acceso en cualquier momento.", "submit": "Permitir", "deny": "Denegar" }, diff --git a/apps/login/locales/it.json b/apps/login/locales/it.json index 1c7a4d495d..af1a60f3cd 100644 --- a/apps/login/locales/it.json +++ b/apps/login/locales/it.json @@ -195,7 +195,7 @@ }, "request": { "title": "{appName} desidera connettersi:", - "description": "Cliccando su Consenti, autorizzi questa app e Zitadel a utilizzare le tue informazioni in conformità con i rispettivi termini di servizio e politiche sulla privacy. Puoi revocare questo accesso in qualsiasi momento.", + "description": "Cliccando su Consenti, autorizzi {appName} e Zitadel a utilizzare le tue informazioni in conformità con i rispettivi termini di servizio e politiche sulla privacy. Puoi revocare questo accesso in qualsiasi momento.", "submit": "Consenti", "deny": "Nega" }, diff --git a/apps/login/src/app/(login)/device/consent/page.tsx b/apps/login/src/app/(login)/device/consent/page.tsx index 64754d5ab8..150c9b4043 100644 --- a/apps/login/src/app/(login)/device/consent/page.tsx +++ b/apps/login/src/app/(login)/device/consent/page.tsx @@ -72,6 +72,7 @@ export default async function Page(props: {

    diff --git a/apps/login/src/components/consent.tsx b/apps/login/src/components/consent.tsx index 15b106e62e..755087c9e9 100644 --- a/apps/login/src/components/consent.tsx +++ b/apps/login/src/components/consent.tsx @@ -13,10 +13,12 @@ export function ConsentScreen({ scope, nextUrl, deviceAuthorizationRequestId, + appName, }: { scope?: string[]; nextUrl: string; deviceAuthorizationRequestId: string; + appName?: string; }) { const t = useTranslations(); const [loading, setLoading] = useState(false); @@ -63,7 +65,7 @@ export function ConsentScreen({

- {t("device.request.description")} + {t("device.request.description", { appName: appName })}

{error && ( From 2f7c628dcdfa0afbb569f9d2f4ffbbf3b1341d37 Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Tue, 6 May 2025 14:55:20 +0200 Subject: [PATCH 45/96] cleanup --- apps/login/locales/de.json | 1 + apps/login/locales/en.json | 1 + apps/login/locales/es.json | 1 + apps/login/locales/it.json | 1 + apps/login/locales/pl.json | 1 + apps/login/locales/ru.json | 1 + apps/login/locales/zh.json | 1 + .../src/app/(login)/device/consent/page.tsx | 2 +- apps/login/src/app/login/route.ts | 11 -- apps/login/src/lib/device.ts | 125 ------------------ 10 files changed, 8 insertions(+), 137 deletions(-) delete mode 100644 apps/login/src/lib/device.ts diff --git a/apps/login/locales/de.json b/apps/login/locales/de.json index 25f2ad4b38..f01d3d8f10 100644 --- a/apps/login/locales/de.json +++ b/apps/login/locales/de.json @@ -207,6 +207,7 @@ } }, "error": { + "noUserCode": "Kein Benutzercode angegeben!", "noDeviceRequest": " Es wurde keine Geräteanforderung gefunden. Bitte überprüfen Sie die URL.", "unknownContext": "Der Kontext des Benutzers konnte nicht ermittelt werden. Stellen Sie sicher, dass Sie zuerst den Benutzernamen eingeben oder einen loginName als Suchparameter angeben.", "sessionExpired": "Ihre aktuelle Sitzung ist abgelaufen. Bitte melden Sie sich erneut an.", diff --git a/apps/login/locales/en.json b/apps/login/locales/en.json index efcb5a8503..49f1fb99da 100644 --- a/apps/login/locales/en.json +++ b/apps/login/locales/en.json @@ -207,6 +207,7 @@ } }, "error": { + "noUserCode": "No user code provided!", "noDeviceRequest": "No device request found.", "unknownContext": "Could not get the context of the user. Make sure to enter the username first or provide a loginName as searchParam.", "sessionExpired": "Your current session has expired. Please login again.", diff --git a/apps/login/locales/es.json b/apps/login/locales/es.json index 9a9f63f5cd..8ec58e2b55 100644 --- a/apps/login/locales/es.json +++ b/apps/login/locales/es.json @@ -207,6 +207,7 @@ } }, "error": { + "noUserCode": "¡No se proporcionó código de usuario!", "noDeviceRequest": "No se encontró ninguna solicitud de dispositivo.", "unknownContext": "No se pudo obtener el contexto del usuario. Asegúrate de ingresar primero el nombre de usuario o proporcionar un loginName como parámetro de búsqueda.", "sessionExpired": "Tu sesión actual ha expirado. Por favor, inicia sesión de nuevo.", diff --git a/apps/login/locales/it.json b/apps/login/locales/it.json index af1a60f3cd..c59aeda2ab 100644 --- a/apps/login/locales/it.json +++ b/apps/login/locales/it.json @@ -207,6 +207,7 @@ } }, "error": { + "noUserCode": "Nessun codice utente fornito!", "noDeviceRequest": "Nessuna richiesta di dispositivo trovata.", "unknownContext": "Impossibile ottenere il contesto dell'utente. Assicurati di inserire prima il nome utente o di fornire un loginName come parametro di ricerca.", "sessionExpired": "La tua sessione attuale è scaduta. Effettua nuovamente l'accesso.", diff --git a/apps/login/locales/pl.json b/apps/login/locales/pl.json index a82efd9807..132c06f10f 100644 --- a/apps/login/locales/pl.json +++ b/apps/login/locales/pl.json @@ -207,6 +207,7 @@ } }, "error": { + "noUserCode": "Nie podano kodu użytkownika!", "noDeviceRequest": "Nie znaleziono żądania urządzenia.", "unknownContext": "Nie udało się pobrać kontekstu użytkownika. Upewnij się, że najpierw wprowadziłeś nazwę użytkownika lub podałeś login jako parametr wyszukiwania.", "sessionExpired": "Twoja sesja wygasła. Zaloguj się ponownie.", diff --git a/apps/login/locales/ru.json b/apps/login/locales/ru.json index f3ac0dfa43..9a3ecd7cdd 100644 --- a/apps/login/locales/ru.json +++ b/apps/login/locales/ru.json @@ -207,6 +207,7 @@ } }, "error": { + "noUserCode": "Не указан код пользователя!", "noDeviceRequest": "Не найдена ни одна заявка на устройство.", "unknownContext": "Не удалось получить контекст пользователя. Укажите имя пользователя или loginName в параметрах поиска.", "sessionExpired": "Ваша сессия истекла. Войдите снова.", diff --git a/apps/login/locales/zh.json b/apps/login/locales/zh.json index 72c4bc4735..d0fdd44d16 100644 --- a/apps/login/locales/zh.json +++ b/apps/login/locales/zh.json @@ -207,6 +207,7 @@ } }, "error": { + "noUserCode": "未提供用户代码!", "noDeviceRequest": "没有找到设备请求。", "unknownContext": "无法获取用户的上下文。请先输入用户名或提供 loginName 作为搜索参数。", "sessionExpired": "当前会话已过期,请重新登录。", diff --git a/apps/login/src/app/(login)/device/consent/page.tsx b/apps/login/src/app/(login)/device/consent/page.tsx index 150c9b4043..9d55f4f6b8 100644 --- a/apps/login/src/app/(login)/device/consent/page.tsx +++ b/apps/login/src/app/(login)/device/consent/page.tsx @@ -22,7 +22,7 @@ export default async function Page(props: { const organization = searchParams?.organization; if (!userCode || !requestId) { - return
{t("error.no_user_code")}
; + return
{t("error.noUserCode")}
; } const _headers = await headers(); diff --git a/apps/login/src/app/login/route.ts b/apps/login/src/app/login/route.ts index 365f83a225..3072f45229 100644 --- a/apps/login/src/app/login/route.ts +++ b/apps/login/src/app/login/route.ts @@ -1,5 +1,4 @@ import { getAllSessions } from "@/lib/cookies"; -import { loginWithDeviceAndSession } from "@/lib/device"; import { idpTypeToSlug } from "@/lib/idp"; import { loginWithOIDCAndSession } from "@/lib/oidc"; import { loginWithSAMLAndSession } from "@/lib/saml"; @@ -126,16 +125,6 @@ export async function GET(request: NextRequest) { sessionCookies, request, }); - } else if (requestId.startsWith("device_")) { - // this finishes the login process for Device Authorization - return loginWithDeviceAndSession({ - serviceUrl, - deviceRequest: requestId.replace("device_", ""), - sessionId, - sessions, - sessionCookies, - request, - }); } } diff --git a/apps/login/src/lib/device.ts b/apps/login/src/lib/device.ts deleted file mode 100644 index 36074803b9..0000000000 --- a/apps/login/src/lib/device.ts +++ /dev/null @@ -1,125 +0,0 @@ -import { Cookie } from "@/lib/cookies"; -import { sendLoginname, SendLoginnameCommand } from "@/lib/server/loginname"; -import { - authorizeOrDenyDeviceAuthorization, - getLoginSettings, -} from "@/lib/zitadel"; -import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb"; -import { NextRequest, NextResponse } from "next/server"; -import { constructUrl } from "./service"; -import { isSessionValid } from "./session"; - -type LoginWithOIDCandSession = { - serviceUrl: string; - deviceRequest: string; - sessionId: string; - sessions: Session[]; - sessionCookies: Cookie[]; - request: NextRequest; -}; -export async function loginWithDeviceAndSession({ - serviceUrl, - deviceRequest, - sessionId, - sessions, - sessionCookies, - request, -}: LoginWithOIDCandSession) { - console.log( - `Login with session: ${sessionId} and deviceRequest: ${deviceRequest}`, - ); - - const selectedSession = sessions.find((s) => s.id === sessionId); - - if (selectedSession && selectedSession.id) { - console.log(`Found session ${selectedSession.id}`); - - const isValid = await isSessionValid({ - serviceUrl, - session: selectedSession, - }); - - console.log("Session is valid:", isValid); - - if (!isValid && selectedSession.factors?.user) { - // if the session is not valid anymore, we need to redirect the user to re-authenticate / - // TODO: handle IDP intent direcly if available - const command: SendLoginnameCommand = { - loginName: selectedSession.factors.user?.loginName, - organization: selectedSession.factors?.user?.organizationId, - requestId: `device_${deviceRequest}`, - }; - - const res = await sendLoginname(command); - - if (res && "redirect" in res && res?.redirect) { - const absoluteUrl = constructUrl(request, res.redirect); - return NextResponse.redirect(absoluteUrl.toString()); - } - } - - const cookie = sessionCookies.find( - (cookie) => cookie.id === selectedSession?.id, - ); - - if (cookie && cookie.id && cookie.token) { - const session = { - sessionId: cookie?.id, - sessionToken: cookie?.token, - }; - - // works not with _rsc request - try { - const authResponse = await authorizeOrDenyDeviceAuthorization({ - serviceUrl, - deviceAuthorizationId: deviceRequest, - session, - }); - if (!authResponse) { - return NextResponse.json( - { error: "An error occurred!" }, - { status: 500 }, - ); - } - } catch (error: unknown) { - // handle already handled gracefully as these could come up if old emails with requestId are used (reset password, register emails etc.) - console.error(error); - if ( - error && - typeof error === "object" && - "code" in error && - error?.code === 9 - ) { - const loginSettings = await getLoginSettings({ - serviceUrl, - organization: selectedSession.factors?.user?.organizationId, - }); - - if (loginSettings?.defaultRedirectUri) { - return NextResponse.redirect(loginSettings.defaultRedirectUri); - } - - const signedinUrl = constructUrl(request, "/signedin"); - - signedinUrl.searchParams.set("requestId", `device_${deviceRequest}`); - - if (selectedSession.factors?.user?.loginName) { - signedinUrl.searchParams.set( - "loginName", - selectedSession.factors?.user?.loginName, - ); - } - if (selectedSession.factors?.user?.organizationId) { - signedinUrl.searchParams.set( - "organization", - selectedSession.factors?.user?.organizationId, - ); - } - return NextResponse.redirect(signedinUrl); - } else { - return NextResponse.json({ error }, { status: 500 }); - } - } - } - } -} From 4588c48fda3301881bb0c464ee105983fe4eb667 Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Tue, 6 May 2025 15:24:25 +0200 Subject: [PATCH 46/96] move flow completion to signedin page --- apps/login/locales/de.json | 6 +- apps/login/locales/en.json | 6 +- apps/login/locales/es.json | 6 +- apps/login/locales/it.json | 6 +- apps/login/locales/pl.json | 6 +- apps/login/locales/ru.json | 6 +- apps/login/locales/zh.json | 6 +- apps/login/src/app/(login)/signedin/page.tsx | 55 ++++++++++++++----- apps/login/src/components/consent.tsx | 6 +- .../login/src/components/device-code-form.tsx | 4 +- apps/login/src/lib/server/device.ts | 20 +++++++ apps/login/src/lib/server/oidc.ts | 16 +----- apps/login/src/lib/zitadel.ts | 4 ++ 13 files changed, 108 insertions(+), 39 deletions(-) create mode 100644 apps/login/src/lib/server/device.ts diff --git a/apps/login/locales/de.json b/apps/login/locales/de.json index f01d3d8f10..87518d74a5 100644 --- a/apps/login/locales/de.json +++ b/apps/login/locales/de.json @@ -166,7 +166,11 @@ "signedin": { "title": "Willkommen {user}!", "description": "Sie sind angemeldet.", - "continue": "Weiter" + "continue": "Weiter", + "error": { + "title": "Fehler", + "description": "Ein Fehler ist aufgetreten." + } }, "verify": { "userIdMissing": "Keine Benutzer-ID angegeben!", diff --git a/apps/login/locales/en.json b/apps/login/locales/en.json index 49f1fb99da..e32d736e70 100644 --- a/apps/login/locales/en.json +++ b/apps/login/locales/en.json @@ -166,7 +166,11 @@ "signedin": { "title": "Welcome {user}!", "description": "You are signed in.", - "continue": "Continue" + "continue": "Continue", + "error": { + "title": "Error", + "description": "An error occurred while trying to sign in." + } }, "verify": { "userIdMissing": "No userId provided!", diff --git a/apps/login/locales/es.json b/apps/login/locales/es.json index 8ec58e2b55..ff2fd9a4cb 100644 --- a/apps/login/locales/es.json +++ b/apps/login/locales/es.json @@ -166,7 +166,11 @@ "signedin": { "title": "¡Bienvenido {user}!", "description": "Has iniciado sesión.", - "continue": "Continuar" + "continue": "Continuar", + "error": { + "title": "Error", + "description": "Ocurrió un error al iniciar sesión." + } }, "verify": { "userIdMissing": "¡No se proporcionó userId!", diff --git a/apps/login/locales/it.json b/apps/login/locales/it.json index c59aeda2ab..792568872d 100644 --- a/apps/login/locales/it.json +++ b/apps/login/locales/it.json @@ -166,7 +166,11 @@ "signedin": { "title": "Benvenuto {user}!", "description": "Sei connesso.", - "continue": "Continua" + "continue": "Continua", + "error": { + "title": "Errore", + "description": "Si è verificato un errore durante il tentativo di accesso." + } }, "verify": { "userIdMissing": "Nessun userId fornito!", diff --git a/apps/login/locales/pl.json b/apps/login/locales/pl.json index 132c06f10f..18326df262 100644 --- a/apps/login/locales/pl.json +++ b/apps/login/locales/pl.json @@ -166,7 +166,11 @@ "signedin": { "title": "Witaj {user}!", "description": "Jesteś zalogowany.", - "continue": "Kontynuuj" + "continue": "Kontynuuj", + "error": { + "title": "Błąd", + "description": "Nie można załadować danych. Sprawdź połączenie z internetem lub spróbuj ponownie później." + } }, "verify": { "userIdMissing": "Nie podano identyfikatora użytkownika!", diff --git a/apps/login/locales/ru.json b/apps/login/locales/ru.json index 9a3ecd7cdd..9eda1730e6 100644 --- a/apps/login/locales/ru.json +++ b/apps/login/locales/ru.json @@ -166,7 +166,11 @@ "signedin": { "title": "Добро пожаловать, {user}!", "description": "Вы вошли в систему.", - "continue": "Продолжить" + "continue": "Продолжить", + "error": { + "title": "Ошибка", + "description": "Не удалось войти в систему. Проверьте свои данные и попробуйте снова." + } }, "verify": { "userIdMissing": "Не указан userId!", diff --git a/apps/login/locales/zh.json b/apps/login/locales/zh.json index d0fdd44d16..8a6200bf33 100644 --- a/apps/login/locales/zh.json +++ b/apps/login/locales/zh.json @@ -166,7 +166,11 @@ "signedin": { "title": "欢迎 {user}!", "description": "您已登录。", - "continue": "继续" + "continue": "继续", + "error": { + "title": "错误", + "description": "登录时发生错误。" + } }, "verify": { "userIdMissing": "未提供用户 ID!", diff --git a/apps/login/src/app/(login)/signedin/page.tsx b/apps/login/src/app/(login)/signedin/page.tsx index 09c96b56d4..0150a57f84 100644 --- a/apps/login/src/app/(login)/signedin/page.tsx +++ b/apps/login/src/app/(login)/signedin/page.tsx @@ -2,7 +2,11 @@ import { Alert, AlertType } from "@/components/alert"; import { Button, ButtonVariants } from "@/components/button"; import { DynamicTheme } from "@/components/dynamic-theme"; import { UserAvatar } from "@/components/user-avatar"; -import { getSessionCookieById } from "@/lib/cookies"; +import { + getMostRecentCookieWithLoginname, + getSessionCookieById, +} from "@/lib/cookies"; +import { completeDeviceAuthorization } from "@/lib/server/device"; import { getServiceUrlFromHeaders } from "@/lib/service"; import { loadMostRecentSession } from "@/lib/session"; import { @@ -41,6 +45,36 @@ export default async function Page(props: { searchParams: Promise }) { const { loginName, requestId, organization, sessionId } = searchParams; + const branding = await getBrandingSettings({ + serviceUrl, + organization, + }); + + // complete device authorization flow if device requestId is present + if (requestId && requestId.startsWith("device_")) { + const cookie = sessionId + ? await getSessionCookieById({ sessionId, organization }) + : await getMostRecentCookieWithLoginname({ + loginName: loginName, + organization: organization, + }); + + await completeDeviceAuthorization(requestId.replace("device_", ""), { + sessionId: cookie.id, + sessionToken: cookie.token, + }).catch((err) => { + return ( + +
+

{t("error.title")}

+

{t("error.description")}

+ {err.message} +
+
+ ); + }); + } + const sessionFactors = sessionId ? await loadSessionById(serviceUrl, sessionId, organization) : await loadMostRecentSession({ @@ -48,11 +82,6 @@ export default async function Page(props: { searchParams: Promise }) { sessionParams: { loginName, organization }, }); - const branding = await getBrandingSettings({ - serviceUrl, - organization, - }); - let loginSettings; if (!requestId) { loginSettings = await getLoginSettings({ @@ -69,6 +98,13 @@ export default async function Page(props: { searchParams: Promise }) {

{t("description")}

+ + {requestId && requestId.startsWith("device_") && ( You can now close this window and return to the device where you @@ -76,13 +112,6 @@ export default async function Page(props: { searchParams: Promise }) { )} - - {/* {sessionFactors?.id && ( )} */} diff --git a/apps/login/src/components/consent.tsx b/apps/login/src/components/consent.tsx index 755087c9e9..6ae3cade45 100644 --- a/apps/login/src/components/consent.tsx +++ b/apps/login/src/components/consent.tsx @@ -1,6 +1,6 @@ "use client"; -import { denyDeviceAuthorization } from "@/lib/server/oidc"; +import { completeDeviceAuthorization } from "@/lib/server/device"; import { useTranslations } from "next-intl"; import Link from "next/link"; import { useRouter } from "next/navigation"; @@ -27,7 +27,9 @@ export function ConsentScreen({ async function denyDeviceAuth() { setLoading(true); - const response = await denyDeviceAuthorization(deviceAuthorizationRequestId) + const response = await completeDeviceAuthorization( + deviceAuthorizationRequestId, + ) .catch(() => { setError("Could not register user"); return; diff --git a/apps/login/src/components/device-code-form.tsx b/apps/login/src/components/device-code-form.tsx index faa77c3cdd..e09adb1147 100644 --- a/apps/login/src/components/device-code-form.tsx +++ b/apps/login/src/components/device-code-form.tsx @@ -36,7 +36,7 @@ export function DeviceCodeForm({ userCode }: { userCode?: string }) { const response = await getDeviceAuthorizationRequest(value.userCode) .catch(() => { - setError("Could not complete the request"); + setError("Could not continue the request"); return; }) .finally(() => { @@ -44,7 +44,7 @@ export function DeviceCodeForm({ userCode }: { userCode?: string }) { }); if (!response || !response.deviceAuthorizationRequest?.id) { - setError("Could not complete the request"); + setError("Could not continue the request"); return; } diff --git a/apps/login/src/lib/server/device.ts b/apps/login/src/lib/server/device.ts new file mode 100644 index 0000000000..d96059f6a6 --- /dev/null +++ b/apps/login/src/lib/server/device.ts @@ -0,0 +1,20 @@ +"use server"; + +import { authorizeOrDenyDeviceAuthorization } from "@/lib/zitadel"; +import { headers } from "next/headers"; +import { getServiceUrlFromHeaders } from "../service"; + +export async function completeDeviceAuthorization( + deviceAuthorizationId: string, + session?: { sessionId: string; sessionToken: string }, +) { + const _headers = await headers(); + const { serviceUrl } = getServiceUrlFromHeaders(_headers); + + // without the session, device auth request is denied + return authorizeOrDenyDeviceAuthorization({ + serviceUrl, + deviceAuthorizationId, + session, + }); +} diff --git a/apps/login/src/lib/server/oidc.ts b/apps/login/src/lib/server/oidc.ts index 3bd0e53df4..4ae01b4a47 100644 --- a/apps/login/src/lib/server/oidc.ts +++ b/apps/login/src/lib/server/oidc.ts @@ -1,9 +1,6 @@ "use server"; -import { - authorizeOrDenyDeviceAuthorization, - getDeviceAuthorizationRequest as zitadelGetDeviceAuthorizationRequest, -} from "@/lib/zitadel"; +import { getDeviceAuthorizationRequest as zitadelGetDeviceAuthorizationRequest } from "@/lib/zitadel"; import { headers } from "next/headers"; import { getServiceUrlFromHeaders } from "../service"; @@ -16,14 +13,3 @@ export async function getDeviceAuthorizationRequest(userCode: string) { userCode, }); } - -export async function denyDeviceAuthorization(deviceAuthorizationId: string) { - const _headers = await headers(); - const { serviceUrl } = getServiceUrlFromHeaders(_headers); - - // without the session, device auth request is denied - return authorizeOrDenyDeviceAuthorization({ - serviceUrl, - deviceAuthorizationId, - }); -} diff --git a/apps/login/src/lib/zitadel.ts b/apps/login/src/lib/zitadel.ts index da690c10e2..cc79754735 100644 --- a/apps/login/src/lib/zitadel.ts +++ b/apps/login/src/lib/zitadel.ts @@ -952,6 +952,10 @@ export async function authorizeOrDenyDeviceAuthorization({ deviceAuthorizationId: string; session?: { sessionId: string; sessionToken: string }; }) { + console.log("authorizeOrDenyDeviceAuthorization"); + + console.log("session", session); + const oidcService = await createServiceForHost(OIDCService, serviceUrl); return oidcService.authorizeOrDenyDeviceAuthorization({ From c8a6cd66a57b8ed79c828c288bbbd270c2fe7a8f Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Tue, 6 May 2025 15:25:17 +0200 Subject: [PATCH 47/96] cleanup --- apps/login/src/app/(login)/signedin/page.tsx | 4 ---- apps/login/src/lib/zitadel.ts | 4 ---- 2 files changed, 8 deletions(-) diff --git a/apps/login/src/app/(login)/signedin/page.tsx b/apps/login/src/app/(login)/signedin/page.tsx index 0150a57f84..48595a3559 100644 --- a/apps/login/src/app/(login)/signedin/page.tsx +++ b/apps/login/src/app/(login)/signedin/page.tsx @@ -112,10 +112,6 @@ export default async function Page(props: { searchParams: Promise }) { )} - {/* {sessionFactors?.id && ( - - )} */} - {loginSettings?.defaultRedirectUri && (
diff --git a/apps/login/src/lib/zitadel.ts b/apps/login/src/lib/zitadel.ts index cc79754735..da690c10e2 100644 --- a/apps/login/src/lib/zitadel.ts +++ b/apps/login/src/lib/zitadel.ts @@ -952,10 +952,6 @@ export async function authorizeOrDenyDeviceAuthorization({ deviceAuthorizationId: string; session?: { sessionId: string; sessionToken: string }; }) { - console.log("authorizeOrDenyDeviceAuthorization"); - - console.log("session", session); - const oidcService = await createServiceForHost(OIDCService, serviceUrl); return oidcService.authorizeOrDenyDeviceAuthorization({ From 1f94e40af7338e65fd62a4a8b4b65ebc4fa23967 Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Tue, 6 May 2025 15:38:24 +0200 Subject: [PATCH 48/96] readme doc --- apps/login/readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/login/readme.md b/apps/login/readme.md index 120fad3cd7..ca7070a901 100644 --- a/apps/login/readme.md +++ b/apps/login/readme.md @@ -373,7 +373,7 @@ On all pages, where the current user is shown, you can jump to this page. This w ### /signedin -This is a success page which shows a completed login flow for a user, which did navigate to the login without a OIDC auth requrest. +This is a success page which shows a completed login flow for a user, which did navigate to the login without a OIDC auth requrest. From here device authorization flows are completed. It checks if the requestId param of starts with `device_` and then executes the `authorizeOrDenyDeviceAuthorization` command. /signedin From 3c28e71c0ed04a964f7180c98d9d1316921d94f1 Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Tue, 6 May 2025 15:48:37 +0200 Subject: [PATCH 49/96] filter for undefined scope values --- apps/login/src/components/consent.tsx | 35 +++++++++++++++------------ 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/apps/login/src/components/consent.tsx b/apps/login/src/components/consent.tsx index 6ae3cade45..7897ed7d7d 100644 --- a/apps/login/src/components/consent.tsx +++ b/apps/login/src/components/consent.tsx @@ -43,27 +43,30 @@ export function ConsentScreen({ } } + console.log("scope", scope); return (
    - {scope?.map((s) => { - const translationKey = `device.scope.${s}`; - const description = t(translationKey, null); + {scope + ?.filter((s) => !!s) + .map((s) => { + const translationKey = `device.scope.${s}`; + const description = t(translationKey, null); - // Check if the key itself is returned and provide a fallback - const resolvedDescription = - description === translationKey ? "" : description; + // Check if the key itself is returned and provide a fallback + const resolvedDescription = + description === translationKey ? "" : description; - return ( -
  • - {s} - {resolvedDescription} -
  • - ); - })} + return ( +
  • + {s} + {resolvedDescription} +
  • + ); + })}

From 606248521b74bef1501e4eb4482fead14ccb7b67 Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Tue, 6 May 2025 16:04:57 +0200 Subject: [PATCH 50/96] Update apps/login/src/components/consent.tsx Co-authored-by: David Skewis --- apps/login/src/components/consent.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/login/src/components/consent.tsx b/apps/login/src/components/consent.tsx index 7897ed7d7d..56026b1e27 100644 --- a/apps/login/src/components/consent.tsx +++ b/apps/login/src/components/consent.tsx @@ -43,7 +43,6 @@ export function ConsentScreen({ } } - console.log("scope", scope); return (

    From e1e9459495b1378028e627310240808f75adcb94 Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Tue, 6 May 2025 16:05:12 +0200 Subject: [PATCH 51/96] Update apps/login/locales/en.json Co-authored-by: David Skewis --- apps/login/locales/en.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/login/locales/en.json b/apps/login/locales/en.json index e32d736e70..b850a2a99b 100644 --- a/apps/login/locales/en.json +++ b/apps/login/locales/en.json @@ -198,7 +198,7 @@ "submit": "Continue" }, "request": { - "title": "{appName} would like to connect:", + "title": "{appName} would like to connect", "description": "By clicking Allow, you allow {appName} and Zitadel to use your information in accordance with their respective terms of service and privacy policies. You can revoke this access at any time.", "submit": "Allow", "deny": "Deny" From 6da4e7e25d9534fe908cc3b08bc26a09818cccec Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Tue, 6 May 2025 16:06:45 +0200 Subject: [PATCH 52/96] Update apps/login/locales/en.json Co-authored-by: David Skewis --- apps/login/locales/en.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/login/locales/en.json b/apps/login/locales/en.json index b850a2a99b..a42679561b 100644 --- a/apps/login/locales/en.json +++ b/apps/login/locales/en.json @@ -205,8 +205,8 @@ }, "scope": { "openid": "Verify your identity.", - "email": "Access to your email address.", - "profile": "Access to your full profile information.", + "email": "View your email address.", + "profile": "View your full profile information.", "offline_access": "Allow offline access to your account." } }, From f6d560ded2ef36fb0391acf0c612547e5d9d4b94 Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Tue, 6 May 2025 16:06:54 +0200 Subject: [PATCH 53/96] Update apps/login/src/components/consent.tsx Co-authored-by: David Skewis --- apps/login/src/components/consent.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/login/src/components/consent.tsx b/apps/login/src/components/consent.tsx index 56026b1e27..ef78169e36 100644 --- a/apps/login/src/components/consent.tsx +++ b/apps/login/src/components/consent.tsx @@ -61,7 +61,6 @@ export function ConsentScreen({ key={s} className="grid grid-cols-4 w-full text-sm flex flex-row items-center bg-background-light-400 dark:bg-background-dark-400 border border-divider-light py-2 px-4 rounded-md transition-all" > - {s} {resolvedDescription} ); From 5a97af410fadcf99a101ce0e06053018f89c5d8b Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Tue, 6 May 2025 16:08:45 +0200 Subject: [PATCH 54/96] cleanup consent screen --- apps/login/src/components/consent.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/login/src/components/consent.tsx b/apps/login/src/components/consent.tsx index ef78169e36..3b89e334c1 100644 --- a/apps/login/src/components/consent.tsx +++ b/apps/login/src/components/consent.tsx @@ -59,9 +59,9 @@ export function ConsentScreen({ return (
  • - {resolvedDescription} + {resolvedDescription}
  • ); })} From 018e478cd6382e7c8b5d2fa7b8a6388b58d39ac8 Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Tue, 6 May 2025 16:14:19 +0200 Subject: [PATCH 55/96] de --- apps/login/locales/de.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/login/locales/de.json b/apps/login/locales/de.json index 87518d74a5..d250bb65ad 100644 --- a/apps/login/locales/de.json +++ b/apps/login/locales/de.json @@ -204,7 +204,7 @@ "deny": "Ablehnen" }, "scope": { - "openid": "Überprüfen Sie Ihre Identität.", + "openid": "Überprüfen Ihrer Identität.", "email": "Zugriff auf Ihre E-Mail-Adresse.", "profile": "Zugriff auf Ihre vollständigen Profilinformationen.", "offline_access": "Erlauben Sie den Offline-Zugriff auf Ihr Konto." From 3f3b3d95bbac3d7888b969b49aa423cf869facd9 Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Tue, 6 May 2025 20:06:37 +0200 Subject: [PATCH 56/96] disclaimer, description --- apps/login/locales/de.json | 1 + apps/login/locales/en.json | 3 ++- apps/login/locales/es.json | 3 ++- apps/login/locales/it.json | 3 ++- apps/login/locales/pl.json | 3 ++- apps/login/locales/ru.json | 3 ++- apps/login/locales/zh.json | 3 ++- apps/login/src/app/(login)/device/consent/page.tsx | 2 ++ apps/login/src/components/consent.tsx | 2 +- 9 files changed, 16 insertions(+), 7 deletions(-) diff --git a/apps/login/locales/de.json b/apps/login/locales/de.json index d250bb65ad..a2c137cf43 100644 --- a/apps/login/locales/de.json +++ b/apps/login/locales/de.json @@ -199,6 +199,7 @@ }, "request": { "title": "{appName} möchte eine Verbindung herstellen:", + "disclaimer": "{appName} hat Zugriff auf:", "description": "Durch Klicken auf Zulassen erlauben Sie {appName} und Zitadel, Ihre Informationen gemäß ihren jeweiligen Nutzungsbedingungen und Datenschutzrichtlinien zu verwenden. Sie können diesen Zugriff jederzeit widerrufen.", "submit": "Zulassen", "deny": "Ablehnen" diff --git a/apps/login/locales/en.json b/apps/login/locales/en.json index a42679561b..63a45c7d15 100644 --- a/apps/login/locales/en.json +++ b/apps/login/locales/en.json @@ -199,7 +199,8 @@ }, "request": { "title": "{appName} would like to connect", - "description": "By clicking Allow, you allow {appName} and Zitadel to use your information in accordance with their respective terms of service and privacy policies. You can revoke this access at any time.", + "description": "{appName} will have access to:", + "disclaimer": "By clicking Allow, you allow {appName} and Zitadel to use your information in accordance with their respective terms of service and privacy policies. You can revoke this access at any time.", "submit": "Allow", "deny": "Deny" }, diff --git a/apps/login/locales/es.json b/apps/login/locales/es.json index ff2fd9a4cb..60570eceb0 100644 --- a/apps/login/locales/es.json +++ b/apps/login/locales/es.json @@ -199,7 +199,8 @@ }, "request": { "title": "{appName} desea conectarse:", - "description": "Al hacer clic en Permitir, autorizas a {appName} y a Zitadel a usar tu información de acuerdo con sus respectivos términos de servicio y políticas de privacidad. Puedes revocar este acceso en cualquier momento.", + "description": "{appName} tendrá acceso a:", + "disclaimer": "Al hacer clic en Permitir, autorizas a {appName} y a Zitadel a usar tu información de acuerdo con sus respectivos términos de servicio y políticas de privacidad. Puedes revocar este acceso en cualquier momento.", "submit": "Permitir", "deny": "Denegar" }, diff --git a/apps/login/locales/it.json b/apps/login/locales/it.json index 792568872d..53894fdf5d 100644 --- a/apps/login/locales/it.json +++ b/apps/login/locales/it.json @@ -199,7 +199,8 @@ }, "request": { "title": "{appName} desidera connettersi:", - "description": "Cliccando su Consenti, autorizzi {appName} e Zitadel a utilizzare le tue informazioni in conformità con i rispettivi termini di servizio e politiche sulla privacy. Puoi revocare questo accesso in qualsiasi momento.", + "description": "{appName} avrà accesso a:", + "disclaimer": "Cliccando su Consenti, autorizzi {appName} e Zitadel a utilizzare le tue informazioni in conformità con i rispettivi termini di servizio e politiche sulla privacy. Puoi revocare questo accesso in qualsiasi momento.", "submit": "Consenti", "deny": "Nega" }, diff --git a/apps/login/locales/pl.json b/apps/login/locales/pl.json index 18326df262..52b802eccb 100644 --- a/apps/login/locales/pl.json +++ b/apps/login/locales/pl.json @@ -199,7 +199,8 @@ }, "request": { "title": "{appName} chce się połączyć:", - "description": "Klikając Zezwól, pozwalasz tej aplikacji i Zitadel na korzystanie z Twoich informacji zgodnie z ich odpowiednimi warunkami użytkowania i politykami prywatności. Możesz cofnąć ten dostęp w dowolnym momencie.", + "description": "{appName} będzie miało dostęp do:", + "disclaimer": "Klikając Zezwól, pozwalasz tej aplikacji i Zitadel na korzystanie z Twoich informacji zgodnie z ich odpowiednimi warunkami użytkowania i politykami prywatności. Możesz cofnąć ten dostęp w dowolnym momencie.", "submit": "Zezwól", "deny": "Odmów" }, diff --git a/apps/login/locales/ru.json b/apps/login/locales/ru.json index 9eda1730e6..197b9663be 100644 --- a/apps/login/locales/ru.json +++ b/apps/login/locales/ru.json @@ -199,7 +199,8 @@ }, "request": { "title": "{appName} хочет подключиться:", - "description": "Нажимая «Разрешить», вы разрешаете этому приложению и Zitadel использовать вашу информацию в соответствии с их условиями использования и политиками конфиденциальности. Вы можете отозвать этот доступ в любое время.", + "description": "{appName} получит доступ к:", + "disclaimer": "Нажимая «Разрешить», вы разрешаете этому приложению и Zitadel использовать вашу информацию в соответствии с их условиями использования и политиками конфиденциальности. Вы можете отозвать этот доступ в любое время.", "submit": "Разрешить", "deny": "Запретить" }, diff --git a/apps/login/locales/zh.json b/apps/login/locales/zh.json index 8a6200bf33..d4319dc051 100644 --- a/apps/login/locales/zh.json +++ b/apps/login/locales/zh.json @@ -199,7 +199,8 @@ }, "request": { "title": "{appName} 想要连接:", - "description": "点击“允许”即表示您允许此应用程序和 Zitadel 根据其各自的服务条款和隐私政策使用您的信息。您可以随时撤销此访问权限。", + "description": "{appName} 将访问:", + "disclaimer": "点击“允许”即表示您允许此应用程序和 Zitadel 根据其各自的服务条款和隐私政策使用您的信息。您可以随时撤销此访问权限。", "submit": "允许", "deny": "拒绝" }, diff --git a/apps/login/src/app/(login)/device/consent/page.tsx b/apps/login/src/app/(login)/device/consent/page.tsx index 9d55f4f6b8..379dad2720 100644 --- a/apps/login/src/app/(login)/device/consent/page.tsx +++ b/apps/login/src/app/(login)/device/consent/page.tsx @@ -69,6 +69,8 @@ export default async function Page(props: { {t("request.title", { appName: deviceAuthorizationRequest?.appName })} +

    {t("request.description")}

    +

    - {t("device.request.description", { appName: appName })} + {t("device.request.disclaimer", { appName: appName })}

    {error && ( From 2a01f0f2e75da85b5a4f4cbe5de7941358595057 Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Wed, 7 May 2025 09:12:58 +0200 Subject: [PATCH 57/96] turbo --- apps/login/turbo.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/apps/login/turbo.json b/apps/login/turbo.json index e8a243feaf..80224125a2 100644 --- a/apps/login/turbo.json +++ b/apps/login/turbo.json @@ -5,6 +5,10 @@ "outputs": ["dist/**", ".next/**", "!.next/cache/**"], "dependsOn": ["^build"] }, + "build:standalone": { + "outputs": ["dist/**", ".next/**", "!.next/cache/**"], + "dependsOn": ["^build"] + }, "test": { "dependsOn": ["@zitadel/client#build"] }, From 4b8b3b4a2ec41032b601820297b41ccd8475e97b Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Wed, 7 May 2025 09:30:52 +0200 Subject: [PATCH 58/96] fix: turbo dep --- apps/login/turbo.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/apps/login/turbo.json b/apps/login/turbo.json index e8a243feaf..80224125a2 100644 --- a/apps/login/turbo.json +++ b/apps/login/turbo.json @@ -5,6 +5,10 @@ "outputs": ["dist/**", ".next/**", "!.next/cache/**"], "dependsOn": ["^build"] }, + "build:standalone": { + "outputs": ["dist/**", ".next/**", "!.next/cache/**"], + "dependsOn": ["^build"] + }, "test": { "dependsOn": ["@zitadel/client#build"] }, From 15937f51501eb58db39b8d6effd069ec02f70855 Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Wed, 7 May 2025 09:40:05 +0200 Subject: [PATCH 59/96] driver opts --- .github/workflows/docker.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 58a48f9882..b8f37c0ce1 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -42,7 +42,7 @@ jobs: - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 with: - driver-opts: 'image=moby/buildkit:v0.11.6' + driver: docker-container - name: Login Public uses: docker/login-action@v3 From 5c53069ed858d4ceb1737f80298b1ca5015a831b Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Wed, 7 May 2025 10:17:55 +0200 Subject: [PATCH 60/96] fix imports --- apps/login/src/app/(login)/device/consent/page.tsx | 2 +- apps/login/src/app/(login)/device/page.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/login/src/app/(login)/device/consent/page.tsx b/apps/login/src/app/(login)/device/consent/page.tsx index 379dad2720..8cc9b4556f 100644 --- a/apps/login/src/app/(login)/device/consent/page.tsx +++ b/apps/login/src/app/(login)/device/consent/page.tsx @@ -1,6 +1,6 @@ import { ConsentScreen } from "@/components/consent"; import { DynamicTheme } from "@/components/dynamic-theme"; -import { getServiceUrlFromHeaders } from "@/lib/service"; +import { getServiceUrlFromHeaders } from "@/lib/service-url"; import { getBrandingSettings, getDefaultOrg, diff --git a/apps/login/src/app/(login)/device/page.tsx b/apps/login/src/app/(login)/device/page.tsx index bde104b631..4353ef5be4 100644 --- a/apps/login/src/app/(login)/device/page.tsx +++ b/apps/login/src/app/(login)/device/page.tsx @@ -1,6 +1,6 @@ import { DeviceCodeForm } from "@/components/device-code-form"; import { DynamicTheme } from "@/components/dynamic-theme"; -import { getServiceUrlFromHeaders } from "@/lib/service"; +import { getServiceUrlFromHeaders } from "@/lib/service-url"; import { getBrandingSettings, getDefaultOrg } from "@/lib/zitadel"; import { Organization } from "@zitadel/proto/zitadel/org/v2/org_pb"; import { getLocale, getTranslations } from "next-intl/server"; From 41fb472a4ba537c1e07a97df1696f14fa485ddac Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Wed, 7 May 2025 10:20:39 +0200 Subject: [PATCH 61/96] imports --- apps/login/src/lib/server/device.ts | 2 +- apps/login/src/lib/server/oidc.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/login/src/lib/server/device.ts b/apps/login/src/lib/server/device.ts index d96059f6a6..5e36facfc8 100644 --- a/apps/login/src/lib/server/device.ts +++ b/apps/login/src/lib/server/device.ts @@ -2,7 +2,7 @@ import { authorizeOrDenyDeviceAuthorization } from "@/lib/zitadel"; import { headers } from "next/headers"; -import { getServiceUrlFromHeaders } from "../service"; +import { getServiceUrlFromHeaders } from "../service-url"; export async function completeDeviceAuthorization( deviceAuthorizationId: string, diff --git a/apps/login/src/lib/server/oidc.ts b/apps/login/src/lib/server/oidc.ts index 4ae01b4a47..36a31fe419 100644 --- a/apps/login/src/lib/server/oidc.ts +++ b/apps/login/src/lib/server/oidc.ts @@ -2,7 +2,7 @@ import { getDeviceAuthorizationRequest as zitadelGetDeviceAuthorizationRequest } from "@/lib/zitadel"; import { headers } from "next/headers"; -import { getServiceUrlFromHeaders } from "../service"; +import { getServiceUrlFromHeaders } from "../service-url"; export async function getDeviceAuthorizationRequest(userCode: string) { const _headers = await headers(); From 1852017a11c8e21f82943dd865fa99ed5accd79b Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Wed, 7 May 2025 13:40:30 +0200 Subject: [PATCH 62/96] i18n ns --- apps/login/src/app/(login)/device/consent/page.tsx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/apps/login/src/app/(login)/device/consent/page.tsx b/apps/login/src/app/(login)/device/consent/page.tsx index 8cc9b4556f..c011da5730 100644 --- a/apps/login/src/app/(login)/device/consent/page.tsx +++ b/apps/login/src/app/(login)/device/consent/page.tsx @@ -15,7 +15,7 @@ export default async function Page(props: { }) { const searchParams = await props.searchParams; const locale = getLocale(); - const t = await getTranslations({ locale, namespace: "device" }); + const t = await getTranslations({ locale }); const userCode = searchParams?.user_code; const requestId = searchParams?.requestId; @@ -66,10 +66,12 @@ export default async function Page(props: {

    - {t("request.title", { appName: deviceAuthorizationRequest?.appName })} + {t("device.request.title", { + appName: deviceAuthorizationRequest?.appName, + })}

    -

    {t("request.description")}

    +

    {t("device.request.description")}

    Date: Wed, 7 May 2025 13:54:21 +0200 Subject: [PATCH 63/96] render appname --- apps/login/src/app/(login)/device/consent/page.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/apps/login/src/app/(login)/device/consent/page.tsx b/apps/login/src/app/(login)/device/consent/page.tsx index c011da5730..75676cd7ff 100644 --- a/apps/login/src/app/(login)/device/consent/page.tsx +++ b/apps/login/src/app/(login)/device/consent/page.tsx @@ -71,7 +71,11 @@ export default async function Page(props: { })} -

    {t("device.request.description")}

    +

    + {t("device.request.description", { + appName: deviceAuthorizationRequest?.appName, + })} +

    Date: Wed, 7 May 2025 14:00:17 +0200 Subject: [PATCH 64/96] autocomplete user code --- apps/login/src/app/(login)/device/page.tsx | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/apps/login/src/app/(login)/device/page.tsx b/apps/login/src/app/(login)/device/page.tsx index 4353ef5be4..1b262c41fc 100644 --- a/apps/login/src/app/(login)/device/page.tsx +++ b/apps/login/src/app/(login)/device/page.tsx @@ -37,13 +37,9 @@ export default async function Page(props: { return (
    - {!userCode && ( - <> -

    {t("usercode.title")}

    -

    {t("usercode.description")}

    - - - )} +

    {t("usercode.title")}

    +

    {t("usercode.description")}

    +
    ); From 9df31eb9d6abc6ab4664a086180adf956c8b4174 Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Wed, 7 May 2025 14:03:18 +0200 Subject: [PATCH 65/96] fallback to openid scope --- apps/login/src/components/consent.tsx | 39 +++++++++++++++------------ 1 file changed, 22 insertions(+), 17 deletions(-) diff --git a/apps/login/src/components/consent.tsx b/apps/login/src/components/consent.tsx index 30d5b4e407..d3d30b3113 100644 --- a/apps/login/src/components/consent.tsx +++ b/apps/login/src/components/consent.tsx @@ -43,28 +43,33 @@ export function ConsentScreen({ } } + const scopes = scope?.filter((s) => !!s); + return (
      - {scope - ?.filter((s) => !!s) - .map((s) => { - const translationKey = `device.scope.${s}`; - const description = t(translationKey, null); + {scopes?.length === 0 && ( + + {t("device.scope.openid")} + + )} + {scopes?.map((s) => { + const translationKey = `device.scope.${s}`; + const description = t(translationKey, null); - // Check if the key itself is returned and provide a fallback - const resolvedDescription = - description === translationKey ? "" : description; + // Check if the key itself is returned and provide a fallback + const resolvedDescription = + description === translationKey ? "" : description; - return ( -
    • - {resolvedDescription} -
    • - ); - })} + return ( +
    • + {resolvedDescription} +
    • + ); + })}

    From 575831f2528306ca4f6d12bb92aec5d678600823 Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Mon, 19 May 2025 14:36:26 +0200 Subject: [PATCH 66/96] set cookie --- apps/login/src/lib/server/verify.ts | 25 ++++++++++++++++++++++++- apps/login/src/lib/zitadel.ts | 23 ++++++++++++++++++++++- 2 files changed, 46 insertions(+), 2 deletions(-) diff --git a/apps/login/src/lib/server/verify.ts b/apps/login/src/lib/server/verify.ts index e7c9f5e715..d56af5e582 100644 --- a/apps/login/src/lib/server/verify.ts +++ b/apps/login/src/lib/server/verify.ts @@ -12,13 +12,16 @@ import { verifyTOTPRegistration, sendEmailCode as zitadelSendEmailCode, } from "@/lib/zitadel"; +import crypto from "crypto"; + import { create } from "@zitadel/client"; import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb"; import { ChecksSchema } from "@zitadel/proto/zitadel/session/v2/session_service_pb"; import { User } from "@zitadel/proto/zitadel/user/v2/user_pb"; -import { headers } from "next/headers"; +import { cookies, headers } from "next/headers"; import { getNextUrl } from "../client"; import { getSessionCookieByLoginName } from "../cookies"; +import { getFingerprintId } from "../fingerprint"; import { getServiceUrlFromHeaders } from "../service-url"; import { loadMostRecentSession } from "../session"; import { checkMFAFactors } from "../verify-helper"; @@ -193,6 +196,26 @@ export async function sendVerification(command: VerifyUserByEmailCommand) { if (session.factors?.user?.loginName) { params.set("loginName", session.factors?.user?.loginName); } + + // set hash of userId and userAgentId to prevent replay attacks, TODO: check on the /authenticator/set page + + const cookiesList = await cookies(); + + const userAgentId = await getFingerprintId(); + + const verificationCheck = crypto + .createHash("sha256") + .update(`${user.userId}:${userAgentId}`) + .digest("hex"); + + await cookiesList.set({ + name: "verificationCheck", + value: verificationCheck, + httpOnly: true, + path: "/", + maxAge: 300, // 5 minutes + }); + return { redirect: `/authenticator/set?${params}` }; } diff --git a/apps/login/src/lib/zitadel.ts b/apps/login/src/lib/zitadel.ts index 508baf1667..409e63e6cf 100644 --- a/apps/login/src/lib/zitadel.ts +++ b/apps/login/src/lib/zitadel.ts @@ -45,8 +45,10 @@ import { VerifyPasskeyRegistrationRequest, VerifyU2FRegistrationRequest, } from "@zitadel/proto/zitadel/user/v2/user_service_pb"; +import crypto from "crypto"; import { unstable_cacheLife as cacheLife } from "next/cache"; -import { getUserAgent } from "./fingerprint"; +import { cookies } from "next/headers"; +import { getFingerprintId, getUserAgent } from "./fingerprint"; import { createServiceForHost } from "./service"; const useCache = process.env.DEBUG !== "true"; @@ -1198,6 +1200,25 @@ export async function setUserPassword({ !(authmethods.authMethodTypes.length === 0) && user.state !== UserState.INITIAL ) { + // check if a verification was done earlier + + const cookiesList = await cookies(); + + const userAgentId = await getFingerprintId(); + + const verificationCheck = crypto + .createHash("sha256") + .update(`${user.userId}:${userAgentId}`) + .digest("hex"); + + await cookiesList.set({ + name: "verificationCheck", + value: verificationCheck, + httpOnly: true, + path: "/", + maxAge: 300, // 5 minutes + }); + return { error: "Provide a code to set a password" }; } } From 949581d81c012322431a0ee62730ab9a7372223c Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Mon, 19 May 2025 14:41:57 +0200 Subject: [PATCH 67/96] fix: enfore secure for production environments --- apps/login/src/lib/cookies.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/login/src/lib/cookies.ts b/apps/login/src/lib/cookies.ts index 76f5580a16..28393f1321 100644 --- a/apps/login/src/lib/cookies.ts +++ b/apps/login/src/lib/cookies.ts @@ -31,7 +31,8 @@ async function setSessionHttpOnlyCookie( value: JSON.stringify(sessions), httpOnly: true, path: "/", - sameSite, + sameSite: process.env.NODE_ENV === "production" ? sameSite : "lax", + secure: process.env.NODE_ENV === "production", }); } From 29eeef798ad10586bcd33a87a241ae905fca19b4 Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Mon, 19 May 2025 14:43:54 +0200 Subject: [PATCH 68/96] add NODE_ENV to turbo.json --- turbo.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/turbo.json b/turbo.json index c8d8b18af3..d26bdd8f56 100644 --- a/turbo.json +++ b/turbo.json @@ -12,7 +12,8 @@ "ZITADEL_API_URL", "ZITADEL_SERVICE_USER_TOKEN", "NEXT_PUBLIC_BASE_PATH", - "CUSTOM_REQUEST_HEADERS" + "CUSTOM_REQUEST_HEADERS", + "NODE_ENV" ], "tasks": { "generate": { From 1ffb9968158a0d8db2a51fefb091f3042cda559c Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Tue, 20 May 2025 09:52:59 +0200 Subject: [PATCH 69/96] improve password handling --- .../login/src/components/register-passkey.tsx | 11 ++ apps/login/src/lib/server/passkeys.ts | 44 +++++- apps/login/src/lib/server/password.ts | 135 +++++++++--------- apps/login/src/lib/zitadel.ts | 47 +----- 4 files changed, 124 insertions(+), 113 deletions(-) diff --git a/apps/login/src/components/register-passkey.tsx b/apps/login/src/components/register-passkey.tsx index 163ab507b8..8687312bbc 100644 --- a/apps/login/src/components/register-passkey.tsx +++ b/apps/login/src/components/register-passkey.tsx @@ -83,6 +83,16 @@ export function RegisterPasskey({ return; } + if ("error" in resp && resp.error) { + setError(resp.error); + return; + } + + if (!("passkeyId" in resp)) { + setError("An error on registering passkey"); + return; + } + const passkeyId = resp.passkeyId; const options: CredentialCreationOptions = (resp.publicKeyCredentialCreationOptions as CredentialCreationOptions) ?? @@ -92,6 +102,7 @@ export function RegisterPasskey({ setError("An error on registering passkey"); return; } + options.publicKey.challenge = coerceToArrayBuffer( options.publicKey.challenge, "challenge", diff --git a/apps/login/src/lib/server/passkeys.ts b/apps/login/src/lib/server/passkeys.ts index 73d12043b0..c5ad990c61 100644 --- a/apps/login/src/lib/server/passkeys.ts +++ b/apps/login/src/lib/server/passkeys.ts @@ -5,6 +5,7 @@ import { getLoginSettings, getSession, getUserByID, + listAuthenticationMethodTypes, registerPasskey, verifyPasskeyRegistration as zitadelVerifyPasskeyRegistration, } from "@/lib/zitadel"; @@ -14,7 +15,8 @@ import { RegisterPasskeyResponse, VerifyPasskeyRegistrationRequestSchema, } from "@zitadel/proto/zitadel/user/v2/user_service_pb"; -import { headers } from "next/headers"; +import crypto from "crypto"; +import { cookies, headers } from "next/headers"; import { userAgent } from "next/server"; import { getNextUrl } from "../client"; import { @@ -22,6 +24,7 @@ import { getSessionCookieById, getSessionCookieByLoginName, } from "../cookies"; +import { getFingerprintId } from "../fingerprint"; import { getServiceUrlFromHeaders } from "../service-url"; import { checkEmailVerification } from "../verify-helper"; import { setSessionAndUpdateCookie } from "./cookie"; @@ -39,7 +42,7 @@ type RegisterPasskeyCommand = { export async function registerPasskeyLink( command: RegisterPasskeyCommand, -): Promise { +): Promise { const { sessionId } = command; const _headers = await headers(); @@ -57,6 +60,43 @@ export async function registerPasskeyLink( sessionToken: sessionCookie.token, }); + if (!session?.session?.factors?.user?.id) { + return { error: "Could not determine user from session" }; + } + + const authmethods = await listAuthenticationMethodTypes({ + serviceUrl, + userId: session?.session?.factors?.user?.id, + }); + + // if the user has no authmethods set, we need to check if the user was verified + // users are redirected from /authenticator/set to /password/set + if (authmethods.authMethodTypes.length !== 0) { + return { + error: + "You have to provide a code or have a valid User Verification Check", + }; + } + + // check if a verification was done earlier + const cookiesList = await cookies(); + const userAgentId = await getFingerprintId(); + + const verificationCheck = crypto + .createHash("sha256") + .update(`${user.userId}:${userAgentId}`) + .digest("hex"); + + const cookieValue = await cookiesList.get("verificationCheck")?.value; + + if (!cookieValue) { + return { error: "User Verification Check has to be done" }; + } + + if (cookieValue !== verificationCheck) { + return { error: "User Verification Check has to be done" }; + } + const [hostname, port] = host.split(":"); if (!hostname) { diff --git a/apps/login/src/lib/server/password.ts b/apps/login/src/lib/server/password.ts index 9e256c71d9..26adb56c7b 100644 --- a/apps/login/src/lib/server/password.ts +++ b/apps/login/src/lib/server/password.ts @@ -13,7 +13,6 @@ import { listAuthenticationMethodTypes, listUsers, passwordReset, - setPassword, setUserPassword, } from "@/lib/zitadel"; import { ConnectError, create } from "@zitadel/client"; @@ -25,13 +24,12 @@ import { } from "@zitadel/proto/zitadel/session/v2/session_service_pb"; import { LoginSettings } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb"; import { User, UserState } from "@zitadel/proto/zitadel/user/v2/user_pb"; -import { - AuthenticationMethodType, - SetPasswordRequestSchema, -} from "@zitadel/proto/zitadel/user/v2/user_service_pb"; -import { headers } from "next/headers"; +import { SetPasswordRequestSchema } from "@zitadel/proto/zitadel/user/v2/user_service_pb"; +import crypto from "crypto"; +import { cookies, headers } from "next/headers"; import { getNextUrl } from "../client"; import { getSessionCookieById, getSessionCookieByLoginName } from "../cookies"; +import { getFingerprintId } from "../fingerprint"; import { getServiceUrlFromHeaders } from "../service-url"; import { checkEmailVerification, @@ -297,6 +295,7 @@ export async function sendPassword(command: UpdateSessionCommand) { return { redirect: url }; } +// this function lets users with code set a password or users with valid User Verification Check export async function changePassword(command: { code?: string; userId: string; @@ -316,11 +315,50 @@ export async function changePassword(command: { } const userId = user.userId; + if (user.state === UserState.INITIAL) { + return { error: "User Initial State is not supported" }; + } + + // check if the user has no password set in order to set a password + if (!command.code) { + const authmethods = await listAuthenticationMethodTypes({ + serviceUrl, + userId, + }); + + // if the user has no authmethods set, we need to check if the user was verified + // users are redirected from /authenticator/set to /password/set + if (authmethods.authMethodTypes.length !== 0) { + return { + error: + "You have to provide a code or have a valid User Verification Check", + }; + } + + // check if a verification was done earlier + const cookiesList = await cookies(); + const userAgentId = await getFingerprintId(); + + const verificationCheck = crypto + .createHash("sha256") + .update(`${user.userId}:${userAgentId}`) + .digest("hex"); + + const cookieValue = await cookiesList.get("verificationCheck")?.value; + + if (!cookieValue) { + return { error: "User Verification Check has to be done" }; + } + + if (cookieValue !== verificationCheck) { + return { error: "User Verification Check has to be done" }; + } + } + return setUserPassword({ serviceUrl, userId, password: command.password, - user, code: command.code, }); } @@ -366,67 +404,32 @@ export async function checkSessionAndSetPassword({ return { error: "Could not load auth methods" }; } - const requiredAuthMethodsForForceMFA = [ - AuthenticationMethodType.OTP_EMAIL, - AuthenticationMethodType.OTP_SMS, - AuthenticationMethodType.TOTP, - AuthenticationMethodType.U2F, - ]; - - const hasNoMFAMethods = requiredAuthMethodsForForceMFA.every( - (method) => !authmethods.authMethodTypes.includes(method), - ); - - const loginSettings = await getLoginSettings({ - serviceUrl, - organization: session.factors.user.organizationId, - }); - - const forceMfa = !!( - loginSettings?.forceMfa || loginSettings?.forceMfaLocalOnly - ); - - // if the user has no MFA but MFA is enforced, we can set a password otherwise we use the token of the user - if (forceMfa && hasNoMFAMethods) { - return setPassword({ serviceUrl, payload }).catch((error) => { - // throw error if failed precondition (ex. User is not yet initialized) - if (error.code === 9 && error.message) { - return { error: "Failed precondition" }; - } else { - throw error; - } + const transport = async (serviceUrl: string, token: string) => { + return createServerTransport(token, { + baseUrl: serviceUrl, }); - } else { - const transport = async (serviceUrl: string, token: string) => { - return createServerTransport(token, { - baseUrl: serviceUrl, - }); - }; + }; - const myUserService = async (serviceUrl: string, sessionToken: string) => { - const transportPromise = await transport(serviceUrl, sessionToken); - return createUserServiceClient(transportPromise); - }; + const myUserService = async (serviceUrl: string, sessionToken: string) => { + const transportPromise = await transport(serviceUrl, sessionToken); + return createUserServiceClient(transportPromise); + }; - const selfService = await myUserService( - serviceUrl, - `${sessionCookie.token}`, - ); + const selfService = await myUserService(serviceUrl, `${sessionCookie.token}`); - return selfService - .setPassword( - { - userId: session.factors.user.id, - newPassword: { password, changeRequired: false }, - }, - {}, - ) - .catch((error: ConnectError) => { - console.log(error); - if (error.code === 7) { - return { error: "Session is not valid." }; - } - throw error; - }); - } + return selfService + .setPassword( + { + userId: session.factors.user.id, + newPassword: { password, changeRequired: false }, + }, + {}, + ) + .catch((error: ConnectError) => { + console.log(error); + if (error.code === 7) { + return { error: "Session is not valid." }; + } + throw error; + }); } diff --git a/apps/login/src/lib/zitadel.ts b/apps/login/src/lib/zitadel.ts index 409e63e6cf..d1fe83434d 100644 --- a/apps/login/src/lib/zitadel.ts +++ b/apps/login/src/lib/zitadel.ts @@ -29,11 +29,7 @@ import { SearchQuery, SearchQuerySchema, } from "@zitadel/proto/zitadel/user/v2/query_pb"; -import { - SendInviteCodeSchema, - User, - UserState, -} from "@zitadel/proto/zitadel/user/v2/user_pb"; +import { SendInviteCodeSchema } from "@zitadel/proto/zitadel/user/v2/user_pb"; import { AddHumanUserRequest, ResendEmailCodeRequest, @@ -45,10 +41,8 @@ import { VerifyPasskeyRegistrationRequest, VerifyU2FRegistrationRequest, } from "@zitadel/proto/zitadel/user/v2/user_service_pb"; -import crypto from "crypto"; import { unstable_cacheLife as cacheLife } from "next/cache"; -import { cookies } from "next/headers"; -import { getFingerprintId, getUserAgent } from "./fingerprint"; +import { getUserAgent } from "./fingerprint"; import { createServiceForHost } from "./service"; const useCache = process.env.DEBUG !== "true"; @@ -1172,13 +1166,11 @@ export async function setUserPassword({ serviceUrl, userId, password, - user, code, }: { serviceUrl: string; userId: string; password: string; - user: User; code?: string; }) { let payload = create(SetPasswordRequestSchema, { @@ -1188,41 +1180,6 @@ export async function setUserPassword({ }, }); - // check if the user has no password set in order to set a password - if (!code) { - const authmethods = await listAuthenticationMethodTypes({ - serviceUrl, - userId, - }); - - // if the user has no authmethods set, we can set a password otherwise we need a code - if ( - !(authmethods.authMethodTypes.length === 0) && - user.state !== UserState.INITIAL - ) { - // check if a verification was done earlier - - const cookiesList = await cookies(); - - const userAgentId = await getFingerprintId(); - - const verificationCheck = crypto - .createHash("sha256") - .update(`${user.userId}:${userAgentId}`) - .digest("hex"); - - await cookiesList.set({ - name: "verificationCheck", - value: verificationCheck, - httpOnly: true, - path: "/", - maxAge: 300, // 5 minutes - }); - - return { error: "Provide a code to set a password" }; - } - } - if (code) { payload = { ...payload, From 93b333837d82794a908b02f431c03e2dfd0d2882 Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Tue, 20 May 2025 14:10:18 +0200 Subject: [PATCH 70/96] helper functions --- apps/login/src/app/(login)/verify/page.tsx | 54 +++++++++------- apps/login/src/lib/server/loginname.ts | 28 +++++++- apps/login/src/lib/server/passkeys.ts | 75 +++++++++++++--------- apps/login/src/lib/server/password.ts | 24 ++----- apps/login/src/lib/server/verify.ts | 8 +-- apps/login/src/lib/verify-helper.ts | 34 +++++++++- 6 files changed, 143 insertions(+), 80 deletions(-) diff --git a/apps/login/src/app/(login)/verify/page.tsx b/apps/login/src/app/(login)/verify/page.tsx index 198a46a5fe..5d7941322a 100644 --- a/apps/login/src/app/(login)/verify/page.tsx +++ b/apps/login/src/app/(login)/verify/page.tsx @@ -6,6 +6,7 @@ import { VerifyRedirectButton } from "@/components/verify-redirect-button"; import { sendEmailCode } from "@/lib/server/verify"; import { getServiceUrlFromHeaders } from "@/lib/service-url"; import { loadMostRecentSession } from "@/lib/session"; +import { checkUserVerification } from "@/lib/verify-helper"; import { getBrandingSettings, getUserByID, @@ -96,14 +97,23 @@ export default async function Page(props: { searchParams: Promise }) { id = userId ?? sessionFactors?.factors?.user?.id; + if (!id) { + throw Error("Failed to get user id"); + } + let authMethods: AuthenticationMethodType[] | null = null; if (human?.email?.isVerified) { - const authMethodsResponse = await listAuthenticationMethodTypes(userId); + const authMethodsResponse = await listAuthenticationMethodTypes({ + serviceUrl, + userId, + }); if (authMethodsResponse.authMethodTypes) { authMethods = authMethodsResponse.authMethodTypes; } } + const hasValidUserVerificationCheck = await checkUserVerification(id); + const params = new URLSearchParams({ userId: userId, initial: "true", // defines that a code is not required and is therefore not shown in the UI @@ -155,27 +165,27 @@ export default async function Page(props: { searchParams: Promise }) { ) )} - {id && - (human?.email?.isVerified ? ( - // show page for already verified users - - ) : ( - // check if auth methods are set - - ))} + {/* show a button to setup auth method for the user otherwise show the UI for reverifying */} + {human?.email?.isVerified && hasValidUserVerificationCheck ? ( + // show page for already verified users + + ) : ( + // check if auth methods are set + + )}

    ); diff --git a/apps/login/src/lib/server/loginname.ts b/apps/login/src/lib/server/loginname.ts index 2ea6004fdc..8754e77d56 100644 --- a/apps/login/src/lib/server/loginname.ts +++ b/apps/login/src/lib/server/loginname.ts @@ -9,7 +9,7 @@ import { idpTypeToIdentityProviderType, idpTypeToSlug } from "../idp"; import { PasskeysType } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb"; import { UserState } from "@zitadel/proto/zitadel/user/v2/user_pb"; import { getServiceUrlFromHeaders } from "../service-url"; -import { checkInvite } from "../verify-helper"; +import { checkEmailVerified, checkUserVerification } from "../verify-helper"; import { getActiveIdentityProviders, getIDPByID, @@ -257,7 +257,7 @@ export async function sendLoginname(command: SendLoginnameCommand) { // this can be expected to be an invite as users created in console have a password set. if (!methods.authMethodTypes || !methods.authMethodTypes.length) { // redirect to /verify invite if no auth method is set and email is not verified - const inviteCheck = checkInvite( + const inviteCheck = checkEmailVerified( session, humanUser, session.factors.user.organizationId, @@ -268,6 +268,30 @@ export async function sendLoginname(command: SendLoginnameCommand) { return inviteCheck; } + // check if user was verified + const isUserVerified = await checkUserVerification( + session.factors.user.id, + ); + if (!isUserVerified) { + const params = new URLSearchParams({ + loginName: session.factors?.user?.loginName as string, + }); + + if (command.requestId) { + params.append("requestId", command.requestId); + } + + if (command.organization || session.factors?.user?.organizationId) { + params.append( + "organization", + command.organization ?? + (session.factors?.user?.organizationId as string), + ); + } + + return { redirect: `/verify?` + params }; + } + const paramsAuthenticatorSetup = new URLSearchParams({ loginName: session.factors?.user?.loginName, userId: session.factors?.user?.id, // verify needs user id diff --git a/apps/login/src/lib/server/passkeys.ts b/apps/login/src/lib/server/passkeys.ts index c5ad990c61..3470629f24 100644 --- a/apps/login/src/lib/server/passkeys.ts +++ b/apps/login/src/lib/server/passkeys.ts @@ -9,14 +9,14 @@ import { registerPasskey, verifyPasskeyRegistration as zitadelVerifyPasskeyRegistration, } from "@/lib/zitadel"; -import { create, Duration } from "@zitadel/client"; +import { create, Duration, Timestamp, timestampDate } from "@zitadel/client"; +import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb"; import { Checks } from "@zitadel/proto/zitadel/session/v2/session_service_pb"; import { RegisterPasskeyResponse, VerifyPasskeyRegistrationRequestSchema, } from "@zitadel/proto/zitadel/user/v2/user_service_pb"; -import crypto from "crypto"; -import { cookies, headers } from "next/headers"; +import { headers } from "next/headers"; import { userAgent } from "next/server"; import { getNextUrl } from "../client"; import { @@ -24,9 +24,11 @@ import { getSessionCookieById, getSessionCookieByLoginName, } from "../cookies"; -import { getFingerprintId } from "../fingerprint"; import { getServiceUrlFromHeaders } from "../service-url"; -import { checkEmailVerification } from "../verify-helper"; +import { + checkEmailVerification, + checkUserVerification, +} from "../verify-helper"; import { setSessionAndUpdateCookie } from "./cookie"; type VerifyPasskeyCommand = { @@ -40,6 +42,22 @@ type RegisterPasskeyCommand = { sessionId: string; }; +function isSessionValid(session: Partial): { + valid: boolean; + verifiedAt?: Timestamp; +} { + const validPassword = session?.factors?.password?.verifiedAt; + const validPasskey = session?.factors?.webAuthN?.verifiedAt; + const stillValid = session.expirationDate + ? timestampDate(session.expirationDate) > new Date() + : true; + + const verifiedAt = validPassword || validPasskey; + const valid = !!((validPassword || validPasskey) && stillValid); + + return { valid, verifiedAt }; +} + export async function registerPasskeyLink( command: RegisterPasskeyCommand, ): Promise { @@ -64,37 +82,30 @@ export async function registerPasskeyLink( return { error: "Could not determine user from session" }; } - const authmethods = await listAuthenticationMethodTypes({ - serviceUrl, - userId: session?.session?.factors?.user?.id, - }); + const sessionValid = isSessionValid(session.session); - // if the user has no authmethods set, we need to check if the user was verified - // users are redirected from /authenticator/set to /password/set - if (authmethods.authMethodTypes.length !== 0) { - return { - error: - "You have to provide a code or have a valid User Verification Check", - }; - } + if (!sessionValid) { + const authmethods = await listAuthenticationMethodTypes({ + serviceUrl, + userId: session.session.factors.user.id, + }); - // check if a verification was done earlier - const cookiesList = await cookies(); - const userAgentId = await getFingerprintId(); + // if the user has no authmethods set, we need to check if the user was verified + if (authmethods.authMethodTypes.length !== 0) { + return { + error: + "You have to authenticate or have a valid User Verification Check", + }; + } - const verificationCheck = crypto - .createHash("sha256") - .update(`${user.userId}:${userAgentId}`) - .digest("hex"); + // check if a verification was done earlier + const hasValidUserVerificationCheck = await checkUserVerification( + session.session.factors.user.id, + ); - const cookieValue = await cookiesList.get("verificationCheck")?.value; - - if (!cookieValue) { - return { error: "User Verification Check has to be done" }; - } - - if (cookieValue !== verificationCheck) { - return { error: "User Verification Check has to be done" }; + if (!hasValidUserVerificationCheck) { + return { error: "User Verification Check has to be done" }; + } } const [hostname, port] = host.split(":"); diff --git a/apps/login/src/lib/server/password.ts b/apps/login/src/lib/server/password.ts index 26adb56c7b..34859d419b 100644 --- a/apps/login/src/lib/server/password.ts +++ b/apps/login/src/lib/server/password.ts @@ -25,16 +25,15 @@ import { import { LoginSettings } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb"; import { User, UserState } from "@zitadel/proto/zitadel/user/v2/user_pb"; import { SetPasswordRequestSchema } from "@zitadel/proto/zitadel/user/v2/user_service_pb"; -import crypto from "crypto"; -import { cookies, headers } from "next/headers"; +import { headers } from "next/headers"; import { getNextUrl } from "../client"; import { getSessionCookieById, getSessionCookieByLoginName } from "../cookies"; -import { getFingerprintId } from "../fingerprint"; import { getServiceUrlFromHeaders } from "../service-url"; import { checkEmailVerification, checkMFAFactors, checkPasswordChangeRequired, + checkUserVerification, } from "../verify-helper"; type ResetPasswordCommand = { @@ -327,7 +326,6 @@ export async function changePassword(command: { }); // if the user has no authmethods set, we need to check if the user was verified - // users are redirected from /authenticator/set to /password/set if (authmethods.authMethodTypes.length !== 0) { return { error: @@ -336,21 +334,11 @@ export async function changePassword(command: { } // check if a verification was done earlier - const cookiesList = await cookies(); - const userAgentId = await getFingerprintId(); + const hasValidUserVerificationCheck = await checkUserVerification( + user.userId, + ); - const verificationCheck = crypto - .createHash("sha256") - .update(`${user.userId}:${userAgentId}`) - .digest("hex"); - - const cookieValue = await cookiesList.get("verificationCheck")?.value; - - if (!cookieValue) { - return { error: "User Verification Check has to be done" }; - } - - if (cookieValue !== verificationCheck) { + if (!hasValidUserVerificationCheck) { return { error: "User Verification Check has to be done" }; } } diff --git a/apps/login/src/lib/server/verify.ts b/apps/login/src/lib/server/verify.ts index d56af5e582..db014431ac 100644 --- a/apps/login/src/lib/server/verify.ts +++ b/apps/login/src/lib/server/verify.ts @@ -21,7 +21,7 @@ import { User } from "@zitadel/proto/zitadel/user/v2/user_pb"; import { cookies, headers } from "next/headers"; import { getNextUrl } from "../client"; import { getSessionCookieByLoginName } from "../cookies"; -import { getFingerprintId } from "../fingerprint"; +import { getOrSetFingerprintId } from "../fingerprint"; import { getServiceUrlFromHeaders } from "../service-url"; import { loadMostRecentSession } from "../session"; import { checkMFAFactors } from "../verify-helper"; @@ -197,11 +197,9 @@ export async function sendVerification(command: VerifyUserByEmailCommand) { params.set("loginName", session.factors?.user?.loginName); } - // set hash of userId and userAgentId to prevent replay attacks, TODO: check on the /authenticator/set page - + // set hash of userId and userAgentId to prevent attacks, checks are done for users with invalid sessions and invalid userAgentId const cookiesList = await cookies(); - - const userAgentId = await getFingerprintId(); + const userAgentId = await getOrSetFingerprintId(); const verificationCheck = crypto .createHash("sha256") diff --git a/apps/login/src/lib/verify-helper.ts b/apps/login/src/lib/verify-helper.ts index 704d7bbef6..763f9dff45 100644 --- a/apps/login/src/lib/verify-helper.ts +++ b/apps/login/src/lib/verify-helper.ts @@ -4,7 +4,10 @@ import { LoginSettings } from "@zitadel/proto/zitadel/settings/v2/login_settings import { PasswordExpirySettings } from "@zitadel/proto/zitadel/settings/v2/password_settings_pb"; import { HumanUser } from "@zitadel/proto/zitadel/user/v2/user_pb"; import { AuthenticationMethodType } from "@zitadel/proto/zitadel/user/v2/user_service_pb"; +import crypto from "crypto"; import moment from "moment"; +import { cookies } from "next/headers"; +import { getOrSetFingerprintId } from "./fingerprint"; import { getUserByID } from "./zitadel"; export function checkPasswordChangeRequired( @@ -44,7 +47,7 @@ export function checkPasswordChangeRequired( } } -export function checkInvite( +export function checkEmailVerified( session: Session, humanUser?: HumanUser, organization?: string, @@ -248,3 +251,32 @@ export async function checkMFAFactors( return { redirect: `/mfa/set?` + params }; } } + +export async function checkUserVerification(userId: string): Promise { + // check if a verification was done earlier + const cookiesList = await cookies(); + const userAgentId = await getOrSetFingerprintId(); + + const verificationCheck = crypto + .createHash("sha256") + .update(`${userId}:${userAgentId}`) + .digest("hex"); + + const cookieValue = await cookiesList.get("verificationCheck")?.value; + + if (!cookieValue) { + console.warn( + "User verification check cookie not found. User verification check failed.", + ); + return false; + } + + if (cookieValue !== verificationCheck) { + console.warn( + `User verification check failed. Expected ${verificationCheck} but got ${cookieValue}`, + ); + return false; + } + + return true; +} From 82234da1f4aa98ef0ccb67a1edba1dc00a2f0726 Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Tue, 20 May 2025 15:27:34 +0200 Subject: [PATCH 71/96] change send approach --- apps/login/cypress/integration/verify.cy.ts | 2 +- apps/login/src/app/(login)/verify/page.tsx | 8 ++++---- apps/login/src/lib/server/verify.ts | 18 ++++++++++++++---- apps/login/src/lib/verify-helper.ts | 3 ++- 4 files changed, 21 insertions(+), 10 deletions(-) diff --git a/apps/login/cypress/integration/verify.cy.ts b/apps/login/cypress/integration/verify.cy.ts index 464bf02e59..ab59b27363 100644 --- a/apps/login/cypress/integration/verify.cy.ts +++ b/apps/login/cypress/integration/verify.cy.ts @@ -90,7 +90,7 @@ describe("verify email", () => { }); // TODO: Avoid uncaught exception in application cy.once("uncaught:exception", () => false); - cy.visit("/verify?userId=221394658884845598&code=abc"); + cy.visit("/verify?userId=221394658884845598&code=abc&send=true"); cy.contains("Could not verify email", { timeout: 10_000 }); }); }); diff --git a/apps/login/src/app/(login)/verify/page.tsx b/apps/login/src/app/(login)/verify/page.tsx index 5d7941322a..567774b0e4 100644 --- a/apps/login/src/app/(login)/verify/page.tsx +++ b/apps/login/src/app/(login)/verify/page.tsx @@ -23,7 +23,7 @@ export default async function Page(props: { searchParams: Promise }) { const t = await getTranslations({ locale, namespace: "verify" }); const tError = await getTranslations({ locale, namespace: "error" }); - const { userId, loginName, code, organization, requestId, invite } = + const { userId, loginName, code, organization, requestId, invite, send } = searchParams; const _headers = await headers(); @@ -44,7 +44,7 @@ export default async function Page(props: { searchParams: Promise }) { let human: HumanUser | undefined; let id: string | undefined; - const doSend = invite !== "true"; + const doSend = send === "true"; const basePath = process.env.NEXT_PUBLIC_BASE_PATH ?? ""; @@ -62,7 +62,7 @@ export default async function Page(props: { searchParams: Promise }) { serviceUrl, userId: sessionFactors?.factors?.user?.id, urlTemplate: - `${host.includes("localhost") ? "http://" : "https://"}${host}${basePath}/verify?code={{.Code}}&userId={{.UserID}}&organization={{.OrgID}}&invite=true` + + `${host.includes("localhost") ? "http://" : "https://"}${host}${basePath}/verify?code={{.Code}}&userId={{.UserID}}&organization={{.OrgID}}` + (requestId ? `&requestId=${requestId}` : ""), }).catch((error) => { console.error("Could not resend verification email", error); @@ -75,7 +75,7 @@ export default async function Page(props: { searchParams: Promise }) { serviceUrl, userId, urlTemplate: - `${host.includes("localhost") ? "http://" : "https://"}${host}${basePath}/verify?code={{.Code}}&userId={{.UserID}}&organization={{.OrgID}}&invite=true` + + `${host.includes("localhost") ? "http://" : "https://"}${host}${basePath}/verify?code={{.Code}}&userId={{.UserID}}&organization={{.OrgID}}` + (requestId ? `&requestId=${requestId}` : ""), }).catch((error) => { console.error("Could not resend verification email", error); diff --git a/apps/login/src/lib/server/verify.ts b/apps/login/src/lib/server/verify.ts index db014431ac..334f3b5e32 100644 --- a/apps/login/src/lib/server/verify.ts +++ b/apps/login/src/lib/server/verify.ts @@ -1,12 +1,12 @@ "use server"; import { + createInviteCode, getLoginSettings, getSession, getUserByID, listAuthenticationMethodTypes, resendEmailCode, - resendInviteCode, verifyEmail, verifyInviteCode, verifyTOTPRegistration, @@ -274,14 +274,24 @@ export async function resendVerification(command: resendVerifyEmailCommand) { const basePath = process.env.NEXT_PUBLIC_BASE_PATH ?? ""; + const doSend = true; + // create a new invite whenever the resend is called return command.isInvite - ? resendInviteCode({ serviceUrl, userId: command.userId }) + ? createInviteCode({ + serviceUrl, + userId: command.userId, + urlTemplate: + `${host.includes("localhost") ? "http://" : "https://"}${host}${basePath}/verify?code={{.Code}}&userId={{.UserID}}&organization={{.OrgID}}&invite=true` + + (command.requestId ? `&requestId=${command.requestId}` : "") + + (doSend ? `&send=${doSend}` : ""), + }) //resendInviteCode({ serviceUrl, userId: command.userId }) : resendEmailCode({ userId: command.userId, serviceUrl, urlTemplate: - `${host.includes("localhost") ? "http://" : "https://"}${host}${basePath}/password/set?code={{.Code}}&userId={{.UserID}}&organization={{.OrgID}}` + - (command.requestId ? `&requestId=${command.requestId}` : ""), + `${host.includes("localhost") ? "http://" : "https://"}${host}${basePath}/verify?code={{.Code}}&userId={{.UserID}}&organization={{.OrgID}}` + + (command.requestId ? `&requestId=${command.requestId}` : "") + + (doSend ? `&send=${doSend}` : ""), }); } diff --git a/apps/login/src/lib/verify-helper.ts b/apps/login/src/lib/verify-helper.ts index 763f9dff45..0385d62a03 100644 --- a/apps/login/src/lib/verify-helper.ts +++ b/apps/login/src/lib/verify-helper.ts @@ -57,7 +57,7 @@ export function checkEmailVerified( const paramsVerify = new URLSearchParams({ loginName: session.factors?.user?.loginName as string, userId: session.factors?.user?.id as string, // verify needs user id - invite: "true", // TODO: check - set this to true as we dont expect old email verification method here + send: "true", // set this to true to request a new code immediately }); if (organization || session.factors?.user?.organizationId) { @@ -87,6 +87,7 @@ export function checkEmailVerification( ) { const params = new URLSearchParams({ loginName: session.factors?.user?.loginName as string, + send: "true", // set this to true as we dont expect old email codes to be valid anymore }); if (requestId) { From 2dac623c1e90d64f36b763a9918eaded8448a355 Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Wed, 21 May 2025 09:23:44 +0200 Subject: [PATCH 72/96] check email sending --- apps/login/cypress/integration/verify.cy.ts | 2 +- apps/login/locales/de.json | 1 + apps/login/locales/en.json | 1 + apps/login/locales/es.json | 1 + apps/login/locales/it.json | 1 + apps/login/locales/pl.json | 1 + apps/login/locales/ru.json | 1 + apps/login/locales/zh.json | 1 + apps/login/src/app/(login)/verify/page.tsx | 8 +++++++- apps/login/src/lib/server/loginname.ts | 3 ++- apps/login/src/lib/server/verify.ts | 8 ++------ 11 files changed, 19 insertions(+), 9 deletions(-) diff --git a/apps/login/cypress/integration/verify.cy.ts b/apps/login/cypress/integration/verify.cy.ts index ab59b27363..464bf02e59 100644 --- a/apps/login/cypress/integration/verify.cy.ts +++ b/apps/login/cypress/integration/verify.cy.ts @@ -90,7 +90,7 @@ describe("verify email", () => { }); // TODO: Avoid uncaught exception in application cy.once("uncaught:exception", () => false); - cy.visit("/verify?userId=221394658884845598&code=abc&send=true"); + cy.visit("/verify?userId=221394658884845598&code=abc"); cy.contains("Could not verify email", { timeout: 10_000 }); }); }); diff --git a/apps/login/locales/de.json b/apps/login/locales/de.json index a2c137cf43..7471cc6ec7 100644 --- a/apps/login/locales/de.json +++ b/apps/login/locales/de.json @@ -181,6 +181,7 @@ "description": "Geben Sie den Code ein, der in der Bestätigungs-E-Mail angegeben ist.", "noCodeReceived": "Keinen Code erhalten?", "resendCode": "Code erneut senden", + "codeSent": "Ein Code wurde gerade an Ihre E-Mail-Adresse gesendet.", "submit": "Weiter" } }, diff --git a/apps/login/locales/en.json b/apps/login/locales/en.json index 63a45c7d15..164761f43d 100644 --- a/apps/login/locales/en.json +++ b/apps/login/locales/en.json @@ -181,6 +181,7 @@ "description": "Enter the Code provided in the verification email.", "noCodeReceived": "Didn't receive a code?", "resendCode": "Resend code", + "codeSent": "A code has just been sent to your email address.", "submit": "Continue" } }, diff --git a/apps/login/locales/es.json b/apps/login/locales/es.json index 60570eceb0..6e5dd43820 100644 --- a/apps/login/locales/es.json +++ b/apps/login/locales/es.json @@ -181,6 +181,7 @@ "description": "Introduce el código proporcionado en el correo electrónico de verificación.", "noCodeReceived": "¿No recibiste un código?", "resendCode": "Reenviar código", + "codeSent": "Se ha enviado un código a tu dirección de correo electrónico.", "submit": "Continuar" } }, diff --git a/apps/login/locales/it.json b/apps/login/locales/it.json index 53894fdf5d..1173906e82 100644 --- a/apps/login/locales/it.json +++ b/apps/login/locales/it.json @@ -181,6 +181,7 @@ "description": "Inserisci il codice fornito nell'email di verifica.", "noCodeReceived": "Non hai ricevuto un codice?", "resendCode": "Invia di nuovo il codice", + "codeSent": "Un codice è stato appena inviato al tuo indirizzo email.", "submit": "Continua" } }, diff --git a/apps/login/locales/pl.json b/apps/login/locales/pl.json index 52b802eccb..ac3758227e 100644 --- a/apps/login/locales/pl.json +++ b/apps/login/locales/pl.json @@ -181,6 +181,7 @@ "description": "Wprowadź kod z wiadomości weryfikacyjnej.", "noCodeReceived": "Nie otrzymałeś kodu?", "resendCode": "Wyślij kod ponownie", + "codeSent": "Kod został właśnie wysłany na twój adres e-mail.", "submit": "Kontynuuj" } }, diff --git a/apps/login/locales/ru.json b/apps/login/locales/ru.json index 197b9663be..48af7d29d3 100644 --- a/apps/login/locales/ru.json +++ b/apps/login/locales/ru.json @@ -181,6 +181,7 @@ "description": "Введите код из письма подтверждения.", "noCodeReceived": "Не получили код?", "resendCode": "Отправить код повторно", + "codeSent": "Код отправлен на ваш email.", "submit": "Продолжить" } }, diff --git a/apps/login/locales/zh.json b/apps/login/locales/zh.json index d4319dc051..526f36a80b 100644 --- a/apps/login/locales/zh.json +++ b/apps/login/locales/zh.json @@ -181,6 +181,7 @@ "description": "输入验证邮件中的验证码。", "noCodeReceived": "没有收到验证码?", "resendCode": "重发验证码", + "codeSent": "刚刚发送了一封包含验证码的电子邮件。", "submit": "继续" } }, diff --git a/apps/login/src/app/(login)/verify/page.tsx b/apps/login/src/app/(login)/verify/page.tsx index 567774b0e4..6975e6a586 100644 --- a/apps/login/src/app/(login)/verify/page.tsx +++ b/apps/login/src/app/(login)/verify/page.tsx @@ -1,4 +1,4 @@ -import { Alert } from "@/components/alert"; +import { Alert, AlertType } from "@/components/alert"; import { DynamicTheme } from "@/components/dynamic-theme"; import { UserAvatar } from "@/components/user-avatar"; import { VerifyForm } from "@/components/verify-form"; @@ -148,6 +148,12 @@ export default async function Page(props: { searchParams: Promise }) { )} + {id && send && ( +
    + {tError("verify.codesent")} +
    + )} + {sessionFactors ? ( Date: Wed, 21 May 2025 09:27:37 +0200 Subject: [PATCH 73/96] i18n key --- apps/login/src/app/(login)/verify/page.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/login/src/app/(login)/verify/page.tsx b/apps/login/src/app/(login)/verify/page.tsx index 6975e6a586..8b04b4e152 100644 --- a/apps/login/src/app/(login)/verify/page.tsx +++ b/apps/login/src/app/(login)/verify/page.tsx @@ -150,7 +150,7 @@ export default async function Page(props: { searchParams: Promise }) { {id && send && (
    - {tError("verify.codesent")} + {tError("verify.codeSent")}
    )} From d59e70fe4891137d38183e8be8485b35031a6b70 Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Wed, 21 May 2025 09:45:58 +0200 Subject: [PATCH 74/96] t i18n --- apps/login/src/app/(login)/verify/page.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/login/src/app/(login)/verify/page.tsx b/apps/login/src/app/(login)/verify/page.tsx index 8b04b4e152..fcbc4fd34a 100644 --- a/apps/login/src/app/(login)/verify/page.tsx +++ b/apps/login/src/app/(login)/verify/page.tsx @@ -150,7 +150,7 @@ export default async function Page(props: { searchParams: Promise }) { {id && send && (
    - {tError("verify.codeSent")} + {t("verify.codeSent")}
    )} From b6e7dba3a6f0d575d8761682309d7ed77fae25e6 Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Wed, 21 May 2025 10:02:31 +0200 Subject: [PATCH 75/96] do not request new code --- apps/login/src/lib/verify-helper.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/login/src/lib/verify-helper.ts b/apps/login/src/lib/verify-helper.ts index 0385d62a03..a25f9ef805 100644 --- a/apps/login/src/lib/verify-helper.ts +++ b/apps/login/src/lib/verify-helper.ts @@ -57,7 +57,7 @@ export function checkEmailVerified( const paramsVerify = new URLSearchParams({ loginName: session.factors?.user?.loginName as string, userId: session.factors?.user?.id as string, // verify needs user id - send: "true", // set this to true to request a new code immediately + // send: "true", // we do not request a new code again }); if (organization || session.factors?.user?.organizationId) { From 14a8e74b6910256aaf2f5f776a20d962f989c5ef Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Wed, 21 May 2025 10:15:17 +0200 Subject: [PATCH 76/96] move userverificationcheck to server action --- apps/login/src/app/(login)/verify/page.tsx | 2 +- apps/login/src/lib/server/loginname.ts | 3 +- apps/login/src/lib/server/passkeys.ts | 6 ++-- apps/login/src/lib/server/password.ts | 2 +- apps/login/src/lib/verification-helper.ts | 34 ++++++++++++++++++++++ apps/login/src/lib/verify-helper.ts | 32 -------------------- 6 files changed, 40 insertions(+), 39 deletions(-) create mode 100644 apps/login/src/lib/verification-helper.ts diff --git a/apps/login/src/app/(login)/verify/page.tsx b/apps/login/src/app/(login)/verify/page.tsx index fcbc4fd34a..e5b2268f14 100644 --- a/apps/login/src/app/(login)/verify/page.tsx +++ b/apps/login/src/app/(login)/verify/page.tsx @@ -6,7 +6,7 @@ import { VerifyRedirectButton } from "@/components/verify-redirect-button"; import { sendEmailCode } from "@/lib/server/verify"; import { getServiceUrlFromHeaders } from "@/lib/service-url"; import { loadMostRecentSession } from "@/lib/session"; -import { checkUserVerification } from "@/lib/verify-helper"; +import { checkUserVerification } from "@/lib/verification-helper"; import { getBrandingSettings, getUserByID, diff --git a/apps/login/src/lib/server/loginname.ts b/apps/login/src/lib/server/loginname.ts index bbe08dfec8..1282def867 100644 --- a/apps/login/src/lib/server/loginname.ts +++ b/apps/login/src/lib/server/loginname.ts @@ -9,7 +9,8 @@ import { idpTypeToIdentityProviderType, idpTypeToSlug } from "../idp"; import { PasskeysType } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb"; import { UserState } from "@zitadel/proto/zitadel/user/v2/user_pb"; import { getServiceUrlFromHeaders } from "../service-url"; -import { checkEmailVerified, checkUserVerification } from "../verify-helper"; +import { checkUserVerification } from "../verification-helper"; +import { checkEmailVerified } from "../verify-helper"; import { getActiveIdentityProviders, getIDPByID, diff --git a/apps/login/src/lib/server/passkeys.ts b/apps/login/src/lib/server/passkeys.ts index 3470629f24..1a26824141 100644 --- a/apps/login/src/lib/server/passkeys.ts +++ b/apps/login/src/lib/server/passkeys.ts @@ -25,10 +25,8 @@ import { getSessionCookieByLoginName, } from "../cookies"; import { getServiceUrlFromHeaders } from "../service-url"; -import { - checkEmailVerification, - checkUserVerification, -} from "../verify-helper"; +import { checkUserVerification } from "../verification-helper"; +import { checkEmailVerification } from "../verify-helper"; import { setSessionAndUpdateCookie } from "./cookie"; type VerifyPasskeyCommand = { diff --git a/apps/login/src/lib/server/password.ts b/apps/login/src/lib/server/password.ts index 34859d419b..56158ddef1 100644 --- a/apps/login/src/lib/server/password.ts +++ b/apps/login/src/lib/server/password.ts @@ -29,11 +29,11 @@ import { headers } from "next/headers"; import { getNextUrl } from "../client"; import { getSessionCookieById, getSessionCookieByLoginName } from "../cookies"; import { getServiceUrlFromHeaders } from "../service-url"; +import { checkUserVerification } from "../verification-helper"; import { checkEmailVerification, checkMFAFactors, checkPasswordChangeRequired, - checkUserVerification, } from "../verify-helper"; type ResetPasswordCommand = { diff --git a/apps/login/src/lib/verification-helper.ts b/apps/login/src/lib/verification-helper.ts new file mode 100644 index 0000000000..2e8565f3ac --- /dev/null +++ b/apps/login/src/lib/verification-helper.ts @@ -0,0 +1,34 @@ +"use server"; + +import crypto from "crypto"; +import { cookies } from "next/headers"; +import { getOrSetFingerprintId } from "./fingerprint"; + +export async function checkUserVerification(userId: string): Promise { + // check if a verification was done earlier + const cookiesList = await cookies(); + const userAgentId = await getOrSetFingerprintId(); + + const verificationCheck = crypto + .createHash("sha256") + .update(`${userId}:${userAgentId}`) + .digest("hex"); + + const cookieValue = await cookiesList.get("verificationCheck")?.value; + + if (!cookieValue) { + console.warn( + "User verification check cookie not found. User verification check failed.", + ); + return false; + } + + if (cookieValue !== verificationCheck) { + console.warn( + `User verification check failed. Expected ${verificationCheck} but got ${cookieValue}`, + ); + return false; + } + + return true; +} diff --git a/apps/login/src/lib/verify-helper.ts b/apps/login/src/lib/verify-helper.ts index a25f9ef805..45de5df315 100644 --- a/apps/login/src/lib/verify-helper.ts +++ b/apps/login/src/lib/verify-helper.ts @@ -4,10 +4,7 @@ import { LoginSettings } from "@zitadel/proto/zitadel/settings/v2/login_settings import { PasswordExpirySettings } from "@zitadel/proto/zitadel/settings/v2/password_settings_pb"; import { HumanUser } from "@zitadel/proto/zitadel/user/v2/user_pb"; import { AuthenticationMethodType } from "@zitadel/proto/zitadel/user/v2/user_service_pb"; -import crypto from "crypto"; import moment from "moment"; -import { cookies } from "next/headers"; -import { getOrSetFingerprintId } from "./fingerprint"; import { getUserByID } from "./zitadel"; export function checkPasswordChangeRequired( @@ -252,32 +249,3 @@ export async function checkMFAFactors( return { redirect: `/mfa/set?` + params }; } } - -export async function checkUserVerification(userId: string): Promise { - // check if a verification was done earlier - const cookiesList = await cookies(); - const userAgentId = await getOrSetFingerprintId(); - - const verificationCheck = crypto - .createHash("sha256") - .update(`${userId}:${userAgentId}`) - .digest("hex"); - - const cookieValue = await cookiesList.get("verificationCheck")?.value; - - if (!cookieValue) { - console.warn( - "User verification check cookie not found. User verification check failed.", - ); - return false; - } - - if (cookieValue !== verificationCheck) { - console.warn( - `User verification check failed. Expected ${verificationCheck} but got ${cookieValue}`, - ); - return false; - } - - return true; -} From 75b682a6465a4fb5079a631728336c29e20cc11b Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Wed, 21 May 2025 11:00:18 +0200 Subject: [PATCH 77/96] only read cookie --- apps/login/src/app/(login)/verify/page.tsx | 2 +- apps/login/src/lib/server/loginname.ts | 3 +- apps/login/src/lib/server/passkeys.ts | 6 ++-- apps/login/src/lib/server/password.ts | 2 +- apps/login/src/lib/verification-helper.ts | 34 ------------------- apps/login/src/lib/verify-helper.ts | 38 ++++++++++++++++++++++ 6 files changed, 45 insertions(+), 40 deletions(-) delete mode 100644 apps/login/src/lib/verification-helper.ts diff --git a/apps/login/src/app/(login)/verify/page.tsx b/apps/login/src/app/(login)/verify/page.tsx index e5b2268f14..fcbc4fd34a 100644 --- a/apps/login/src/app/(login)/verify/page.tsx +++ b/apps/login/src/app/(login)/verify/page.tsx @@ -6,7 +6,7 @@ import { VerifyRedirectButton } from "@/components/verify-redirect-button"; import { sendEmailCode } from "@/lib/server/verify"; import { getServiceUrlFromHeaders } from "@/lib/service-url"; import { loadMostRecentSession } from "@/lib/session"; -import { checkUserVerification } from "@/lib/verification-helper"; +import { checkUserVerification } from "@/lib/verify-helper"; import { getBrandingSettings, getUserByID, diff --git a/apps/login/src/lib/server/loginname.ts b/apps/login/src/lib/server/loginname.ts index 1282def867..bbe08dfec8 100644 --- a/apps/login/src/lib/server/loginname.ts +++ b/apps/login/src/lib/server/loginname.ts @@ -9,8 +9,7 @@ import { idpTypeToIdentityProviderType, idpTypeToSlug } from "../idp"; import { PasskeysType } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb"; import { UserState } from "@zitadel/proto/zitadel/user/v2/user_pb"; import { getServiceUrlFromHeaders } from "../service-url"; -import { checkUserVerification } from "../verification-helper"; -import { checkEmailVerified } from "../verify-helper"; +import { checkEmailVerified, checkUserVerification } from "../verify-helper"; import { getActiveIdentityProviders, getIDPByID, diff --git a/apps/login/src/lib/server/passkeys.ts b/apps/login/src/lib/server/passkeys.ts index 1a26824141..3470629f24 100644 --- a/apps/login/src/lib/server/passkeys.ts +++ b/apps/login/src/lib/server/passkeys.ts @@ -25,8 +25,10 @@ import { getSessionCookieByLoginName, } from "../cookies"; import { getServiceUrlFromHeaders } from "../service-url"; -import { checkUserVerification } from "../verification-helper"; -import { checkEmailVerification } from "../verify-helper"; +import { + checkEmailVerification, + checkUserVerification, +} from "../verify-helper"; import { setSessionAndUpdateCookie } from "./cookie"; type VerifyPasskeyCommand = { diff --git a/apps/login/src/lib/server/password.ts b/apps/login/src/lib/server/password.ts index 56158ddef1..34859d419b 100644 --- a/apps/login/src/lib/server/password.ts +++ b/apps/login/src/lib/server/password.ts @@ -29,11 +29,11 @@ import { headers } from "next/headers"; import { getNextUrl } from "../client"; import { getSessionCookieById, getSessionCookieByLoginName } from "../cookies"; import { getServiceUrlFromHeaders } from "../service-url"; -import { checkUserVerification } from "../verification-helper"; import { checkEmailVerification, checkMFAFactors, checkPasswordChangeRequired, + checkUserVerification, } from "../verify-helper"; type ResetPasswordCommand = { diff --git a/apps/login/src/lib/verification-helper.ts b/apps/login/src/lib/verification-helper.ts deleted file mode 100644 index 2e8565f3ac..0000000000 --- a/apps/login/src/lib/verification-helper.ts +++ /dev/null @@ -1,34 +0,0 @@ -"use server"; - -import crypto from "crypto"; -import { cookies } from "next/headers"; -import { getOrSetFingerprintId } from "./fingerprint"; - -export async function checkUserVerification(userId: string): Promise { - // check if a verification was done earlier - const cookiesList = await cookies(); - const userAgentId = await getOrSetFingerprintId(); - - const verificationCheck = crypto - .createHash("sha256") - .update(`${userId}:${userAgentId}`) - .digest("hex"); - - const cookieValue = await cookiesList.get("verificationCheck")?.value; - - if (!cookieValue) { - console.warn( - "User verification check cookie not found. User verification check failed.", - ); - return false; - } - - if (cookieValue !== verificationCheck) { - console.warn( - `User verification check failed. Expected ${verificationCheck} but got ${cookieValue}`, - ); - return false; - } - - return true; -} diff --git a/apps/login/src/lib/verify-helper.ts b/apps/login/src/lib/verify-helper.ts index 45de5df315..e8a18c053c 100644 --- a/apps/login/src/lib/verify-helper.ts +++ b/apps/login/src/lib/verify-helper.ts @@ -4,7 +4,10 @@ import { LoginSettings } from "@zitadel/proto/zitadel/settings/v2/login_settings import { PasswordExpirySettings } from "@zitadel/proto/zitadel/settings/v2/password_settings_pb"; import { HumanUser } from "@zitadel/proto/zitadel/user/v2/user_pb"; import { AuthenticationMethodType } from "@zitadel/proto/zitadel/user/v2/user_service_pb"; +import crypto from "crypto"; import moment from "moment"; +import { cookies } from "next/headers"; +import { getFingerprintIdCookie } from "./fingerprint"; import { getUserByID } from "./zitadel"; export function checkPasswordChangeRequired( @@ -249,3 +252,38 @@ export async function checkMFAFactors( return { redirect: `/mfa/set?` + params }; } } + +export async function checkUserVerification(userId: string): Promise { + // check if a verification was done earlier + const cookiesList = await cookies(); + + // only read cookie to prevent issues on page.tsx + const userAgentId = await getFingerprintIdCookie(); + + if (!userAgentId || userAgentId.value) { + return false; + } + + const verificationCheck = crypto + .createHash("sha256") + .update(`${userId}:${userAgentId}`) + .digest("hex"); + + const cookieValue = await cookiesList.get("verificationCheck")?.value; + + if (!cookieValue) { + console.warn( + "User verification check cookie not found. User verification check failed.", + ); + return false; + } + + if (cookieValue !== verificationCheck) { + console.warn( + `User verification check failed. Expected ${verificationCheck} but got ${cookieValue}`, + ); + return false; + } + + return true; +} From f6d546a8eae27fb03cc8b9cdf843f1d3c6e570de Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Wed, 21 May 2025 11:09:35 +0200 Subject: [PATCH 78/96] autosend --- apps/login/src/lib/verify-helper.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/login/src/lib/verify-helper.ts b/apps/login/src/lib/verify-helper.ts index e8a18c053c..7be8c46d52 100644 --- a/apps/login/src/lib/verify-helper.ts +++ b/apps/login/src/lib/verify-helper.ts @@ -57,7 +57,7 @@ export function checkEmailVerified( const paramsVerify = new URLSearchParams({ loginName: session.factors?.user?.loginName as string, userId: session.factors?.user?.id as string, // verify needs user id - // send: "true", // we do not request a new code again + send: "true", // we request a new email code once the page is loaded }); if (organization || session.factors?.user?.organizationId) { From f1cb6213335276b800778be4933b4acc85694f2b Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Wed, 21 May 2025 11:42:52 +0200 Subject: [PATCH 79/96] fix user verification check --- apps/login/src/lib/server/loginname.ts | 2 +- apps/login/src/lib/server/verify.ts | 3 +-- apps/login/src/lib/verify-helper.ts | 6 +++--- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/apps/login/src/lib/server/loginname.ts b/apps/login/src/lib/server/loginname.ts index bbe08dfec8..83a6f90abb 100644 --- a/apps/login/src/lib/server/loginname.ts +++ b/apps/login/src/lib/server/loginname.ts @@ -275,7 +275,7 @@ export async function sendLoginname(command: SendLoginnameCommand) { if (!isUserVerified) { const params = new URLSearchParams({ loginName: session.factors?.user?.loginName as string, - // send: "true", // set this to true to request a new code immediately + send: "true", // set this to true to request a new code immediately }); if (command.requestId) { diff --git a/apps/login/src/lib/server/verify.ts b/apps/login/src/lib/server/verify.ts index 07756f60ab..0ab4c5465d 100644 --- a/apps/login/src/lib/server/verify.ts +++ b/apps/login/src/lib/server/verify.ts @@ -6,7 +6,6 @@ import { getSession, getUserByID, listAuthenticationMethodTypes, - resendEmailCode, verifyEmail, verifyInviteCode, verifyTOTPRegistration, @@ -283,7 +282,7 @@ export async function resendVerification(command: resendVerifyEmailCommand) { `${host.includes("localhost") ? "http://" : "https://"}${host}${basePath}/verify?code={{.Code}}&userId={{.UserID}}&organization={{.OrgID}}&invite=true` + (command.requestId ? `&requestId=${command.requestId}` : ""), }) //resendInviteCode({ serviceUrl, userId: command.userId }) - : resendEmailCode({ + : sendEmailCode({ userId: command.userId, serviceUrl, urlTemplate: diff --git a/apps/login/src/lib/verify-helper.ts b/apps/login/src/lib/verify-helper.ts index 7be8c46d52..dbd9b2796b 100644 --- a/apps/login/src/lib/verify-helper.ts +++ b/apps/login/src/lib/verify-helper.ts @@ -258,15 +258,15 @@ export async function checkUserVerification(userId: string): Promise { const cookiesList = await cookies(); // only read cookie to prevent issues on page.tsx - const userAgentId = await getFingerprintIdCookie(); + const fingerPrintCookie = await getFingerprintIdCookie(); - if (!userAgentId || userAgentId.value) { + if (!fingerPrintCookie || !fingerPrintCookie.value) { return false; } const verificationCheck = crypto .createHash("sha256") - .update(`${userId}:${userAgentId}`) + .update(`${userId}:${fingerPrintCookie.value}`) .digest("hex"); const cookieValue = await cookiesList.get("verificationCheck")?.value; From 58cb2a5fedbf557309973d583a7cb87706501499 Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Wed, 21 May 2025 17:02:22 +0200 Subject: [PATCH 80/96] rm invite page --- apps/login/src/app/(login)/invite/page.tsx | 73 ----------------- .../src/app/(login)/invite/success/page.tsx | 81 ------------------- 2 files changed, 154 deletions(-) delete mode 100644 apps/login/src/app/(login)/invite/page.tsx delete mode 100644 apps/login/src/app/(login)/invite/success/page.tsx diff --git a/apps/login/src/app/(login)/invite/page.tsx b/apps/login/src/app/(login)/invite/page.tsx deleted file mode 100644 index 11e9d732ee..0000000000 --- a/apps/login/src/app/(login)/invite/page.tsx +++ /dev/null @@ -1,73 +0,0 @@ -import { Alert, AlertType } from "@/components/alert"; -import { DynamicTheme } from "@/components/dynamic-theme"; -import { InviteForm } from "@/components/invite-form"; -import { getServiceUrlFromHeaders } from "@/lib/service-url"; -import { - getBrandingSettings, - getDefaultOrg, - getLoginSettings, - getPasswordComplexitySettings, -} from "@/lib/zitadel"; -import { getLocale, getTranslations } from "next-intl/server"; -import { headers } from "next/headers"; - -export default async function Page(props: { - searchParams: Promise>; -}) { - const searchParams = await props.searchParams; - const locale = getLocale(); - const t = await getTranslations({ locale, namespace: "invite" }); - - let { firstname, lastname, email, organization } = searchParams; - - const _headers = await headers(); - const { serviceUrl } = getServiceUrlFromHeaders(_headers); - - if (!organization) { - const org = await getDefaultOrg({ serviceUrl }); - if (!org) { - throw new Error("No default organization found"); - } - - organization = org.id; - } - - const loginSettings = await getLoginSettings({ - serviceUrl, - organization, - }); - - const passwordComplexitySettings = await getPasswordComplexitySettings({ - serviceUrl, - organization, - }); - - const branding = await getBrandingSettings({ - serviceUrl, - organization, - }); - - return ( - -
    -

    {t("title")}

    -

    {t("description")}

    - - {!loginSettings?.allowRegister ? ( - {t("notAllowed")} - ) : ( - {t("info")} - )} - - {passwordComplexitySettings && loginSettings?.allowRegister && ( - - )} -
    -
    - ); -} diff --git a/apps/login/src/app/(login)/invite/success/page.tsx b/apps/login/src/app/(login)/invite/success/page.tsx deleted file mode 100644 index 1b12a5b903..0000000000 --- a/apps/login/src/app/(login)/invite/success/page.tsx +++ /dev/null @@ -1,81 +0,0 @@ -import { Alert, AlertType } from "@/components/alert"; -import { Button, ButtonVariants } from "@/components/button"; -import { DynamicTheme } from "@/components/dynamic-theme"; -import { UserAvatar } from "@/components/user-avatar"; -import { getServiceUrlFromHeaders } from "@/lib/service-url"; -import { getBrandingSettings, getDefaultOrg, getUserByID } from "@/lib/zitadel"; -import { HumanUser, User } from "@zitadel/proto/zitadel/user/v2/user_pb"; -import { getLocale, getTranslations } from "next-intl/server"; -import { headers } from "next/headers"; -import Link from "next/link"; - -export default async function Page(props: { - searchParams: Promise>; -}) { - const searchParams = await props.searchParams; - const locale = getLocale(); - const t = await getTranslations({ locale, namespace: "invite" }); - - let { userId, organization } = searchParams; - - const _headers = await headers(); - const { serviceUrl } = getServiceUrlFromHeaders(_headers); - - if (!organization) { - const org = await getDefaultOrg({ serviceUrl }); - if (!org) { - throw new Error("No default organization found"); - } - - organization = org.id; - } - - const branding = await getBrandingSettings({ - serviceUrl, - organization, - }); - - let user: User | undefined; - let human: HumanUser | undefined; - if (userId) { - const userResponse = await getUserByID({ - serviceUrl, - userId, - }); - if (userResponse) { - user = userResponse.user; - if (user?.type.case === "human") { - human = user.type.value as HumanUser; - } - } - } - - return ( - -
    -

    {t("success.title")}

    -

    {t("success.description")}

    - {user && ( - - )} - {human?.email?.isVerified ? ( - {t("success.verified")} - ) : ( - {t("success.notVerifiedYet")} - )} -
    - - - - -
    -
    -
    - ); -} From b393f36b6f638ab0285ad0ed062ea3b0f679c295 Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Fri, 23 May 2025 09:15:57 +0200 Subject: [PATCH 81/96] fix "0" --- apps/login/src/components/sign-in-with-idp.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/login/src/components/sign-in-with-idp.tsx b/apps/login/src/components/sign-in-with-idp.tsx index f08ebd821b..c0ab844242 100644 --- a/apps/login/src/components/sign-in-with-idp.tsx +++ b/apps/login/src/components/sign-in-with-idp.tsx @@ -74,7 +74,7 @@ export function SignInWithIdp({ return (
    - {identityProviders?.map(renderIDPButton)} + {identityProviders.length && identityProviders?.map(renderIDPButton)} {state?.error && (
    {state?.error} From eea139eca608b4f9d502eb8fb9f06308bc65867e Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Fri, 23 May 2025 10:55:53 +0200 Subject: [PATCH 82/96] recheck for valid user verification on /authenticator/set remove check on the /verify page itself --- .../app/(login)/authenticator/set/page.tsx | 37 +++++++++++++++++-- apps/login/src/app/(login)/verify/page.tsx | 5 +-- .../src/components/verify-redirect-button.tsx | 14 ++++++- apps/login/src/lib/server/verify.ts | 6 ++- 4 files changed, 52 insertions(+), 10 deletions(-) diff --git a/apps/login/src/app/(login)/authenticator/set/page.tsx b/apps/login/src/app/(login)/authenticator/set/page.tsx index 3e1b49eed0..63704b87eb 100644 --- a/apps/login/src/app/(login)/authenticator/set/page.tsx +++ b/apps/login/src/app/(login)/authenticator/set/page.tsx @@ -7,6 +7,7 @@ import { UserAvatar } from "@/components/user-avatar"; import { getSessionCookieById } from "@/lib/cookies"; import { getServiceUrlFromHeaders } from "@/lib/service-url"; import { loadMostRecentSession } from "@/lib/session"; +import { checkUserVerification } from "@/lib/verify-helper"; import { getActiveIdentityProviders, getBrandingSettings, @@ -18,6 +19,7 @@ import { import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb"; import { getLocale, getTranslations } from "next-intl/server"; import { headers } from "next/headers"; +import { redirect } from "next/navigation"; export default async function Page(props: { searchParams: Promise>; @@ -92,20 +94,49 @@ export default async function Page(props: { }); } - if (!sessionWithData) { + if ( + !sessionWithData || + !sessionWithData.factors || + !sessionWithData.factors.user + ) { return {tError("unknownContext")}; } const branding = await getBrandingSettings({ serviceUrl, - organization: sessionWithData.factors?.user?.organizationId, + organization: sessionWithData.factors.user?.organizationId, }); const loginSettings = await getLoginSettings({ serviceUrl, - organization: sessionWithData.factors?.user?.organizationId, + organization: sessionWithData.factors.user?.organizationId, }); + // check if user was verified recently + const isUserVerified = await checkUserVerification( + sessionWithData.factors.user?.id, + ); + + if (!isUserVerified) { + const params = new URLSearchParams({ + loginName: sessionWithData.factors.user.loginName as string, + send: "true", // set this to true to request a new code immediately + }); + + if (requestId) { + params.append("requestId", requestId); + } + + if (organization || sessionWithData.factors.user.organizationId) { + params.append( + "organization", + organization ?? (sessionWithData.factors.user.organizationId as string), + ); + } + + redirect(`/verify?` + params); + } + const identityProviders = await getActiveIdentityProviders({ serviceUrl, orgId: sessionWithData.factors?.user?.organizationId, diff --git a/apps/login/src/app/(login)/verify/page.tsx b/apps/login/src/app/(login)/verify/page.tsx index fcbc4fd34a..c9136b4669 100644 --- a/apps/login/src/app/(login)/verify/page.tsx +++ b/apps/login/src/app/(login)/verify/page.tsx @@ -6,7 +6,6 @@ import { VerifyRedirectButton } from "@/components/verify-redirect-button"; import { sendEmailCode } from "@/lib/server/verify"; import { getServiceUrlFromHeaders } from "@/lib/service-url"; import { loadMostRecentSession } from "@/lib/session"; -import { checkUserVerification } from "@/lib/verify-helper"; import { getBrandingSettings, getUserByID, @@ -112,8 +111,6 @@ export default async function Page(props: { searchParams: Promise }) { } } - const hasValidUserVerificationCheck = await checkUserVerification(id); - const params = new URLSearchParams({ userId: userId, initial: "true", // defines that a code is not required and is therefore not shown in the UI @@ -172,7 +169,7 @@ export default async function Page(props: { searchParams: Promise }) { )} {/* show a button to setup auth method for the user otherwise show the UI for reverifying */} - {human?.email?.isVerified && hasValidUserVerificationCheck ? ( + {human?.email?.isVerified ? ( // show page for already verified users (""); const [loading, setLoading] = useState(false); + const router = useRouter(); async function submitAndContinue(): Promise { setLoading(true); @@ -50,7 +52,7 @@ export function VerifyRedirectButton({ } as SendVerificationRedirectWithoutCheckCommand; } - await sendVerificationRedirectWithoutCheck(command) + const response = await sendVerificationRedirectWithoutCheck(command) .catch(() => { setError("Could not verify"); return; @@ -58,6 +60,16 @@ export function VerifyRedirectButton({ .finally(() => { setLoading(false); }); + + if (response && "error" in response && response.error) { + setError(response.error); + return; + } + + if (response && "redirect" in response && response.redirect) { + router.push(response.redirect); + return true; + } } return ( diff --git a/apps/login/src/lib/server/verify.ts b/apps/login/src/lib/server/verify.ts index 0ab4c5465d..34176a45e6 100644 --- a/apps/login/src/lib/server/verify.ts +++ b/apps/login/src/lib/server/verify.ts @@ -71,14 +71,16 @@ export async function sendVerification(command: VerifyUserByEmailCommand) { serviceUrl, userId: command.userId, verificationCode: command.code, - }).catch(() => { + }).catch((error) => { + console.warn(error); return { error: "Could not verify invite" }; }) : await verifyEmail({ serviceUrl, userId: command.userId, verificationCode: command.code, - }).catch(() => { + }).catch((error) => { + console.warn(error); return { error: "Could not verify email" }; }); From 0c6972d068e9c3fed8d0fee9530b22643a2aace2 Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Fri, 23 May 2025 11:57:44 +0200 Subject: [PATCH 83/96] cleanup verification logic --- apps/login/src/app/(login)/verify/page.tsx | 49 +++++----------------- 1 file changed, 10 insertions(+), 39 deletions(-) diff --git a/apps/login/src/app/(login)/verify/page.tsx b/apps/login/src/app/(login)/verify/page.tsx index c9136b4669..121e250dc3 100644 --- a/apps/login/src/app/(login)/verify/page.tsx +++ b/apps/login/src/app/(login)/verify/page.tsx @@ -2,17 +2,11 @@ import { Alert, AlertType } from "@/components/alert"; import { DynamicTheme } from "@/components/dynamic-theme"; import { UserAvatar } from "@/components/user-avatar"; import { VerifyForm } from "@/components/verify-form"; -import { VerifyRedirectButton } from "@/components/verify-redirect-button"; import { sendEmailCode } from "@/lib/server/verify"; import { getServiceUrlFromHeaders } from "@/lib/service-url"; import { loadMostRecentSession } from "@/lib/session"; -import { - getBrandingSettings, - getUserByID, - listAuthenticationMethodTypes, -} from "@/lib/zitadel"; +import { getBrandingSettings, getUserByID } from "@/lib/zitadel"; import { HumanUser, User } from "@zitadel/proto/zitadel/user/v2/user_pb"; -import { AuthenticationMethodType } from "@zitadel/proto/zitadel/user/v2/user_service_pb"; import { getLocale, getTranslations } from "next-intl/server"; import { headers } from "next/headers"; @@ -100,17 +94,6 @@ export default async function Page(props: { searchParams: Promise }) { throw Error("Failed to get user id"); } - let authMethods: AuthenticationMethodType[] | null = null; - if (human?.email?.isVerified) { - const authMethodsResponse = await listAuthenticationMethodTypes({ - serviceUrl, - userId, - }); - if (authMethodsResponse.authMethodTypes) { - authMethods = authMethodsResponse.authMethodTypes; - } - } - const params = new URLSearchParams({ userId: userId, initial: "true", // defines that a code is not required and is therefore not shown in the UI @@ -168,27 +151,15 @@ export default async function Page(props: { searchParams: Promise }) { ) )} - {/* show a button to setup auth method for the user otherwise show the UI for reverifying */} - {human?.email?.isVerified ? ( - // show page for already verified users - - ) : ( - // check if auth methods are set - - )} + {/* always show the code form / TODO improve UI for email links which were already used (currently we get an error code 3 due being reused) */} +
    ); From 446768b5e4855f8651714dec620eacc2c8adb345 Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Fri, 23 May 2025 12:05:06 +0200 Subject: [PATCH 84/96] fix invite resend --- apps/login/src/lib/server/verify.ts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/apps/login/src/lib/server/verify.ts b/apps/login/src/lib/server/verify.ts index 34176a45e6..d4c5e889fe 100644 --- a/apps/login/src/lib/server/verify.ts +++ b/apps/login/src/lib/server/verify.ts @@ -1,11 +1,11 @@ "use server"; import { - createInviteCode, getLoginSettings, getSession, getUserByID, listAuthenticationMethodTypes, + resendInviteCode, verifyEmail, verifyInviteCode, verifyTOTPRegistration, @@ -275,14 +275,10 @@ export async function resendVerification(command: resendVerifyEmailCommand) { const basePath = process.env.NEXT_PUBLIC_BASE_PATH ?? ""; - // create a new invite whenever the resend is called return command.isInvite - ? createInviteCode({ + ? resendInviteCode({ serviceUrl, userId: command.userId, - urlTemplate: - `${host.includes("localhost") ? "http://" : "https://"}${host}${basePath}/verify?code={{.Code}}&userId={{.UserID}}&organization={{.OrgID}}&invite=true` + - (command.requestId ? `&requestId=${command.requestId}` : ""), }) //resendInviteCode({ serviceUrl, userId: command.userId }) : sendEmailCode({ userId: command.userId, From a1a6326fae24e084e2602f455d745bd2a7eb28af Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Fri, 23 May 2025 12:05:18 +0200 Subject: [PATCH 85/96] cleanup --- apps/login/src/lib/server/verify.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/login/src/lib/server/verify.ts b/apps/login/src/lib/server/verify.ts index d4c5e889fe..88bb0139b3 100644 --- a/apps/login/src/lib/server/verify.ts +++ b/apps/login/src/lib/server/verify.ts @@ -279,7 +279,7 @@ export async function resendVerification(command: resendVerifyEmailCommand) { ? resendInviteCode({ serviceUrl, userId: command.userId, - }) //resendInviteCode({ serviceUrl, userId: command.userId }) + }) : sendEmailCode({ userId: command.userId, serviceUrl, From dffa94878efd7b8bf79e0c48924d56c24edf74f3 Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Fri, 23 May 2025 13:41:18 +0200 Subject: [PATCH 86/96] !! --- apps/login/src/components/sign-in-with-idp.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/login/src/components/sign-in-with-idp.tsx b/apps/login/src/components/sign-in-with-idp.tsx index c0ab844242..7632a29cc1 100644 --- a/apps/login/src/components/sign-in-with-idp.tsx +++ b/apps/login/src/components/sign-in-with-idp.tsx @@ -74,7 +74,7 @@ export function SignInWithIdp({ return (
    - {identityProviders.length && identityProviders?.map(renderIDPButton)} + {!!identityProviders.length && identityProviders?.map(renderIDPButton)} {state?.error && (
    {state?.error} From e3047602404140b4434015bc575119bd6b1df4d6 Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Fri, 23 May 2025 14:09:18 +0200 Subject: [PATCH 87/96] revert mfa check on password change --- apps/login/src/lib/server/password.ts | 87 +++++++++++++++++++-------- 1 file changed, 63 insertions(+), 24 deletions(-) diff --git a/apps/login/src/lib/server/password.ts b/apps/login/src/lib/server/password.ts index 34859d419b..3786145157 100644 --- a/apps/login/src/lib/server/password.ts +++ b/apps/login/src/lib/server/password.ts @@ -13,6 +13,7 @@ import { listAuthenticationMethodTypes, listUsers, passwordReset, + setPassword, setUserPassword, } from "@/lib/zitadel"; import { ConnectError, create } from "@zitadel/client"; @@ -24,7 +25,10 @@ import { } from "@zitadel/proto/zitadel/session/v2/session_service_pb"; import { LoginSettings } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb"; import { User, UserState } from "@zitadel/proto/zitadel/user/v2/user_pb"; -import { SetPasswordRequestSchema } from "@zitadel/proto/zitadel/user/v2/user_service_pb"; +import { + AuthenticationMethodType, + SetPasswordRequestSchema, +} from "@zitadel/proto/zitadel/user/v2/user_service_pb"; import { headers } from "next/headers"; import { getNextUrl } from "../client"; import { getSessionCookieById, getSessionCookieByLoginName } from "../cookies"; @@ -392,32 +396,67 @@ export async function checkSessionAndSetPassword({ return { error: "Could not load auth methods" }; } - const transport = async (serviceUrl: string, token: string) => { - return createServerTransport(token, { - baseUrl: serviceUrl, - }); - }; + const requiredAuthMethodsForForceMFA = [ + AuthenticationMethodType.OTP_EMAIL, + AuthenticationMethodType.OTP_SMS, + AuthenticationMethodType.TOTP, + AuthenticationMethodType.U2F, + ]; - const myUserService = async (serviceUrl: string, sessionToken: string) => { - const transportPromise = await transport(serviceUrl, sessionToken); - return createUserServiceClient(transportPromise); - }; + const hasNoMFAMethods = requiredAuthMethodsForForceMFA.every( + (method) => !authmethods.authMethodTypes.includes(method), + ); - const selfService = await myUserService(serviceUrl, `${sessionCookie.token}`); + const loginSettings = await getLoginSettings({ + serviceUrl, + organization: session.factors.user.organizationId, + }); - return selfService - .setPassword( - { - userId: session.factors.user.id, - newPassword: { password, changeRequired: false }, - }, - {}, - ) - .catch((error: ConnectError) => { - console.log(error); - if (error.code === 7) { - return { error: "Session is not valid." }; + const forceMfa = !!( + loginSettings?.forceMfa || loginSettings?.forceMfaLocalOnly + ); + + // if the user has no MFA but MFA is enforced, we can set a password otherwise we use the token of the user + if (forceMfa && hasNoMFAMethods) { + return setPassword({ serviceUrl, payload }).catch((error) => { + // throw error if failed precondition (ex. User is not yet initialized) + if (error.code === 9 && error.message) { + return { error: "Failed precondition" }; + } else { + throw error; } - throw error; }); + } else { + const transport = async (serviceUrl: string, token: string) => { + return createServerTransport(token, { + baseUrl: serviceUrl, + }); + }; + + const myUserService = async (serviceUrl: string, sessionToken: string) => { + const transportPromise = await transport(serviceUrl, sessionToken); + return createUserServiceClient(transportPromise); + }; + + const selfService = await myUserService( + serviceUrl, + `${sessionCookie.token}`, + ); + + return selfService + .setPassword( + { + userId: session.factors.user.id, + newPassword: { password, changeRequired: false }, + }, + {}, + ) + .catch((error: ConnectError) => { + console.log(error); + if (error.code === 7) { + return { error: "Session is not valid." }; + } + throw error; + }); + } } From 0af7185a903580bb5ac9b7b0f07564d6dce7714d Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Fri, 23 May 2025 18:18:26 +0200 Subject: [PATCH 88/96] fix: use invite code whenever authmethods are zero, otherwise use email code --- .../app/(login)/authenticator/set/page.tsx | 1 + apps/login/src/app/(login)/mfa/set/page.tsx | 1 + apps/login/src/app/(login)/verify/page.tsx | 73 +++--- .../src/app/(login)/verify/success/page.tsx | 111 +++++++++ apps/login/src/components/verify-form.tsx | 5 + apps/login/src/lib/server/loginname.ts | 90 +++---- apps/login/src/lib/server/verify.ts | 231 ++++++++++-------- 7 files changed, 331 insertions(+), 181 deletions(-) create mode 100644 apps/login/src/app/(login)/verify/success/page.tsx diff --git a/apps/login/src/app/(login)/authenticator/set/page.tsx b/apps/login/src/app/(login)/authenticator/set/page.tsx index 63704b87eb..5a8dfe810d 100644 --- a/apps/login/src/app/(login)/authenticator/set/page.tsx +++ b/apps/login/src/app/(login)/authenticator/set/page.tsx @@ -120,6 +120,7 @@ export default async function Page(props: { if (!isUserVerified) { const params = new URLSearchParams({ loginName: sessionWithData.factors.user.loginName as string, + invite: "true", send: "true", // set this to true to request a new code immediately }); diff --git a/apps/login/src/app/(login)/mfa/set/page.tsx b/apps/login/src/app/(login)/mfa/set/page.tsx index c7f2fa6599..11c44a22fa 100644 --- a/apps/login/src/app/(login)/mfa/set/page.tsx +++ b/apps/login/src/app/(login)/mfa/set/page.tsx @@ -134,6 +134,7 @@ export default async function Page(props: { {!(loginName || sessionId) && {tError("unknownContext")}} + {/* this happens if you register a user and open up the email verification link on a different device than the device where the registration was made. */} {!valid && {tError("sessionExpired")}} {isSessionValid(sessionWithData).valid && diff --git a/apps/login/src/app/(login)/verify/page.tsx b/apps/login/src/app/(login)/verify/page.tsx index 121e250dc3..cecaa5fcf7 100644 --- a/apps/login/src/app/(login)/verify/page.tsx +++ b/apps/login/src/app/(login)/verify/page.tsx @@ -2,7 +2,7 @@ import { Alert, AlertType } from "@/components/alert"; import { DynamicTheme } from "@/components/dynamic-theme"; import { UserAvatar } from "@/components/user-avatar"; import { VerifyForm } from "@/components/verify-form"; -import { sendEmailCode } from "@/lib/server/verify"; +import { sendEmailCode, sendInviteEmailCode } from "@/lib/server/verify"; import { getServiceUrlFromHeaders } from "@/lib/service-url"; import { loadMostRecentSession } from "@/lib/session"; import { getBrandingSettings, getUserByID } from "@/lib/zitadel"; @@ -21,11 +21,6 @@ export default async function Page(props: { searchParams: Promise }) { const _headers = await headers(); const { serviceUrl } = getServiceUrlFromHeaders(_headers); - const host = _headers.get("host"); - - if (!host || typeof host !== "string") { - throw new Error("No host found"); - } const branding = await getBrandingSettings({ serviceUrl, @@ -41,29 +36,25 @@ export default async function Page(props: { searchParams: Promise }) { const basePath = process.env.NEXT_PUBLIC_BASE_PATH ?? ""; - if ("loginName" in searchParams) { - sessionFactors = await loadMostRecentSession({ - serviceUrl, - sessionParams: { - loginName, - organization, - }, - }); + async function sendEmail() { + const host = _headers.get("host"); - if (doSend && sessionFactors?.factors?.user?.id) { - await sendEmailCode({ + if (!host || typeof host !== "string") { + throw new Error("No host found"); + } + + if (invite === "true") { + await sendInviteEmailCode({ serviceUrl, - userId: sessionFactors?.factors?.user?.id, + userId, urlTemplate: - `${host.includes("localhost") ? "http://" : "https://"}${host}${basePath}/verify?code={{.Code}}&userId={{.UserID}}&organization={{.OrgID}}` + + `${host.includes("localhost") ? "http://" : "https://"}${host}${basePath}/verify?code={{.Code}}&userId={{.UserID}}&organization={{.OrgID}}&invite=true` + (requestId ? `&requestId=${requestId}` : ""), }).catch((error) => { console.error("Could not resend verification email", error); throw Error("Failed to send verification email"); }); - } - } else if ("userId" in searchParams && userId) { - if (doSend) { + } else { await sendEmailCode({ serviceUrl, userId, @@ -75,6 +66,24 @@ export default async function Page(props: { searchParams: Promise }) { throw Error("Failed to send verification email"); }); } + } + + if ("loginName" in searchParams) { + sessionFactors = await loadMostRecentSession({ + serviceUrl, + sessionParams: { + loginName, + organization, + }, + }); + + if (doSend && sessionFactors?.factors?.user?.id) { + await sendEmail(); + } + } else if ("userId" in searchParams && userId) { + if (doSend) { + await sendEmail(); + } const userResponse = await getUserByID({ serviceUrl, @@ -151,15 +160,19 @@ export default async function Page(props: { searchParams: Promise }) { ) )} - {/* always show the code form / TODO improve UI for email links which were already used (currently we get an error code 3 due being reused) */} - + {/* always show the code form, except code is an invite code and the email is verified */} + {invite === "true" && human?.email?.isVerified ? ( + {t("success")} + ) : ( + + )}
    ); diff --git a/apps/login/src/app/(login)/verify/success/page.tsx b/apps/login/src/app/(login)/verify/success/page.tsx new file mode 100644 index 0000000000..aed9f79854 --- /dev/null +++ b/apps/login/src/app/(login)/verify/success/page.tsx @@ -0,0 +1,111 @@ +import { DynamicTheme } from "@/components/dynamic-theme"; +import { UserAvatar } from "@/components/user-avatar"; +import { getSessionCookieById } from "@/lib/cookies"; +import { getServiceUrlFromHeaders } from "@/lib/service-url"; +import { loadMostRecentSession } from "@/lib/session"; +import { + getBrandingSettings, + getLoginSettings, + getSession, + getUserByID, +} from "@/lib/zitadel"; +import { HumanUser, User } from "@zitadel/proto/zitadel/user/v2/user_pb"; +import { getLocale, getTranslations } from "next-intl/server"; +import { headers } from "next/headers"; + +async function loadSessionById( + serviceUrl: string, + sessionId: string, + organization?: string, +) { + const recent = await getSessionCookieById({ sessionId, organization }); + return getSession({ + serviceUrl, + sessionId: recent.id, + sessionToken: recent.token, + }).then((response) => { + if (response?.session) { + return response.session; + } + }); +} + +export default async function Page(props: { searchParams: Promise }) { + const searchParams = await props.searchParams; + const locale = getLocale(); + const t = await getTranslations({ locale, namespace: "signedin" }); + + const _headers = await headers(); + const { serviceUrl } = getServiceUrlFromHeaders(_headers); + + const { loginName, requestId, organization, userId } = searchParams; + + const branding = await getBrandingSettings({ + serviceUrl, + organization, + }); + + const sessionFactors = await loadMostRecentSession({ + serviceUrl, + sessionParams: { loginName, organization }, + }).catch((error) => { + console.warn("Error loading session:", error); + }); + + let loginSettings; + if (!requestId) { + loginSettings = await getLoginSettings({ + serviceUrl, + organization, + }); + } + + const id = userId ?? sessionFactors?.factors?.user?.id; + + if (!id) { + throw Error("Failed to get user id"); + } + + const userResponse = await getUserByID({ + serviceUrl, + userId: id, + }); + + let user: User | undefined; + let human: HumanUser | undefined; + + if (userResponse) { + user = userResponse.user; + if (user?.type.case === "human") { + human = user.type.value as HumanUser; + } + } + + return ( + +
    +

    + {t("title", { user: sessionFactors?.factors?.user?.displayName })} +

    +

    {t("description")}

    + + {sessionFactors ? ( + + ) : ( + user && ( + + ) + )} +
    +
    + ); +} diff --git a/apps/login/src/components/verify-form.tsx b/apps/login/src/components/verify-form.tsx index e09642eecf..0933f598dd 100644 --- a/apps/login/src/components/verify-form.tsx +++ b/apps/login/src/components/verify-form.tsx @@ -63,6 +63,11 @@ export function VerifyForm({ setLoading(false); }); + if (response && "error" in response && response?.error) { + setError(response.error); + return; + } + return response; } diff --git a/apps/login/src/lib/server/loginname.ts b/apps/login/src/lib/server/loginname.ts index 83a6f90abb..1e7a1fe3de 100644 --- a/apps/login/src/lib/server/loginname.ts +++ b/apps/login/src/lib/server/loginname.ts @@ -9,7 +9,6 @@ import { idpTypeToIdentityProviderType, idpTypeToSlug } from "../idp"; import { PasskeysType } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb"; import { UserState } from "@zitadel/proto/zitadel/user/v2/user_pb"; import { getServiceUrlFromHeaders } from "../service-url"; -import { checkEmailVerified, checkUserVerification } from "../verify-helper"; import { getActiveIdentityProviders, getIDPByID, @@ -254,62 +253,63 @@ export async function sendLoginname(command: SendLoginnameCommand) { userId: session.factors?.user?.id, }); - // this can be expected to be an invite as users created in console have a password set. + // always resend invite if user has no auth method set if (!methods.authMethodTypes || !methods.authMethodTypes.length) { // redirect to /verify invite if no auth method is set and email is not verified - const inviteCheck = checkEmailVerified( - session, - humanUser, - session.factors.user.organizationId, - command.requestId, - ); + // const inviteCheck = checkEmailVerified( + // session, + // humanUser, + // session.factors.user.organizationId, + // command.requestId, + // ); - if (inviteCheck?.redirect) { - return inviteCheck; - } + // if (inviteCheck?.redirect) { + // return inviteCheck; + // } - // check if user was verified recently - const isUserVerified = await checkUserVerification( - session.factors.user.id, - ); - if (!isUserVerified) { - const params = new URLSearchParams({ - loginName: session.factors?.user?.loginName as string, - send: "true", // set this to true to request a new code immediately - }); - - if (command.requestId) { - params.append("requestId", command.requestId); - } - - if (command.organization || session.factors?.user?.organizationId) { - params.append( - "organization", - command.organization ?? - (session.factors?.user?.organizationId as string), - ); - } - - return { redirect: `/verify?` + params }; - } - - const paramsAuthenticatorSetup = new URLSearchParams({ - loginName: session.factors?.user?.loginName, - userId: session.factors?.user?.id, // verify needs user id + // // check if user was verified recently + // const isUserVerified = await checkUserVerification( + // session.factors.user.id, + // ); + // if (!isUserVerified) { + const params = new URLSearchParams({ + loginName: session.factors?.user?.loginName as string, + send: "true", // set this to true to request a new code immediately + invite: "true", }); + if (command.requestId) { + params.append("requestId", command.requestId); + } + if (command.organization || session.factors?.user?.organizationId) { - paramsAuthenticatorSetup.append( + params.append( "organization", - command.organization ?? session.factors?.user?.organizationId, + command.organization ?? + (session.factors?.user?.organizationId as string), ); } - if (command.requestId) { - paramsAuthenticatorSetup.append("requestId", command.requestId); - } + return { redirect: `/verify?` + params }; + // } - return { redirect: "/authenticator/set?" + paramsAuthenticatorSetup }; + // const paramsAuthenticatorSetup = new URLSearchParams({ + // loginName: session.factors?.user?.loginName, + // userId: session.factors?.user?.id, // verify needs user id + // }); + + // if (command.organization || session.factors?.user?.organizationId) { + // paramsAuthenticatorSetup.append( + // "organization", + // command.organization ?? session.factors?.user?.organizationId, + // ); + // } + + // if (command.requestId) { + // paramsAuthenticatorSetup.append("requestId", command.requestId); + // } + + // return { redirect: "/authenticator/set?" + paramsAuthenticatorSetup }; } if (methods.authMethodTypes.length == 1) { diff --git a/apps/login/src/lib/server/verify.ts b/apps/login/src/lib/server/verify.ts index 88bb0139b3..dd93cc0935 100644 --- a/apps/login/src/lib/server/verify.ts +++ b/apps/login/src/lib/server/verify.ts @@ -1,6 +1,7 @@ "use server"; import { + createInviteCode, getLoginSettings, getSession, getUserByID, @@ -93,88 +94,26 @@ export async function sendVerification(command: VerifyUserByEmailCommand) { } let session: Session | undefined; - let user: User | undefined; + const userResponse = await getUserByID({ + serviceUrl, + userId: command.userId, + }); - if ("loginName" in command) { - const sessionCookie = await getSessionCookieByLoginName({ - loginName: command.loginName, - organization: command.organization, - }).catch((error) => { - console.warn("Ignored error:", error); - }); - - if (!sessionCookie) { - return { error: "Could not load session cookie" }; - } - - session = await getSession({ - serviceUrl, - sessionId: sessionCookie.id, - sessionToken: sessionCookie.token, - }).then((response) => { - if (response?.session) { - return response.session; - } - }); - - if (!session?.factors?.user?.id) { - return { error: "Could not create session for user" }; - } - - const userResponse = await getUserByID({ - serviceUrl, - userId: session?.factors?.user?.id, - }); - - if (!userResponse?.user) { - return { error: "Could not load user" }; - } - - user = userResponse.user; - } else { - const userResponse = await getUserByID({ - serviceUrl, - userId: command.userId, - }); - - if (!userResponse || !userResponse.user) { - return { error: "Could not load user" }; - } - - user = userResponse.user; - - const checks = create(ChecksSchema, { - user: { - search: { - case: "loginName", - value: userResponse.user.preferredLoginName, - }, - }, - }); - - session = await createSessionAndUpdateCookie({ - checks, - requestId: command.requestId, - }); - } - - if (!session?.factors?.user?.id) { - return { error: "Could not create session for user" }; - } - - if (!session?.factors?.user?.id) { - return { error: "Could not create session for user" }; - } - - if (!user) { + if (!userResponse || !userResponse.user) { return { error: "Could not load user" }; } - const loginSettings = await getLoginSettings({ - serviceUrl, - organization: user.details?.resourceOwner, + const user = userResponse.user; + + const sessionCookie = await getSessionCookieByLoginName({ + loginName: + "loginName" in command ? command.loginName : user.preferredLoginName, + organization: command.organization, + }).catch((error) => { + console.warn("Ignored error:", error); // checked later }); + // load auth methods for user const authMethodResponse = await listAuthenticationMethodTypes({ serviceUrl, userId: user.userId, @@ -190,6 +129,36 @@ export async function sendVerification(command: VerifyUserByEmailCommand) { authMethodResponse.authMethodTypes && authMethodResponse.authMethodTypes.length == 0 ) { + if (!sessionCookie) { + const checks = create(ChecksSchema, { + user: { + search: { + case: "loginName", + value: userResponse.user.preferredLoginName, + }, + }, + }); + + session = await createSessionAndUpdateCookie({ + checks, + requestId: command.requestId, + }); + } else { + session = await getSession({ + serviceUrl, + sessionId: sessionCookie.id, + sessionToken: sessionCookie.token, + }).then((response) => { + if (response?.session) { + return response.session; + } + }); + } + + if (!session) { + return { error: "Could not create session" }; + } + const params = new URLSearchParams({ sessionId: session.id, }); @@ -218,44 +187,80 @@ export async function sendVerification(command: VerifyUserByEmailCommand) { return { redirect: `/authenticator/set?${params}` }; } - // redirect to mfa factor if user has one, or redirect to set one up - const mfaFactorCheck = await checkMFAFactors( - serviceUrl, - session, - loginSettings, - authMethodResponse.authMethodTypes, - command.organization, - command.requestId, - ); + // if no session found and user is not invited, only show success page, + // if user is invited, recreate invite flow to not depend on session - if (mfaFactorCheck?.redirect) { - return mfaFactorCheck; - } + if (!sessionCookie || !session?.factors?.user?.id) { + const verifySuccessParams = new URLSearchParams({}); - // login user if no additional steps are required - if (command.requestId && session.id) { - const nextUrl = await getNextUrl( + if (command.userId) { + verifySuccessParams.set("userId", command.userId); + } + + if ( + ("loginName" in command && command.loginName) || + user.preferredLoginName + ) { + verifySuccessParams.set( + "loginName", + "loginName" in command && command.loginName + ? command.loginName + : user.preferredLoginName, + ); + } + if (command.requestId) { + verifySuccessParams.set("requestId", command.requestId); + } + if (command.organization) { + verifySuccessParams.set("organization", command.organization); + } + + return { redirect: `/verify/success?${verifySuccessParams}` }; + } else { + const loginSettings = await getLoginSettings({ + serviceUrl, + organization: user.details?.resourceOwner, + }); + + // redirect to mfa factor if user has one, or redirect to set one up + const mfaFactorCheck = await checkMFAFactors( + serviceUrl, + session, + loginSettings, + authMethodResponse.authMethodTypes, + command.organization, + command.requestId, + ); + + if (mfaFactorCheck?.redirect) { + return mfaFactorCheck; + } + + // login user if no additional steps are required + if (command.requestId && session.id) { + const nextUrl = await getNextUrl( + { + sessionId: session.id, + requestId: command.requestId, + organization: + command.organization ?? session.factors?.user?.organizationId, + }, + loginSettings?.defaultRedirectUri, + ); + + return { redirect: nextUrl }; + } + + const url = await getNextUrl( { - sessionId: session.id, - requestId: command.requestId, - organization: - command.organization ?? session.factors?.user?.organizationId, + loginName: session.factors.user.loginName, + organization: session.factors?.user?.organizationId, }, loginSettings?.defaultRedirectUri, ); - return { redirect: nextUrl }; + return { redirect: url }; } - - const url = await getNextUrl( - { - loginName: session.factors.user.loginName, - organization: session.factors?.user?.organizationId, - }, - loginSettings?.defaultRedirectUri, - ); - - return { redirect: url }; } type resendVerifyEmailCommand = { @@ -279,6 +284,11 @@ export async function resendVerification(command: resendVerifyEmailCommand) { ? resendInviteCode({ serviceUrl, userId: command.userId, + }).catch((error) => { + if (error.code === 9) { + return { error: "User is already verified!" }; + } + return { error: "Could not resend invite" }; }) : sendEmailCode({ userId: command.userId, @@ -303,6 +313,15 @@ export async function sendEmailCode(command: sendEmailCommand) { }); } +export async function sendInviteEmailCode(command: sendEmailCommand) { + // TODO: change this to sendInvite + return createInviteCode({ + serviceUrl: command.serviceUrl, + userId: command.userId, + urlTemplate: command.urlTemplate, + }); +} + export type SendVerificationRedirectWithoutCheckCommand = { organization?: string; requestId?: string; From 8204312892f5f7230d4587769051a6242d8eff00 Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Mon, 26 May 2025 08:57:13 +0200 Subject: [PATCH 89/96] change logic --- apps/login/locales/de.json | 3 +- apps/login/locales/en.json | 3 +- apps/login/locales/es.json | 3 +- apps/login/locales/it.json | 3 +- apps/login/locales/pl.json | 3 +- apps/login/locales/ru.json | 3 +- apps/login/locales/zh.json | 3 +- apps/login/src/app/(login)/verify/page.tsx | 10 +- .../src/app/(login)/verify/success/page.tsx | 8 +- apps/login/src/lib/server/verify.ts | 97 ++++++++++--------- 10 files changed, 71 insertions(+), 65 deletions(-) diff --git a/apps/login/locales/de.json b/apps/login/locales/de.json index 7471cc6ec7..8b3d4b311e 100644 --- a/apps/login/locales/de.json +++ b/apps/login/locales/de.json @@ -174,7 +174,8 @@ }, "verify": { "userIdMissing": "Keine Benutzer-ID angegeben!", - "success": "Erfolgreich verifiziert", + "successTitle": "Benutzer verifiziert", + "successDescription": "Der Benutzer wurde erfolgreich verifiziert.", "setupAuthenticator": "Authentifikator einrichten", "verify": { "title": "Benutzer verifizieren", diff --git a/apps/login/locales/en.json b/apps/login/locales/en.json index 164761f43d..daaaeba108 100644 --- a/apps/login/locales/en.json +++ b/apps/login/locales/en.json @@ -174,7 +174,8 @@ }, "verify": { "userIdMissing": "No userId provided!", - "success": "The user has been verified successfully.", + "successTitle": "User verified", + "successDescription": "The user has been verified successfully.", "setupAuthenticator": "Setup authenticator", "verify": { "title": "Verify user", diff --git a/apps/login/locales/es.json b/apps/login/locales/es.json index 6e5dd43820..b7dd57b4c0 100644 --- a/apps/login/locales/es.json +++ b/apps/login/locales/es.json @@ -174,7 +174,8 @@ }, "verify": { "userIdMissing": "¡No se proporcionó userId!", - "success": "¡Verificación exitosa!", + "successTitle": "Usuario verificado", + "successDescription": "El usuario ha sido verificado con éxito.", "setupAuthenticator": "Configurar autenticador", "verify": { "title": "Verificar usuario", diff --git a/apps/login/locales/it.json b/apps/login/locales/it.json index 1173906e82..f476da3402 100644 --- a/apps/login/locales/it.json +++ b/apps/login/locales/it.json @@ -174,7 +174,8 @@ }, "verify": { "userIdMissing": "Nessun userId fornito!", - "success": "Verifica effettuata con successo!", + "successTitle": "Utente verificato", + "successDescription": "L'utente è stato verificato con successo.", "setupAuthenticator": "Configura autenticatore", "verify": { "title": "Verifica utente", diff --git a/apps/login/locales/pl.json b/apps/login/locales/pl.json index ac3758227e..4dd607f3cb 100644 --- a/apps/login/locales/pl.json +++ b/apps/login/locales/pl.json @@ -174,7 +174,8 @@ }, "verify": { "userIdMissing": "Nie podano identyfikatora użytkownika!", - "success": "Użytkownik został pomyślnie zweryfikowany.", + "successTitle": "Weryfikacja zakończona", + "successDescription": "Użytkownik został pomyślnie zweryfikowany.", "setupAuthenticator": "Skonfiguruj uwierzytelnianie", "verify": { "title": "Zweryfikuj użytkownika", diff --git a/apps/login/locales/ru.json b/apps/login/locales/ru.json index 48af7d29d3..e8bbac212b 100644 --- a/apps/login/locales/ru.json +++ b/apps/login/locales/ru.json @@ -174,7 +174,8 @@ }, "verify": { "userIdMissing": "Не указан userId!", - "success": "Пользователь успешно подтверждён.", + "successTitle": "Пользователь подтверждён", + "successDescription": "Пользователь успешно подтверждён.", "setupAuthenticator": "Настроить аутентификатор", "verify": { "title": "Подтверждение пользователя", diff --git a/apps/login/locales/zh.json b/apps/login/locales/zh.json index 526f36a80b..7bc4ecf68a 100644 --- a/apps/login/locales/zh.json +++ b/apps/login/locales/zh.json @@ -174,7 +174,8 @@ }, "verify": { "userIdMissing": "未提供用户 ID!", - "success": "用户验证成功。", + "successTitle": "用户已验证", + "successDescription": "用户已成功验证。", "setupAuthenticator": "设置认证器", "verify": { "title": "验证用户", diff --git a/apps/login/src/app/(login)/verify/page.tsx b/apps/login/src/app/(login)/verify/page.tsx index cecaa5fcf7..aeb6952e40 100644 --- a/apps/login/src/app/(login)/verify/page.tsx +++ b/apps/login/src/app/(login)/verify/page.tsx @@ -36,7 +36,7 @@ export default async function Page(props: { searchParams: Promise }) { const basePath = process.env.NEXT_PUBLIC_BASE_PATH ?? ""; - async function sendEmail() { + async function sendEmail(userId: string) { const host = _headers.get("host"); if (!host || typeof host !== "string") { @@ -51,7 +51,7 @@ export default async function Page(props: { searchParams: Promise }) { `${host.includes("localhost") ? "http://" : "https://"}${host}${basePath}/verify?code={{.Code}}&userId={{.UserID}}&organization={{.OrgID}}&invite=true` + (requestId ? `&requestId=${requestId}` : ""), }).catch((error) => { - console.error("Could not resend verification email", error); + console.error("Could not send invitation email", error); throw Error("Failed to send verification email"); }); } else { @@ -62,7 +62,7 @@ export default async function Page(props: { searchParams: Promise }) { `${host.includes("localhost") ? "http://" : "https://"}${host}${basePath}/verify?code={{.Code}}&userId={{.UserID}}&organization={{.OrgID}}` + (requestId ? `&requestId=${requestId}` : ""), }).catch((error) => { - console.error("Could not resend verification email", error); + console.error("Could not send verification email", error); throw Error("Failed to send verification email"); }); } @@ -78,11 +78,11 @@ export default async function Page(props: { searchParams: Promise }) { }); if (doSend && sessionFactors?.factors?.user?.id) { - await sendEmail(); + await sendEmail(sessionFactors.factors.user.id); } } else if ("userId" in searchParams && userId) { if (doSend) { - await sendEmail(); + await sendEmail(userId); } const userResponse = await getUserByID({ diff --git a/apps/login/src/app/(login)/verify/success/page.tsx b/apps/login/src/app/(login)/verify/success/page.tsx index aed9f79854..678687a7f6 100644 --- a/apps/login/src/app/(login)/verify/success/page.tsx +++ b/apps/login/src/app/(login)/verify/success/page.tsx @@ -33,7 +33,7 @@ async function loadSessionById( export default async function Page(props: { searchParams: Promise }) { const searchParams = await props.searchParams; const locale = getLocale(); - const t = await getTranslations({ locale, namespace: "signedin" }); + const t = await getTranslations({ locale, namespace: "verify" }); const _headers = await headers(); const { serviceUrl } = getServiceUrlFromHeaders(_headers); @@ -84,10 +84,8 @@ export default async function Page(props: { searchParams: Promise }) { return (
    -

    - {t("title", { user: sessionFactors?.factors?.user?.displayName })} -

    -

    {t("description")}

    +

    {t("successTitle")}

    +

    {t("successDescription")}

    {sessionFactors ? ( { + if (response?.session) { + return response.session; + } + }); + } + // load auth methods for user const authMethodResponse = await listAuthenticationMethodTypes({ serviceUrl, @@ -143,16 +155,6 @@ export async function sendVerification(command: VerifyUserByEmailCommand) { checks, requestId: command.requestId, }); - } else { - session = await getSession({ - serviceUrl, - sessionId: sessionCookie.id, - sessionToken: sessionCookie.token, - }).then((response) => { - if (response?.session) { - return response.session; - } - }); } if (!session) { @@ -187,10 +189,9 @@ export async function sendVerification(command: VerifyUserByEmailCommand) { return { redirect: `/authenticator/set?${params}` }; } - // if no session found and user is not invited, only show success page, + // if no session found only show success page, // if user is invited, recreate invite flow to not depend on session - - if (!sessionCookie || !session?.factors?.user?.id) { + if (!session?.factors?.user?.id) { const verifySuccessParams = new URLSearchParams({}); if (command.userId) { @@ -216,51 +217,51 @@ export async function sendVerification(command: VerifyUserByEmailCommand) { } return { redirect: `/verify/success?${verifySuccessParams}` }; - } else { - const loginSettings = await getLoginSettings({ - serviceUrl, - organization: user.details?.resourceOwner, - }); + } - // redirect to mfa factor if user has one, or redirect to set one up - const mfaFactorCheck = await checkMFAFactors( - serviceUrl, - session, - loginSettings, - authMethodResponse.authMethodTypes, - command.organization, - command.requestId, - ); + const loginSettings = await getLoginSettings({ + serviceUrl, + organization: user.details?.resourceOwner, + }); - if (mfaFactorCheck?.redirect) { - return mfaFactorCheck; - } + // redirect to mfa factor if user has one, or redirect to set one up + const mfaFactorCheck = await checkMFAFactors( + serviceUrl, + session, + loginSettings, + authMethodResponse.authMethodTypes, + command.organization, + command.requestId, + ); - // login user if no additional steps are required - if (command.requestId && session.id) { - const nextUrl = await getNextUrl( - { - sessionId: session.id, - requestId: command.requestId, - organization: - command.organization ?? session.factors?.user?.organizationId, - }, - loginSettings?.defaultRedirectUri, - ); + if (mfaFactorCheck?.redirect) { + return mfaFactorCheck; + } - return { redirect: nextUrl }; - } - - const url = await getNextUrl( + // login user if no additional steps are required + if (command.requestId && session.id) { + const nextUrl = await getNextUrl( { - loginName: session.factors.user.loginName, - organization: session.factors?.user?.organizationId, + sessionId: session.id, + requestId: command.requestId, + organization: + command.organization ?? session.factors?.user?.organizationId, }, loginSettings?.defaultRedirectUri, ); - return { redirect: url }; + return { redirect: nextUrl }; } + + const url = await getNextUrl( + { + loginName: session.factors.user.loginName, + organization: session.factors?.user?.organizationId, + }, + loginSettings?.defaultRedirectUri, + ); + + return { redirect: url }; } type resendVerifyEmailCommand = { From 80f106dc6553042e8e535054b64deac9263030ad Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Mon, 26 May 2025 10:17:34 +0200 Subject: [PATCH 90/96] cleanup --- apps/login/src/app/(login)/verify/page.tsx | 23 ++++++-------- apps/login/src/lib/server/loginname.ts | 36 ---------------------- 2 files changed, 9 insertions(+), 50 deletions(-) diff --git a/apps/login/src/app/(login)/verify/page.tsx b/apps/login/src/app/(login)/verify/page.tsx index aeb6952e40..43aff0242b 100644 --- a/apps/login/src/app/(login)/verify/page.tsx +++ b/apps/login/src/app/(login)/verify/page.tsx @@ -52,7 +52,7 @@ export default async function Page(props: { searchParams: Promise }) { (requestId ? `&requestId=${requestId}` : ""), }).catch((error) => { console.error("Could not send invitation email", error); - throw Error("Failed to send verification email"); + throw Error("Failed to send invitation email"); }); } else { await sendEmailCode({ @@ -160,19 +160,14 @@ export default async function Page(props: { searchParams: Promise }) { ) )} - {/* always show the code form, except code is an invite code and the email is verified */} - {invite === "true" && human?.email?.isVerified ? ( - {t("success")} - ) : ( - - )} +
    ); diff --git a/apps/login/src/lib/server/loginname.ts b/apps/login/src/lib/server/loginname.ts index 1e7a1fe3de..fa75929702 100644 --- a/apps/login/src/lib/server/loginname.ts +++ b/apps/login/src/lib/server/loginname.ts @@ -255,23 +255,6 @@ export async function sendLoginname(command: SendLoginnameCommand) { // always resend invite if user has no auth method set if (!methods.authMethodTypes || !methods.authMethodTypes.length) { - // redirect to /verify invite if no auth method is set and email is not verified - // const inviteCheck = checkEmailVerified( - // session, - // humanUser, - // session.factors.user.organizationId, - // command.requestId, - // ); - - // if (inviteCheck?.redirect) { - // return inviteCheck; - // } - - // // check if user was verified recently - // const isUserVerified = await checkUserVerification( - // session.factors.user.id, - // ); - // if (!isUserVerified) { const params = new URLSearchParams({ loginName: session.factors?.user?.loginName as string, send: "true", // set this to true to request a new code immediately @@ -291,25 +274,6 @@ export async function sendLoginname(command: SendLoginnameCommand) { } return { redirect: `/verify?` + params }; - // } - - // const paramsAuthenticatorSetup = new URLSearchParams({ - // loginName: session.factors?.user?.loginName, - // userId: session.factors?.user?.id, // verify needs user id - // }); - - // if (command.organization || session.factors?.user?.organizationId) { - // paramsAuthenticatorSetup.append( - // "organization", - // command.organization ?? session.factors?.user?.organizationId, - // ); - // } - - // if (command.requestId) { - // paramsAuthenticatorSetup.append("requestId", command.requestId); - // } - - // return { redirect: "/authenticator/set?" + paramsAuthenticatorSetup }; } if (methods.authMethodTypes.length == 1) { From fb2f1d03379e404a61f91c624a9be6c1f5789db8 Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Mon, 26 May 2025 15:54:20 +0200 Subject: [PATCH 91/96] use createinvite --- apps/login/src/lib/server/verify.ts | 7 ++++--- apps/login/src/lib/zitadel.ts | 15 --------------- 2 files changed, 4 insertions(+), 18 deletions(-) diff --git a/apps/login/src/lib/server/verify.ts b/apps/login/src/lib/server/verify.ts index 5591da7290..518d6c679b 100644 --- a/apps/login/src/lib/server/verify.ts +++ b/apps/login/src/lib/server/verify.ts @@ -6,7 +6,6 @@ import { getSession, getUserByID, listAuthenticationMethodTypes, - resendInviteCode, verifyEmail, verifyInviteCode, verifyTOTPRegistration, @@ -282,9 +281,12 @@ export async function resendVerification(command: resendVerifyEmailCommand) { const basePath = process.env.NEXT_PUBLIC_BASE_PATH ?? ""; return command.isInvite - ? resendInviteCode({ + ? createInviteCode({ serviceUrl, userId: command.userId, + urlTemplate: + `${host.includes("localhost") ? "http://" : "https://"}${host}${basePath}/verify?code={{.Code}}&userId={{.UserID}}&organization={{.OrgID}}&invite=true` + + (command.requestId ? `&requestId=${command.requestId}` : ""), }).catch((error) => { if (error.code === 9) { return { error: "User is already verified!" }; @@ -315,7 +317,6 @@ export async function sendEmailCode(command: sendEmailCommand) { } export async function sendInviteEmailCode(command: sendEmailCommand) { - // TODO: change this to sendInvite return createInviteCode({ serviceUrl: command.serviceUrl, userId: command.userId, diff --git a/apps/login/src/lib/zitadel.ts b/apps/login/src/lib/zitadel.ts index d1fe83434d..a0e91a021c 100644 --- a/apps/login/src/lib/zitadel.ts +++ b/apps/login/src/lib/zitadel.ts @@ -502,21 +502,6 @@ export async function verifyInviteCode({ return userService.verifyInviteCode({ userId, verificationCode }, {}); } -export async function resendInviteCode({ - serviceUrl, - userId, -}: { - serviceUrl: string; - userId: string; -}) { - const userService: Client = await createServiceForHost( - UserService, - serviceUrl, - ); - - return userService.resendInviteCode({ userId }, {}); -} - export async function sendEmailCode({ serviceUrl, userId, From ff871aacdb44706b6e17b99f8db7b459fb93710b Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Mon, 26 May 2025 16:18:57 +0200 Subject: [PATCH 92/96] rm verify redirect button --- apps/login/src/app/(login)/mfa/set/page.tsx | 1 - .../src/components/verify-redirect-button.tsx | 102 ------------------ 2 files changed, 103 deletions(-) delete mode 100644 apps/login/src/components/verify-redirect-button.tsx diff --git a/apps/login/src/app/(login)/mfa/set/page.tsx b/apps/login/src/app/(login)/mfa/set/page.tsx index 11c44a22fa..c7f2fa6599 100644 --- a/apps/login/src/app/(login)/mfa/set/page.tsx +++ b/apps/login/src/app/(login)/mfa/set/page.tsx @@ -134,7 +134,6 @@ export default async function Page(props: { {!(loginName || sessionId) && {tError("unknownContext")}} - {/* this happens if you register a user and open up the email verification link on a different device than the device where the registration was made. */} {!valid && {tError("sessionExpired")}} {isSessionValid(sessionWithData).valid && diff --git a/apps/login/src/components/verify-redirect-button.tsx b/apps/login/src/components/verify-redirect-button.tsx deleted file mode 100644 index c968da86df..0000000000 --- a/apps/login/src/components/verify-redirect-button.tsx +++ /dev/null @@ -1,102 +0,0 @@ -"use client"; - -import { - sendVerificationRedirectWithoutCheck, - SendVerificationRedirectWithoutCheckCommand, -} from "@/lib/server/verify"; -import { AuthenticationMethodType } from "@zitadel/proto/zitadel/user/v2/user_service_pb"; -import { useTranslations } from "next-intl"; -import { useRouter } from "next/navigation"; -import { useState } from "react"; -import { Alert, AlertType } from "./alert"; -import { BackButton } from "./back-button"; -import { Button, ButtonVariants } from "./button"; -import { Spinner } from "./spinner"; - -export function VerifyRedirectButton({ - userId, - loginName, - requestId, - authMethods, - organization, -}: { - userId?: string; - loginName?: string; - requestId: string; - authMethods: AuthenticationMethodType[] | null; - organization?: string; -}) { - const t = useTranslations("verify"); - const [error, setError] = useState(""); - - const [loading, setLoading] = useState(false); - const router = useRouter(); - - async function submitAndContinue(): Promise { - setLoading(true); - - let command = { - organization, - requestId, - } as SendVerificationRedirectWithoutCheckCommand; - - if (userId) { - command = { - ...command, - userId, - } as SendVerificationRedirectWithoutCheckCommand; - } else if (loginName) { - command = { - ...command, - loginName, - } as SendVerificationRedirectWithoutCheckCommand; - } - - const response = await sendVerificationRedirectWithoutCheck(command) - .catch(() => { - setError("Could not verify"); - return; - }) - .finally(() => { - setLoading(false); - }); - - if (response && "error" in response && response.error) { - setError(response.error); - return; - } - - if (response && "redirect" in response && response.redirect) { - router.push(response.redirect); - return true; - } - } - - return ( - <> - {t("success")} - - {error && ( -
    - {error} -
    - )} - -
    - - - {authMethods?.length === 0 && ( - - )} -
    - - ); -} From 5e573c8c9c3d48d12d66ead8b6b8e7715456c65c Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Mon, 26 May 2025 16:23:32 +0200 Subject: [PATCH 93/96] cleanup unused method --- apps/login/src/lib/server/verify.ts | 167 ---------------------------- 1 file changed, 167 deletions(-) diff --git a/apps/login/src/lib/server/verify.ts b/apps/login/src/lib/server/verify.ts index 518d6c679b..aff3ebef02 100644 --- a/apps/login/src/lib/server/verify.ts +++ b/apps/login/src/lib/server/verify.ts @@ -16,7 +16,6 @@ import crypto from "crypto"; import { create } from "@zitadel/client"; import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb"; import { ChecksSchema } from "@zitadel/proto/zitadel/session/v2/session_service_pb"; -import { User } from "@zitadel/proto/zitadel/user/v2/user_pb"; import { cookies, headers } from "next/headers"; import { getNextUrl } from "../client"; import { getSessionCookieByLoginName } from "../cookies"; @@ -323,169 +322,3 @@ export async function sendInviteEmailCode(command: sendEmailCommand) { urlTemplate: command.urlTemplate, }); } - -export type SendVerificationRedirectWithoutCheckCommand = { - organization?: string; - requestId?: string; -} & ( - | { userId: string; loginName?: never } - | { userId?: never; loginName: string } -); - -export async function sendVerificationRedirectWithoutCheck( - command: SendVerificationRedirectWithoutCheckCommand, -) { - const _headers = await headers(); - const { serviceUrl } = getServiceUrlFromHeaders(_headers); - - if (!("loginName" in command || "userId" in command)) { - return { error: "No userId, nor loginname provided" }; - } - - let session: Session | undefined; - let user: User | undefined; - - if ("loginName" in command) { - const sessionCookie = await getSessionCookieByLoginName({ - loginName: command.loginName, - organization: command.organization, - }).catch((error) => { - console.warn("Ignored error:", error); - }); - - if (!sessionCookie) { - return { error: "Could not load session cookie" }; - } - - session = await getSession({ - serviceUrl, - sessionId: sessionCookie.id, - sessionToken: sessionCookie.token, - }).then((response) => { - if (response?.session) { - return response.session; - } - }); - - if (!session?.factors?.user?.id) { - return { error: "Could not create session for user" }; - } - - const userResponse = await getUserByID({ - serviceUrl, - userId: session?.factors?.user?.id, - }); - - if (!userResponse?.user) { - return { error: "Could not load user" }; - } - - user = userResponse.user; - } else if ("userId" in command) { - const userResponse = await getUserByID({ - serviceUrl, - userId: command.userId, - }); - - if (!userResponse?.user) { - return { error: "Could not load user" }; - } - - user = userResponse.user; - - const checks = create(ChecksSchema, { - user: { - search: { - case: "loginName", - value: userResponse.user.preferredLoginName, - }, - }, - }); - - session = await createSessionAndUpdateCookie({ - checks, - requestId: command.requestId, - }); - } - - if (!session?.factors?.user?.id) { - return { error: "Could not create session for user" }; - } - - if (!session?.factors?.user?.id) { - return { error: "Could not create session for user" }; - } - - if (!user) { - return { error: "Could not load user" }; - } - - const authMethodResponse = await listAuthenticationMethodTypes({ - serviceUrl, - userId: user.userId, - }); - - if (!authMethodResponse || !authMethodResponse.authMethodTypes) { - return { error: "Could not load possible authenticators" }; - } - - // if no authmethods are found on the user, redirect to set one up - if ( - authMethodResponse && - authMethodResponse.authMethodTypes && - authMethodResponse.authMethodTypes.length == 0 - ) { - const params = new URLSearchParams({ - sessionId: session.id, - }); - - if (session.factors?.user?.loginName) { - params.set("loginName", session.factors?.user?.loginName); - } - return { redirect: `/authenticator/set?${params}` }; - } - - const loginSettings = await getLoginSettings({ - serviceUrl, - organization: user.details?.resourceOwner, - }); - - // redirect to mfa factor if user has one, or redirect to set one up - const mfaFactorCheck = await checkMFAFactors( - serviceUrl, - session, - loginSettings, - authMethodResponse.authMethodTypes, - command.organization, - command.requestId, - ); - - if (mfaFactorCheck?.redirect) { - return mfaFactorCheck; - } - - // login user if no additional steps are required - if (command.requestId && session.id) { - const nextUrl = await getNextUrl( - { - sessionId: session.id, - requestId: command.requestId, - organization: - command.organization ?? session.factors?.user?.organizationId, - }, - loginSettings?.defaultRedirectUri, - ); - - return { redirect: nextUrl }; - } - - const url = await getNextUrl( - { - loginName: session.factors.user.loginName, - organization: session.factors?.user?.organizationId, - }, - loginSettings?.defaultRedirectUri, - ); - - return { redirect: url }; -} From 5cfc458779938a42a2ecee29afa9f1f9ff490c38 Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Mon, 26 May 2025 16:26:23 +0200 Subject: [PATCH 94/96] cleanup --- apps/login/src/app/(login)/verify/page.tsx | 2 -- apps/login/src/lib/server/verify.ts | 17 +++++++++++------ 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/apps/login/src/app/(login)/verify/page.tsx b/apps/login/src/app/(login)/verify/page.tsx index 43aff0242b..7634ff063a 100644 --- a/apps/login/src/app/(login)/verify/page.tsx +++ b/apps/login/src/app/(login)/verify/page.tsx @@ -45,7 +45,6 @@ export default async function Page(props: { searchParams: Promise }) { if (invite === "true") { await sendInviteEmailCode({ - serviceUrl, userId, urlTemplate: `${host.includes("localhost") ? "http://" : "https://"}${host}${basePath}/verify?code={{.Code}}&userId={{.UserID}}&organization={{.OrgID}}&invite=true` + @@ -56,7 +55,6 @@ export default async function Page(props: { searchParams: Promise }) { }); } else { await sendEmailCode({ - serviceUrl, userId, urlTemplate: `${host.includes("localhost") ? "http://" : "https://"}${host}${basePath}/verify?code={{.Code}}&userId={{.UserID}}&organization={{.OrgID}}` + diff --git a/apps/login/src/lib/server/verify.ts b/apps/login/src/lib/server/verify.ts index aff3ebef02..2de5cbf586 100644 --- a/apps/login/src/lib/server/verify.ts +++ b/apps/login/src/lib/server/verify.ts @@ -301,23 +301,28 @@ export async function resendVerification(command: resendVerifyEmailCommand) { }); } -type sendEmailCommand = { - serviceUrl: string; +type SendEmailCommand = { userId: string; urlTemplate: string; }; -export async function sendEmailCode(command: sendEmailCommand) { +export async function sendEmailCode(command: SendEmailCommand) { + const _headers = await headers(); + const { serviceUrl } = getServiceUrlFromHeaders(_headers); + return zitadelSendEmailCode({ - serviceUrl: command.serviceUrl, + serviceUrl, userId: command.userId, urlTemplate: command.urlTemplate, }); } -export async function sendInviteEmailCode(command: sendEmailCommand) { +export async function sendInviteEmailCode(command: SendEmailCommand) { + const _headers = await headers(); + const { serviceUrl } = getServiceUrlFromHeaders(_headers); + return createInviteCode({ - serviceUrl: command.serviceUrl, + serviceUrl, userId: command.userId, urlTemplate: command.urlTemplate, }); From 872a9b42aeccc208f74e22914cefc3f7132079a1 Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Mon, 26 May 2025 16:28:25 +0200 Subject: [PATCH 95/96] cleanup --- apps/login/src/lib/server/verify.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/login/src/lib/server/verify.ts b/apps/login/src/lib/server/verify.ts index 2de5cbf586..cf60f739b3 100644 --- a/apps/login/src/lib/server/verify.ts +++ b/apps/login/src/lib/server/verify.ts @@ -292,7 +292,7 @@ export async function resendVerification(command: resendVerifyEmailCommand) { } return { error: "Could not resend invite" }; }) - : sendEmailCode({ + : zitadelSendEmailCode({ userId: command.userId, serviceUrl, urlTemplate: From ce9a28d440246e5bac868077146eef25b289c081 Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Wed, 28 May 2025 11:50:26 +0200 Subject: [PATCH 96/96] idp length check --- apps/login/src/app/(login)/authenticator/set/page.tsx | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/apps/login/src/app/(login)/authenticator/set/page.tsx b/apps/login/src/app/(login)/authenticator/set/page.tsx index 5a8dfe810d..95b89af92d 100644 --- a/apps/login/src/app/(login)/authenticator/set/page.tsx +++ b/apps/login/src/app/(login)/authenticator/set/page.tsx @@ -184,13 +184,12 @@ export default async function Page(props: { > )} - {loginSettings?.allowExternalIdp && identityProviders && ( + {loginSettings?.allowExternalIdp && !!identityProviders.length && ( <> - {identityProviders.length && ( -
    -

    {t("linkWithIDP")}

    -
    - )} +
    +

    {t("linkWithIDP")}

    +
    +