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 |