.dev
Apps & OAuth

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
ParameterRequiredNotes
client_idYesFrom app registration
redirect_uriYesMust exactly match a registered URI
response_typeYesAlways code
scopeRecommendedSpace-separated; omitted → the app's registered scopes (or auth if none)
stateRecommendedRandom per session; verify on callback (CSRF protection)
code_challenge + code_challenge_method=S256Public clients: requiredPKCE — hash of a random verifier you keep locally
tokenOptionalPass-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_STATE

Denied — 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_SECRET

Run 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:

errorCause / fix
invalid_requestMissing/malformed parameter — check required params above
invalid_clientUnknown client_id, wrong secret, or inactive app
invalid_grantCode already used, expired, or redirect_uri/client_id mismatch with authorize
invalid_scopeRequested scopes outside the app's registration — never silently downgraded
access_deniedUser 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.

On this page