Authorization code flow
Step-by-step OAuth 2.0 authorization code walkthrough with PKCE, error handling and SDK helpers.
Build the authorize URL
Redirect the user's browser to the accounts host:
https://accounts.feeef.org/oauth/authorize
?client_id=CLIENT_ID
&redirect_uri=https%3A%2F%2Fyourapp.com%2Foauth%2Fcallback
&response_type=code
&scope=auth%20store.read
&state=RANDOM_STATE
&code_challenge=BASE64URL_S256_OF_VERIFIER
&code_challenge_method=S256| Parameter | Required | Notes |
|---|---|---|
client_id | Yes | From app registration |
redirect_uri | Yes | Must exactly match a registered URI |
response_type | Yes | Always code |
scope | Recommended | Space-separated; omitted → the app's registered scopes (or auth if none) |
state | Recommended | Random per session; verify on callback (CSRF protection) |
code_challenge + code_challenge_method=S256 | Public clients: required | PKCE — hash of a random verifier you keep locally |
token | Optional | Pass-through session token for SPAs that already hold a Feeef token |
PKCE in 3 lines — generate a random 43–128 char verifier, send its S256 hash at authorize, send the raw verifier at token exchange:
const verifier = crypto.randomUUID() + crypto.randomUUID() // or random charset
const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(verifier))
const challenge = btoa(String.fromCharCode(...new Uint8Array(digest)))
.replaceAll('+', '-').replaceAll('/', '_').replace(/=+$/, '')If the user isn't signed in, accounts redirects them to signin?next=<authorize-url> and back.
Then the consent screen shows your app name, logo and requested scopes with Accept / Reject.
Handle the callback
Approved — your redirect_uri receives:
https://yourapp.com/oauth/callback?code=AUTHORIZATION_CODE&state=RANDOM_STATEDenied — you get an error instead:
https://yourapp.com/oauth/callback?error=access_denied&error_description=...&state=...Verify state matches what you stored, then exchange the code promptly — codes are
single-use and short-lived (~10 minutes).
Exchange the code for a token
POST form-encoded to the API host.
curl -X POST https://api.feeef.org/v1/oauth/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d grant_type=authorization_code \
-d code=AUTHORIZATION_CODE \
-d redirect_uri=https://yourapp.com/oauth/callback \
-d client_id=CLIENT_ID \
-d client_secret=CLIENT_SECRETRun this server-side only. Add code_verifier too if you sent a challenge (recommended).
Success:
{
"access_token": "oat_...",
"token_type": "Bearer",
"expires_in": 7776000,
"scope": "auth store.read"
}Use and manage the token
curl https://api.feeef.org/v1/users/auth \
-H "Authorization: Bearer oat_..."Sign the user out by revoking:
curl -X POST https://api.feeef.org/v1/oauth/revoke \
-H "Content-Type: application/x-www-form-urlencoded" \
-d token=oat_...With the SDKs
import { FeeeF, buildAuthorizeUrl } from 'feeef'
// 1. Send the user to consent (accounts origin, no /v1)
const url = buildAuthorizeUrl('https://accounts.feeef.org', {
clientId: CLIENT_ID,
redirectUri: 'https://yourapp.com/oauth/callback',
scope: 'auth store.read',
state,
codeChallenge, // PKCE
codeChallengeMethod: 'S256',
})
// 2. Exchange on your callback route (API base)
const feeef = new FeeeF({ apiKey: '', baseURL: 'https://api.feeef.org/v1' })
const token = await feeef.oauth.exchangeAuthorizationCode({
code,
redirectUri: 'https://yourapp.com/oauth/callback',
clientId: CLIENT_ID,
clientSecret: CLIENT_SECRET, // or codeVerifier for public clients
})
// 3. Who is this?
const { user } = await feeef.users.signinWithToken(token.access_token)Also available: feeef.oauth.revokeToken({ token }) and feeef.oauth.introspectToken({ token }).
Errors
OAuth endpoints return RFC 6749 error bodies:
error | Cause / fix |
|---|---|
invalid_request | Missing/malformed parameter — check required params above |
invalid_client | Unknown client_id, wrong secret, or inactive app |
invalid_grant | Code already used, expired, or redirect_uri/client_id mismatch with authorize |
invalid_scope | Requested scopes outside the app's registration — never silently downgraded |
access_denied | User pressed Reject on consent |
login_required (JSON 401) | Non-browser client hit authorize unauthenticated; body includes login_url |
Security checklist
- Register every environment's redirect URI (dev, staging, prod) — matching is exact.
- Always send and verify
state. - Use PKCE everywhere, even for confidential clients.
- Client secrets live server-side only; rotate via Regenerate secret if leaked.
- Exchange codes immediately; never log full codes or tokens.