OAuth
Let people sign in to your app with their haunt.gg account.
haunt.gg is an OAuth 2.1 and OpenID Connect provider. Register an app in your
dashboard and you can put a Log in with haunt.gg button in your own project:
the user approves what your app may read, and you get an access token and an
OpenID Connect id_token — plus a refresh token if you asked for one.
There is nothing bespoke to implement. Point any OIDC client library at the
issuer https://haunt.gg and it configures itself from the discovery document;
authorization code + PKCE, refresh, userinfo, revocation and introspection all
behave exactly as the specs say.
Register your app
Apps are created in the haunt.gg dashboard under Developer
(https://haunt.gg/dashboard/developer). Dynamic client registration is not
available, and there is no public-client mode: every app is confidential and
gets a client_id plus a client_secret.
The client secret is shown once, right after you create the app or reset it. It is stored hashed, so nobody — not even haunt.gg staff — can show it to you again. Resetting the secret takes effect immediately, with no grace period: everything still using the old secret starts failing at the token endpoint the moment you confirm.
You also pick the app's redirect URLs — up to 10, each at most 250 characters.
They are matched exactly, path included: no wildcards, no # fragment.
Plain http is allowed only on 127.0.0.1 and localhost, everything else
must be https. A native app may instead register a private-use scheme, which
has to be reverse-domain style — it needs a dot, e.g. gg.haunt.myapp:/callback.
If a native or CLI app listens on a random port, register http://127.0.0.1/…
rather than http://localhost/…: the port is ignored only for the loopback
IP, never for the hostname. Details.
The flow
Send the user to haunt.gg
Redirect the browser to the authorization endpoint with your client_id,
one of your registered redirect_uri values, response_type=code, the
scope list, a state value you generate, and a PKCE challenge. PKCE with
S256 is mandatory for every app.
The user signs in and approves
haunt.gg handles the login and shows its own consent screen listing exactly the permissions your app requested. If the same user already approved the same set of permissions, the screen is skipped.
haunt.gg calls your redirect URL
The browser comes back to your redirect_uri with code, your state, and
an iss parameter you should verify equals https://haunt.gg.
Exchange the code for tokens
From your server, POST the code together with your client_secret and
the PKCE verifier to the token endpoint. You get an access token, an
id_token, and — if you asked for offline_access — a refresh token.
Read the user
Call the userinfo endpoint with the access token, or just read the claims
out of the id_token.
The token exchange must happen server-side. /api/auth/oauth2/token sends no
CORS headers and answers no preflight, and every app needs its client secret
to authenticate — a browser-only exchange cannot work.
Permissions
There are five. An app can never ask for more than it registered — requesting
anything else fails the whole request with invalid_scope.
| Scope | On the consent screen | What it gives you |
|---|---|---|
openid | Sign in | The id_token, and permission to call userinfo. Without it there is no OpenID Connect flow, so always ask for it. |
identify | Basic profile | Who the user is — sub, uid, name, preferred_username, picture, profile. Every claim in detail. |
email | Email address | email and email_verified. |
connections | Connected accounts | The Discord and Last.fm accounts the user linked to haunt.gg. |
offline_access | Refresh session | A refresh token. Leave it out and the token response contains no refresh_token at all — once the access token expires an hour later, the user has to walk through the flow again. |
Most integrations want openid identify, plus offline_access if the app does
anything on the user's behalf while they are away.
Email address and Connected accounts are locked. Your app can only
register them once haunt.gg staff unlocked them for your account — open a
support ticket at https://haunt.gg/dashboard/support/new and tell us what
your app does. The other three are available to everyone.
The OIDC standard profile scope does not exist here — ask for
identify.
Endpoints
Token lifetimes
| Token | Lives for | Notes |
|---|---|---|
| Authorization code | 10 minutes | Single use. The login and consent step has to finish inside the same window. |
| Access token | 1 hour | Opaque by default — treat it as a random string, not as a JWT. |
| Refresh token | 30 days | Sliding: every refresh issues a new one and resets the 30 days. |
id_token | 10 hours | Signed with EdDSA. Keys are at https://haunt.gg/api/auth/jwks. |
What is not supported
- Dynamic client registration — apps are created in the dashboard, and
registration_endpointis absent from the discovery document. - Public clients —
token_endpoint_auth_methods_supportednever containsnone. Every app authenticates with its secret. - The
client_credentialsgrant — it appears ingrant_types_supported, but haunt.gg apps are registered forauthorization_codeandrefresh_tokenonly, so it always answersunauthorized_client. - RP-initiated logout —
end_session_endpointis advertised but not enabled for haunt.gg apps. Sign the user out in your own app instead. plainPKCE — onlyS256is accepted.
When the user takes access back
A user can disconnect your app at any time under Account → Settings →
Connections. That deletes their approval and every access and refresh
token your app holds for them, so your next refresh answers invalid_grant.
Send them through the authorization flow again to reconnect.
haunt.gg staff can also suspend an app. A suspended app is refused at the
authorization endpoint with client_disabled, every token grant answers
invalid_client, and all of its tokens are deleted — but user approvals are
kept, so restoring the app does not re-prompt everyone.
Rate limits
100 requests per minute per IP address, counted separately for each endpoint —
so a burst of userinfo calls never eats into your token budget. Going over
answers 429 with an X-Retry-After header (seconds to wait) and the body
{"message":"Too many requests. Please try again later."}. The discovery
document is cached and not rate limited.