Skip to main content

max / makenotwork

12.6 KB · 209 lines History Blame Raw
1 # OAuth Flow
2
3 Multithreaded uses MNW as its sole identity provider. All authentication goes through an OAuth 2.0 Authorization Code flow with PKCE (Proof Key for Code Exchange). No passwords are stored locally.
4
5 ## Environment Variables
6
7 | Variable | Required | Default | Example |
8 |----------|----------|---------|---------|
9 | `MNW_BASE_URL` | Yes | `http://127.0.0.1:3000` | `https://makenot.work` |
10 | `OAUTH_CLIENT_ID` | Yes | -- | `mt-forums-6378957b452bbbc906c3db8edd072d64` |
11 | `OAUTH_REDIRECT_URI` | Yes | `http://127.0.0.1:3400/auth/callback` | `https://forums.makenot.work/auth/callback` |
12 | `COOKIE_SECURE` | No | `true` | `false` (local dev over HTTP) |
13 | `PLATFORM_ADMIN_ID` | No | -- | UUID of the platform admin user |
14
15 ## PKCE OAuth Flow (Step by Step)
16
17 ```
18 Browser Multithreaded MNW (OAuth Provider)
19 │ │ │
20 │ 1. GET /auth/login │ │
21 │──────────────────────>│ │
22 │ │ 2. Generate verifier, │
23 │ │ challenge, state │
24 │ │ Store in session │
25 │ │ │
26 │ 3. 302 Redirect │ │
27 │<──────────────────────│ │
28 │ │
29 │ 4. GET /oauth/authorize?response_type=code │
30 │ &client_id=...&redirect_uri=... │
31 │ &state=...&code_challenge=... │
32 │ &code_challenge_method=S256 │
33 │──────────────────────────────────────────────────>│
34 │ │
35 │ 5. User authenticates on MNW │
36 │<─────────────────────────────────────────────────>│
37 │ │
38 │ 6. 302 Redirect to redirect_uri │
39 │ ?code=...&state=... │
40 │<──────────────────────────────────────────────────│
41 │ │
42 │ 7. GET /auth/callback │
43 │ ?code=...&state=... │
44 │──────────────────────>│ │
45 │ │ 8. Verify state nonce │
46 │ │ Retrieve verifier │
47 │ │ Clean up OAuth data │
48 │ │ │
49 │ │ 9. POST /oauth/token │
50 │ │ {grant_type, code, │
51 │ │ redirect_uri, │
52 │ │ code_verifier, │
53 │ │ client_id} │
54 │ │──────────────────────────>│
55 │ │ │
56 │ │ 10. {access_token: ...} │
57 │ │<──────────────────────────│
58 │ │ │
59 │ │ 11. GET /oauth/userinfo │
60 │ │ Authorization: │
61 │ │ Bearer <token> │
62 │ │──────────────────────────>│
63 │ │ │
64 │ │ 12. {user_id, username, │
65 │ │ display_name, │
66 │ │ avatar_url} │
67 │ │<──────────────────────────│
68 │ │ │
69 │ │ 13. Upsert local user │
70 │ │ Check suspension │
71 │ │ Save session │
72 │ │ Cycle session ID │
73 │ │ │
74 │ 14. 302 Redirect / │ │
75 │<──────────────────────│ │
76 ```
77
78 ### Detailed Steps
79
80 1. **User clicks "Log in"**: browser sends `GET /auth/login`.
81 2. **Generate PKCE material**: 32-byte random verifier (base64url), SHA-256 challenge (base64url), 16-byte state nonce (hex). Verifier and state stored in session.
82 3. **Redirect to MNW**: 302 to `{MNW_BASE_URL}/oauth/authorize` with query params: `response_type=code`, `client_id`, `redirect_uri`, `state`, `code_challenge`, `code_challenge_method=S256`.
83 4. **MNW authorize endpoint**: MNW shows its login/consent UI.
84 5. **User authenticates**: enters credentials on MNW (or is already logged in).
85 6. **MNW redirects back**: 302 to `redirect_uri` with `code` and `state` query params.
86 7. **Browser follows redirect**: `GET /auth/callback?code=...&state=...`.
87 8. **Validate state and retrieve verifier**: compare `state` param against session value (reject on mismatch). Retrieve PKCE verifier from session. Remove both from session.
88 9. **Token exchange**: `POST {MNW_BASE_URL}/oauth/token` with JSON body: `grant_type=authorization_code`, `code`, `redirect_uri`, `code_verifier`, `client_id`. No client_secret (PKCE replaces it).
89 10. **Receive access token**: MNW responds with `{ access_token: "..." }`.
90 11. **Fetch user info**: `GET {MNW_BASE_URL}/oauth/userinfo` with `Authorization: Bearer {access_token}`.
91 12. **Receive user profile**: `{ user_id, username, display_name, avatar_url }`.
92 13. **Local processing**: upsert user into `users` table (keyed on `mnw_account_id`), check `suspended_at` (fail-closed: DB errors block login), save `SessionUser` (user_id, username, display_name) to session, cycle session ID to prevent fixation.
93 14. **Redirect home**: 302 to `/`.
94
95 ## Session Management
96
97 - **Store**: `tower-sessions` with `PostgresStore` (auto-migrated table).
98 - **Cookie name**: `mt_session`.
99 - **Expiry**: 7 days of inactivity (`OnInactivity`). Reset on each request.
100 - **SameSite**: `Lax` (allows top-level navigations but blocks cross-origin POST).
101 - **Secure flag**: controlled by `COOKIE_SECURE` env var. `true` in production (HTTPS), `false` for local HTTP dev.
102 - **Expired session cleanup**: background tokio task runs every 3600 seconds, deletes expired rows from the session table.
103 - **Session ID cycling**: on successful login, `session.cycle_id()` generates a new session ID to prevent session fixation attacks.
104
105 ### Session Keys
106
107 | Key | Type | Set When |
108 |-----|------|----------|
109 | `user_id` | `Uuid` | Login success |
110 | `username` | `String` | Login success |
111 | `display_name` | `Option<String>` | Login success |
112 | `oauth_state` | `String` | Login initiated (cleared after callback) |
113 | `pkce_verifier` | `String` | Login initiated (cleared after callback) |
114 | `mnw_refresh_token` | `String` | Login success; rotated on each `/auth/refresh` |
115 | `csrf_token` | `String` | First state-changing request or template render |
116
117 ## Auth Extractors
118
119 ### `MaybeUser(Option<SessionUser>)`
120
121 - Infallible extractor. Always succeeds.
122 - Returns `Some(SessionUser)` if the session contains valid user data, `None` otherwise.
123 - Used by public routes that show different UI for logged-in vs anonymous users.
124
125 ### `PlatformAdmin(SessionUser)`
126
127 - Fallible extractor. Returns `404 Not Found` if the user is not logged in or is not the platform admin.
128 - Compares `session.user_id` against `config.platform_admin_id`.
129 - Returns 404 (not 403) to hide admin routes from non-admins.
130 - Used by `/_admin/*` routes.
131
132 ### `SessionUser`
133
134 - Not an extractor itself. Data struct stored in sessions.
135 - Fields: `user_id: Uuid`, `username: String`, `display_name: Option<String>`.
136
137 ## Token Handling
138
139 MT is the reference relying party for MNW's OAuth provider, dogfooding both the
140 back-channel refresh-token flow (default) and the front-channel `prompt=none`
141 silent re-auth. The governing principle: **the MT session governs login
142 longevity; the MNW token governs only perk freshness.** Removing the stored
143 token therefore never forces a re-login.
144
145 - **Access tokens are never stored.** Login requests `scope=profile:read
146 perks:read offline_access`; MNW returns a short-lived (≈5 min) *userinfo-scoped*
147 access token plus a rotating refresh token. The access token is used once at
148 callback to fetch userinfo, then discarded. It carries a userinfo audience, so
149 even if captured it cannot act on MNW's sync API.
150 - **Refresh tokens are stored and rotated.** Only the scoped refresh token is
151 persisted (`mnw_refresh_token`). `POST /auth/refresh` trades it via
152 `grant_type=refresh_token` for a fresh access token **and a new refresh token**
153 (rotation: the prior token is invalidated; reuse is theft-detectable on the
154 MNW side). The new refresh token replaces the stored one.
155 - **A dead refresh token does not log the user out.** If refresh returns
156 `invalid_grant` (expired/rotated/revoked), MT clears the stored token and
157 returns `401` from `/auth/refresh` **without** flushing the session. The user
158 stays logged in with last-known perks and can re-link their MNW account; the
159 session's own 7-day inactivity expiry governs logout.
160 - **`prompt=none` silent re-auth (`GET /auth/reverify`)** is the
161 zero-credential-at-rest alternative third-party RPs can choose: MT bounces the
162 browser through `/oauth/authorize?prompt=none` (no `offline_access`, so no
163 refresh token is issued), uses the returned short-lived token for one userinfo
164 fetch, and stores nothing. If the MNW session has lapsed, MNW redirects back
165 with `error=login_required` and MT keeps the last-known perks.
166 - The PKCE verifier is ephemeral: generated at login initiation, consumed at
167 callback, never persisted beyond the session.
168
169 ## CSRF Protection
170
171 Separate from OAuth but relevant to authenticated requests:
172
173 - SHA-256 random token (32 bytes, 64 hex chars) generated per session.
174 - Stored in session under `csrf_token` key.
175 - Included in HTML via meta tag, sent on POST/PUT/PATCH/DELETE via `X-CSRF-Token` header.
176 - Validated by `csrf_middleware` with constant-time comparison.
177 - Exempt paths: `/auth/*`, `/api/health`, `/_test/*`.
178
179 ## Error Codes
180
181 All OAuth errors redirect to `/?error={code}`. The error code is a query parameter on the home page.
182
183 | Error Code | Cause |
184 |------------|-------|
185 | `state_mismatch` | `state` param does not match session value. Possible CSRF or expired session. |
186 | `missing_verifier` | PKCE verifier not found in session. Session expired between login click and callback. |
187 | `token_request_failed` | Network error contacting MNW `/oauth/token`. |
188 | `token_exchange_failed` | MNW returned non-2xx from `/oauth/token`. Invalid code, expired code, or bad verifier. |
189 | `token_parse_failed` | MNW token response was not valid JSON or missing `access_token`. |
190 | `userinfo_request_failed` | Network error contacting MNW `/oauth/userinfo`. |
191 | `userinfo_fetch_failed` | MNW returned non-2xx from `/oauth/userinfo`. Token invalid or expired. |
192 | `userinfo_parse_failed` | Userinfo response was not valid JSON or missing expected fields. |
193 | `user_upsert_failed` | Database error inserting/updating local user record. |
194 | `internal_error` | Database error checking suspension status (fail-closed). |
195 | `account_suspended` | User's local account has `suspended_at` set. Login blocked. |
196
197 ## Logout
198
199 `POST /auth/logout` flushes the entire session (removes all keys, deletes session row) and redirects to `/`.
200
201 ## Key Paths
202
203 - `src/auth.rs`: PKCE helpers, session user, login/callback/logout handlers
204 - `src/config.rs`: `Config::from_env()`, all OAuth-related env vars
205 - `src/csrf.rs`: CSRF token generation, middleware, constant-time comparison
206 - `src/internal_auth.rs`: HMAC-SHA256 auth for MNW-to-MT internal API (separate from OAuth)
207 - `src/main.rs`: session store setup, session layer config, middleware stack
208 - `deploy/README.md`, `deploy/deploy.sh`: deploy procedure (production env vars are provisioned on-server, not committed to the repo)
209