rforssen.net – Authentication Strategy, Principles & Design
GET https://api.rforssen.net/auth/login/google?popup=true&redirect=.../auth/login/google/callbackhttps://www.rforssen.net/oauth-popup-complete.htmlBroadcastChannel("auth") with { type: "auth:success" }/auth/me and updates UIcredentials: "include") —/auth/me may return 401. This is expected when the user is logged out.
Frontend treats 401 as “public/anonymous”.
/rforssen_net/
js/
auth/
authClient.js
initAuth.js
setupAuthUI.js
authtest/
index.html
js/
authUI.js (app-specific UI demo)
oauth-popup-complete.html
Other apps (e.g. /media, /genealogy, admin pages) can have their own
./js/authUI.js while reusing the shared core in /js/auth/.
| Endpoint | Purpose |
|---|---|
GET /auth/me | Return current session user (401 if not logged in) |
GET /auth/login/google | Start Google OAuth flow (popup-capable) |
GET /auth/login/google/callback | OAuth callback (server-side session creation) |
POST /auth/logout | Clear session cookie |
If user is NOT authenticated (via 401):
{
"authenticated": false,
"is_authorized": false,
"user": null,
"role": "public"
}
If user IS authenticated:
{
"authenticated": true,
"is_authorized": true,
"user": {
"email": "...",
"name": "...",
"role": "admin | family | public | ..."
},
"role": "admin | family | public | ..."
}
| Module | Responsibility | Scope |
|---|---|---|
/js/auth/authClient.js |
|
✅ Shared across all apps |
/js/auth/initAuth.js |
Creates an authClient with apiBase, popup redirect, and channel name.
Returns { auth, authClient } or { authClient } depending on split.
|
✅ Shared across all apps |
/js/auth/setupAuthUI.js |
One-call helper used by pages to wire auth: fetch initial auth state → render badge → attach login/logout handlers → optionally refresh. | ✅ Shared across all apps |
./js/authUI.js |
App-specific auth badge/menu rendering and DOM wiring.
Calls provided callbacks onLogin() / onLogout().
|
❌ App-specific (one per app) |
The popup completion page sends a success message to the opener via BroadcastChannel,
then closes itself. This avoids COOP restrictions where the opener cannot close the popup.
<!-- /oauth-popup-complete.html -->
<script>
try {
const bc = new BroadcastChannel("auth");
bc.postMessage({ type: "auth:success" });
bc.close();
} catch (e) {}
setTimeout(() => window.close(), 150);
</script>
popup.close().
Let the popup close itself.
Roland maintains multiple environments:
No single authentication pattern fits all of them. Therefore the platform supports two complementary authentication strategies:
Use this strategy when the page or app should:
Typical use cases:
/auth/me (cookie included)
Google OAuth + secure session cookies.
| Endpoint | Purpose |
|---|---|
GET /auth/login/google | Start login |
GET /auth/login/google/callback | Complete login |
GET /auth/me | Check who user is |
POST /auth/logout | Log out |
If user is NOT authenticated (401):
{
"authenticated": false,
"is_authorized": false,
"user": null,
"role": "public"
}
If user IS authenticated:
{
"authenticated": true,
"is_authorized": true,
"user": {
"email": "...",
"name": "...",
"role": "admin | family | public | ..."
},
"role": "admin | family | public | ..."
}
/auth/me (with credentials: "include")SESSION_COOKIE_SECURE = True SESSION_COOKIE_HTTPONLY = True SESSION_COOKIE_SAMESITE = "Lax"
Only allow trusted redirect destinations.
This section explains where authentication is invoked in a frontend application, and which parts are reusable vs app-specific.
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.
Every in-app authenticated page invokes authentication in exactly three situations:
page load →
setupAuthUI() →
fetchAuth() →
GET /auth/me (credentials: include) →
renderAuthBadge()
decide page behavior
BroadcastChannel.
The app then refreshes auth state and re-renders.
popup login →
oauth-popup-complete.html →
BroadcastChannel("auth") "auth:success" →
(opener) fetchAuth() →
GET /auth/me →
re-render auth badge / enable features
This guarantees:
POST /auth/logout
logout click →
POST /auth/logout →
session cleared →
GET /auth/me →
unauthenticated state →
render login button
Authentication is always explicit and intentional.
| Layer | Responsibility | Reusable? |
|---|---|---|
| Auth Core |
/js/auth/authClient.js,
/js/auth/initAuth.js,
/js/auth/setupAuthUI.js
|
✅ Yes (shared across all apps) |
| Auth UI |
./js/authUI.js (badge/menu rendering and DOM wiring)
|
❌ No (one per app) |
| Page Logic | Application-specific functionality and data fetching | ❌ No (page-specific) |
<div id="authBadge"></div>./js/authUI.jssetupAuthUI()
<div id="authBadge"></div>
<script type="module">
import { setupAuthUI } from "../js/auth/setupAuthUI.js";
import { renderAuthBadge, wireAuthUI } from "./js/authUI.js";
const { auth } = await setupAuthUI({ renderAuthBadge, wireAuthUI });
if (auth.is_authorized) {
// app-specific load here
}
</script>
credentials: "include" so the session cookie is sent to api.rforssen.net.
This keeps the platform predictable, debuggable, reusable, and difficult to break accidentally.
Use this when:
Typical use cases:
oauth2-proxy can inject headers:
X-Auth-User X-Auth-Email
Backend may use them — or ignore them.
| 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 |
They are not competing approaches — they are deliberately chosen tools, each excellent at its job.
/auth/me contract