OAuth Architecture
Authentication strategy, principles, and module design for rforssen.net. Two complementary models — In-App and Edge — each chosen deliberately for the job they do.
Mental Model
Google proves identity once per login. The backend then creates a session ticket — a secure cookie — and trusts that cookie on every subsequent request. Google is not consulted again until the session ends.
api.rforssen.net as long as credentials: "include" is set.
Authentication is explicit and intentional — it is invoked at three well-defined points only: page load, after popup login, and on logout. It is not a background service polling continuously.
Authentication Flow
The full popup-based OAuth 2.0 / OpenID Connect login sequence:
GET /auth/login/google?popup=true&redirect=… in a popup window./auth/login/google/callback. Flask verifies state + nonce, exchanges the auth code for tokens, extracts user identity and role, and writes a secure HttpOnly session cookie (domain=.rforssen.net)./oauth-popup-complete.html. That page posts { type: "auth:success" } on BroadcastChannel("auth"), then closes itself. The main window receives the message.GET /auth/me (with credentials: "include"). Backend returns the authenticated user. UI updates — login badge replaced by user avatar/pill./auth/me may return 401. This is expected when no session exists. The frontend treats 401 as "anonymous / public" — never as an error condition.
Popup completion page
The popup closes itself via script to avoid COOP restrictions. The opener must never attempt to call popup.close().
// /oauth-popup-complete.html
try {
const bc = new BroadcastChannel("auth");
bc.postMessage({ type: "auth:success" });
bc.close();
} catch (e) {}
setTimeout(() => window.close(), 150);js
Module Layout
The auth library is split into a shared core (reused by all apps) and a per-app UI layer.
/rforssen_net/
js/
auth/
authClient.js # shared
initAuth.js # shared
setupAuthUI.js # shared
oauth-popup-complete.html
/autonomo/expenses/
js/
authUI.js # app-specific
index.js
/media/
js/
authUI.js # app-specific
index.jstree
| Module | Responsibility | Scope |
|---|---|---|
| authClient.js | fetchAuth() → /auth/me, loginWithPopup() → opens popup + listens on BroadcastChannel, logout() → POST /auth/logout |
shared |
| initAuth.js | Instantiates authClient with apiBase, popup redirect URL, and channel name. Returns { authClient }. |
shared |
| setupAuthUI.js | One-call entry point. Fetches initial auth state → renders badge → wires login/logout handlers. The only file a page needs to import from /js/auth/. |
shared |
| authUI.js | App-specific badge and menu rendering. Exports renderAuthBadge and wireAuthUI. Copy from an existing app and restyle. |
per-app |
Authentication Strategies
The platform deliberately supports two models. They are not competing — they solve different problems.
In-App Authentication
- Page loads for everyone
- UI adapts to login state
- Supports roles & personalization
- Public + private content coexist
- Family-friendly UX
Edge Authentication
- Nothing renders without login
- Traefik + oauth2-proxy gate
- App never sees anonymous traffic
- No information leakage
- Fail-safe by design
In-App Authentication
Use this when the page should load for anyone but behave differently based on login state and role.
Typical use cases: media platform, genealogy, landing page with controlled visibility, any family-facing application.
How it works
On page load the app calls /auth/me. The response determines what content is shown and what actions are enabled. The backend always enforces authorization — frontend role checks are UX convenience only.
The three authentication entry points
Every in-app page invokes authentication in exactly three situations — no more:
| When | Mechanism | Result |
|---|---|---|
| Page load | setupAuthUI() → fetchAuth() → GET /auth/me |
Initial badge render, content gating |
| After popup login | BroadcastChannel auth:success → fetchAuth() |
Re-render badge, unlock features |
| Logout | POST /auth/logout → fetchAuth() |
Reset to anonymous state |
Adding auth to a new page
See the dedicated how-to guide for the step-by-step minimal pattern:
Edge Authentication
Use this when nothing should be visible without login. No public mode. No partial rendering. No information leakage.
Typical use cases: phpMyAdmin, admin dashboards, cluster management tools, sensitive internal pages.
How it works
Traffic flows through Traefik → oauth2-proxy → application. Unauthenticated requests are blocked at the proxy — the application never receives them. The app itself requires no auth code.
Request
→ Traefik Ingress
→ oauth2-proxy
authenticated? → forward to app
not authenticated? → block (Google login redirect)flow
Optional identity headers
oauth2-proxy can inject X-Auth-User and X-Auth-Email headers if the app wants to know who is logged in. The app may use them or ignore them.
Which Strategy To Use?
| Need | Strategy |
|---|---|
| User-facing experience with public + private content | In-App Auth |
| Roles and personalization | In-App Auth |
| Family or guest usability | In-App Auth |
| Admin tools, phpMyAdmin | Edge Auth |
| Cluster dashboards, internal tools | Edge Auth |
| Nothing visible without login | Edge Auth |
API Endpoints
| Endpoint | Purpose |
|---|---|
| GET /auth/me | Return current session user. Returns 401 if not logged in. Single source of truth for auth state. |
| GET /auth/login/google | Start Google OAuth flow. Accepts ?popup=true&redirect=…. |
| GET /auth/login/google/callback | OAuth callback. Verifies state/nonce, exchanges code for tokens, writes session cookie. |
| POST /auth/logout | Clear the session cookie and end the session. |
/auth/me Contract
Not authenticated (HTTP 401)
{
"authenticated": false,
"is_authorized": false,
"user": null,
"role": "public"
}json
Authenticated (HTTP 200)
{
"authenticated": true,
"is_authorized": true,
"user": {
"email": "…",
"name": "…",
"role": "superuser | whitelisted | public"
},
"role": "superuser | whitelisted | public"
}json
/auth/me is the single source of truth for auth state. The frontend never guesses; it always derives state from this endpoint or from API response codes.