Velocity • Durability

3. Authentication

Session login, identity, password changes, Platform Admin tenant switching, and long-lived API tokens for non-interactive callers (CI, CSI drivers, monitoring collectors).

Endpoint Summary

Path Summary Method
/api/v1/auth/change-password Change own password POST
/api/v1/auth/login Login POST
/api/v1/auth/me Current user identity GET
/api/v1/auth/refresh Refresh tokens POST
/api/v1/auth/tenant-switch Switch into a tenant (Platform Admin only) POST
/api/v1/auth/tokens List your API tokens GET
/api/v1/auth/tokens Issue an API token POST
/api/v1/auth/tokens/{token_id} Read one of your API tokens GET
/api/v1/auth/tokens/{token_id} Revoke an API token DELETE

POST /api/v1/auth/change-password

Change own password

Change the signed-in user’s own password, verifying the current one. Acts on the caller’s identity from the token. Not available for accounts managed by an external identity provider. For administrative resets, use PATCH /tenants/{tenant_id}/users/{user_id}.

Authentication: required (PasswordLogin or BearerJWT).

Request Body (application/json, required)

Schema: ChangePasswordRequest

Self-service change-password payload. Carries no user/tenant id - the caller comes from the token.

Field Type Required Default Example Description
current_password string (password) yes n/a The caller’s current password, verified server-side.
new_password string (password) yes n/a New password. Must contain at least 8 characters with an upper-case letter, a lower-case letter, a digit, and a special character, and must not exceed 72 bytes.

Example

{
   "current_password" : "Curr3nt!pw",
   "new_password" : "N3w!password"
}

Responses

Status Description Schema
204 No content. Password changed successfully. -
400 The payload is well-formed but conflicts with the stored account state. detail carries a human-readable explanation of which rule was broken. -
401 Unauthorized. The Bearer token is missing, invalid, or expired. -
403 Password change is not available for this account (managed by an external identity provider). -
404 The caller’s account no longer exists (e.g. it was deleted after the token was issued). -
422 The new password is malformed - too short, too long, or missing a required character class. Reported in the standard validation-error shape, pointing at the offending field. -

POST /api/v1/auth/login

Login

Authenticate with username and password and receive a JWT access and refresh token pair. The username names the tenant it belongs to, <tenant-name>/<user> (for example fintech/alice); an unprefixed name belongs to the platform tenant.

Authentication: not required.

Request Body (application/x-www-form-urlencoded, required)

Schema: LoginForm

Credentials for POST /auth/login, where the user name carries the tenant.

Any other form field an OAuth2 client sends is accepted and ignored.

Field Type Required Default Example Description
password string (password) yes n/a The user password.
username string yes n/a User name, tenant-qualified as <tenant-name>/<user> (for example fintech/alice). An unprefixed name belongs to the platform tenant.

Example

{
   "password" : "Secret1!",
   "username" : "admin"
}

Responses

Status Description Schema
200 Token pair issued successfully. TokenResponse
401 Unauthorized. Wrong password, unknown user, or the target tenant could not be resolved. Every case answers with the same body to avoid user and tenant enumeration. -
422 Validation Error HTTPValidationError

Response Body (200, TokenResponse)

Field Type Required Default Example Description
access_token string yes n/a Short-lived JWT access token.
expires_in integer yes n/a 3600 Access token lifetime in seconds.
refresh_token string yes n/a Long-lived JWT refresh token.
token_type literal bearer no bearer

GET /api/v1/auth/me

Current user identity

Returns the authenticated user’s identity, the effective tenant, and what the caller may do there: the roles they hold and every permission those roles grant, as resource:action. The tenant is resolved from the tid claim in the JWT, and roles and permissions are read from that tenant’s policy - the same one the server enforces against. While a tenant switch is active, tenant_id is the tenant being acted in, the access reported is the one held there, and the delegation fields describe the switch - enough for the client to show an “acting as” banner.

