Help & integration guide
How to sign in, use OAuth, call the API, and manage sessions on this account service.
Overview
Central login, OAuth2-style authorization code flow, bearer-token API, email verification, and password reset for connected sites.
Sign in & accounts
Register
Visit /register. The first account becomes admin. Passwords must be at least 8 characters. A verification email is sent when SMTP is configured.
Sign in
Visit /login and submit your username or email and password. On success you receive the account_session cookie and are redirected to / or a safe return_to path (must start with /).
Sign out
POST to /logout with a CSRF token. This clears the cookie and revokes the server session.
OAuth for connected sites
This service implements an OAuth2-style authorization code flow. Each site is registered by domain name; the domain is the client_id.
1. Register your site (admin)
An admin adds your domain and callback paths at /admin, e.g. domain timfalken.com with path /auth/callback. Copy the client_secret immediately — it is only shown once.
2. Redirect the user to authorize
GET https://auth.timfalken.com/oauth/authorize
?client_id=timfalken.com
&redirect_uri=https://blog.timfalken.com/auth/callback
&response_type=code
&state=RANDOM_CSRF_TOKEN
stateis required — verify it matches on callback.- Query strings on
redirect_uriare stripped; register the path only. - Subdomains of the registered domain are allowed.
- Unauthenticated users are sent to
/loginand returned here after sign-in.
3. Exchange the code (server-side)
In your callback handler (e.g. /auth/callback), exchange the code on the server. Use the same redirect_uri as in step 2.
POST https://auth.timfalken.com/oauth/token
Content-Type: application/x-www-form-urlencoded
Accept: application/json
User-Agent: MySiteAccountClient/1.0
grant_type=authorization_code
&code=AUTHORIZATION_CODE
&redirect_uri=https://blog.timfalken.com/auth/callback
&client_id=timfalken.com
&client_secret=YOUR_SECRET
Response includes access_token, token_type: Bearer, expires_in, and a user object. Authorization codes expire in 5 minutes and are single-use.
Server-to-server HTTP client
Connected sites call /oauth/token and /api/* from PHP or another backend HTTP client. Send these headers on every account request:
User-Agent: MySiteAccountClient/1.0— identify your integration (site name + version).Accept: application/json— receive JSON success and error payloads.Content-Type: application/x-www-form-urlencoded— forPOST /oauth/token.Authorization: Bearer <access_token>— for/api/me,/api/session/extend, and/api/logout.
Store the access_token in your site session after a successful token exchange. Parse the JSON body in your callback handler and map error fields to your login error UI.
API endpoints
All API routes require Authorization: Bearer <access_token> obtained from the token exchange.
GET /api/me
Validate the token and return the current user.
GET https://auth.timfalken.com/api/me
Authorization: Bearer ACCESS_TOKEN
Accept: application/json
User-Agent: MySiteAccountClient/1.0
POST /api/session/extend
Reset the session expiry to another full hour. Call this during active use before the token expires.
POST https://auth.timfalken.com/api/session/extend
Authorization: Bearer ACCESS_TOKEN
Accept: application/json
User-Agent: MySiteAccountClient/1.0
Response: {"expires_at":"...","expires_in":3600}
POST /api/logout
Revoke the bearer token. Response: {"ok":true}.
Sessions
- Web sessions use the
account_sessioncookie (session_type=web). They work for the account site and OAuth consent, not as API bearer tokens. - API sessions use bearer tokens (
session_type=oauth) from/oauth/token. They are locked to the host that started the OAuth flow. - Default TTL is 1 hour for both. Bearer tokens can be extended with
POST /api/session/extend. - Expired or invalid tokens return
401on API routes. - Password reset revokes all sessions for that user.
Email verification & password reset
- Verification link:
GET /verify-email?token=...(valid 24 hours). - Resend while signed in:
POST /resend-verification. - Forgot password:
/forgot-password— only verified emails receive a reset link. - Reset link:
GET /reset-password?token=...(valid 1 hour). - Admins configure SMTP at
/admin?tab=smtpand can send a test email.
Admin
Admins manage OAuth domains, callback paths, client secrets, and SMTP at /admin.
- Register domains with at least one callback path.
- Regenerate client secrets from the domains tab; old secrets stop working immediately.
- SMTP: port 587 + TLS (STARTTLS) is typical; use 465 + SSL if your provider requires it.
Machine-readable specification
For scripts, integrations, and AI agents, use the JSON spec (also advertised in the page <head> via rel="alternate" and the Link response header):
https://auth.timfalken.com/help.json
Clients that send Accept: application/json (without preferring HTML) are redirected from /help to /help.json automatically.
The JSON includes all endpoints, flows, constants, authentication modes, and integration checklist. It is the canonical structured reference; this page is the human-readable view.
View embedded JSON spec
{
"schema_version": "1.0",
"service": "account-auth",
"title": "Account authentication service",
"description": "Central login, OAuth2-style authorization code flow, bearer-token API, email verification, and password reset for connected sites.",
"base_url": "https://auth.timfalken.com",
"documentation_urls": {
"human": "https://auth.timfalken.com/help",
"machine": "https://auth.timfalken.com/help.json"
},
"discovery": {
"alternate_link": {
"rel": "alternate",
"type": "application/json",
"href": "/help.json",
"location": "HTML head on GET /help"
},
"link_header": "Link: </help.json>; rel=\"alternate\"; type=\"application/json\"",
"accept_negotiation": "GET /help redirects to /help.json when Accept prefers application/json over text/html"
},
"requirements": {
"https_required_in_production": true,
"password_min_length": 8,
"redirect_uri_scheme": [
"https"
]
},
"constants": {
"session_ttl_seconds": 3600,
"oauth_code_ttl_seconds": 300,
"email_verify_ttl_seconds": 86400,
"password_reset_ttl_seconds": 3600,
"web_cookie_name": "account_session",
"csrf_field": "csrf"
},
"authentication": {
"web_session": {
"type": "http_only_cookie",
"cookie_name": "account_session",
"set_on": [
"POST /login",
"POST /register"
],
"cleared_on": [
"POST /logout"
],
"ttl_seconds": 3600,
"sliding_expiry": "Cookie max-age resets on login; use POST /api/session/extend for bearer tokens.",
"notes": [
"Web sessions use session_type=web. OAuth bearer tokens use session_type=oauth and are not accepted as web cookies.",
"Cookies are Secure when HTTPS is detected (including X-Forwarded-Proto: https)."
]
},
"api_bearer": {
"type": "authorization_header",
"header": "Authorization: Bearer <access_token>",
"obtained_via": "POST /oauth/token after authorization code exchange",
"ttl_seconds": 3600,
"host_lock": "OAuth tokens are locked to the redirect_uri host that initiated the flow."
},
"oauth_client": {
"client_id": "Registered domain name (e.g. timfalken.com)",
"client_secret": "Shown once when domain is registered or regenerated in /admin",
"redirect_uri_rules": [
"Scheme must be https (http only when ACCOUNT_ALLOW_HTTP=1).",
"Query strings are stripped during validation; register the path only.",
"Fragments are rejected.",
"Host must belong to the registered domain (including subdomains).",
"Path must be registered as a callback path for that domain."
]
},
"csrf": {
"required_on": "All browser POST forms",
"field_name": "csrf",
"invalid_handling": "Redirect with error or HTTP 400 for OAuth authorize POST"
},
"server_side_http_client": {
"applies_to": [
"POST /oauth/token",
"GET /api/me",
"POST /api/session/extend",
"POST /api/logout"
],
"required_headers": {
"User-Agent": "A descriptive client name and version, e.g. MySiteAccountClient/1.0",
"Accept": "application/json"
},
"token_exchange_headers": {
"Content-Type": "application/x-www-form-urlencoded",
"Accept": "application/json",
"User-Agent": "Your client identifier"
},
"api_headers": {
"Authorization": "Bearer <access_token>",
"Accept": "application/json",
"User-Agent": "Your client identifier"
},
"response_format": "JSON object; success and error payloads use Content-Type application/json",
"implementation_notes": [
"Perform token exchange in your OAuth callback handler on the server.",
"Use the same redirect_uri value as in the authorize redirect.",
"Store client_secret only in server-side configuration.",
"Parse the JSON body; map error fields to your site login error handling."
]
}
},
"flows": [
{
"id": "web_sign_in",
"title": "Sign in on the account site",
"audience": "human",
"steps": [
"Visit GET /login.",
"Submit POST /login with csrf, identifier (username or email), and password.",
"On success, account_session cookie is set and the browser redirects to / or return_to.",
"Sign out via POST /logout (requires csrf)."
]
},
{
"id": "web_register",
"title": "Create an account",
"audience": "human",
"steps": [
"Visit GET /register.",
"Submit POST /register with csrf, username, email, and password (min 8 characters).",
"Verification email is sent when SMTP is configured."
]
},
{
"id": "email_verification",
"title": "Verify email address",
"audience": "human",
"steps": [
"User receives email with link to GET /verify-email?token=...",
"Valid token marks email verified and redirects to /login.",
"Signed-in unverified users can POST /resend-verification.",
"Password reset only works for verified emails."
]
},
{
"id": "password_reset",
"title": "Reset password",
"audience": "human",
"steps": [
"Visit GET /forgot-password and submit email via POST /forgot-password.",
"If a verified account matches, email contains GET /reset-password?token=...",
"Submit new password via POST /reset-password; all sessions for that user are revoked."
]
},
{
"id": "oauth_authorization_code",
"title": "OAuth authorization code (connected sites)",
"audience": "integrator",
"steps": [
"Redirect browser to GET /oauth/authorize?client_id=DOMAIN&redirect_uri=URL&response_type=code&state=CSRF",
"Unauthenticated users are sent to /login?return_to=... and return after sign-in.",
"User approves on consent screen; POST /oauth/authorize issues redirect to redirect_uri?code=...&state=...",
"In your callback handler, POST /oauth/token with the same redirect_uri, User-Agent, and Accept application/json.",
"Store access_token server-side and use Authorization Bearer on /api/* routes."
]
},
{
"id": "server_side_http",
"title": "Server-to-server HTTP client",
"audience": "integrator",
"steps": [
"Set account base URL to your HTTPS auth host (e.g. https://auth.timfalken.com).",
"Send User-Agent identifying your site client (e.g. MySiteAccountClient/1.0).",
"Send Accept: application/json on POST /oauth/token and all /api/* requests.",
"POST /oauth/token with Content-Type application/x-www-form-urlencoded and the authorization code from the callback.",
"Parse the JSON response; on success store access_token and user in your site session.",
"Send Authorization: Bearer <access_token> on subsequent API calls with the same User-Agent and Accept headers."
]
},
{
"id": "session_extend",
"title": "Extend API session",
"audience": "integrator",
"steps": [
"Before access_token expires (default 1 hour), call POST /api/session/extend with Bearer token.",
"Response includes new expires_at and expires_in (3600).",
"Call periodically during active use; expired tokens return 401."
]
}
],
"endpoints": [
{
"method": "GET",
"path": "/",
"auth": "none",
"description": "Home page; shows sign-in state and links."
},
{
"method": "GET",
"path": "/help",
"auth": "none",
"description": "Human-readable integration and usage guide."
},
{
"method": "GET",
"path": "/help.json",
"auth": "none",
"description": "Machine-readable JSON specification of all auth functionality.",
"response": "application/json"
},
{
"method": "GET",
"path": "/login",
"auth": "none",
"description": "Sign-in form. Supports return_to query param (relative path only)."
},
{
"method": "POST",
"path": "/login",
"auth": "none",
"content_type": "application/x-www-form-urlencoded",
"body": [
"csrf",
"identifier",
"password",
"return_to?"
],
"success": "302 redirect; sets account_session cookie"
},
{
"method": "GET",
"path": "/register",
"auth": "none",
"description": "Registration form."
},
{
"method": "POST",
"path": "/register",
"auth": "none",
"content_type": "application/x-www-form-urlencoded",
"body": [
"csrf",
"username",
"email",
"password"
],
"success": "302 redirect; sets account_session cookie; may send verification email"
},
{
"method": "POST",
"path": "/logout",
"auth": "web_cookie",
"content_type": "application/x-www-form-urlencoded",
"body": [
"csrf"
],
"success": "302 to /login; clears cookie and revokes session"
},
{
"method": "GET",
"path": "/verify-email",
"auth": "none",
"query": [
"token"
],
"description": "Email verification link handler."
},
{
"method": "GET",
"path": "/forgot-password",
"auth": "none",
"description": "Request password reset form."
},
{
"method": "POST",
"path": "/forgot-password",
"auth": "none",
"body": [
"csrf",
"email"
],
"description": "Always shows generic success message (no email enumeration)."
},
{
"method": "GET",
"path": "/reset-password",
"auth": "none",
"query": [
"token"
],
"description": "Password reset form when token is valid."
},
{
"method": "POST",
"path": "/reset-password",
"auth": "none",
"body": [
"csrf",
"token",
"password",
"password_confirm"
]
},
{
"method": "POST",
"path": "/resend-verification",
"auth": "web_cookie",
"body": [
"csrf"
],
"description": "Resend verification email for signed-in unverified user."
},
{
"method": "GET",
"path": "/oauth/authorize",
"auth": "web_cookie (after login redirect)",
"query": [
"client_id",
"redirect_uri",
"response_type=code",
"state"
],
"description": "OAuth authorization; shows consent when authenticated.",
"example": "https://auth.timfalken.com/oauth/authorize?client_id=timfalken.com&redirect_uri=https%3A%2F%2Fblog.timfalken.com%2Fauth%2Fcallback&response_type=code&state=RANDOM"
},
{
"method": "POST",
"path": "/oauth/authorize",
"auth": "web_cookie",
"body": [
"csrf",
"client_id",
"redirect_uri",
"state"
],
"success": "302 redirect to redirect_uri with code and state"
},
{
"method": "POST",
"path": "/oauth/token",
"auth": "client_secret",
"content_type": "application/x-www-form-urlencoded",
"headers": {
"Content-Type": "application/x-www-form-urlencoded",
"Accept": "application/json",
"User-Agent": "Client identifier (recommended: YourSiteAccountClient/1.0)"
},
"body": [
"grant_type=authorization_code",
"code",
"client_id",
"client_secret",
"redirect_uri"
],
"response": {
"access_token": "string",
"token_type": "Bearer",
"expires_in": 3600,
"user": [
"id",
"username",
"email"
]
},
"example_request": "POST https://auth.timfalken.com/oauth/token\nContent-Type: application/x-www-form-urlencoded\nAccept: application/json\nUser-Agent: MySiteAccountClient/1.0\n\ngrant_type=authorization_code&code=...&redirect_uri=...&client_id=...&client_secret=..."
},
{
"method": "GET",
"path": "/api/me",
"auth": "bearer",
"response": {
"user": "user_object"
}
},
{
"method": "POST",
"path": "/api/session/extend",
"auth": "bearer",
"description": "Sliding session renewal; resets TTL to session_ttl_seconds.",
"response": {
"expires_at": "UTC datetime string",
"expires_in": 3600
},
"example_request": "POST https://auth.timfalken.com/api/session/extend\nAuthorization: Bearer ACCESS_TOKEN"
},
{
"method": "POST",
"path": "/api/logout",
"auth": "bearer",
"response": {
"ok": true
},
"description": "Revokes the bearer token server-side."
},
{
"method": "GET",
"path": "/admin",
"auth": "admin_web_cookie",
"query": [
"tab=domains|smtp"
],
"description": "Admin UI for OAuth domains and SMTP settings."
},
{
"method": "POST",
"path": "/admin/domains",
"auth": "admin_web_cookie",
"body": [
"csrf",
"domain",
"paths (newline-separated)"
]
},
{
"method": "POST",
"path": "/admin/smtp",
"auth": "admin_web_cookie",
"description": "Save SMTP settings; blank password keeps existing password."
},
{
"method": "POST",
"path": "/admin/smtp/test",
"auth": "admin_web_cookie",
"description": "Send test email using form values."
}
],
"user_object": {
"id": "integer user id",
"username": "string",
"email": "string",
"is_admin": "boolean; first registered user is admin",
"email_verified": "boolean; required for password reset emails"
},
"integration_checklist": [
"Deploy over HTTPS and set account_url in config.local.php if needed.",
"Admin registers client domain and callback paths at /admin.",
"Store client_secret server-side only.",
"Redirect users to GET /oauth/authorize with state for CSRF protection.",
"In the callback handler, POST /oauth/token with Content-Type application/x-www-form-urlencoded.",
"Send User-Agent and Accept application/json on every server-to-server account request.",
"Persist access_token server-side (session) and use Authorization Bearer on /api/* calls.",
"Call GET /api/me to validate tokens; POST /api/session/extend before expiry.",
"Call POST /api/logout to revoke bearer tokens on sign-out."
]
}