OAuth Architecture
rforssen.net – Authentication Strategy, Principles & Design
Authentication Mental Model
Principle
- Google proves identity once per login session
- The backend creates a session ticket (Flask session cookie)
- The browser automatically sends that cookie on every backend request
- The backend trusts its own cookie — not Google each time
This is how mature OAuth-backed platforms are designed:
Google provides identity
one time, the platform enforces trust continuously.
Authentication Flow
1️⃣ Popup navigates to /auth/login/google
2️⃣ That request redirects the popup to Google
3️⃣ Google authenticates the user
4️⃣ Google redirects the popup to /auth/login/google/callback
Backend now:
– verifies state
– verifies nonce
– exchanges auth code for tokens
– extracts user identity
– creates Flask session cookie (stores user identity securely)
5️⃣ Backend redirects popup to oauth-popup-complete.html
6️⃣ That page notifies the main window via BroadcastChannel
7️⃣ Popup closes
8️⃣ Main page now knows the user is logged in
9️⃣ From now on every backend request automatically includes the session cookie —
so the backend always knows who the user is, simply by verifying the cookie.
This document defines how authentication works across the rforssen.net platform.
Different apps have different security needs, so the platform deliberately supports two authentication models.
Roland maintains multiple environments:
- Personal development tools
- Administrative / platform tools
- Family-facing applications
- Mixed public / private services
Authentication Strategies
No single authentication pattern fits all of them. Therefore the platform supports
two complementary authentication strategies:
1️⃣ In-App Authentication
2️⃣ Edge Authentication
1️⃣ In-App Authentication
User-Aware UI + Filtered Content
Objective
Use this strategy when the page or app should:
- Load even if the user is not logged in
- Adapt its behavior depending on login
- Support roles such as “public / family / Roland”
- Provide rich UI experience
- Allow public + private content in same app
Typical use cases:
- Media platform
- Genealogy
- Landing page with controlled visibility
- Any family-facing application
Model
User opens page → Page loads → Page calls /auth/me
If authenticated → personalize UI
If not authenticated → show login + limited content
Backend still enforces actual security.
Backend Requirements
Google OAuth + Flask session cookies.
| Endpoint | Purpose |
| /auth/login/google | Start login |
| /auth/login/google/callback | Complete login |
| /auth/me | Check who user is |
| /auth/logout | Log out |
/auth/me Contract
If user is NOT authenticated:
{
"authenticated": false
}
If user IS authenticated:
{
"authenticated": true,
"user": {
"email": "...",
"name": "...",
"sub": "google-id"
},
"role": "me | family | public"
}
Frontend Responsibilities
- Call
/auth/me at page load
- Store auth in app state
- Show login if not logged in
- Show avatar + logout if logged in
- Enable / disable features depending on role
Frontend role logic improves UX.
Backend checks provide real security.
Backend Security Rules
- Verify session cookie
- Verify identity
- Verify role
- Reject unauthorized calls
Cookie Security Requirements
SESSION_COOKIE_SECURE = True
SESSION_COOKIE_HTTPONLY = True
SESSION_COOKIE_SAMESITE = "Lax"
Redirect Safety
Do NOT allow redirect to arbitrary domains.
Only allow trusted redirect destinations.
Summary
Use In-App Authentication when you want public + authenticated users,
personalization, roles, and mixed-audience experiences.
🔁 Authentication Invocation Model (Frontend)
This section explains where authentication is actually invoked in a frontend application,
and which parts are reusable vs page-specific.
Mental Model
Authentication in rforssen.net is event-driven, not global.
There is no background authentication process running continuously.
Instead, authentication is invoked explicitly at a small number of well-defined points.
The Three Authentication Entry Points
Every in-app authenticated page invokes authentication in exactly three situations:
1️⃣ Page Load (Initial State)
When the page loads, it probes authentication state to decide:
- Is the user logged in?
- Which role applies?
- What content should be visible?
onload →
fetchAuth() →
/auth/me →
renderAuthBadge()
decide page behavior
2️⃣ After Popup Login (Dynamic State Change)
When login happens in a popup window, the main page is notified via
BroadcastChannel.
Instead of duplicating logic, the page simply re-runs the same authentication bootstrap.
BroadcastChannel "auth:success" →
initAuth() →
same logic as page load
This guarantees:
- No duplicated logic
- Identical behavior whether login happened before or after page load
3️⃣ Logout (Explicit Session Destruction)
Logout is an imperative action:
- Frontend calls
/auth/logout
- Backend clears the session
- Frontend reloads the page
logout click →
POST /auth/logout →
session cleared →
page reload →
/auth/me → unauthenticated
What Does NOT Invoke Authentication
The following actions do not trigger authentication checks:
- Rendering UI components
- Fetching application data
- Clicking internal UI elements
- Navigation inside the page
Authentication is always explicit and intentional.
Reusable vs Page-Specific Code
| Layer |
Responsibility |
Reusable? |
| Auth Core |
fetchAuth(), OAuth popup flow,
/auth/me, /auth/logout
|
✅ Yes (shared across all apps) |
| Page Bootstrap |
Call fetchAuth(), render auth badge,
decide public vs private behavior
|
⚠️ Small, repeated per page |
| Page Logic |
Application-specific functionality
|
❌ No (page-specific) |
Minimal Pattern for a New Page
Any new page that wants in-app authentication only needs this:
import { fetchAuth, renderAuthBadge } from "./js/auth.js";
window.addEventListener("load", async () => {
const auth = await fetchAuth();
renderAuthBadge(auth);
if (!auth.authenticated) return;
// Page-specific logic starts here
});
Design Principle
Authentication is a gate, not a background service.
Pages decide when to check it.
Backend always enforces it.
This keeps the platform predictable, debuggable, reusable,
and difficult to break accidentally.
2️⃣ Edge Authentication
Hard Gate / Full Access Blocker
Objective
Use this when:
- Page must NOT be visible without login
- No public access allowed
- No partial visibility allowed
- No information leakage accepted
Typical use cases:
- phpMyAdmin
- Admin tools
- Cluster & maintenance utilities
- Dangerous dev tools
- Sensitive dashboard systems
Model
Request → Traefik → oauth2-proxy → Application
If not authenticated → block
If authenticated → allow access
Implementation Summary
- Ingress sends traffic through oauth2-proxy
- oauth2-proxy handles Google login
- Only authenticated traffic reaches application
Optional Identity Injection
oauth2-proxy can inject headers:
X-Auth-User
X-Auth-Email
Backend may use them — or ignore them.
Behavior Rules
- No page content without login
- No public mode
- No anonymous visibility
- Fail safe if auth fails
Summary
Use Edge Authentication when you need strict perimeter
security, platform protection, and hard blocking of all unknown users.
Which Strategy To Use?
| Need | Strategy |
| User-facing experience | In-App Auth |
| Public + Private mixed | In-App Auth |
| Roles & permissions | In-App Auth |
| Family experiences | In-App Auth |
| Admin tools | Edge Auth |
| phpMyAdmin | Edge Auth |
| Internal dashboards | Edge Auth |
| No UI unless logged in | Edge Auth |
Platform Philosophy
Platform / Admin / Infrastructure
➡ Edge Authentication
Family Apps / User Experiences
➡ In-App Authentication
They are not competing approaches —
they are deliberately chosen tools,
each excellent at its job.
Future Enhancements (Optional)
- Unified auth role logic
- Shared identity model
- Standardized /auth/me contract
- Backend integration with edge identity
- Failover auth strategy