Architecture reference

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.

Identity: Google OAuth 2.0
Session: Flask server-side cookie
Domain: .rforssen.net

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.

The key insight: Google provides identity one time. The platform enforces trust continuously via its own session cookie. The browser sends that cookie automatically on every request to 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:

1
Popup opens login URL
Browser opens GET /auth/login/google?popup=true&redirect=… in a popup window.
2
Flask redirects to Google
Flask sets CSRF state + nonce in the session, then redirects the popup to Google's OAuth consent screen.
3
User authenticates with Google
Google shows the login / consent screen. User completes it.
4
Callback — session created
Google redirects back to /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).
5
Popup completion page notifies main window
Flask redirects the popup to /oauth-popup-complete.html. That page posts { type: "auth:success" } on BroadcastChannel("auth"), then closes itself. The main window receives the message.
6
Main window refreshes auth state
Main window calls GET /auth/me (with credentials: "include"). Backend returns the authenticated user. UI updates — login badge replaced by user avatar/pill.
Note: On initial page load, /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
ModuleResponsibilityScope
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
Design decision: keep core auth logic centralized and versioned once. Let each app own its own UX — badge location, dropdown style, redirect behavior — without touching the shared core.

Authentication Strategies

The platform deliberately supports two models. They are not competing — they solve different problems.

Strategy 1

In-App Authentication

  • Page loads for everyone
  • UI adapts to login state
  • Supports roles & personalization
  • Public + private content coexist
  • Family-friendly UX
Strategy 2

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.

Rule: Frontend filtering improves UX. Backend authorization provides real security. Never rely solely on frontend role checks to protect data or operations.

The three authentication entry points

Every in-app page invokes authentication in exactly three situations — no more:

WhenMechanismResult
Page load setupAuthUI()fetchAuth()GET /auth/me Initial badge render, content gating
After popup login BroadcastChannel auth:successfetchAuth() Re-render badge, unlock features
Logout POST /auth/logoutfetchAuth() 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.

When NOT to use Edge Auth: if the public should see the page, if family needs a normal UX, or if different role experiences are needed. Edge Auth is a hard gate — it has no concept of roles or partial access.

Which Strategy To Use?

NeedStrategy
User-facing experience with public + private contentIn-App Auth
Roles and personalizationIn-App Auth
Family or guest usabilityIn-App Auth
Admin tools, phpMyAdminEdge Auth
Cluster dashboards, internal toolsEdge Auth
Nothing visible without loginEdge Auth
Platform philosophy: Family apps and user experiences → In-App Auth. Platform, admin, and infrastructure → Edge Auth. They are deliberately chosen tools, each excellent at its job.

API Endpoints

EndpointPurpose
GET /auth/meReturn current session user. Returns 401 if not logged in. Single source of truth for auth state.
GET /auth/login/googleStart Google OAuth flow. Accepts ?popup=true&redirect=….
GET /auth/login/google/callbackOAuth callback. Verifies state/nonce, exchanges code for tokens, writes session cookie.
POST /auth/logoutClear 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
Invariant: /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.