Files
zitadel/apps/docs/content/guides/integrate/login-ui/passkey.mdx
T
d8ea041c56 docs: introduce versioned docs and migrate to fuma (#11166)
## Todos for release

- [x] Configure Env in docs project on vercel
- [x] Configure Root Path in the docs project on vercel
- [ ] Remove old CSP https://github.com/zitadel/website/pull/1592

## What we did

This pull request migrates the project documentation from the old
`docs/` directory to the new `apps/docs/` directory, introduces a new
documentation system built with Next.js and Fumadocs, and updates all
relevant references, configuration files, and documentation to reflect
this change. It also adds new configuration and ignore files for the new
documentation app, updates CI and linting to exclude the new docs from
certain checks, and revises the contributing guidelines accordingly.

**Documentation System Migration and New Docs App**

* Migrated all documentation from `docs/` to `apps/docs/`, and updated
all references in `README.md`, `CONTRIBUTING.md`, and other files to
point to the new location.
[[1]](diffhunk://#diff-eca12c0a30e25b4b46522ebf89465a03ba72a03f540796c979137931d8f92055L585-L640)
[[2]](diffhunk://#diff-eca12c0a30e25b4b46522ebf89465a03ba72a03f540796c979137931d8f92055L660-R607)
[[3]](diffhunk://#diff-eca12c0a30e25b4b46522ebf89465a03ba72a03f540796c979137931d8f92055L740-R687)
[[4]](diffhunk://#diff-b335630551682c19a781afebcf4d07bf978fb1f8ac04c6bf87428ed5106870f5L2-R3)
[[5]](diffhunk://#diff-b335630551682c19a781afebcf4d07bf978fb1f8ac04c6bf87428ed5106870f5L30-R30)
* Added a new Next.js/Fumadocs-based documentation app under
`apps/docs/`, including core app files, layouts, routing, search API,
and a comprehensive `README.md` with development and contribution
instructions.
[[1]](diffhunk://#diff-5a1b07344a2c1b4d3f37b23ff1388b62cd9f57dea3c1cd23d8a0412b7602b132R1-R74)
[[2]](diffhunk://#diff-462b9ad1eabbb7d1c29bb9c36e4931eb180fd7190900e6d2babf8f4d66ad1c28R1-R39)
[[3]](diffhunk://#diff-e16ae25660ded787b10ac35dea96d5ecaacf895dae0afc8a9bd4382dc79a8c87R1-R7)
[apps/docs/app/[[...slug]]/layout.tsxR1-R81](diffhunk://#diff-59e08acde4e805b7aeccef1dcf98f1d71dfc550777e6b9402085cee0e9fa4e0aR1-R81),
[apps/docs/app/[[...slug]]/page.tsxR1-R79](diffhunk://#diff-e5df3f80d0fa01e12d63d81f29c57a9d14e846c78fd3beabb2ef768e38fd9580R1-R79),
[[4]](diffhunk://#diff-389b34918e040cacaa87cd7201ffa462cc2b0b716736f537e3d3c660ac69353bR1-R7)
[[5]](diffhunk://#diff-d3b03416d1c457b19f1c27b26f2db741412df841b875c26ccadc36e4522247f4R1-R29)
[[6]](diffhunk://#diff-c8fb8339570a5305809be7c618e14705fd86390dc278e6ce8ba224a7bc8b0c3cR1-R25)

**Configuration and Tooling Updates**

* Updated `.github/workflows/codeql.yml`, `.golangci.yaml`, and
`.github/dependabot.yml` to properly handle the new docs app: excluded
`apps/docs` from certain checks, added npm dependency updates for the
docs app, and excluded generated content.
[[1]](diffhunk://#diff-12783128521e452af0cfac94b99b8d250413c516ec71fe6d97dbea666ff7ba27L8-R14)
[[2]](diffhunk://#diff-9917ddc9f1c3304218f7269265b746d997c5c0615478177b5fceecd33ef47cb5R5-R6)
[[3]](diffhunk://#diff-9917ddc9f1c3304218f7269265b746d997c5c0615478177b5fceecd33ef47cb5R126-R129)
[[4]](diffhunk://#diff-dd4fbda47e51f1e35defb9275a9cd9c212ecde0b870cba89ddaaae65c5f3cd28R89-R106)
* Updated `.devcontainer/devcontainer.json` to use the latest Go 1.25.3
version for consistency.

**Licensing and Miscellaneous**

* Added `apps/docs/` to the list of licensed directories in
`LICENSING.md`.

These changes ensure the documentation is now maintained in a modern,
scalable system and all project tooling is updated to support the new
structure.

---------

Co-authored-by: Federico Coppede <[email protected]>
2026-01-29 08:38:36 -08:00

284 lines
8.6 KiB
Plaintext

---
title: Using Passkeys in a Custom Login UI
sidebar_label: Passkeys
---
import BrowserRegisterWebAuthN from './_browser_register_webauthn.mdx';
import BrowserSigninWebAuthN from './_browser_signin_webauthn.mdx';
import UpdateSessionWebAuthN from './_update_session_webauthn.mdx';
Passkeys are a replacement for passwords that provide faster, easier, and more secure sign-ins to websites and apps even across multiple devices.
Unlike passwords, passkeys are phishing-resistant and can improve the user experience and security at the same time.
Passkeys and there underlying protocols are a standard defined by the [FIDO Standard](https://fidoalliance.org/) .
## Register Passkey
### Flow
![](../../../../public/img/guides/login-ui/passkey-registration-flow.png)
There are two options to onboard users with passkeys:
1. You send the user a link (email, sms, ...) with an embedded code, so the user is able to register passkey on any capable device
2. You start the passkey registration directly on the current device used by a user
### Send Registration Link
When you want to send a link to your user, that enables them to register a new passkey device, you can choose if ZITADEL sends the code or ZITADEL can return the code in the API response. This way you can send a link through any channel of your choice (email, sms, phone, in-person, postal, ...).
If you asked ZITADEL to send the link to the user please make sure to populate the link with the needed values that point towards your registration UI.
More detailed information about the API: [Send Registration Link Documentation](/reference/api/user/zitadel.user.v2.UserService.CreatePasskeyRegistrationLink)
Request Example:
Send either the sendLink or the returnCode (empty message) in the request body, depending on the use case you have.
```bash
curl --request POST \
--url https://${CUSTOM_DOMAIN}/v2/users/$USER_ID/passkeys/registration_link \
--header 'Accept: application/json' \
--header 'Authorization: Bearer '"$TOKEN"''\
--header 'Content-Type: application/json' \
--data '{
"sendLink": {
"urlTemplate": "https://example.com/passkey/register?userID={{.UserID}}&orgID={{.OrgID}}&codeID={{.CodeID}}&code={{.Code}}"
},
"returnCode": {}
}'
```
Response Example:
The code is only filled if returnCode has been requested in the Request.Passkey
```bash
{
"details": {
"sequence": "632",
"changeDate": "2023-06-28T08:09:51.257699Z",
"resourceOwner": "163840776835432705"
},
"code": {
"id": "220526087715684609",
"code": "2KEpIeQGSBBd"
}
}
```
### Start Passkey Registration
When starting the passkey registration you can optionally send the registration code from the step above to ZITADEL to pair it with a specific user.
By specifying the authenticator type you can choose if the passkey should be cross platform or not. Per default all types are allowed:
- PASSKEY_AUTHENTICATOR_UNSPECIFIED
- PASSKEY_AUTHENTICATOR_PLATFORM
- PASSKEY_AUTHENTICATOR_CROSS_PLATFORM
The API response will provide you the public key credential options, this will be used by the browser to obtain a signed challenge.
More detailed information about the API: [Start Passkey Registration Documentation](/reference/api/user/zitadel.user.v2.UserService.RegisterPasskey)
Request Example:
The code only has to be filled if the user did get a registration code.
```bash
curl --request POST \
--url https://${CUSTOM_DOMAIN}/v2/users/$USER_ID/passkeys \
--header 'Accept: application/json' \
--header 'Authorization: Bearer '"$TOKEN"''\
--header 'Content-Type: application/json' \
--data '{
"code": {
"id": "220526087715684609",
"code": "2KEpIeQGSBBd"
},
"authenticator": "PASSKEY_AUTHENTICATOR_UNSPECIFIED"
}'
```
Response Example:
```bash
{
"details": {
"sequence": "633",
"changeDate": "2023-06-28T08:10:26.725981Z",
"resourceOwner": "163840776835432705"
},
"passkeyId": "220526147258024193",
"publicKeyCredentialCreationOptions": {
"publicKey": {
"attestation": "none",
"authenticatorSelection": {
"userVerification": "required"
},
"challenge": "JM1uLbVQR2xZJ210DA7E-3j0Cd9rHKUSmc8NyIJBtAY",
"pubKeyCredParams": [
{
"alg": -7,
"type": "public-key"
},
{
"alg": -35,
"type": "public-key"
},
{
"alg": -36,
"type": "public-key"
},
{
"alg": -257,
"type": "public-key"
},
{
"alg": -258,
"type": "public-key"
},
{
"alg": -259,
"type": "public-key"
},
{
"alg": -37,
"type": "public-key"
},
{
"alg": -38,
"type": "public-key"
},
{
"alg": -39,
"type": "public-key"
},
{
"alg": -8,
"type": "public-key"
}
],
"rp": {
"id": "example.domain.com",
"name": "ZITADEL"
},
"timeout": 300000,
"user": {
"displayName": "Minnie Mouse",
"id": "MjE4NjYyNTk2OTE4NjQwODk3",
"name": "[email protected]"
}
}
}
}
```
### Register new Passkey on current device
<BrowserRegisterWebAuthN components={props.components} />
### Verify Passkey in ZITADEL
In the next request you have to verify the Passkey within ZITADEL.
Include the public key credential you got from the browser in your request.
You can give the Passkey a name, which makes it easier for the user to identify the registered authentication methods.
Example: Google Pixel, iCloud Keychain, Yubikey, etc
More detailed information about the API: [Verify Passkey Registration Documentation](/reference/api/user/zitadel.user.v2.UserService.VerifyPasskeyRegistration)
Example Request:
```bash
curl --request POST \
--url https://${CUSTOM_DOMAIN}/v2/users/$USER_ID/passkeys/$PASSKEY_ID \
--header 'Accept: application/json' \
--header 'Authorization: Bearer '"$TOKEN"''\
--header 'Content-Type: application/json' \
--data '{
"publicKeyCredential": {
"type": "public-key",
"id": "pawVarF4xPxLFmfCnRkwXWeTrKGzabcAi92LEI1WC00",
"rawId": "pawVarF4xPxLFmfCnRkwXWeTrKGzabcAi92LEI1WC00",
"response": {
"attestationObject": "o2NmbXRmcGFja2VkZ2F0dFN0bXSiY2FsZyZjc2lnWEcwRQIgRKS3VpeE9tfExXRzkoUKnG4rQWPvtSSt4YtDGgTx32oCIQDPey-2YJ4uIg-QCM4jj6aE2U3tgMFM_RP7Efx6xRu3JGhhdXRoRGF0YVikSZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2NFAAAAADju76085Yhmlt1CEOHkwLQAIKWsFWqxeMT8SxZnwp0ZMF1nk6yhs2m3AIvdixCNVgtNpQECAyYgASFYIMGUDSP2FAQn2MIfPMy7cyB_Y30VqixVgGULTBtFjfRiIlggjUGfQo3_-CrMmH3S-ZQkFKWKnNBQEAMkFtG-9A4zqW0",
"clientDataJSON": "eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoiQlhXdHh0WGxJeFZZa0pHT1dVaUVmM25zby02aXZKdWw2YmNmWHdMVlFIayIsIm9yaWdpbiI6Imh0dHBzOi8vbG9jYWxob3N0OjgwODAifQ"
}
},
"passkeyName": "Google Pixel"
}'
```
You have successfully registered a new passkey to the user.
Next step is to authenticate the user with the new registered passkey.
## Login with Passkey
### Flow
![](../../../../public/img/guides/login-ui/passkey-login-flow.png)
### Create Session
First step is to ask the user for his username and create a new session with the ZITADEL API.
When creating the new session make sure to include the challenge for passkey, resp. webAuthN with a required user verification and the domain of your login UI.
The response will include the public key credential request options for the passkey in the challenges.
More detailed information about the API: [Create Session Documentation](/reference/api/session/zitadel.session.v2.SessionService.CreateSession)
Example Request:
```bash
curl --request POST \
--url https://${CUSTOM_DOMAIN}/v2/sessions \
--header 'Accept: application/json' \
--header 'Authorization: Bearer '"$TOKEN"''\
--header 'Content-Type: application/json' \
--data '{
"checks": {
"user": {
"loginName": "[email protected]"
}
},
"metadata": {},
"challenges": {
"webAuthN": {
"domain": "example.domain.com",
"userVerificationRequirement": "USER_VERIFICATION_REQUIREMENT_REQUIRED"
}
}
}'
```
Example Response:
```bash
{
"details": {
"sequence": "2",
"changeDate": "2023-06-27",
"resourceOwner": "69629023906488334"
},
"sessionId": "d654e6ba-70a3-48ef-a95d-37c8d8a7901a",
"sessionToken": "string",
"challenges": {
"webAuthN": {
"publicKeyCredentialRequestOptions": {
"publicKey": {
"allowCredentials": [
{
"id": "ATmqBg-99qyOZk2zloPdJQyS2R7IkFT7v9Hoos_B_nM",
"type": "public-key"
}
],
"challenge": "GAOHYz2jE69kJMYo6Laij8yWw9-dKKgbViNhfuy0StA",
"rpId": "example.domain.com",
"timeout": 300000,
"userVerification": "required"
}
}
}
}
}
```
### Signing in Browser
<BrowserSigninWebAuthN components={props.components} />
### Update Session with WebAuthN
<UpdateSessionWebAuthN components={props.components} />