Authentication: required (PasswordLogin or BearerJWT).

Responses

Status Description Schema
200 User identity, effective tenant, and the access held in it. MeResponse
401 Unauthorized. The Bearer token is missing, invalid, or expired. -

Response Body (200, MeResponse)

Field Type Required Default Example Description
actor_tenant_id string (uuid), nullable no null 00000000-0000-0000-0000-000000000000 Home tenant of the acting user; set only while a tenant switch is active.
actor_user_id string (uuid), nullable no null 00000000-0000-0000-0000-000000000001 The user that asked for the switch; set only while a tenant switch is active.
is_delegated boolean no false True while the caller acts in another tenant through a tenant switch. user_id is then the platform-support principal, and the delegation fields name the real caller.
is_platform_admin boolean yes n/a True if the authenticated user is the built-in Platform Admin account acting in its own tenant. Informational only - it grants nothing that the platform policy does not already grant. Always false while a tenant switch is active.
permissions array of string no n/a ["volumes:list","volumes:create","users:view"] Every permission the caller’s roles grant, as resource:action, deduplicated and in policy order. This is the effective set the server enforces against, so a client can gate a control on membership alone without knowing which role supplied it.
roles array of string no n/a ["admin"] Roles the caller holds in the effective tenant. Empty when the caller is bound to no role there - a role defined by another tenant grants nothing here. The full catalog, with the permissions of every role the tenant defines, is at GET /tenants/roles.
tenant_id string (uuid) yes n/a 123e4567-e89b-12d3-a456-426614174000 Effective tenant UUID. Resolved from the tid claim in the JWT.
user_id string (uuid) yes n/a 123e4567-e89b-12d3-a456-426614174000 The authenticated user identifier (JWT sub claim).

Example

{
   "is_delegated" : false,
   "is_platform_admin" : false,
   "permissions" : [
      "tenants:view",
      "volumes:list",
      "volumes:view",
      "users:list",
      "users:view",
      "users:update",
      "users:delete",
      "observability:list",
      "observability:view",
      "roles:view",
      "roles:update"
   ],
   "roles" : [
      "admin"
   ],
   "tenant_id" : "123e4567-e89b-12d3-a456-426614174000",
   "user_id" : "123e4567-e89b-12d3-a456-426614174000"
}

POST /api/v1/auth/refresh

Refresh tokens

Exchange a valid refresh token for a new JWT access and refresh token pair.

Authentication: not required.

Request Body (application/x-www-form-urlencoded, required)

Schema: Body_refresh_api_v1_auth_refresh_post

Field Type Required Default Example Description
refresh_token string yes n/a

Responses

Status Description Schema
200 New token pair issued successfully. TokenResponse
401 Unauthorized. The refresh token is invalid, expired, or is not a refresh token. -
422 Validation Error HTTPValidationError

Response Body (200, TokenResponse)

Field Type Required Default Example Description
access_token string yes n/a Short-lived JWT access token.
expires_in integer yes n/a 3600 Access token lifetime in seconds.
refresh_token string yes n/a Long-lived JWT refresh token.
token_type literal bearer no bearer

POST /api/v1/auth/tenant-switch

Switch into a tenant (Platform Admin only)

Exchange your current access token for a short-lived token scoped to the requested tenant. Only callers with tenants:switch can use this endpoint. The delegated token includes tid (target tenant) and act (original caller), has no refresh token, and does not change stored RBAC policy.

In Swagger UI, paste the returned access_token into BearerJWT; logging out of it ends the switch.

Authentication: required (PasswordLogin or BearerJWT).

Request Body (application/json, required)

Schema: TenantSwitchRequest

Platform Admin request to exchange the current access token for a target-tenant one.

