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.
2. Request browser consent
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 usesReferrer-Policy: no-referrerand 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.