From 8e64300f232dfe1775e673fcd5f57ba46c282d6e Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Thu, 24 Jul 2025 15:08:56 +0200 Subject: [PATCH] fix: remove duplicate readme.md to resolve case collision - Remove lowercase readme.md that was causing case collision warnings - Keep standard uppercase README.md - Fixes warning: 'the following paths have collided (e.g. case-sensitive paths on a case-insensitive filesystem)' --- apps/login/readme.md | 491 ++++++++++++++++--------------------------- 1 file changed, 180 insertions(+), 311 deletions(-) diff --git a/apps/login/readme.md b/apps/login/readme.md index ca7070a901..cdde703f48 100644 --- a/apps/login/readme.md +++ b/apps/login/readme.md @@ -1,12 +1,104 @@ -# ZITADEL Login UI +# ZITADEL TypeScript with Turborepo -This is going to be our next UI for the hosted login. It's based on Next.js 13 and its introduced `app/` directory. +This repository contains all TypeScript and JavaScript packages and applications you need to create your own ZITADEL +Login UI. -## Flow Diagram +collage of login screens -This diagram shows the available pages and flows. +[![npm package](https://img.shields.io/npm/v/@zitadel/proto.svg?style=for-the-badge&logo=npm&logoColor=white)](https://www.npmjs.com/package/@zitadel/proto) +[![npm package](https://img.shields.io/npm/v/@zitadel/client.svg?style=for-the-badge&logo=npm&logoColor=white)](https://www.npmjs.com/package/@zitadel/client) -> Note that back navigation or retries are not displayed. +**⚠️ This repo and packages are in beta state and subject to change ⚠️** + +The scope of functionality of this repo and packages is under active development. + +The `@zitadel/client` package is using [@connectrpc/connect](https://github.com/connectrpc/connect-es#readme). + +You can read the [contribution guide](/CONTRIBUTING.md) on how to contribute. +Questions can be raised in our [Discord channel](https://discord.gg/erh5Brh7jE) or as +a [GitHub issue](https://github.com/zitadel/typescript/issues). + +## Developing Your Own ZITADEL Login UI + +We think the easiest path of getting up and running, is the following: + +1. Fork and clone this repository +1. Rename the file .github/dependabot.example.yml to .github/dependabot.yml so you don't miss version and security updates. +1. [Run the ZITADEL Cloud login UI locally](#run-login-ui) +1. Make changes to the code and see the effects live on your local machine +1. Study the rest of this README.md and get familiar and comfortable with how everything works. +1. Decide on a way of how you want to build and run your login UI. + You can reuse ZITADEL Clouds way. + But if you need more freedom, you can also import the packages you need into your self built application. + +## Included Apps And Packages + +- `login`: The login UI used by ZITADEL Cloud, powered by Next.js +- `@zitadel/client`: shared client utilities for node and browser environments +- `@zitadel/proto`: Protocol Buffers (proto) definitions used by ZITADEL projects + +Each package and app is 100% [TypeScript](https://www.typescriptlang.org/). + +### Login + +The login is currently in a work in progress state. +The goal is to implement a login UI, using the session API of ZITADEL, which also implements the OIDC Standard and is +ready to use for everyone. + +In the first phase we want to have a MVP login ready with the OIDC Standard and a basic feature set. In a second step +the features will be extended. + +This list should show the current implementation state, and also what is missing. +You can already use the current state, and extend it with your needs. + +#### Features list + +- [x] Local User Registration (with Password) +- [x] User Registration and Login with external Provider + - [x] Google + - [x] GitHub + - [x] GitHub Enterprise + - [x] GitLab + - [x] GitLab Enterprise + - [x] Azure + - [x] Apple + - [x] Generic OIDC + - [x] Generic OAuth + - [x] Generic JWT + - [x] LDAP + - [x] SAML SP +- Multifactor Registration an Login + - [x] Passkeys + - [x] TOTP + - [x] OTP: Email Code + - [x] OTP: SMS Code +- [x] Password Change/Reset +- [x] Domain Discovery +- [x] Branding +- OIDC Standard + + - [x] Authorization Code Flow with PKCE + - [x] AuthRequest `hintUserId` + - [x] AuthRequest `loginHint` + - [x] AuthRequest `prompt` + - [x] Login + - [x] Select Account + - [ ] Consent + - [x] Create + - Scopes + - [x] `openid email profile address`` + - [x] `offline access` + - [x] `urn:zitadel:iam:org:idp:id:{idp_id}` + - [x] `urn:zitadel:iam:org:project:id:zitadel:aud` + - [x] `urn:zitadel:iam:org:id:{orgid}` + - [x] `urn:zitadel:iam:org:domain:primary:{domain}` + - [ ] AuthRequest UI locales + + #### Flow diagram + + This diagram shows the available pages and flows. + + > Note that back navigation or retries are not displayed. ```mermaid flowchart TD @@ -48,347 +140,124 @@ This diagram shows the available pages and flows. verify --> B[signedin] ``` -### /loginname +You can find a more detailed documentation of the different pages [here](./apps/login/readme.md). -This page shows a loginname field and Identity Providers to login or register. -If `loginSettings(org?).allowRegister` is `true`, it also shows a link to jump to /register +#### Custom translations -/loginame +The new login uses the [SettingsApi](https://zitadel.com/docs/apis/resources/settings_service_v2/settings-service-get-hosted-login-translation) to load custom translations. +Translations can be overriden at both the instance and organization levels. +To find the keys more easily, you can inspect the HTML and search for a `data-i18n-key` attribute, or look at the defaults in `/apps/login/locales/[locale].ts`. +![Custom Translations](.github/custom-i18n.png) -Requests to the APIs made: +## Tooling -- `getLoginSettings(org?)` -- `getLegalAndSupportSettings(org?)` -- `getIdentityProviders(org?)` -- `getBrandingSettings(org?)` -- `getActiveIdentityProviders(org?)` -- `startIdentityProviderFlow` -- `listUsers(org?)` -- `listAuthenticationMethodTypes` -- `getOrgsByDomain` -- `createSession()` -- `getSession()` +- [TypeScript](https://www.typescriptlang.org/) for static type checking +- [ESLint](https://eslint.org/) for code linting +- [Prettier](https://prettier.io) for code formatting -After a loginname is entered, a `listUsers` request is made using the loginName query to identify already registered users. +## Useful Commands -**USER FOUND:** If only one user is found, we query `listAuthenticationMethodTypes` to identify future steps. -If no authentication methods are found, we render an error stating: _User has no available authentication methods._ (exception see below.) -Now if only one method is found, we continue with the corresponding step (/password, /passkey). -If multiple methods are set, we prefer passkeys over any other method, so we redirect to /passkey, second option is IDP, and third is password. -If password is the next step, we check `loginSettings.passkeysType` for PasskeysType.ALLOWED, and prompt the user to setup passkeys afterwards. +- `make login-quality` - Check the quality of your code against a production build without installing any dependencies besides Docker +- `pnpm generate` - Build proto stubs for the client package +- `pnpm dev` - Develop all packages and the login app +- `pnpm build` - Build all packages and the login app +- `pnpm clean` - Clean up all `node_modules` and `dist` folders (runs each package's clean script) -**NO USER FOUND:** If no user is found, we check whether registering is allowed using `loginSettings.allowRegister`. -If `loginSettings?.allowUsernamePassword` is not allowed we continue to check for available IDPs. If a single IDP is available, we directly redirect the user to signup. +Learn more about developing the login UI in the [contribution guide](/CONTRIBUTING.md). -If no single IDP is set, we check for `loginSettings.allowUsernamePassword` and if no organization is set as context, we check whether we can discover a organization from the loginname of the user (using: `getOrgsByDomain`). Then if an organization is found, we check whether domainDiscovery is allowed on it and redirect the user to /register page including the discovered domain or without. +## Versioning And Publishing Packages -If no previous condition is met we throw an error stating the user was not found. +Package publishing has been configured using [Changesets](https://github.com/changesets/changesets). +Here is their [documentation](https://github.com/changesets/changesets#documentation) for more information about the +workflow. -**EXCEPTIONS:** If the outcome after this order produces a no authentication methods found, or user not found, we check whether `loginSettings?.ignoreUnknownUsernames` is set to `true` as in this case we redirect to the /password page regardless (to prevent username guessing). +The [GitHub Action](https://github.com/changesets/action) needs an `NPM_TOKEN` and `GITHUB_TOKEN` in the repository +settings. The [Changesets bot](https://github.com/apps/changeset-bot) should also be installed on the GitHub repository. -> NOTE: This page at this stage beeing ignores local sessions and executes a reauthentication. This is a feature which is not implemented yet. +Read the [changesets documentation](https://github.com/changesets/changesets/blob/main/docs/automating-changesets.md) +for more information about this automation -> NOTE: We ignore `loginSettings.allowExternalIdp` as the information whether IDPs are available comes as response from `getActiveIdentityProviders(org?)`. If a user has a cookie for the same loginname, a new session is created regardless and overwrites the old session. The old session is not deleted from the login as for now. +### Run Login UI -> NOTE: `listAuthenticationMethodTypes()` does not consider different domains for u2f methods or passkeys. The check whether a user should be redirected to one of the pages `/passkey` or `/u2f`, should be extended to use a domain filter (https://github.com/zitadel/zitadel/issues/8615) +To run the application make sure to install the dependencies with -### /password +```sh +pnpm install +``` -This page shows a password field to hydrate the current session with password as a factor. -Below the password field, a reset password link is shown which allows to send a reset email. +then generate the GRPC stubs with -/password +```sh +pnpm generate +``` -Requests to the APIs made: +To run the application against a local ZITADEL instance, run the following command: -- `getLoginSettings(org?)` -- `getBrandingSettings(org?)` -- `listAuthenticationMethodTypes` -- `getSession()` -- `updateSession()` -- `listUsers()` -- `getUserById()` +```sh +pnpm run-zitadel +``` -**MFA AVAILABLE:** After the password has been submitted, additional authentication methods are loaded. -If the user has set up an additional **single** second factor, it is redirected to add the next factor. Depending on the available method he is redirected to `/otp/time-based`,`/otp/sms?`, `/otp/email?` or `/u2f?`. If the user has multiple second factors, he is redirected to `/mfa` to select his preferred method to continue. +This sets up ZITADEL using docker compose and writes the configuration to the file `apps/login/.env.local`. -**NO MFA, USER STATE INITIAL** If the user has no MFA methods and is in an initial state, we redirect to `/password/change` where a new password can be set. +
+Alternatively, use another environment +You can develop against any ZITADEL instance in which you have sufficient rights to execute the following steps. +Just create or overwrite the file `apps/login/.env.local` yourself. +Add your instances base URL to the file at the key `ZITADEL_API_URL`. +Go to your instance and create a service user for the login application. +The login application creates users on your primary organization and reads policy data. +For the sake of simplicity, just make the service user an instance member with the role `IAM_OWNER`. +Create a PAT and copy it to the file `apps/login/.env.local` using the key `ZITADEL_SERVICE_USER_TOKEN`. -**NO MFA, FORCE MFA:** If no MFA method is available, and the settings force MFA, the user is sent to `/mfa/set` which prompts to setup a second factor. +The file should look similar to this: -**PROMPT PASSKEY** If the settings do not enforce MFA, we check if passkeys are allowed with `loginSettings?.passkeysType == PasskeysType.ALLOWED` and redirect the user to `/passkey/set` if no passkeys are setup. This step can be skipped. +``` +ZITADEL_API_URL=https://zitadel-tlx3du.us1.zitadel.cloud +ZITADEL_SERVICE_USER_TOKEN=1S6w48thfWFI2klgfwkCnhXJLf9FQ457E-_3H74ePQxfO3Af0Tm4V5Xi-ji7urIl_xbn-Rk +``` -If none of the previous conditions apply, we continue to sign in. +
-> NOTE: `listAuthenticationMethodTypes()` does not consider different domains for u2f methods or passkeys. The check whether a user should be redirected to one of the pages `/passkey` or `/u2f`, should be extended to use a domain filter (https://github.com/zitadel/zitadel/issues/8615) +Start the login application in dev mode: -### /password/change +```sh +pnpm dev +``` -This page allows to change the password. It is used after a user is in an initial state and is required to change the password, or it can be directly invoked with an active session. +Open the login application with your favorite browser at `localhost:3000`. +Change the source code and see the changes live in your browser. -/password/change +Make sure the application still behaves as expected by running all tests -Requests to the APIs made: +```sh +pnpm test +``` -- `getLoginSettings(org?)` -- `getPasswordComplexitySettings(user?)` -- `getBrandingSettings(org?)` -- `getSession()` -- `setPassword()` +To satisfy your unique workflow requirements, check out the package.json in the root directory for more detailed scripts. -> NOTE: The request to change the password is using the session of the user itself not the service user, therefore no code is required. +### Run Login UI Acceptance tests -### /password/set +To run the acceptance tests you need a running ZITADEL environment and a component which receives HTTP requests for the emails and sms's. +This component should also be able to return the content of these notifications, as the codes and links are used in the login flows. +There is a basic implementation in Golang available under [the sink package](./acceptance/sink). -This page allows to set a password. It is used after a user has requested to reset the password on the `/password` page. +To setup ZITADEL with the additional Sink container for handling the notifications: -/password/set +```sh +pnpm run-sink +``` -Requests to the APIs made: +Then you can start the acceptance tests with: -- `getLoginSettings(org?)` -- `getPasswordComplexitySettings(user?)` -- `getBrandingSettings(org?)` -- `getUserByID()` -- `setPassword()` +```sh +pnpm test:acceptance +``` -The page allows to enter a code or be invoked directly from a email link which prefills the code. The user can enter a new password and submit. +### Deploy to Vercel -### /otp/[method] +To deploy your own version on Vercel, navigate to your instance and create a service user. +Then create a personal access token (PAT), copy and set it as ZITADEL_SERVICE_USER_TOKEN, then navigate to your instance +settings and make sure it gets IAM_OWNER permissions. +Finally set your instance url as ZITADEL_API_URL. Make sure to set it without trailing slash. -This page shows a code field to check an otp method. The session of the user is then hydrated with the respective factor. Supported methods are `time-based`, `sms` and `email`. - -/otp/[method] - -Requests to the APIs made: - -- `getBrandingSettings(org?)` -- `getSession()` -- `updateSession()` - -If `email` or `sms` is requested as method, the current session of the user is updated to request the challenge. This will trigger an email or sms which can be entered in the code field. -The `time-based` (TOTP) method does not require a trigger, therefore no `updateSession()` is performed and no resendLink under the code field is shown. - -The submission of the code updates the session and continues to sign in the user. - -### /u2f - -This page requests a webAuthN challenge for the user and updates the session afterwards. - -/u2f - -Requests to the APIs made: - -- `getBrandingSettings(org?)` -- `getSession()` -- `updateSession()` - -When updating the session for the webAuthN challenge, we set `userVerificationRequirement` to `UserVerificationRequirement.DISCOURAGED` as this will request the webAuthN method as second factor and not as primary method. -After updating the session, the user is **always** signed in. :warning: required as this page is a follow up for setting up a u2f method. - -### /passkey - -This page requests a webAuthN challenge for the user and updates the session afterwards. -It is invoked directly after setting up a passkey `/passkey/set` or when loggin in a user after `/loginname`. - -/passkey - -Requests to the APIs made: - -- `getBrandingSettings(org?)` -- `getSession()` -- `updateSession()` - -When updating the session for the webAuthN challenge, we set `userVerificationRequirement` to `UserVerificationRequirement.REQUIRED` as this will request the webAuthN method as primary method to login. -After updating the session, the user is **always** signed in. :warning: required as this page is a follow up for setting up a passkey - -> NOTE: This page currently does not check whether a user contains passkeys. If this method is not available, this page should not be used. - -### /mfa/set - -This page loads login settings and the authentication methods for a user and shows setup options. - -/mfa/set - -Requests to the APIs made: - -- `getBrandingSettings(org?)` -- `getLoginSettings(user.org)` :warning: context taken from session -- `getSession()` -- `listAuthenticationMethodTypes()` -- `getUserByID()` - -If a user has already setup a certain method, a checkbox is shown alongside the button and the button is disabled. -OTP Email and OTP SMS only show up if the user has verified email or phone. -If the user chooses a method he is redirected to one of `/otp/time-based/set`, `/u2f/set`, `/otp/email/set`, or `/otp/sms/set`. -At the moment, U2F methods are hidden if a method is already added on the users resource. Reasoning is that the page should only be invoked for prompts. A self service page which shows up multiple u2f factors is implemented at a later stage. - -> NOTE: The session and therefore the user factor defines which login settings are checked for available options. - -> NOTE: `listAuthenticationMethodTypes()` does not consider different domains for u2f or passkeys. The check whether a user should be redirected to one of the pages `/passkey/set` or `/u2f/set`, should be extended to use a domain filter (https://github.com/zitadel/zitadel/issues/8615) - -### /passkey/set - -This page sets a passkey method for a user. This page can be either enforced, or optional depending on the Login Settings. - - -/passkey/set - -Requests to the APIs made: - -- `getBrandingSettings(org?)` -- `getSession()` -- `createPasskeyRegistrationLink()` TODO: check if this can be used with the session token (mfa required (AUTHZ-Kl3p0)) -- `registerPasskey()` -- `verifyPasskey()` - -If the loginname decides to redirect the user to this page, a button to skip appears which will sign the user in afterwards. -After a passkey is registered, we redirect the user to `/passkey` to verify it again and sign in with the new method. The `createPasskeyRegistrationLink()` uses the token of the session which is determined by the flow. - -> NOTE: this page allows passkeys to be created only if the current session is valid (self service), or no authentication method is set (register). TODO: to be implemented. - -> NOTE: Redirecting the user to `/passkey` will not be required in future and the currently used session will be hydrated directly after registering. (https://github.com/zitadel/zitadel/issues/8611) - -### /otp/time-based/set - -This page registers a time based OTP method for a user. - -/otp/time-based/set - -Requests to the APIs made: - -- `getBrandingSettings(org?)` -- `getSession()` -- `registerTOTP()` -- `verifyTOTP()` - -After the setup is done, the user is redirected to verify the TOTP method on `/otp/time-based`. - -> NOTE: Redirecting the user to `/otp/time-based` will not be required in future and the currently used session will be hydrated directly. (https://github.com/zitadel/zitadel/issues/8611) - -### /otp/email/set /otp/sms/set - -This page registers either an Email OTP method or SMS OTP method for a user. - -Requests to the APIs made: - -- `getBrandingSettings(org?)` -- `getSession()` -- `addOTPEmail()` / `addOTPSMS()` - -This page directly calls `addOTPEmail()` or `addOTPSMS()` when invoked and shows a success message. -Right afterwards, redirects to verify the method. - -### /u2f/set - -This page registers a U2F method for a user. - -/u2f/set - -Requests to the APIs made: - -- `getBrandingSettings(org?)` -- `getSession()` -- `registerU2F()` :warning: TODO: check if this can be used with the session token (mfa required (AUTHZ-Kl3p0)) -- `verifyU2FRegistration()` - -After a u2f method is registered, we redirect the user to `/passkey` to verify it again and sign in with the new method. The `createPasskeyRegistrationLink()` uses the token of the session which is determined by the flow. - -> NOTE: Redirecting the user to `/passkey` will not be required in future and the currently used session will be hydrated directly after registering. (https://github.com/zitadel/zitadel/issues/8611) - -### /register - -This page shows a register page, which gets firstname and lastname of a user as well as the email. It offers to setup a user, using password or passkeys. - -/register - -register with password - -Requests to the APIs made: - -- `listOrganizations()` :warning: TODO: determine the default organization if no context is set -- `getLegalAndSupportSettings(org)` -- `getPasswordComplexitySettings()` -- `getBrandingSettings()` -- `addHumanUser()` -- `createSession()` -- `getSession()` - -To register a user, the organization where the resource will be created is determined first. If no context is provided via url, we fall back to the default organization of the instance. - -**PASSWORD:** If a password is set, the user is created as a resource, then a session using the password check is created immediately. After creating the session, the user is directly logged in and eventually redirected back to the application. - -**PASSKEY:** If passkey is selected, the user is created as a resource first, then a session using the userId is created immediately. This session does not yet contain a check, we therefore redirect the user to setup a passkey at `/passkey/set`. As the passkey set page verifies the passkey right afterwards, the process ends with a signed in user. - -> NOTE: https://github.com/zitadel/zitadel/issues/8616 to determine the default organization of an instance must be implemented in order to correctly use the legal-, login-, branding- and complexitysettings. - -> NOTE: TODO: check which methods are allowed in the login settings, loginSettings.allowUsernamePassword / check for passkey - -### /idp - -This page doubles as /loginname but limits it to choose from IDPs - -/idp - -Requests to the APIs made: - -- `getBrandingSettings(org?)` -- `getActiveIdentityProviders(org?)` -- `startIdentityProviderFlow()` - -### /idp/[method]/success /idp/[method]/failure - -Both /success and /failure pages are designed to intercept the responses from the IDPs and decide on how to continue with the process. - -### /verify - -This page verifies the email to be valid. It page of the login can also be invoked without an active session. -The context of the user is taken from the url and is set in the email template. - -/accounts - -Requests to the APIs made: - -- `getBrandingSettings(org?)` -- `getLoginSettings(org?)` -- `verifyEmail()` - -If the page is invoked with an active session (right after a register with password), the user is signed in or redirected to the loginname if no context is known. - -> NOTE: This page will be extended to support invitations. In such case, authentication methods of the user are loaded and if none available, shown as possible next step (`/passkey/set`, `password/set`). - -### /accounts - -This page shows an overview of all current sessions. -Sessions with invalid token show a red dot on the right side, Valid session a green dot, and its last verified date. - -/accounts - -This page is a starting point for self management, reauthentication, or can be used to clear local sessions. -This page is also shown if used with OIDC and `prompt: select_account`. - -On all pages, where the current user is shown, you can jump to this page. This way, a session can quickly be reused if valid. - -jump to accounts - -### /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. From here device authorization flows are completed. It checks if the requestId param of starts with `device_` and then executes the `authorizeOrDenyDeviceAuthorization` command. - -/signedin - -In future, self service options to jump to are shown below, like: - -- change password -- setup passkeys -- setup mfa -- change profile -- logout - -> NOTE: This page has to be explicitly enabled or act as a fallback if no default redirect is set. - -## Currently NOT Supported - -- forceMFA on login settings is not checked for IDPs - -Also note that IDP logins are considered as valid MFA. An additional MFA check will be implemented in future if enforced. +[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fzitadel%2Ftypescript&env=ZITADEL_API_URL,ZITADEL_SERVICE_USER_TOKEN&root-directory=apps/login&envDescription=Setup%20a%20service%20account%20with%20IAM_LOGIN_CLIENT%20membership%20on%20your%20instance%20and%20provide%20its%20personal%20access%20token.&project-name=zitadel-login&repository-name=zitadel-login)