Field Type Required Default Example Description
tenant_id string (uuid) yes n/a e97b94d6-9cc1-4325-a267-1064ae538ccd Target tenant to act in.
ttl_minutes integer, nullable no null 60, 480 Requested lifetime in minutes. Clamped server-side to the supported range (30 minutes - 12 hours); the granted lifetime is returned in expires_in.

Example

{
   "tenant_id" : "e97b94d6-9cc1-4325-a267-1064ae538ccd",
   "ttl_minutes" : 60
}

Responses

Status Description Schema
200 Token scoped to the target tenant. DelegatedTokenResponse
401 Unauthorized. The Bearer token is missing, invalid, or expired. -
403 Forbidden. The caller lacks tenants:switch in its effective tenant policy. Only the platform-admin role holds it - so a switched-in token, which holds the target platform-support role, cannot switch again. -
404 The target tenant does not exist. -
409 The target tenant is the caller’s own tenant, so there is nothing to switch into. -
422 Validation Error HTTPValidationError

Response Body (200, DelegatedTokenResponse)

Field Type Required Default Example Description
access_token string yes n/a Short-lived JWT scoped to the target tenant.
expires_in integer yes n/a 3600 Granted token lifetime in seconds.
tenant_id string (uuid) yes n/a Target tenant the token acts in (tid claim).
token_type literal bearer no bearer

GET /api/v1/auth/tokens

List your API tokens

Return the caller’s own API tokens, newest first. Carries no secret and no digest - the token itself is unrecoverable after issuance. Revoked and expired tokens stay listed, so a revocation stays visible; neither holds a slot or its name any more.

Authentication: required (PasswordLogin or BearerJWT).

Parameters

Name In Type Required Description
active_only query boolean no Return only records whose state is active.

Responses

Status Description Schema
200 The caller’s tokens. ApiTokenListResponse
401 Unauthorized. The Bearer token is missing, invalid, or expired. -
422 Validation Error HTTPValidationError

Response Body (200, ApiTokenListResponse)

Field Type Required Default Example Description
tokens array of ApiTokenResponse object yes n/a Token metadata, newest first.
total integer yes n/a Number of tokens returned.

POST /api/v1/auth/tokens

Issue an API token

Issue a long-lived credential for a non-interactive caller - a CLI, a CSI driver, a CI job, a monitoring collector. The token authenticates as you, carrying the roles you hold now and never exceeding what you hold at the time of each request.

The token is returned once, in this response, and cannot be retrieved again.

Requires an interactive session: an API token cannot issue another, and a tenant switch cannot leave one behind.

Authentication: required (PasswordLogin or BearerJWT).

Request Body (application/json, required)

Schema: ApiTokenCreateRequest

Request to issue a long-lived API token for the caller.

Field Type Required Default Example Description
description string, nullable no null What this token is for. Optional; name remains the handle an operator identifies it by.
expires_in_days integer yes n/a 30, 90, 180, 365 Lifetime in days. Required, and must name one of the offered values (30, 90, 180, 365), so a long lifetime is always a deliberate choice.
name string yes n/a Name for this token, unique among the caller’s usable tokens. An operator identifies a token by it. A revoked or expired token releases its name, so a rotated credential can keep the name it had.

Example

{
   "description" : "Nightly backup runner",
   "expires_in_days" : 90,
   "name" : "nightly-backup@ci-runner-04"
}

Responses

Status Description Schema
201 Token issued. The only response that contains the secret. ApiTokenCreatedResponse
400 Bad request. -
401 Unauthorized. The Bearer token is missing, invalid, or expired. -
403 Forbidden. The caller may not issue a token here: the credential presented is itself an API token, the session is a tenant switch, the role is not permitted to create tokens, or the request came from a browser application. -
409 Conflict. The caller already has a usable token with this name. -
422 Validation Error HTTPValidationError

Response Body (201, ApiTokenCreatedResponse)

