Help & integration guide

How to sign in, use OAuth, call the API, and manage sessions on this account service.

Home

Overview

Central login, OAuth2-style authorization code flow, bearer-token API, email verification, and password reset for connected sites.

Base URL
https://auth.timfalken.com
Session lifetime
3600 seconds (1 hour)
Web cookie
account_session (HttpOnly, SameSite=Lax)

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
  • state is required — verify it matches on callback.
  • Query strings on redirect_uri are stripped; register the path only.
  • Subdomains of the registered domain are allowed.
  • Unauthenticated users are sent to /login and 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 — for POST /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_session cookie (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 401 on 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=smtp and 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."
    ]
}