Integration walkthrough
Create your app, then run the authorization-code + PKCE flow end to end against Hep.gg.
Integration walkthrough
This page walks through the full authorization-code flow with PKCE: build a code verifier and challenge, send the user to the authorize endpoint, exchange the returned code for tokens, and read the user's claims. Every host below is real and copy-pasteable.
Create your app
Sign-in apps are self-serve. In the Hep.gg dashboard, open Login -> My Apps, then New app. You configure:
- Name (plus an optional description, icon, and launch URL shown to users).
- Redirect URIs: the exact URIs Hep.gg may redirect back to after sign-in. The
redirect_uriyou send to the authorize endpoint must match one of these byte-for-byte. - Who can sign in: leave it open to any signed-in Hep.gg user, or restrict it to one or more of your own groups.
- PKCE: required by default. Leave it on unless your client cannot support it.
Open the app afterwards to choose the scopes it may request (openid is always included; toggle profile, email, groups, offline_access), set the access and refresh token lifetimes, rotate the secret, or delete the app. Your account can hold several apps; the exact limit is shown on the My Apps page and scales with your plan.
When you create the app you receive a client_id (32 hex characters) and a client_secret (a long base64url string).
Restricting who can sign in
By default any signed-in Hep.gg user with a verified email can sign in to your app. To limit it to specific people, use groups:
- In the dashboard, open Login -> My Groups and create a group.
- Add members by their exact Hep.gg user ID. You can look the ID up to confirm the right person before adding them.
- Back in your app's settings, set Who can sign in to that group (you can select more than one).
After that, only members of a group you selected can complete sign-in; everyone else is stopped at the Hep.gg sign-in step and never reaches your app. A user's group names are also delivered to your app through the groups scope, so you can branch on them in your own code.
Step 1: Build the PKCE pair
PKCE (S256) is required by default. Generate a high-entropy code_verifier, then derive the code_challenge as the base64url-encoded SHA-256 of the verifier. Keep the verifier on the client that started the flow; you send it to the token endpoint in step 3.
# code_verifier: 43-128 chars, URL-safe
code_verifier=$(openssl rand -base64 60 | tr '+/' '-_' | tr -d '=\n')
# code_challenge = base64url( sha256( code_verifier ) )
code_challenge=$(printf '%s' "$code_verifier" \
| openssl dgst -binary -sha256 \
| openssl base64 | tr '+/' '-_' | tr -d '=\n')
echo "verifier: $code_verifier"
echo "challenge: $code_challenge"import crypto from "node:crypto";
const codeVerifier = crypto.randomBytes(48).toString("base64url");
const codeChallenge = crypto
.createHash("sha256")
.update(codeVerifier)
.digest("base64url");
// Persist codeVerifier in the user's session, keyed by `state`.Step 2: Send the user to authorize
Redirect the user's browser to this URL with the query parameters below. This is a browser flow: it relies on the user's Hep.gg session cookie, so it must run as a top-level navigation in the browser, not a server-side or XHR request. There is no client_secret here.
response_typecode. Any other value is rejected.client_idredirect_uriscopeopenid. Each scope must be enabled for your client. See Scopes and claims.statecode_challengecode_challenge_methodplainS256 (recommended) or plain. Always use S256. If omitted while a challenge is present, it is treated as plain.nonceid_token as the nonce claim. Recommended; verify it on return to defend against replay.Example authorization URL:
https://hep.gg/api/v1/login/oauth/authorize
?response_type=code
&client_id=0a1b2c3d4e5f60718293a4b5c6d7e8f9
&redirect_uri=https://yourapp.example.com/callback
&scope=openid%20profile%20email
&state=xyz123
&code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM
&code_challenge_method=S256
&nonce=n-0S6_WzA2MjWhat happens at the authorize endpoint
Hep.gg validates the request, then handles the user's session:
- If the user is not signed in to Hep.gg, they are redirected to
/signinand returned to your authorize URL after authenticating. - The user must have a verified email. Without one, they are sent to their Hep.gg profile to add one before the flow can complete.
- Suspended users (Hep.gg access denied after a prior approval) are blocked.
- If your app has
allowed_groups, the user must belong to at least one.
On a fresh consent, Hep.gg shows a consent screen. If the user already has an active refresh token for your app, consent is skipped silently. Once approved, the browser is redirected back to your redirect_uri.
Success redirect
https://yourapp.example.com/callback?code=<authorization_code>&state=xyz123Verify that state matches the value you sent, then exchange the code immediately. Codes are single-use and expire after 60 seconds.
Error responses
- User declines consent: redirect to your
redirect_uriwith?error=access_denied&error_description=User+denied+consent&state=.... - Redirectable validation errors (for example
unsupported_response_type,invalid_scope,invalid_request): redirect to yourredirect_uriwith?error=<code>&error_description=...&state=.... - Non-redirectable errors (unknown
client_id, or aredirect_urithat is not registered): an HTTP400JSON body{ "ok": false, "error": "...", "error_description": "..." }. Hep.gg will not redirect to an unregistered URI.
Step 3: Exchange the code for tokens
https://hep.gg/api/v1/login/oauth/tokenAuth requiredCall this from your backend with Content-Type: application/x-www-form-urlencoded. The endpoint requires client authentication. Two methods are supported:
client_secret_basic: HTTP Basic header,Authorization: Basic base64(client_id:client_secret).client_secret_post:client_idandclient_secretin the form body.
Bad or missing client credentials return 401 with { "error": "invalid_client" }.
grant_typeauthorization_code.coderedirect_uriredirect_uri you used at the authorize endpoint.code_verifiercurl -X POST https://hep.gg/api/v1/login/oauth/token \
-u "$CLIENT_ID:$CLIENT_SECRET" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d grant_type=authorization_code \
-d code="$AUTH_CODE" \
-d redirect_uri="https://yourapp.example.com/callback" \
-d code_verifier="$CODE_VERIFIER"const body = new URLSearchParams({
grant_type: "authorization_code",
code: authCode,
redirect_uri: "https://yourapp.example.com/callback",
code_verifier: codeVerifier,
});
const basic = Buffer.from(`${clientId}:${clientSecret}`).toString("base64");
const res = await fetch("https://hep.gg/api/v1/login/oauth/token", {
method: "POST",
headers: {
Authorization: `Basic ${basic}`,
"Content-Type": "application/x-www-form-urlencoded",
},
body,
});
const tokens = await res.json();A 200 response is a token set:
{
"access_token": "eyJhbGciOiJSUzI1Ni␣...",
"id_token": "eyJhbGciOiJSUzI1Ni␣...",
"refresh_token": "Yz3w␣...",
"token_type": "Bearer",
"expires_in": 3600,
"scope": "openid profile email"
}id_tokenis present when theopenidscope was granted (always, since it is required).refresh_tokenis present only when theoffline_accessscope was granted. See Tokens and security.expires_inis the access token lifetime in seconds (default3600, configurable per app).
Errors return HTTP 400 or 401 with an OAuth error code: invalid_request, invalid_grant, unsupported_grant_type, or invalid_client.
Step 4: Read the user's claims
You can decode the id_token directly (after verifying it, see Tokens and security), or call the UserInfo endpoint with the access token.
https://hep.gg/api/v1/login/oauth/userinfoAuth requiredPass the access token as a bearer token. No body or query parameters; the identity and the granted scopes come from the token itself.
curl https://hep.gg/api/v1/login/oauth/userinfo \
-H "Authorization: Bearer $ACCESS_TOKEN"const res = await fetch("https://hep.gg/api/v1/login/oauth/userinfo", {
headers: { Authorization: `Bearer ${tokens.access_token}` },
});
const user = await res.json();A 200 response returns the claims allowed by the token's scopes (sub is always present):
{
"sub": "u_8f2a1c9b",
"email": "player@example.com",
"email_verified": true,
"name": "Player One",
"nickname": "Player One",
"preferred_username": "PlayerOne",
"picture": "https://cdn.hep.gg/avatars/u_8f2a1c9b.png",
"groups": ["staff", "beta"]
}A missing or invalid bearer token returns 401 with header WWW-Authenticate: Bearer realm="hep.gg" and body { "error": "invalid_token" }.
See Scopes and claims for the exact mapping of scope to claim.