Skip to content
Omicron

Reference

Identity OAuth

The identity-only authorization code flow, Mastodon-compatible endpoints, security requirements, and limits.

Omicron supports a deliberately small, Mastodon-compatible OAuth surface for verifying the consenting account’s public identity. It is not the Mastodon API, OpenID Connect, or a way to obtain an Omicron browser session or general API JWT. NodeInfo continues to advertise software.name: "omicron".

Only confidential clients and the authorization code grant are supported. The only scope is read:accounts, and clients must request it explicitly at registration and authorization. Broad read, write scopes, implicit grants, password grants, client-credentials grants, public clients, refresh tokens, and out-of-band callbacks are not supported.

Endpoints

All paths are relative to the instance’s canonical public origin.

Method Path Purpose
GET /.well-known/oauth-authorization-server Issuer, endpoint, grant, authentication-method, scope, and S256 metadata
POST /api/v1/apps Register a confidential application
GET /oauth/authorize Browser sign-in and explicit consent
POST /oauth/token Exchange a single-use authorization code
POST /oauth/revoke Revoke one of the client’s identity tokens
GET /api/v1/accounts/verify_credentials Verify the token owner’s minimal public identity

The browser consent page also uses GET and POST /api/oauth/authorize. GET /api/oauth/connections lists the signed-in user’s active tokens, and DELETE /api/oauth/connections/:id revokes an owned token. These backing APIs require a live Better Auth cookie session, not an identity token or general API JWT. Consent decisions and disconnect requests additionally require an Origin header matching the instance’s canonical origin exactly.

1. Register the application

POST /api/v1/apps accepts a JSON object or application/x-www-form-urlencoded body.

Field Required Meaning
client_name Yes Nonblank display name, at most 100 characters; not a verified identity
redirect_uris Yes One to ten exact callback URLs separated by whitespace or newlines
scopes Yes Exactly read:accounts
website No Application website, at most 2,048 characters

Callbacks and the website must use HTTPS, without credentials or fragments. HTTP is accepted only for localhost, 127.0.0.1, or [::1] development URLs. Callbacks must not already contain code, state, error, or error_description query parameters. The complete callback list is capped at 8,192 characters; an authorization callback must fit the 2,048-character authorization parameter limit.

{
  "client_name": "Example directory",
  "redirect_uris": "https://application.example/oauth/callback",
  "scopes": "read:accounts",
  "website": "https://application.example"
}

A successful response is 200 with id, name, website, redirect_uri, client_id, client_secret, and an empty legacy vapid_key field. The latter does not advertise Web Push support. The client secret is returned only at registration; Omicron stores its hash. Keep it server-side and out of browser storage, URLs, analytics, and logs.

Send the browser to /oauth/authorize with these query parameters:

Parameter Required Value
client_id Yes The registered client ID
redirect_uri Yes Exactly one registered callback, including its query string
response_type Yes code
scope Yes Exactly read:accounts
state Recommended Unpredictable, browser-session-bound value, at most 2,048 characters
code_challenge Recommended Base64url SHA-256 digest of the PKCE verifier, without padding
code_challenge_method With a challenge S256; plain is not supported

Always generate and validate state on the client side. Omicron preserves it, but does not replace the client’s responsibility to bind a callback to the browser that initiated sign-in. Do not place private information in it.

S256 PKCE is optional for compatibility with confidential clients, but is recommended even when the client has a secret. Generate a fresh 43–128-character verifier from OAuth’s unreserved ASCII characters and retain it server-side for the exchange. A challenge must be 43 base64url characters. Once authorized, PKCE cannot be omitted or downgraded at the token endpoint.

Omicron validates the client, exact callback, requested scope, and challenge before showing consent. Invalid requests fail locally instead of redirecting to an untrusted callback. The user signs in using the normal instance login; consent requires a live cookie session and is never automatic. Its one-use ticket is bound to that account, session, and authorization request and expires after 10 minutes.

