Olympus Docs
Identity

Identifiers and verification

How Kratos identifies users and confirms their addresses

A Kratos identity has zero or more identifiers, fields that let the user log in. The most common is email, but a schema can declare any trait as an identifier.

Identifier extension

In an identity schema, a trait becomes an identifier via the ory.sh/kratos extension:

"email": {
  "type": "string",
  "format": "email",
  "title": "Email",
  "ory.sh/kratos": {
    "credentials": {
      "password": { "identifier": true },
      "webauthn": { "identifier": true }
    },
    "verification": { "via": "email" },
    "recovery": { "via": "email" }
  }
}

This declares:

  • email is the username when logging in with password.
  • email is the username when authenticating with WebAuthn.
  • email can be verified via email (a verification flow can be initiated for this trait).
  • email can be used for recovery (recovery codes are sent to this address).

Multiple identifiers

A schema can declare more than one identifier. For example, allowing login by either email or a custom username:

"username": {
  "type": "string",
  "pattern": "^[a-z0-9_]{3,32}$",
  "ory.sh/kratos": {
    "credentials": { "password": { "identifier": true } }
  }
},
"email": {
  "type": "string",
  "format": "email",
  "ory.sh/kratos": {
    "credentials": { "password": { "identifier": true } },
    "verification": { "via": "email" }
  }
}

The user can submit identifier: "alice" or identifier: "alice@example.com" and Kratos will look up by either.

Identifier uniqueness

Identifiers are globally unique within a domain (per Kratos instance). Two identities cannot have the same email in CIAM, but the same email can exist in CIAM and IAM (different Kratos instances, different domains, see CIAM/IAM isolation).

Case sensitivity: emails are normalized to lowercase. Custom username identifiers are case-sensitive unless the schema sets a format or pattern that normalizes.

Verification

A verifiable address is one that's been declared verification: { via: "email" } in the schema. Kratos tracks its state:

StateMeaning
pendingVerification has been initiated but not completed.
sentVerification email has been dispatched.
completedThe user has clicked the verification link.

Stored on the identity:

"verifiable_addresses": [
  {
    "value": "user@example.com",
    "verified": true,
    "via": "email",
    "status": "completed",
    "verified_at": "2026-04-14T12:00:00Z"
  }
]

Verification triggers

  • Automatic on registration: if the registration hook includes verification.
  • Automatic on email change: when a verifiable trait changes via the settings flow, verification is re-triggered.
  • Manual: user clicks "Verify" in their settings; admin clicks "Resend verification" in Athena.

Verification enforcement

By default, Kratos allows unverified users to use their account. To require verification:

selfservice:
  flows:
    login:
      after:
        password:
          hooks:
            - hook: require_verified_address

Every login checks; unverified users redirect to the verification flow. Olympus production enables this (see Security, Email verification).

Recovery

Recovery uses an identifier in the same way:

POST /self-service/recovery?flow=...
{ "email": "user@example.com" }

If the email is a recovery-enabled identifier, Kratos sends a recovery code. See Identity, Flow recovery.

Olympus-specific: do not trust OIDC email_verified

When a user logs in via Google / GitHub / Apple, the OIDC ID token may include email_verified=true. Olympus does not trust this. Kratos always runs its own verification.

The reasoning: an attacker who controls a mailbox via DNS takeover or registrar compromise could obtain Auth0/Google credentials with email_verified=true matching an Olympus identity's email, then use pre-linking to steal access. By requiring Olympus's own verification, we eliminate this path.

See Security, OIDC email_verified trust.

On this page