# Auth API

Sessions. 21 operations under /api/v1. Errors are RFC 9457 problem+json with a stable `code` field. The complete schemas are in https://nebulactrl.com/api/v1/openapi.json.

## GET /auth-providers

Configured single-sign-on providers (instance admin)

Authentication: session cookie or `Authorization: Bearer nct_…` token.

Response 200: OK (AdminProvidersOutputBody)
  - `items` (ProviderBody[] | null, required)
Errors: 401 Unauthorized; 403 Forbidden; 500 Internal Server Error

## POST /auth-providers

Add a single-sign-on provider (instance admin)

OIDC issuers are checked through discovery (`/.well-known/openid-configuration`) before saving.

Authentication: session cookie or `Authorization: Bearer nct_…` token.

Request body (application/json, required): ProviderInputBody
  - `allowedDomains` (string[] | null)
  - `clientId` (string, required)
  - `clientSecret` (string): Write-only. Required on create; empty on update keeps the stored secret
  - `enabled` (boolean)
  - `id` (string): Create only: the ID the provider gets, so its callback URL (/api/v1/auth/oidc/{id}/callback) can be registered at the identity provider before saving. Generated when omitted; 409 provider_id_taken when in use
  - `issuerUrl` (string)
  - `kind` (enum: oidc | github, required)
  - `name` (string, required)
  - `scopes` (string[] | null)
  - `trustMfa` (boolean)

Response 201: Created (ProviderBody)
  - `allowedDomains` (string[] | null, required): Empty allows any verified email
  - `clientId` (string, required)
  - `createdAt` (string (date-time), required)
  - `enabled` (boolean, required)
  - `id` (string, required)
  - `issuerUrl` (string, required): OIDC issuer; for GitHub an optional Enterprise Server base URL
  - `kind` (enum: oidc | github, required)
  - `name` (string, required)
  - `redirectUri` (string, required): Register this callback URL at the provider
  - `scopes` (string[] | null, required)
  - `trustMfa` (boolean, required): Skip the local 2FA step when the provider asserts MFA (amr claim)
Errors: 401 Unauthorized; 403 Forbidden; 422 Unprocessable Entity; 500 Internal Server Error

## PUT /auth-providers/{id}

Replace a single-sign-on provider's settings (instance admin)

Authentication: session cookie or `Authorization: Bearer nct_…` token.

Parameters:
- `id` (path, string, required)

Request body (application/json, required): ProviderInputBody
  - `allowedDomains` (string[] | null)
  - `clientId` (string, required)
  - `clientSecret` (string): Write-only. Required on create; empty on update keeps the stored secret
  - `enabled` (boolean)
  - `id` (string): Create only: the ID the provider gets, so its callback URL (/api/v1/auth/oidc/{id}/callback) can be registered at the identity provider before saving. Generated when omitted; 409 provider_id_taken when in use
  - `issuerUrl` (string)
  - `kind` (enum: oidc | github, required)
  - `name` (string, required)
  - `scopes` (string[] | null)
  - `trustMfa` (boolean)

Response 200: OK (ProviderBody)
  - `allowedDomains` (string[] | null, required): Empty allows any verified email
  - `clientId` (string, required)
  - `createdAt` (string (date-time), required)
  - `enabled` (boolean, required)
  - `id` (string, required)
  - `issuerUrl` (string, required): OIDC issuer; for GitHub an optional Enterprise Server base URL
  - `kind` (enum: oidc | github, required)
  - `name` (string, required)
  - `redirectUri` (string, required): Register this callback URL at the provider
  - `scopes` (string[] | null, required)
  - `trustMfa` (boolean, required): Skip the local 2FA step when the provider asserts MFA (amr claim)
Errors: 401 Unauthorized; 403 Forbidden; 404 Not Found; 422 Unprocessable Entity; 500 Internal Server Error

## DELETE /auth-providers/{id}

Remove a single-sign-on provider and its linked identities (instance admin)

Authentication: session cookie or `Authorization: Bearer nct_…` token.

Parameters:
- `id` (path, string, required)

Response 204: No Content
Errors: 401 Unauthorized; 403 Forbidden; 404 Not Found; 422 Unprocessable Entity; 500 Internal Server Error

## GET /auth-settings

Instance sign-in settings (instance admin)

Authentication: session cookie or `Authorization: Bearer nct_…` token.