Approval returns a code and the original state to the registered callback. Denial returns error=access_denied and the original state, without a code. Authorization codes expire after 5 minutes and are single-use.

3. Exchange the code

POST /oauth/token accepts JSON or form-encoded parameters:

Parameter Required Value
grant_type Yes authorization_code
client_id and client_secret Unless using HTTP Basic Registered confidential-client credentials
code Yes The authorization code
redirect_uri Yes Exactly the callback authorized with this code
code_verifier If PKCE was authorized The original verifier
scope No If sent, exactly read:accounts; cannot expand permissions

HTTP Basic client authentication is also supported: form-encode the client ID and secret separately, join with :, then base64-encode the result. Do not send credentials both in the header and in the body.

{
  "access_token": "omi_id_<opaque-credential>",
  "token_type": "Bearer",
  "scope": "read:accounts",
  "created_at": 1790000000,
  "expires_in": 3600
}

The timestamp above is illustrative. The token lasts one hour, has no refresh token, and works only on the identity endpoint. Credentials are stored hashed. Code consumption and token creation are atomic: parallel exchanges cannot both succeed. Detected reuse revokes a previously issued token for that code and client, so do not blindly retry an exchange after an ambiguous network failure; restart authorization instead.

4. Verify identity

Send Authorization: Bearer <access_token> to GET /api/v1/accounts/verify_credentials. The response contains only:

{
  "id": "<stable-local-account-id>",
  "username": "ada",
  "acct": "ada",
  "display_name": "Ada",
  "url": "https://blog.example/@ada",
  "uri": "https://blog.example/users/ada"
}

Treat the account ID together with the issuer origin as the identity key. Display names can change. Email, roles, credentials, avatar metadata, counts, and private account data are not included. A browser cookie or normal Omicron API JWT cannot substitute for an identity token on this endpoint. Expired or revoked tokens, and tokens belonging to suspended or deleted accounts, fail with 401.

Revoke access

POST /oauth/revoke accepts the same two client-authentication methods as the token endpoint plus a token parameter. It returns 200 {} for an unknown, already-revoked, or differently owned token as well; only the authenticated client’s token is affected. Users can independently revoke their own tokens in Connected applications.

Revocation and expiry prevent subsequent identity verification. They do not end a third party’s already-established session or erase information it received earlier.

Errors and limits

OAuth protocol errors use error and error_description, with codes including invalid_request, invalid_client, invalid_scope, invalid_grant, unsupported_grant_type, unsupported_response_type, and invalid_token. Missing browser sessions and origin failures use the normal Omicron JSON error shape. A 401 identity failure carries a Bearer authentication challenge; invalid client authentication carries an HTTP Basic challenge.

  • JSON and form bodies are capped at 16,384 bytes, including chunked bodies.
  • Duplicate query-string and form-encoded parameter names are rejected.
  • OAuth responses use Cache-Control: no-store; the consent page also uses Referrer-Policy: no-referrer and cannot be framed.
  • Endpoint-specific source-IP limits apply separately from normal API writes; see Rate limits.
  • Cleanup runs at startup and hourly. Expired tickets, codes, and tokens are removed. Never-approved application registrations older than 30 days are removed when they have no live pending credentials; previously approved clients are retained.

Reverse proxy and upgrades

The shipped Caddyfile sends application registration, token exchange, revocation, and identity verification directly to the backend. These machine endpoints must not receive bot challenges or the frontend’s browser form-origin checks. The consent page remains in the frontend; OAuth browser endpoints are also exempt from the managed IP access rules.

For a custom proxy, preserve these routes and the instance’s canonical public origin. Do not disable browser CSRF protections globally to accommodate a server-to-server client. Plain frontend-only development proxies are subject to SvelteKit’s form-origin checks; use the bundled gateway or the backend directly for form-encoded machine requests.

The feature needs no new environment variables. Existing instances must apply the additive migration and refresh the bind-mounted proxy and bot-policy files; see Upgrading → Identity OAuth.

Found a mistake?Edit this page on GitHub.