Skip to content

Commit 713e40a

Browse files
feat: documentation for external_id (#2254)
1 parent cdaf9d2 commit 713e40a

File tree

2 files changed

+228
-0
lines changed

2 files changed

+228
-0
lines changed
Lines changed: 226 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,226 @@
1+
---
2+
id: external-id
3+
title: External Identifiers
4+
---
5+
6+
# Using external identifiers (`external_id`)
7+
8+
This guide explains how to configure and use the `external_id` field in Ory Kratos to support external primary identifiers such as
9+
`customer_id`, `employee_id`, or similar. This is especially useful for migrations from systems where you need to preserve
10+
identifiers or support user-defined primary identifiers.
11+
12+
:::note
13+
14+
The `external_id` must be unique across all identities. If you attempt to import multiple identities with the same `external_id`,
15+
the operation will fail with a `409 Conflict`.
16+
17+
:::
18+
19+
## Overview
20+
21+
Traditionally, Ory Kratos identifies users using an internal `identity.id` UUID. With the `external_id` feature, you can:
22+
23+
- Assign a unique, domain-specific identifier to each identity.
24+
- Query and manage identities using `external_id`.
25+
- Use `external_id` as the `sub` (subject) claim in JWTs.
26+
- Preserve identity semantics across systems during migration.
27+
28+
This helps simplify migrations, reduce mapping layers, and align Kratos with your existing infrastructure.
29+
30+
## Configuration
31+
32+
### 1. Use `external_id` via api, not schema
33+
34+
The `external_id` is **not** part of the identity JSON Schema. Instead, it is a dedicated top-level attribute in API requests that
35+
create or update identities.
36+
37+
:::warning
38+
39+
Do not add `external_id` to your identity schema definition. It is handled separately by Kratos internally.
40+
41+
:::
42+
43+
### 2. Use `external_id` in jwt `sub` claim
44+
45+
Set the `subject_source` to `external_id` in the tokenization config:
46+
47+
```yaml
48+
session:
49+
whoami:
50+
tokenizer:
51+
templates:
52+
jwt_template_1:
53+
jwks_url: base64://... # A JSON Web Key Set (required)
54+
claims_mapper_url: base64://... # A JsonNet template for modifying the claims
55+
ttl: 1m # 1 minute (defaults to 10 minutes)
56+
subject_source: external_id
57+
another_jwt_template:
58+
jwks_url: base64://... # A JSON Web Key Set
59+
```
60+
61+
This will populate the `sub` claim in JWTs with the value of `external_id`.
62+
63+
:::warning
64+
65+
If `external_id` is not set for a user when `subject_source` is `external_id`, tokenization will fail.
66+
67+
:::
68+
69+
## API usage
70+
71+
### Create an identity with `external_id`
72+
73+
```http
74+
POST /admin/identities
75+
Content-Type: application/json
76+
77+
{
78+
"schema_id": "default",
79+
"traits": {
80+
"email": "[email protected]"
81+
},
82+
"external_id": "customer-12345"
83+
}
84+
```
85+
86+
### Get identity by `external_id`
87+
88+
```http
89+
GET /admin/identities/by/external/customer-12345
90+
```
91+
92+
#### Optional query parameter
93+
94+
- `include_credential=password,oidc,...` — Include specific credentials in the response.
95+
96+
#### Example:
97+
98+
```http
99+
GET /admin/identities/by/external/customer-12345?include_credential=password
100+
```
101+
102+
#### Response:
103+
104+
```json
105+
{
106+
"id": "uuid-abc123",
107+
"external_id": "customer-12345",
108+
"traits": {
109+
"email": "[email protected]"
110+
},
111+
"credentials": {
112+
"password": { ... }
113+
}
114+
}
115+
```
116+
117+
#### Error responses:
118+
119+
- `404` – Identity not found.
120+
- `409` – Duplicate `external_id` on creation.
121+
- `400` – Invalid request structure.
122+
123+
:::note
124+
125+
There are no other APIs that support `external_id`, for the APIs that require a Kratos `identity_id` you need to use the
126+
`Get identity by external id` API above and use the identity id from there.
127+
128+
:::
129+
130+
## JWT configuration
131+
132+
### Jsonnet example
133+
134+
When [tokenizing sessions](../../identities/session-to-jwt-cors), `external_id` is available in the session context:
135+
136+
```jsonnet
137+
local claims = std.extVar('claims');
138+
local session = std.extVar('session');
139+
140+
{
141+
claims: {
142+
iss: claims.iss + "/additional-component",
143+
schema_id: session.identity.schema_id,
144+
external_id: session.identity.external_id,
145+
session: session,
146+
}
147+
}
148+
```
149+
150+
### Token behavior with `external_id`
151+
152+
If `subject_source` is set to `external_id` in the tokenizer template, the JWT's `sub` claim becomes:
153+
154+
```json
155+
{
156+
"sub": "customer-12345"
157+
}
158+
```
159+
160+
:::warning
161+
162+
If `external_id` is missing, tokenization will fail.
163+
164+
:::
165+
166+
## Migration guide
167+
168+
To migrate from an existing system, you can bulk import identities into Kratos and set their `external_id` using the
169+
[Batch API](https://www.ory.sh/docs/reference/api#tag/identity/operation/batchPatchIdentities).
170+
171+
### Use `PATCH /admin/identities`
172+
173+
#### Example: Basic import
174+
175+
```json
176+
[
177+
{
178+
"schema_id": "default",
179+
"external_id": "customer-001",
180+
"traits": {
181+
"email": "[email protected]"
182+
}
183+
}
184+
]
185+
```
186+
187+
#### Example: With pre-hashed password
188+
189+
```json
190+
[
191+
{
192+
"schema_id": "default",
193+
"external_id": "customer-002",
194+
"traits": {
195+
"email": "[email protected]"
196+
},
197+
"credentials": {
198+
"password": {
199+
"config": {
200+
"hashed_password": "$2b$12$abc123..." // bcrypt hash
201+
}
202+
}
203+
}
204+
}
205+
]
206+
```
207+
208+
### Migration tips
209+
210+
- Pre-hash passwords to avoid timeouts
211+
- Validate `external_id` uniqueness before import
212+
- Import in smaller batches (≤ 200 identities if using plaintext passwords)
213+
214+
## Troubleshooting
215+
216+
| Error / Code | Context | Description |
217+
| --------------------------- | ---------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
218+
| `409 Conflict` | **Migration** (batch import) | One or more identities have a duplicate `external_id`. Ensure all values are unique. |
219+
| `400 Bad Request` | **Migration** (batch import) | The request payload is invalid or improperly formatted. Check JSON structure and required fields. |
220+
| `504 Gateway Timeout` | **Migration** (batch import) | The batch is too large or includes plaintext passwords. Reduce batch size or [pre-hash passwords](https://www.ory.sh/docs/kratos/manage-identities/import-user-accounts-identities#pre-hashing-passwords). |
221+
| `500 Internal Server Error` | **Session token generation** | If `subject_source = external_id` is configured, the session will not tokenize unless the identity has an `external_id` set. |
222+
223+
For advanced examples, see:
224+
225+
- [Tokenization with Jsonnet](../../identities/session-to-jwt-cors)
226+
- [Importing Users in Bulk](../../kratos/manage-identities/import-user-accounts-identities#bulk-import-identities-from-other-providers)

src/sidebar.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -433,6 +433,8 @@ const kratos: SidebarItemsConfig = [
433433
"kratos/manage-identities/scim/okta",
434434
],
435435
},
436+
437+
"kratos/manage-identities/external-id",
436438
],
437439
},
438440
{

0 commit comments

Comments
 (0)