Authorization
Chess Scanner uses the standard OAuth 2.0 Authorization Code flow with PKCE, and PKCE is required for every client. You run this flow once per user to get an access token, then call the API with it.
These endpoints live on the OAuth provider:
| Step | Endpoint |
|---|---|
| Authorize (browser redirect) | GET https://chessscanner.com/api/auth/oauth2/authorize |
| Token exchange (server-side) | POST https://chessscanner.com/api/auth/oauth2/token |
1. Create a PKCE pair
Generate a random code_verifier and derive a code_challenge from it:
import { randomBytes, createHash } from "node:crypto";
const codeVerifier = randomBytes(48).toString("base64url");
const codeChallenge = createHash("sha256")
.update(codeVerifier)
.digest()
.toString("base64url");
Keep the code_verifier around for step 3, for example in a short-lived,
httpOnly cookie. Also generate a random state value to protect against CSRF.
2. Redirect the user to authorize
Send the user's browser to the authorize endpoint:
https://chessscanner.com/api/auth/oauth2/authorize
?response_type=code
&client_id=YOUR_CLIENT_ID
&redirect_uri=https://your-app.com/oauth/callback
&scope=user.read databases.read games.read offline_access
&state=RANDOM_STATE
&code_challenge=CODE_CHALLENGE
&code_challenge_method=S256
The user signs in if needed, approves the consent screen, and comes back to your
redirect_uri with a code and the state:
https://your-app.com/oauth/callback?code=AUTH_CODE&state=RANDOM_STATE
Add &prompt=consent to show the consent screen every time. This is handy while
testing, since an already-authorized user is otherwise sent straight back.
3. Exchange the code for a token
On your server, check that state matches what you sent, then POST to the token
endpoint as application/x-www-form-urlencoded:
curl -X POST https://chessscanner.com/api/auth/oauth2/token \
-d grant_type=authorization_code \
-d code=AUTH_CODE \
-d redirect_uri=https://your-app.com/oauth/callback \
-d client_id=YOUR_CLIENT_ID \
-d client_secret=YOUR_CLIENT_SECRET \
-d code_verifier=YOUR_CODE_VERIFIER
Response:
{
"access_token": "…",
"token_type": "Bearer",
"expires_in": 3600,
"scope": "user.read databases.read games.read"
}
4. Use the token
Call the API with the access token:
curl https://api.chessscanner.com/api/v1/me \
-H "Authorization: Bearer ACCESS_TOKEN"
See the API Reference for all endpoints.
Refresh tokens (long-lived access)
Ask for the offline_access scope (as in the example above) and you'll get a
refresh token along with the access token:
{
"access_token": "…",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "…",
"scope": "user.read databases.read games.read offline_access"
}
Store the refresh token securely. When the access token expires, trade the refresh token for a new one without any user interaction:
curl -X POST https://chessscanner.com/api/auth/oauth2/token \
-d grant_type=refresh_token \
-d refresh_token=YOUR_REFRESH_TOKEN \
-d client_id=YOUR_CLIENT_ID \
-d client_secret=YOUR_CLIENT_SECRET
That's how a backend can keep pulling a user's games over time. If the response
includes a new refresh_token, replace the one you stored.
Without offline_access you don't get a refresh token, so you have to run the
flow again once the access token expires. Refresh tokens stop working when the
user or the app owner revokes the app's tokens.
Good to know
- The token endpoint uses
client_secret_post, so sendclient_idandclient_secretin the request body (as shown above). - The scopes you request have to be a subset of what your app was granted,
otherwise you get
invalid_scope. - Tokens are opaque, not JWTs. Validate them by calling the API rather than decoding them.