Sign in with Bodek
Bodek Accounts is a standard OAuth 2.0 and OpenID Connect provider. Add a “Sign in with Bodek” button to any site or app: your users authenticate with their existing Bodek account — including their two-factor method — and you receive a verified identity without ever handling their password.
Issuer https://accounts.bodek.usFlow Authorization Code (+ PKCE for public clients)
Tokens Opaque bearer access tokens, optional refresh tokens
The provider implements the authorization code grant, refresh-token grant, a UserInfo
endpoint, token introspection and revocation, plus a discovery document. Access tokens are
opaque (validate them at /userinfo or /introspect rather than
decoding them locally).
Register an app
Create a client in the OAuth apps console. You choose:
| Field | Notes |
|---|---|
| App type | Confidential (server-side, gets a client secret) or Public (SPA/mobile, no secret, PKCE required). |
| Redirect URIs | Exact-match list. Absolute https:// URLs;
http:// only for localhost. No fragments. |
| Scopes | Subset of the supported scopes (see below). openid is always included. |
| Homepage / logo | Optional. Shown to users on the consent screen. |
On creation you receive a client_id and, for confidential apps, a
client_secret shown once. Store the secret server-side only.
You can rotate it, edit redirect URIs and scopes, disable, or delete the app at any time.
Discovery & endpoints
Most OIDC libraries configure themselves from the discovery document:
GET https://accounts.bodek.us/.well-known/openid-configuration
| Purpose | Endpoint |
|---|---|
| Authorization | https://accounts.bodek.us/authorize |
| Token | https://accounts.bodek.us/token |
| UserInfo | https://accounts.bodek.us/userinfo |
| Introspection | https://accounts.bodek.us/introspect |
| Revocation | https://accounts.bodek.us/revoke |
Scopes
| Scope | Grants |
|---|---|
openid | Marks the request as OpenID Connect. Returns a sub (stable user id). |
profile | Name, given/family name, picture, account type. |
email | Email address and whether it is verified. |
offline_access | Issues a refresh token so you can act after the access token expires. |
Requested scopes are intersected with the scopes you registered for the app and with what the provider supports — anything outside that set is silently dropped. The granted scope is returned with the token.
Authorization code flow (server-side)
For confidential apps with a backend. Three steps: redirect the user, receive a code on your callback, exchange the code for tokens from your server.
1. Redirect the user to /authorize
Generate a random state, store it in the session, and send the user to:
https://accounts.bodek.us/authorize?
response_type=code
&client_id=YOUR_CLIENT_ID
&redirect_uri=https://your-app.com/oauth/callback
&scope=openid%20profile%20email
&state=RANDOM_STATE
Optional prompt=login forces a fresh login; prompt=select_account
shows the account chooser.
2. Handle the callback
Bodek redirects back with ?code=…&state=… (or ?error=…). Verify
state matches what you stored, then exchange the code.
3. Exchange the code at /token
curl -X POST https://accounts.bodek.us/token \
-d grant_type=authorization_code \
-d code=THE_CODE \
-d redirect_uri=https://your-app.com/oauth/callback \
-d client_id=YOUR_CLIENT_ID \
-d client_secret=YOUR_CLIENT_SECRET
Minimal PHP example
<?php
session_start();
$issuer = 'https://accounts.bodek.us';
$clientId = 'YOUR_CLIENT_ID';
$clientSecret = 'YOUR_CLIENT_SECRET';
$redirectUri = 'https://your-app.com/oauth/callback';
// Step 1 — start.php
$_SESSION['oauth_state'] = bin2hex(random_bytes(16));
$params = http_build_query([
'response_type' => 'code',
'client_id' => $clientId,
'redirect_uri' => $redirectUri,
'scope' => 'openid profile email',
'state' => $_SESSION['oauth_state'],
]);
header('Location: ' . $issuer . '/authorize?' . $params);
// Step 2 + 3 — callback.php
if (($_GET['state'] ?? '') !== ($_SESSION['oauth_state'] ?? '!')) {
http_response_code(400); exit('Bad state');
}
$ch = curl_init($issuer . '/token');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POSTFIELDS => http_build_query([
'grant_type' => 'authorization_code',
'code' => $_GET['code'] ?? '',
'redirect_uri' => $redirectUri,
'client_id' => $clientId,
'client_secret' => $clientSecret,
]),
]);
$tok = json_decode(curl_exec($ch), true);
// Fetch the verified profile
$ch = curl_init($issuer . '/userinfo');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $tok['access_token']],
]);
$user = json_decode(curl_exec($ch), true);
// $user['sub'], $user['email'], $user['name'] …
Public apps (PKCE)
Single-page apps and mobile clients have no safe place for a secret, so register a
public app and use PKCE. The provider requires a
code_challenge for public clients.
- Create a random
code_verifier(43–128 chars). - Derive
code_challenge = BASE64URL(SHA256(code_verifier)). - Send
code_challengeandcode_challenge_method=S256on the authorize request. - Send the original
code_verifieron the token request (no client secret).
// Browser (Web Crypto)
const verifier = base64url(crypto.getRandomValues(new Uint8Array(32)));
const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(verifier));
const challenge = base64url(new Uint8Array(digest));
sessionStorage.setItem('pkce_verifier', verifier);
location = 'https://accounts.bodek.us/authorize?' + new URLSearchParams({
response_type: 'code',
client_id: 'YOUR_CLIENT_ID',
redirect_uri: 'https://your-app.com/callback',
scope: 'openid profile email',
state: crypto.randomUUID(),
code_challenge: challenge,
code_challenge_method: 'S256',
});
// On callback — exchange with the verifier, no secret
fetch('https://accounts.bodek.us/token', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'authorization_code',
code: new URLSearchParams(location.search).get('code'),
redirect_uri: 'https://your-app.com/callback',
client_id: 'YOUR_CLIENT_ID',
code_verifier: sessionStorage.getItem('pkce_verifier'),
}),
});
base64url = standard base64 with +/ → -_ and trailing = stripped.
Token response
{
"access_token": "…",
"token_type": "Bearer",
"expires_in": 3600,
"scope": "openid profile email",
"refresh_token": "…" // only when offline_access was granted
}
Access tokens live for one hour. Authorization codes are single-use and expire in 60 seconds — replaying a code revokes every token derived from it.
UserInfo
Send the access token as a bearer token to retrieve the user's claims (filtered by the granted scope).
curl https://accounts.bodek.us/userinfo \
-H "Authorization: Bearer ACCESS_TOKEN"
{
"sub": "42",
"name": "Ada Lovelace",
"given_name": "Ada",
"family_name": "Lovelace",
"picture": "https://accounts.bodek.us/uploads/profiles/…jpg",
"email": "ada@example.com",
"email_verified": true
}
Use sub as the stable account identifier in your database — it never changes,
even if the user updates their email or name.
Refresh tokens
Request the offline_access scope to receive a refresh token. Exchange it for a
fresh access token when the old one expires:
curl -X POST https://accounts.bodek.us/token \
-d grant_type=refresh_token \
-d refresh_token=YOUR_REFRESH_TOKEN \
-d client_id=YOUR_CLIENT_ID \
-d client_secret=YOUR_CLIENT_SECRET # omit for public clients
Refresh tokens rotate: each use returns a new refresh token and invalidates the old one. Re-using a spent refresh token is treated as theft and revokes the whole chain — always store the latest token you receive.
Introspection
Confidential apps can check whether a token is still active. Authenticate with your client credentials.
curl -X POST https://accounts.bodek.us/introspect \
-d token=ACCESS_TOKEN \
-d client_id=YOUR_CLIENT_ID \
-d client_secret=YOUR_CLIENT_SECRET
{ "active": true, "scope": "openid profile email", "client_id": "…", "sub": "42", "exp": 1750000000 }
Revoke a token
Invalidate an access or refresh token immediately — for example on logout.
curl -X POST https://accounts.bodek.us/revoke \
-d token=THE_TOKEN \
-d token_type_hint=refresh_token \
-d client_id=YOUR_CLIENT_ID \
-d client_secret=YOUR_CLIENT_SECRET
Two-factor & security
Authentication always happens on Bodek Accounts, never in your app. When a user authorizes your
app, they complete their entire Bodek login first — password plus any enabled
second factor (authenticator app, email code, or passkey). Only after that, and after they pick
an account and approve the requested scopes on the consent screen, does Bodek issue an
authorization code to your redirect_uri. Your app cannot bypass or weaken that step.
- Always send and verify a unique
statevalue to defend against CSRF. - Use PKCE for any client that can't keep a secret (and you may use it for confidential clients too).
- Keep the client secret on your server. If it leaks, rotate it from the console — the old one stops working at once.
- Redirect URIs are exact-match: register every callback you use.
- Disabling or deleting an app revokes its live tokens immediately.
Errors
Authorization errors come back on your redirect URI as query parameters
(error, error_description, state). Token, introspection
and revocation errors return JSON with an error code.
| error | Meaning |
|---|---|
invalid_request | A required parameter is missing or malformed (e.g. PKCE missing for a public client). |
invalid_client | Unknown client, wrong secret, or the app is disabled. |
invalid_grant | Code/refresh token is expired, already used, or the redirect URI doesn't match. |
unsupported_response_type | Only response_type=code is supported. |
access_denied | The user declined. |