This document completes Step 1B of the OAuth-Plan. It describes how the authentication system behaves when things go wrong: popup issues, Google errors, CSRF problems, missing cookies, expired sessions, and 401 responses during normal usage.
The goal is to have a clear, stable “resilience spec”: for each scenario we define:
Step 1B is documentation-only. It does not necessarily mean everything is already implemented exactly like this, but it is the target behaviour specification that later phases will converge to.
The following scenarios are considered in this milestone:
| ID | Scenario | Where It Happens | Primary Symptom |
|---|---|---|---|
| E1 | Popup closed before login completes | Browser / Frontend | Promise rejection: "Popup closed" |
| E2 | Google OAuth error (redirect_uri mismatch, consent error, etc.) | Google → Flask callback | Redirect to error page or JSON error from callback |
| E3 | CSRF / state mismatch or missing cookie | Flask callback | {"error":"csrf_state_mismatch"} or similar |
| E4 | Session cookie missing/blocked at /auth/me | Browser / Flask | /auth/me returns 401, user seen as logged out |
| E5 | Session lost/expired while user is using the app | Browser / Flask | API starts returning 401 mid-session |
| E6 | BroadcastChannel / popup success notification never received | Browser | Main window “waits” and user experiences confusion |
| E7 | Internal auth exceptions (e.g. parsing id_token, network issues) | Flask auth backend | 5xx or structured error JSON |
Frontend UX
loginWithPopup() runs a timer that checks popup.closed. If the popup closes before the auth:success BroadcastChannel message is received, the Promise is rejected with new Error("Popup closed").console.error("Login failed or cancelled:", err) on the frontend.Backend UX
google_callback sees query parameters error or fails to exchange the code.OAuthError or token exchange fails with HTTP error.{"error":"oauth_error","detail":"..."} , or/auth/error page for better UX (optional improvement).auth:success, so login remains “not authenticated”.Backend
request.args.get("state") differs from session["oauth_state_dbg"], or"session" not in request.cookies).{"error":"csrf_state_missing_cookie"} or {"error":"csrf_state_mismatch"} with HTTP 400.auth:success and stays unauthenticated.Frontend Backend
GET /api/auth/me returns 401 with {"authenticated": false}.{"authenticated": false} when no session is present.fetchAuth() treats any non-200 as “not authenticated”.console.warn("Auth probe error:", e) on fetch failures.Frontend Backend UX
/api/video_server/get_directories) suddenly start returning 401.{"ok": false, "error":"not_authenticated","authenticated": false}
checkGetDirectoriesAuth() and similar helpers.appState.auth to {authenticated:false}.Frontend
auth:success./auth/me now returns authenticated (or not).loginWithPopup() currently depends on BroadcastChannel. Future robustness plan:fetchAuth() retry to double-check whether login actually completed.console.warn("Popup closed but no auth:success broadcast received").Backend
google_callback.{"error":"internal_error","detail": str(e)} with HTTP 500.auth:success.Generic representation of a failed login attempt (covers E1/E2/E3/E7).
sequenceDiagram
participant U as User
participant B as Browser (Main Window)
participant P as Popup
participant API as Flask Auth API
participant G as Google OAuth
U->>B: Click "Sign in"
B->>API: GET /auth/login/google?popup=true
API-->>G: Redirect to Google OAuth
G-->>U: Show Google Login Page
U->>G: Interact (login or cancel)
alt User closes popup (E1)
P--xB: <no auth:success>
B-->>B: Promise reject("Popup closed")
B-->>U: Stay logged out, show optional message
else Google / config error (E2)
G-->>API: Redirect with error
API-->>P: JSON / error page (oauth_error)
P--xB: <no auth:success>
B-->>U: Stay logged out, optional "Sign-in failed" hint
else CSRF / state mismatch (E3)
G-->>API: Redirect with invalid/missing state
API-->>P: 400 {"error":"csrf_state_mismatch"}
P--xB: <no auth:success>
else Internal error (E7)
API->>API: Exception
API-->>P: 500 {"error":"internal_error"}
P--xB: <no auth:success>
end
Note over B: User remains unauthenticated
auth badge still shows "Sign in"
This diagram shows mid-session expiry (E5) when the user is already inside the app.
sequenceDiagram
participant U as User
participant B as Browser App
participant API as Flask API
U->>B: Interacts with app (clicks "Refresh directories")
B->>API: GET /api/video_server/get_directories (with cookie)
API-->>B: 200 OK (authenticated)
Note over B: Time passes, server restarts or session expires
U->>B: Interacts again (refresh, analyze, etc.)
B->>API: GET /api/video_server/get_directories (old cookie)
API-->>B: 401 {"ok":false, "error":"not_authenticated"}
B-->>B: Treat user as logged out
B-->>U: Show banner "Session expired, please sign in again"
B->>API: (optional) GET /auth/me
API-->>B: 401 {"authenticated":false}
Consolidated PlantUML sequence that shows success vs error branches.
@startuml
title OAuth Login - Success vs Failure Variants
actor User
participant "Browser (Main Window)" as B
participant "Popup Window" as P
participant "Flask API" as API
participant "Google OAuth" as G
User -> B: Click "Sign in"
B -> API: GET /auth/login/google?popup=true
API -> G: Redirect to Google OAuth
G -> User: Login / Consent Screen
alt SUCCESS
User -> G: Accept / login
G -> API: Redirect with code
API -> G: Exchange code -> token
G -> API: Token + user info
API -> API: Store session
API -> P: Redirect /oauth-popup-complete.html
P -> B: Broadcast auth:success
B -> API: GET /auth/me
API -> B: 200 authenticated
B -> User: UI shows "Signed in"
else POPUP CLOSED (E1)
User -> P: Close window
P -x B: <no auth:success>
B -> B: Promise reject("Popup closed")
B -> User: Stay logged out, optional info
else OAUTH ERROR (E2)
G -> API: Redirect with error
API -> P: Show {"error":"oauth_error"}
P -x B: <no auth:success>
B -> User: "Sign-in failed"
else CSRF / STATE MISMATCH (E3)
G -> API: Redirect with invalid state
API -> P: 400 {"error":"csrf_state_mismatch"}
P -x B: <no auth:success>
else INTERNAL ERROR (E7)
API -> API: Exception
API -> P: 500 {"error":"internal_error"}
P -x B: <no auth:success>
end
@enduml
To keep behaviour predictable and easy to reason about, the following invariants are defined:
/api/auth/me is the single source of truth for “is the user authenticated?”.{"ok": false, "error": "not_authenticated"}).fetchAuth() or API responses./auth/me upon popup close).
Step 1B of the OAuth-Plan is now documented.
All major failure and edge-case flows have:
Implementation can now be aligned with this spec, and deviations can be treated as bugs or future design changes.
Next milestone: Step 1C – Environment Split & Redirect-URI / Config Strategy.