Customize identity schemas
This document explains how to customize your identity model with a custom identity schema. A custom identity schema allows you to add custom fields to your identities such as birthday or job title.
Use the JSON Schema Validator to check your identity schemas. If you're not familiar with JSON Schemas, study the official examples to get familiar with the structure.
Writing your first custom identity schema
This is the minimum viable identity schema:
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"traits": {
"type": "object"
}
}
}
Traits translate into fields presented in the user-facing UI. In the minimum viable identity schema, there are no traits which means that the UI doesn't present any fields. Let's change that by adding three fields:
- first name
- last name
- username
- Identity schema
- Generated UI
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"traits": {
"type": "object",
"properties": {
"username": {
"title": "Username",
"type": "string"
},
"name": {
"type": "object",
"properties": {
"first": {
"title": "First name",
"type": "string"
},
"last": {
"title": "Last name",
"type": "string"
}
}
}
}
}
}
}
To log in, the user needs to provide their ID and password. The identity schema does not yet state which field is the ID used for login. Let's change that by adding the following schema extension:
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"traits": {
"type": "object",
"properties": {
"username": {
"title": "Username",
"type": "string",
"ory.sh/kratos": {
"credentials": {
"password": {
"identifier": true
}
}
}
},
"name": {
"type": "object",
"properties": {
"first": {
"title": "First name",
"type": "string"
},
"last": {
"title": "Last name",
"type": "string"
}
}
}
}
}
}
}
Ory supports all JSON Schema types (string
, number
, integer
, boolean
, object
). Use string
for text fields, boolean
for checkbox fields, and integer
or number
for integral or floating-point numbers. If you want to know more about these types,
read the the JSON Schema documentation.
Avoid complex identity schemas and use the identity schema for basic profile information. The more complex the schema, the more difficult it is to introduce changes in the future. Complex schemas make it difficult to generate user interfaces automatically.
Custom user interface labels
The title
property defines the description of the field rendered in the UI. This schema has a field which asks users to provide
their GitHub handle, which is clear thanks to the field description defined in the title
property.
- Identity schema
- Generated UI
{
"$id": "https://schemas.ory.sh/presets/kratos/identity.basic.schema.json",
"title": "Person",
"type": "object",
"properties": {
"traits": {
"type": "object",
"properties": {
"email": {
"type": "string",
"format": "email",
"title": "Email",
"ory.sh/kratos": {
"credentials": {
"password": {
"identifier": true
}
},
"recovery": {
"via": "email"
},
"verification": {
"via": "email"
}
}
},
"name": {
"type": "string",
"title": "What's your GitHub handle?"
}
}
}
},
"required": [
"email"
],
"additionalProperties": false
}
Identity schema extensions
Because the system doesn't know which fields have system-relevant meaning, you have to specify that in the schema. For example:
- This email address should be used for recovering a lost password.
- This identifier (username or email) should be used for logging in with a password.
- This is the phone number used for SMS 2FA.
The vocabulary extension can be used within a property. Let's take a look at the email identity schema preset. It uses all available identity schema extensions:
{
"$id": "https://schemas.ory.sh/presets/kratos/identity.email.schema.json",
"title": "Person",
"type": "object",
"properties": {
"traits": {
"type": "object",
"properties": {
"email": {
"type": "string",
"format": "email",
"title": "E-Mail",
"ory.sh/kratos": {
"credentials": {
"password": {
"identifier": true
},
"webauthn": {
"identifier": true
},
"totp": {
"account_name": true
}
},
"recovery": {
"via": "email"
},
"verification": {
"via": "email"
}
},
"maxLength": 320
}
},
"required": ["email"],
"additionalProperties": false
}
}
}
Multiple identifiers
It is possible to have multiple identifiers. For example, you could have a username and an email address that the user can use to sign in:
{
"$id": "https://schemas.ory.sh/presets/kratos/identity.email.schema.json",
"title": "Person",
"type": "object",
"properties": {
"traits": {
"type": "object",
"properties": {
"username": {
"type": "string",
"minLength": 6,
"title": "Username",
"ory.sh/kratos": {
"credentials": {
"password": {
"identifier": true
}
}
}
},
"email": {
"type": "string",
"format": "email",
"title": "E-Mail",
"ory.sh/kratos": {
"credentials": {
"password": {
"identifier": true
},
"webauthn": {
"identifier": true
},
"totp": {
"account_name": true
}
},
"recovery": {
"via": "email"
},
"verification": {
"via": "email"
}
},
"maxLength": 320
}
},
"required": ["email"],
"additionalProperties": false
}
}
}
You can also specify an array of elements:
{
"$id": "https://schemas.ory.sh/presets/kratos/identity.email.schema.json",
"title": "Person",
"type": "object",
"properties": {
"traits": {
"type": "object",
"properties": {
"emails": {
"type": "array",
"items": [
{
"type": "string",
"format": "email",
"title": "E-Mail",
"ory.sh/kratos": {
"credentials": {
"password": {
"identifier": true
},
"webauthn": {
"identifier": true
},
"totp": {
"account_name": true
}
},
"recovery": {
"via": "email"
},
"verification": {
"via": "email"
}
},
"maxLength": 320
}
]
}
},
"required": ["email"],
"additionalProperties": false
}
}
}
Password login identifier
You can configure Ory Identities to use specific fields as the identity's identifier. In this example, the password
is set as
the identifier:
{
"ory.sh/kratos": {
"credentials": {
"password": {
"identifier": true
}
}
}
}
Email
Let's assume the identity's traits are
traits:
# These are just examples
email: office@ory.sh
name:
first: Aeneas
last: Rekkas
favorite_animal: Dog
accepted_tos: true
and we using a JSON Schema that uses the email
field as the identifier for the password flow:
{
$id: "http://mydomain.com/schemas/v2/customer.schema.json",
$schema: "http://json-schema.org/draft-07/schema#",
title: "A customer (v2)",
type: "object",
properties: {
traits: {
type: "object",
properties: {
email: {
title: "E-Mail",
type: "string",
format: "email",
// This tells Ory Identities that the field should be used as the "username" for the username and password flow.
"ory.sh/kratos": {
credentials: {
password: {
identifier: true,
},
},
},
},
name: {
type: "object",
properties: {
first: {
type: "string",
},
last: {
type: "string",
},
},
},
favorite_animal: {
type: "string",
},
accepted_tos: {
type: "string",
},
},
required: ["email"],
additionalProperties: false,
},
},
}
In this example, Ory understands that traits.email='office@ory.sh'
is the identifier for this identity. The system must get
office@ory.sh
and a password to sign in an user.
Username and Password Credentials contains more information and examples about credentials usage.
Note that the format
field of the identity schema will perform validation of the given trait. In this example, the email address
is validated using the JSON Schema rule
set.
Phone number
Let's extend the identity schema from the previous chapter with a phone number:
{
$id: "http://mydomain.com/schemas/v2/customer.schema.json",
$schema: "http://json-schema.org/draft-07/schema#",
title: "A customer (v2)",
type: "object",
properties: {
traits: {
type: "object",
properties: {
email: {
title: "E-Mail",
type: "string",
format: "email",
// This tells Ory Identities that the field should be used as the "username" for the Username and Password Flow.
"ory.sh/kratos": {
credentials: {
password: {
identifier: true,
},
},
},
},
phone: {
title: "Phone",
type: "string",
format: "tel",
// The phone number is marked as an identifier. This allows the user to log in with both email and phone number.
"ory.sh/kratos": {
credentials: {
password: {
identifier: true,
},
},
},
},
name: {
type: "object",
properties: {
first: {
type: "string",
},
last: {
type: "string",
},
},
},
favorite_animal: {
type: "string",
},
accepted_tos: {
type: "string",
},
},
required: ["email"],
additionalProperties: false,
},
},
}
By using the "format": "tel"
field we enable validation of phone numbers using the Golang
port of Google's libphonenumber.
Using the following identity schema extension we specify that the field is used as the "ID" when signing in using a password:
{
"ory.sh/kratos": {
"credentials": {
"password": {
"identifier": true
}
}
}
}
Passwordless WebAuthn login identifier
Using the following identity schema extension we specify that the field is used as the "ID" when using passwordless WebAuthn login:
{
"ory.sh/kratos": {
"credentials": {
"webauthn": {
"identifier": true
}
}
}
}
TOTP display username
The following identity schema extension specifies that this field should be displayed in the Authenticator App as the account name.
{
"ory.sh/kratos": {
"credentials": {
"totp": {
"account_name": true
}
}
}
}
Verification address
Specifies that the field is verifiable by sending an email with a verification code or link.
{
"ory.sh/kratos": {
"verification": {
"via": "email"
}
}
}
Recovery address
Specifies that the field can be used to send an account recovery code or link.
{
"ory.sh/kratos": {
"recovery": {
"via": "email"
}
}
}
Mandatory fields for registration
Use required
to define which identity schema fields users must fill in during registration. When users try to register without
filling in the mandatory fields, Ory Account Experience shows a message that tells users they must provide the mandatory
information to proceed. Additionally, required fields are marked with an asterisk for easy visual identification.
The sample identity schema has two mandatory fields: email address and last name.
For nested objects such as name
, add the required
property inside of the object.
- Identity schema
- Generated UI
{
"$id": "https://schemas.ory.sh/presets/kratos/identity.basic.schema.json",
"title": "Person",
"type": "object",
"properties": {
"traits": {
"type": "object",
"properties": {
"email": {
"type": "string",
"format": "email",
"title": "Email address",
"ory.sh/kratos": {
"credentials": {
"password": {
"identifier": true
},
"webauthn": {
"identifier": true
},
"totp": {
"account_name": true
}
},
"recovery": {
"via": "email"
},
"verification": {
"via": "email"
}
},
"maxLength": 320
},
"name": {
"type": "object",
"required": [
"last"
],
"properties": {
"first": {
"type": "string",
"title": "First name",
"maxLength": 256
},
"last": {
"type": "string",
"title": "Last name",
"maxLength": 256
}
}
}
},
"required": [
"email"
],
"additionalProperties": false
}
}
}
Choosing between username, email, and phone number
Before you start, you need to decide what data you want to collect from your users and why. It's hard to change this decision afterwards, so make sure you've taken everything into account.
When logging in, the user will use a login identifier and a password to sign up and in. The identifier can be:
- a username - "john.doe" or "johndoe123" or "oryuser"
- an email address -
john.doe@gmail.com
- a phone number -
+49-1234-4321-1234-4321
All of these approaches have up- and downsides.
Using the email address as the login identifier is easy to remember, doesn't require additional fields (because the email address is already being collected), and is usually unique. It's usually unique because sometimes companies use a "shared" email account (for example office@acme.org) to access services. In that case, multiple real identities are using the same email identifier to log in.
The email address however represents a unique identifier and personally identifiable information (PII). An attacker could for
example check if the email address john.doe@gmail.com
is registered at for example an adult website and use that information for
blackmail (see Account Enumeration Attacks).
The same considerations apply to using a phone number as the primary registration & login identifier.
Using a free text username reduces the privacy risk because it's much harder to make a connection between the username and a real world identity. It's still possible in cases where users choose a username such as "john.doe.from.newyork.1970", but finding the right username identifier is still difficult and there is plausible deniability because anyone could use that username.
A free text username however requires capturing additional fields (for example an email address for password resets / account
recovery) and is hard to remember. It's often very difficult to find unique usernames as people tend to use a combination of their
names and initials such as john.doe
which has a high chance of collision. Therefore, one ends up with usernames such as
john.doe1234432
.
It's important to understand that Ory Identities lowercases all password
identifiers and therefore email addresses. Characters
+
or .
which have special meaning for some email providers such as Gmail aren't normalized:
userNAME
is equal tousername
foo@BaR.com
is equal tofoo@bar.com
foo+baz@bar.com
is NOT equal tofoo@bar.com
foo.baz@bar.com
is NOT equal tofoobaz@bar.com
You need to decide which route you want to take.
Examples
Let's take a look at some examples!
Email and password
To use the email address as the login identifier, define the following identity schema:
{
"$id": "https://example.com/registration.schema.json",
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Person",
"type": "object",
"properties": {
"traits": {
"type": "object",
"properties": {
"email": {
"type": "string",
"format": "email",
"ory.sh/kratos": {
"credentials": {
"password": {
"identifier": true
}
}
}
}
}
}
}
}
Multiple emails and password
You can allow users to sign up with multiple email addresses and use any of them to log in:
{
"$id": "https://example.com/registration.schema.json",
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Person",
"type": "object",
"properties": {
"traits": {
"type": "object",
"properties": {
"emails": {
"type": "array",
"items": {
"type": "string",
"format": "email",
"ory.sh/kratos": {
"credentials": {
"password": {
"identifier": true
}
}
}
}
}
}
}
}
}
Username and password
To use a username as the login identifier, define the following identity schema:
{
"$id": "https://example.com/registration.schema.json",
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Person",
"type": "object",
"properties": {
"traits": {
"type": "object",
"properties": {
"username": {
"type": "string",
"ory.sh/kratos": {
"credentials": {
"password": {
"identifier": true
}
}
}
}
}
}
}
}
Username and email and password
You may also mix usernames and passwords:
{
"$id": "https://example.com/registration.schema.json",
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Person",
"type": "object",
"properties": {
"traits": {
"type": "object",
"properties": {
"email": {
"type": "string",
"format": "email",
"ory.sh/kratos": {
"credentials": {
"password": {
"identifier": true
}
}
}
},
"username": {
"type": "string",
"ory.sh/kratos": {
"credentials": {
"password": {
"identifier": true
}
}
}
}
}
}
}
}
Phone number and password
{
"$id": "https://example.com/registration.schema.json",
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Person",
"type": "object",
"properties": {
"traits": {
"type": "object",
"properties": {
"phone": {
"type": "string",
"format": "tel",
"ory.sh/kratos": {
"credentials": {
"password": {
"identifier": true
}
}
}
}
}
}
}
}
Advanced schema
The following identity schema includes first name, last name, a nickname, and numerical fields for the users' age. There are also two checkboxes for specifying the newsletter subscription and enterprise customer status.
This identity schema is just an example. It contains too many fields to be used in production.
- Identity schema
- Generated UI
{
"$id": "https://schemas.ory.sh/presets/kratos/identity.basic.schema.json",
"title": "Person",
"type": "object",
"properties": {
"traits": {
"type": "object",
"properties": {
"email": {
"type": "string",
"format": "email",
"title": "Email",
"ory.sh/kratos": {
"credentials": {
"password": {
"identifier": true
}
},
"recovery": {
"via": "email"
},
"verification": {
"via": "email"
}
}
},
"name": {
"type": "object",
"properties": {
"first": {
"type": "string",
"title": "Your first name"
},
"last": {
"type": "string",
"title": "Your last name"
},
"nickname": {
"type": "string",
"title": "Your nickname"
}
}
},
"age": {
"type": "integer",
"title": "How old are you?"
},
"newsletter": {
"type": "boolean",
"title": "Newsletter subscription"
},
"enterprise": {
"type": "boolean",
"title": "Are you an Enterprise customer?"
}
},
"required": [
"email"
],
"additionalProperties": false
}
}
}
Additional properties
Keep your identities clean by denying
additionalProperties
:
{
properties: {
traits: {
type: "object",
properties: {
// ...
},
additionalProperties: false,
},
},
}