OAuth 2.0
Application registration, authorization code, PKCE, scopes, refresh rotation and revocation with examples.
Register an application
Open Developers → Applications, choose an alphanumeric name, optional CDN image, requested scopes and exact redirect URLs. Save a confidential client secret immediately: only its hash is stored and plaintext is shown once. Rotate it if lost or exposed.
HTTP redirects are allowed only for localhost, 127.0.0.1, ::1 or hosts ending .local; every other redirect must use HTTPS. Matching is exact. Native/public clients cannot keep a secret and must require S256 PKCE.
Authorization request
GET https://api.beatkhana.com/api/oauth/authorize?
response_type=code&
client_id=YOUR_CLIENT_ID&
redirect_uri=https%3A%2F%2Fexample.com%2Fcallback&
scope=rest%3Auser%3Aread%20ws%3Aread&
state=RANDOM_CSRF_VALUE&
code_challenge=BASE64URL_SHA256_VERIFIER&
code_challenge_method=S256Generate unpredictable state per attempt, store it in the user's server session, and compare it in constant time after redirect. A successful approval returns code and state. Denial returns an OAuth error. Never exchange an unverified callback.
Exchange the code
Authorization codes are single-use and expire after ten minutes. Confidential clients should authenticate with HTTP Basic; public clients provide client_id and code_verifier without a secret.
curl -u 'CLIENT_ID:CLIENT_SECRET' \
-H 'Content-Type: application/x-www-form-urlencoded' \
-d 'grant_type=authorization_code' \
-d 'code=RETURNED_CODE' \
-d 'redirect_uri=https://example.com/callback' \
https://api.beatkhana.com/api/oauth/token{
"access_token": "...",
"token_type": "Bearer",
"expires_in": 5184000,
"refresh_token": "...",
"scope": "rest:user:read ws:read"
}Use Authorization: Bearer ACCESS_TOKEN for API calls.
Refresh rotation
curl -u 'CLIENT_ID:CLIENT_SECRET' \
-H 'Content-Type: application/x-www-form-urlencoded' \
-d 'grant_type=refresh_token' \
-d 'refresh_token=CURRENT_REFRESH_TOKEN' \
https://api.beatkhana.com/api/oauth/tokenEvery successful refresh invalidates the submitted refresh token and returns a replacement. Store the replacement atomically. Reuse of rotated tokens must be treated as a compromised session.
Revoke
POST form-encoded token=... with client authentication to https://api.beatkhana.com/api/oauth/revoke. Deleting an OAuth application deletes its pending requests, codes and refresh tokens. Rotating the application secret does not justify putting it in a browser or desktop binary.
Scopes
| Scope | Capability |
|---|---|
rest:user:read | Read /users/@me. |
rest:user:write | Update self and implies user read. |
rest:read | Read REST resources and implies user read. |
rest:write | Request REST mutations and implies REST/user read; user roles still apply. |
ws:read | Read-only socket subscriptions/data. |
ws:write | Socket commands and implies ws:read; roles still apply. |
tournamentassistant | Specialized TA management/client access. |
tournamentassistant:game | Specialized in-game TA access. |
Server example
app.get('/auth/beatkhana/callback', async (req, res) => {
verifyState(req.query.state, req.session.oauthState);
const body = new URLSearchParams({
grant_type: 'authorization_code',
code: String(req.query.code),
redirect_uri: 'https://example.com/auth/beatkhana/callback',
});
const token = await fetch('https://api.beatkhana.com/api/oauth/token', {
method: 'POST',
headers: {
authorization: `Basic ${Buffer.from(`${clientId}:${clientSecret}`).toString('base64')}`,
'content-type': 'application/x-www-form-urlencoded',
},
body,
});
if (!token.ok) return res.status(502).send(await token.text());
await saveTokensAtomically(req.session.userId, await token.json());
res.redirect('/');
});