Response 200: OK (AuthSettingsBody)
  - `allowOpenSignup` (boolean, required): Let anyone create an account through an identity provider without an invite (default off)
Errors: 401 Unauthorized; 403 Forbidden; 500 Internal Server Error

## PUT /auth-settings

Change instance sign-in settings (instance admin)

Authentication: session cookie or `Authorization: Bearer nct_…` token.

Request body (application/json, required): AuthSettingsBody
  - `allowOpenSignup` (boolean, required): Let anyone create an account through an identity provider without an invite (default off)

Response 200: OK (AuthSettingsBody)
  - `allowOpenSignup` (boolean, required): Let anyone create an account through an identity provider without an invite (default off)
Errors: 401 Unauthorized; 403 Forbidden; 422 Unprocessable Entity; 500 Internal Server Error

## GET /auth/2fa

What can finish the pending sign-in

Authentication: none.

Response 200: OK (SecondFactorMethodsOutputBody)
  - `passkey` (boolean, required): A passkey can finish the sign-in
  - `totp` (boolean, required): An authenticator-app or recovery code can finish the sign-in
Errors: 401 Unauthorized; 500 Internal Server Error

## POST /auth/2fa

Finish sign-in with a TOTP or recovery code

Follows a login that answered 401 `two_factor_required` (or a provider login that redirected to `/login?step=2fa`). The pending login expires after 5 minutes and after 5 wrong codes; limited to 10 attempts per minute per client IP.

Authentication: none.

Request body (application/json, required): SecondFactorInputBody
  - `code` (string, required): A 6-digit authenticator code or a recovery code

Response 200: OK (MeBody)
  - `createdAt` (string (date-time), required)
  - `email` (string, required)
  - `id` (string, required)
  - `instanceAdmin` (boolean, required): The first user: manages instance settings
  - `name` (string, required)
  - `principal` (PrincipalBody, required)
  - `workspaceId` (string, required)
Errors: 401 Unauthorized (`two_factor_required`); 422 Unprocessable Entity; 429 Too Many Requests; 500 Internal Server Error

## POST /auth/2fa/passkey

Finish sign-in with a passkey as the second factor

The pending login expires after 5 minutes and after 5 wrong answers; limited to 10 attempts per minute per client IP.

Authentication: none.

Request body (application/json, required): any

Response 200: OK (MeBody)
  - `createdAt` (string (date-time), required)
  - `email` (string, required)
  - `id` (string, required)
  - `instanceAdmin` (boolean, required): The first user: manages instance settings
  - `name` (string, required)
  - `principal` (PrincipalBody, required)
  - `workspaceId` (string, required)
Errors: 401 Unauthorized; 422 Unprocessable Entity; 429 Too Many Requests; 500 Internal Server Error

## POST /auth/2fa/passkey/options

Ask the account's passkeys to finish a pending sign-in

Follows a login that answered 401 `two_factor_required`. Returns the WebAuthn request options for `navigator.credentials.get`; the answer goes to `POST /auth/2fa/passkey`.

Authentication: none.

Response 200: OK (any)
Errors: 401 Unauthorized (`two_factor_required`); 409 Conflict; 429 Too Many Requests; 500 Internal Server Error

## POST /auth/login

Sign in with email and password (sets the session cookie)

Limited to 10 attempts per minute per client IP.

Authentication: none.

Request body (application/json, required): LoginInputBody
  - `email` (string, required)
  - `password` (string, required)

Response 200: OK (MeBody)
  - `createdAt` (string (date-time), required)
  - `email` (string, required)
  - `id` (string, required)
  - `instanceAdmin` (boolean, required): The first user: manages instance settings
  - `name` (string, required)
  - `principal` (PrincipalBody, required)
  - `workspaceId` (string, required)
Errors: 401 Unauthorized; 422 Unprocessable Entity; 429 Too Many Requests; 500 Internal Server Error

## POST /auth/logout

Destroy the current session

Authentication: none.

Response 204: No Content

## GET /auth/oidc/{providerId}/callback

OAuth/OIDC redirect target of the identity provider

Completes the sign-in and redirects to `/` (or `/login?step=2fa` / `/login?error=<code>`).

Authentication: none.

Parameters:
- `providerId` (path, string, required)
- `code` (query, string)
- `state` (query, string)
- `error` (query, string)

Response 302: Found

## GET /auth/oidc/{providerId}/start

Redirect to the identity provider

Authentication: none.