Field Type Required Default Example Description
created_at string (date-time) yes n/a When the secret was created. Never changes.
created_by Principal object yes n/a Who issued the token.
description string, nullable no null What this token is for, if a description was given.
expires_at string (date-time) yes n/a Absolute expiry.
id string yes n/a Public token identifier. Appears in audit events; not a secret.
name string yes n/a The handle given at issuance, unique among the owner’s usable tokens.
revoked_at string (date-time), nullable no null When the token was revoked, if it was. The only lifecycle fact the record stores.
revoked_by Principal object, nullable no null Who revoked the token, if it was.
roles array of string, nullable no null The role ceiling held in the record: the owner’s own roles as at issuance. Not chosen - a token carries what its owner held, and never more than they hold now. Null for a service account key, which carries no ceiling: it resolves whatever the account holds at the time of each request.
state ApiTokenState (active, disabled) yes n/a Whether the token still authenticates. Computed per response, never stored.
state_reason ApiTokenStateReason (expired, revoked), nullable no null Why the token is disabled. Null while it is active. Revocation outranks expiry.
token string yes n/a vdt1_6XuU1pyDS0abcDEF12345w_9fK3mZq7Xw2Lb1cD_hT8n...7bQ2xM The token. Returned once and never retrievable again; a lost token must be replaced.
warning string no Store this token now. It cannot be retrieved again. Reminder that the secret is shown exactly once.

GET /api/v1/auth/tokens/{token_id}

Read one of your API tokens

Metadata for one token you own. A token belonging to another user answers the same as one that does not exist, so the endpoint cannot be used to probe another inventory.

Authentication: required (PasswordLogin or BearerJWT).

Parameters

Name In Type Required Description
token_id path string yes Public token identifier, as returned at issuance.

Responses

Status Description Schema
200 Token metadata. ApiTokenResponse
401 Unauthorized. The Bearer token is missing, invalid, or expired. -
404 Not found. No such token, or it belongs to another user - both answer the same so an inventory cannot be probed. -
422 Validation Error HTTPValidationError

Response Body (200, ApiTokenResponse)

Field Type Required Default Example Description
created_at string (date-time) yes n/a When the secret was created. Never changes.
created_by Principal object yes n/a Who issued the token.
description string, nullable no null What this token is for, if a description was given.
expires_at string (date-time) yes n/a Absolute expiry.
id string yes n/a Public token identifier. Appears in audit events; not a secret.
name string yes n/a The handle given at issuance, unique among the owner’s usable tokens.
revoked_at string (date-time), nullable no null When the token was revoked, if it was. The only lifecycle fact the record stores.
revoked_by Principal object, nullable no null Who revoked the token, if it was.
roles array of string, nullable no null The role ceiling held in the record: the owner’s own roles as at issuance. Not chosen - a token carries what its owner held, and never more than they hold now. Null for a service account key, which carries no ceiling: it resolves whatever the account holds at the time of each request.
state ApiTokenState (active, disabled) yes n/a Whether the token still authenticates. Computed per response, never stored.
state_reason ApiTokenStateReason (expired, revoked), nullable no null Why the token is disabled. Null while it is active. Revocation outranks expiry.

DELETE /api/v1/auth/tokens/{token_id}

Revoke an API token

Revoke one token you own. It stops authenticating on the very next request - the record is read on every request, so there is no cache to wait out.

Idempotent: revoking an already-revoked token succeeds and keeps the first revocation on record. The record is retained as a tombstone, so a later use of the token reports as revoked rather than as unknown.

Unlike issuance, this is open to a token-authenticated caller, so a compromised client can always burn its own credential.

Authentication: required (PasswordLogin or BearerJWT).

Parameters

Name In Type Required Description
token_id path string yes Public token identifier, as returned at issuance.

Responses

Status Description Schema
204 No content. The token no longer authenticates. -
401 Unauthorized. The Bearer token is missing, invalid, or expired. -
404 Not found. No such token, or it belongs to another user - both answer the same so an inventory cannot be probed. -
422 Validation Error HTTPValidationError