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:

FieldNotes
App typeConfidential (server-side, gets a client secret) or Public (SPA/mobile, no secret, PKCE required).
Redirect URIsExact-match list. Absolute https:// URLs; http:// only for localhost. No fragments.
ScopesSubset of the supported scopes (see below). openid is always included.
Homepage / logoOptional. 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
PurposeEndpoint
Authorizationhttps://accounts.bodek.us/authorize
Tokenhttps://accounts.bodek.us/token
UserInfohttps://accounts.bodek.us/userinfo
Introspectionhttps://accounts.bodek.us/introspect
Revocationhttps://accounts.bodek.us/revoke

Scopes

ScopeGrants
openidMarks the request as OpenID Connect. Returns a sub (stable user id).
profileName, given/family name, picture, account type.
emailEmail address and whether it is verified.
offline_accessIssues 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.

  1. Create a random code_verifier (43–128 chars).
  2. Derive code_challenge = BASE64URL(SHA256(code_verifier)).
  3. Send code_challenge and code_challenge_method=S256 on the authorize request.
  4. Send the original code_verifier on 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

GET https://accounts.bodek.us/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

POST https://accounts.bodek.us/introspect

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

POST https://accounts.bodek.us/revoke

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 state value 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.

errorMeaning
invalid_requestA required parameter is missing or malformed (e.g. PKCE missing for a public client).
invalid_clientUnknown client, wrong secret, or the app is disabled.
invalid_grantCode/refresh token is expired, already used, or the redirect URI doesn't match.
unsupported_response_typeOnly response_type=code is supported.
access_deniedThe user declined.