Parameters:
- `providerId` (path, string, required)
- `invite` (query, string): Team invite token that authorises sign-up of a new account

Response 302: Found
Errors: 404 Not Found; 422 Unprocessable Entity; 429 Too Many Requests; 500 Internal Server Error

## POST /auth/passkey/login

Finish a passwordless sign-in with a passkey (sets the session cookie)

A passkey verified on the authenticator (PIN or biometric) counts as two factors: the session is second-factor verified and no code is asked for.

Authentication: none.

Request body (application/json, required): any

Response 200: OK (MeBody)
  - `createdAt` (string (date-time), required)
  - `email` (string, required)
  - `id` (string, required)
  - `instanceAdmin` (boolean, required): The first user: manages instance settings
  - `name` (string, required)
  - `principal` (PrincipalBody, required)
  - `workspaceId` (string, required)
Errors: 401 Unauthorized; 422 Unprocessable Entity; 429 Too Many Requests; 500 Internal Server Error

## POST /auth/passkey/options

Start a passwordless sign-in with a passkey

Returns the WebAuthn request options for `navigator.credentials.get`; the answer goes to `POST /auth/passkey/login`. The options expire after 5 minutes. Limited to 10 attempts per minute per client IP.

Authentication: none.

Response 200: OK (any)
Errors: 422 Unprocessable Entity; 429 Too Many Requests; 500 Internal Server Error

## GET /auth/password-reset

Whether password reset by email is available

Authentication: none.

Response 200: OK (PasswordResetStatusOutputBody)
  - `available` (boolean, required): Whether the login page may offer Forgot password: an email can be sent and the control plane has a public URL

## POST /auth/password-reset/confirm

Set a new password with a reset link

Single use. Ends every session of the account; two-factor authentication still applies at the next sign-in. 400 `invalid_reset_token` for an unknown, expired or used link; limited to 10 attempts per minute per client IP.

Authentication: none.

Request body (application/json, required): PasswordResetConfirmInputBody
  - `password` (string, required)
  - `token` (string, required)

Response 204: No Content
Errors: 400 Bad Request (`invalid_reset_token`); 422 Unprocessable Entity; 429 Too Many Requests; 500 Internal Server Error

## POST /auth/password-reset/request

Email a password reset link

Answers 202 whether or not the account exists, so it cannot be used to find out which emails are registered; accounts that sign in only through an identity provider get no email. The link works once and expires after 30 minutes. Limited to 10 requests per hour per client IP and 3 per hour per email address. 503 `password_reset_unavailable` when the instance has no SMTP relay or team email channel, or no public URL.

Authentication: none.

Request body (application/json, required): PasswordResetRequestInputBody
  - `email` (string, required)

Response 202: Accepted (PasswordResetRequestOutputBody)
  - `status` (enum: requested, required): Always 'requested': the answer is the same whether or not the account exists
Errors: 422 Unprocessable Entity; 429 Too Many Requests; 500 Internal Server Error; 503 Service Unavailable (`password_reset_unavailable`)

## GET /auth/providers

Enabled single-sign-on providers for the login page

Authentication: none.

Response 200: OK (ProvidersOutputBody)
  - `providers` (PublicProviderBody[] | null, required)

## POST /instance/sign-in-providers/test

Test an OpenID Connect issuer without saving it (instance admin)

Reads `/.well-known/openid-configuration` of the issuer the way saving a provider does and checks that the document names the same issuer and has the authorization endpoint, token endpoint and key set. Answers 200 either way: `ok` is false with a specific `code` and `message` when the issuer cannot be used.

Authentication: session cookie or `Authorization: Bearer nct_…` token.

Request body (application/json, required): TestIssuerInputBody
  - `issuerUrl` (string, required): The OIDC issuer to read the discovery document of

Response 200: OK (TestIssuerBody)
  - `authorizationEndpoint` (string): Where people are sent to sign in (on success)
  - `code` (enum: invalid_issuer | host_not_found | unreachable | timeout | tls_invalid | discovery_not_found | discovery_invalid | issuer_mismatch | discovery_incomplete): Why the issuer cannot be used (on failure)
  - `issuer` (string): The issuer the provider reported (on success)
  - `message` (string): What to fix (on failure)
  - `ok` (boolean, required): The issuer serves a usable OpenID configuration
Errors: 401 Unauthorized; 403 Forbidden; 422 Unprocessable Entity; 500 Internal Server Error
