Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Endpoints

OAuth2 / OIDC

EndpointDescription
GET /.well-known/openid-configurationOIDC Discovery
GET /.well-known/oauth-authorization-serverThe same document under the RFC 8414 name
GET /.well-known/jwks.jsonJSON Web Key Set
GET/POST /authorizeAuthorization endpoint (login page)
POST /tokenToken endpoint
GET/POST /userinfoUserInfo endpoint
POST /introspectToken Introspection (RFC 7662)
POST /revokeToken Revocation (RFC 7009)
GET/POST /logoutOIDC End Session / Logout (alias: /end_session)
GET /ui/logoutDashboard session logout (the web UI's Logout button)
POST /device_authorizationDevice Authorization (RFC 8628; alias: /device/code)
GET/POST /deviceDevice verification page
POST /clients/forgetDrop one cached client ID metadata document (web UI)
POST /registerDynamic client registration (RFC 7591), opt-in
GET/DELETE /register/<client_id>Read or remove a registration (RFC 7592)

The two discovery paths return the same document: nanoidp is one server with one set of endpoints, and a client that speaks only OAuth looks under the RFC 8414 name. Both derive the issuer the same way, so issuer_from_request applies to both. The metadata is served at the root form of the name only, which is the correct one for an issuer without a path component; an issuer with a path would want RFC 8414's path-suffixed form, and nanoidp's OIDC document has always assumed the root form too.

The two /register paths exist only while oauth.dynamic_registration.enabled is set, and are open when they do: the flag is the gate, not the management_secret. See Dynamic client registration.

An accepted GET /authorize stores the validated request as an authorization transaction on the server (#346), bound to the browser through the session cookie, and renders a login page whose forms carry its transaction_id. The request (response_type, client_id, redirect_uri, scope, state, PKCE, nonce, claims, resource) is validated once, on that GET; the POST that completes the login uses what the transaction holds and never reads OAuth parameters from its body. A transaction expires after 10 minutes, yields at most one code, and can only be used with the cookie of the browser that created it. It is a snapshot: a configuration change while it is open does not revalidate it, and only a client that no longer exists ends it.

A POST names its transaction with transaction_id; when its own query string also carries OAuth request parameters, they must be exactly those of the request that created the transaction. A POST without transaction_id whose query string carries a complete OAuth request is a direct entry point, with or without a GET before it: that request is validated on the POST like a GET's, and the login runs against a transaction that ends with the POST unless a TOTP code is still to come. A POST with neither, such as a script posting username/password after its GET with the same cookie jar, continues the one pending transaction of that browser; with none or several pending it is refused with invalid_request rather than guessing. A GET without OAuth request parameters (other query parameters do not count) follows the same "exactly one" rule, which is how a GET carrying only login_hint applies the hint to the pending request (#250/#325/#328/#346).

curl examples for every grant are in Requesting tokens.

What /token, /device_authorization, /userinfo, /introspect and POST /api/users/{username}/token answer carries Cache-Control: no-store and Pragma: no-cache, success and error alike (RFC 6749 §5.1 for the responses that carry tokens or credentials; a policy of nanoidp's own for user data and token state, so that nothing between a test client and the IdP answers from a cache). The two registration responses that carry credentials (#190) say the same. A response the endpoint itself did not build, such as a 405, a 429 from the rate limiter or a 503 while the configuration cannot be read, carries neither; nor do the discovery documents and the JWKS, which are public and cacheable.

The standard OIDC profile / email claims (email, email_verified, preferred_username, ...) are served from GET /userinfo, not embedded in the tokens - see Tokens and claims.

SAML

EndpointDescription
GET /saml/metadataIdP Metadata
GET /saml/cert.pemIdP signing certificate (PEM)
GET/POST /saml/ssoSingle Sign-On (supports both HTTP-POST and HTTP-Redirect bindings)
POST /saml/attribute-queryAttribute Query (SOAP, backend-to-backend)

Bindings, strict-binding mode, response signing, and canonicalization are covered in SAML options.

/saml/attribute-query is unauthenticated by design - the same model as the REST read surfaces (reads are never gated): nanoidp is a testing IdP and its user directory is test data. On a shared instance, anyone who can reach the endpoint can read any configured user's attributes; deploy accordingly. An unknown NameID gets a SAML error status (Requester/UnknownPrincipal), never a fabricated assertion. Every query it answers is audited, refusals included (#309), so the same reachability that lets a stranger read attributes also lets one fill the audit ring and push older entries out of it; the recorded query id is truncated so a single request cannot store more than a name's worth of text - in the audit entry only: the InResponseTo of the answer carries the id exactly as it was sent.

REST API

EndpointDescription
GET /api/healthHealth check
GET /api/usersList the effective users, declared and runtime, each with its origin
GET /api/users/{username}Get user details, declared or runtime (origin)
POST /api/users/{username}/tokenGenerate a token for a user (testing); the user and client_id resolve like any login, runtime ones included. Optional JSON body: exp_minutes, and client_id (must name a real client) which binds the token and issues a spendable refresh_token; without client_id the response is an access token only (no refresh_token, since one with no client binding is refused since 3.0, #73).
GET /api/auditGet audit log
GET /api/audit/statsAudit log statistics
POST /api/audit/clearClear the audit log
GET /api/configGet current configuration
POST /api/config/reloadReload configuration. A rejected reload answers a JSON 422 (kind: invalid for files that cannot be read or do not validate, activation for a signing configuration that cannot be used) and the running configuration stays in effect; a strict hook or plugin failure answers 503
POST /api/keys/rotateRotate cryptographic keys. A refusal is {"success": false, "error": <fixed message>, "kind": ...}: 409 when jwt.external_keys is configured, since the operator's key is not nanoidp's to replace (external_keys_not_rotatable), and when this process cannot write the keys directory (keys_directory_not_writable, or lock_namespace_unavailable when its lock file cannot even exist); 503 when the directory's lock, shared with other NanoIDP processes, could not be taken: lock_timeout (another process is starting or rotating) with Retry-After, lock_unsupported (the filesystem has no advisory locks) without, since a retry does not help. The directory a refusal is about is in the server log, not in the body
GET /api/keys/infoGet key information

Runtime identities

Disposable users and clients for a test run, created on the running IdP and never written to the declared files unless promoted; see Disposable test identities.

EndpointDescription
POST /api/runtime/usersCreate a runtime user: a users.yaml entry plus username
GET /api/runtime/usersList runtime users
GET /api/runtime/users/{username}Get a runtime user
DELETE /api/runtime/users/{username}Delete a runtime user
POST /api/runtime/users/{username}/promoteWrite it into users.yaml and retire the runtime object
POST /api/runtime/clientsCreate a runtime client: an oauth.clients[] entry
GET /api/runtime/clientsList runtime clients
GET /api/runtime/clients/{client_id}Get a runtime client
DELETE /api/runtime/clients/{client_id}Delete a runtime client
POST /api/runtime/clients/{client_id}/promoteWrite it into settings.yaml and retire the runtime object
DELETE /api/runtimeRemove every runtime user and client; answers users_deleted and clients_deleted