Commit b03192b
authored
idp: add 'Sign in as a different user' on consent page (JavaScriptSolidServer#384) (JavaScriptSolidServer#385)
* idp: add 'Sign in as a different user' on consent page (JavaScriptSolidServer#384)
Adds a switch-account link to the OIDC Authorize Access page next to
the "Signed in as <email>" line, so users with multiple identities
don't have to manually clear browser state to switch.
Implementation (~70 LOC across 3 files):
- src/idp/interactions.js: new handleSwitchAccount() — looks up the
in-flight Interaction, destroys the bound oidc-provider Session via
provider.Session.findByUid(...).destroy(), mutates the interaction's
prompt back to {name:'login'} (preserving the original authz params),
clears the user-agent's session cookies, and redirects to the same
/idp/interaction/:uid URL. handleInteractionGet then re-renders as
the login page; the resume action picks up the original client_id /
redirect_uri / state on next login. Returns 404 on missing
interaction (cleaner than abort's 500).
- src/idp/index.js: import + register POST /idp/interaction/:uid/switch.
- src/idp/views.js: small inline-style link inside the "Signed in as …"
paragraph on consentPage. Uses an unstyled <button> in a <form> so
it's a real POST (no GET-via-link CSRF surface).
Why re-use the same interaction uid (rather than ending the session and
issuing a fresh /idp/auth) — preserves the OIDC params (state, nonce,
PKCE challenge, redirect_uri) so the requesting app's flow continues
unchanged. New /idp/auth would also re-trigger loadExistingGrant which
might silently auto-approve from another stale grant.
Verified:
- 48/48 idp.test.js tests pass (no regressions to existing flows)
- Boot smoke test: /idp/interaction/nonexistent/switch returns 404,
/idp landing renders 200
- Static analysis of the consent flow matches the agent's research:
Session.findByUid + .destroy() in oidc-provider 9.6 work cleanly,
`prompt` and `session` are in Interaction's IN_PAYLOAD so the
mutation persists through the filesystem adapter.
Out of scope (follow-ups worth filing if pushed):
- CSRF tokens on consent forms (matches existing confirm/abort
behavior; the interaction uid is the unguessable token)
- Multi-account picker (vs a hard switch) for users with >2 stored
sessions
* idp: address Copilot review on JavaScriptSolidServer#384 — prompt validation, error masking, HTML, tests
Four fixes per the review pass:
1. handleSwitchAccount now validates interaction.prompt.name === 'consent'
before mutating. Returns 400 otherwise. Prevents a crafted POST from
corrupting an in-flight non-consent interaction (login, passkey, etc.).
Also clears interaction.result so a stale result.login from a prior
identity can't influence the next resume.
2. The 500 error path no longer surfaces err.message to the browser.
Adapter / fs / oidc-provider errors can leak file paths and other
internals; full error is already going to request.log.error. Generic
"Something went wrong" is what the user sees.
3. <p> with <form> inside it was invalid HTML — browsers implicitly close
the <p> before the <form>, breaking the intended inline layout. The
"Signed in as <email> · Sign in as a different user" element is now
a <div> with the same flex styling.
4. Added test coverage in test/idp.test.js — three tests for the new
route: success path (302 + interaction mutated to login + cookies
cleared), 400 on non-consent prompt (interaction untouched), 404
on unknown uid. Tests write synthetic Interaction records directly
to the filesystem adapter at <DATA_DIR>/.idp/interaction/<uid>.json
so they don't have to walk a full OIDC client flow.
Also caught and fixed: reply.clearCookie() doesn't exist (JSS doesn't
register @fastify/cookie). Replaced with reply.header('Set-Cookie',
[...expired cookie strings]) which Fastify emits as multiple Set-Cookie
headers.
51/51 idp.test.js tests pass.
* idp: Copilot review pass 2 — 303 See Other + getSetCookie() in test
Two more legit fixes:
1. Redirect changed from 302 to 303 (See Other). 302 leaves the UA
free to repeat the POST to the Location target, which on this
handler would re-trigger /switch in a loop. 303 unambiguously
forces GET on the Location, which is what we want.
2. Test uses res.headers.getSetCookie() instead of headers.get().
Per the Fetch spec, Set-Cookie is a "forbidden header name" on
the standard .get() — return value is implementation-defined and
may collapse multiple Set-Cookie response headers into a single
string or return null. .getSetCookie() returns an array, which is
what we want for asserting on multiple cookie clears.
Test asserts now stricter:
- status === 303
- >= 3 Set-Cookie headers (one per cleared cookie name)
- one of them clears _session
- every Set-Cookie carries an expiration (Max-Age=0 or 1970 Expires)
51/51 idp.test.js tests pass.
* idp: Copilot review pass 3 — also clear _session.legacy.sig
The IdP config uses signed cookies (provider.js cookies.long.signed = true),
so every session cookie name has a paired `.sig`. We were already clearing
_session and _session.sig and _session.legacy, but missed _session.legacy.sig.
Without it, the browser would carry a lingering signature cookie until the
normal maxAge.
Test now asserts >= 4 Set-Cookie headers and verifies every one of the four
expected names is among them.
51/51 idp.test.js tests pass.
* idp: Copilot review pass 4 — match redirect arg order, fail loud on missing getSetCookie
Two fixes:
1. reply.redirect now uses (statusCode, url) — matches the rest of the
codebase (src/server.js:637, src/tunnel/index.js:222) and avoids
relying on the alternate Fastify overload (which is removed in
Fastify 5).
2. Test now hard-fails when Headers.getSetCookie() is missing instead
of silently asserting on []. The previous `getSetCookie?.() || []`
pattern was a phantom fallback: if the method were missing, the
array would be empty and the next assertion (length >= 4) would
fail with "got 0" — but the failure mode would point at "no
cookies were sent" rather than "your Node is too old to inspect
Set-Cookies via fetch." Explicit assertion clarifies which is which.
51/51 idp.test.js tests pass.
* idp: Copilot review pass 5 — Node 18 compat for cookie-clearing test
The previous test asserted Headers.getSetCookie() exists, but
package.json declares engines.node >= 18 and getSetCookie is only on
Node 19.7+. Failing loud was an improvement but still meant the test
suite was broken on a declared-supported Node.
Switched the cookie-inspection test to use node:http directly via a
small rawPost() helper. http.request gives res.headers['set-cookie'] as
a real array on every Node version that has node:http (i.e. all of
them), so engines.node >= 18 stays honest with no version-gated test
branches.
Other tests in the block still use fetch — they don't need raw header
access and the existing pattern is preserved.
51/51 idp.test.js tests pass.1 parent 95e076b commit b03192b
4 files changed
Lines changed: 217 additions & 1 deletion
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
11 | 11 | | |
12 | 12 | | |
13 | 13 | | |
| 14 | + | |
14 | 15 | | |
15 | 16 | | |
16 | 17 | | |
| |||
324 | 325 | | |
325 | 326 | | |
326 | 327 | | |
| 328 | + | |
| 329 | + | |
| 330 | + | |
| 331 | + | |
| 332 | + | |
| 333 | + | |
| 334 | + | |
327 | 335 | | |
328 | 336 | | |
329 | 337 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
297 | 297 | | |
298 | 298 | | |
299 | 299 | | |
| 300 | + | |
| 301 | + | |
| 302 | + | |
| 303 | + | |
| 304 | + | |
| 305 | + | |
| 306 | + | |
| 307 | + | |
| 308 | + | |
| 309 | + | |
| 310 | + | |
| 311 | + | |
| 312 | + | |
| 313 | + | |
| 314 | + | |
| 315 | + | |
| 316 | + | |
| 317 | + | |
| 318 | + | |
| 319 | + | |
| 320 | + | |
| 321 | + | |
| 322 | + | |
| 323 | + | |
| 324 | + | |
| 325 | + | |
| 326 | + | |
| 327 | + | |
| 328 | + | |
| 329 | + | |
| 330 | + | |
| 331 | + | |
| 332 | + | |
| 333 | + | |
| 334 | + | |
| 335 | + | |
| 336 | + | |
| 337 | + | |
| 338 | + | |
| 339 | + | |
| 340 | + | |
| 341 | + | |
| 342 | + | |
| 343 | + | |
| 344 | + | |
| 345 | + | |
| 346 | + | |
| 347 | + | |
| 348 | + | |
| 349 | + | |
| 350 | + | |
| 351 | + | |
| 352 | + | |
| 353 | + | |
| 354 | + | |
| 355 | + | |
| 356 | + | |
| 357 | + | |
| 358 | + | |
| 359 | + | |
| 360 | + | |
| 361 | + | |
| 362 | + | |
| 363 | + | |
| 364 | + | |
| 365 | + | |
| 366 | + | |
| 367 | + | |
| 368 | + | |
| 369 | + | |
| 370 | + | |
| 371 | + | |
| 372 | + | |
| 373 | + | |
| 374 | + | |
| 375 | + | |
| 376 | + | |
| 377 | + | |
| 378 | + | |
| 379 | + | |
| 380 | + | |
| 381 | + | |
| 382 | + | |
300 | 383 | | |
301 | 384 | | |
302 | 385 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
503 | 503 | | |
504 | 504 | | |
505 | 505 | | |
506 | | - | |
| 506 | + | |
| 507 | + | |
| 508 | + | |
| 509 | + | |
| 510 | + | |
| 511 | + | |
| 512 | + | |
| 513 | + | |
| 514 | + | |
507 | 515 | | |
508 | 516 | | |
509 | 517 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
4 | 4 | | |
5 | 5 | | |
6 | 6 | | |
| 7 | + | |
7 | 8 | | |
8 | 9 | | |
9 | 10 | | |
| |||
182 | 183 | | |
183 | 184 | | |
184 | 185 | | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
| 197 | + | |
| 198 | + | |
| 199 | + | |
| 200 | + | |
| 201 | + | |
| 202 | + | |
| 203 | + | |
| 204 | + | |
| 205 | + | |
| 206 | + | |
| 207 | + | |
| 208 | + | |
| 209 | + | |
| 210 | + | |
| 211 | + | |
| 212 | + | |
| 213 | + | |
| 214 | + | |
| 215 | + | |
| 216 | + | |
| 217 | + | |
| 218 | + | |
| 219 | + | |
| 220 | + | |
| 221 | + | |
| 222 | + | |
| 223 | + | |
| 224 | + | |
| 225 | + | |
| 226 | + | |
| 227 | + | |
| 228 | + | |
| 229 | + | |
| 230 | + | |
| 231 | + | |
| 232 | + | |
| 233 | + | |
| 234 | + | |
| 235 | + | |
| 236 | + | |
| 237 | + | |
| 238 | + | |
| 239 | + | |
| 240 | + | |
| 241 | + | |
| 242 | + | |
| 243 | + | |
| 244 | + | |
| 245 | + | |
| 246 | + | |
| 247 | + | |
| 248 | + | |
| 249 | + | |
| 250 | + | |
| 251 | + | |
| 252 | + | |
| 253 | + | |
| 254 | + | |
| 255 | + | |
| 256 | + | |
| 257 | + | |
| 258 | + | |
| 259 | + | |
| 260 | + | |
| 261 | + | |
| 262 | + | |
| 263 | + | |
| 264 | + | |
| 265 | + | |
| 266 | + | |
| 267 | + | |
| 268 | + | |
| 269 | + | |
| 270 | + | |
| 271 | + | |
| 272 | + | |
| 273 | + | |
| 274 | + | |
| 275 | + | |
| 276 | + | |
| 277 | + | |
| 278 | + | |
| 279 | + | |
| 280 | + | |
| 281 | + | |
| 282 | + | |
| 283 | + | |
| 284 | + | |
| 285 | + | |
| 286 | + | |
| 287 | + | |
| 288 | + | |
| 289 | + | |
| 290 | + | |
| 291 | + | |
| 292 | + | |
| 293 | + | |
| 294 | + | |
| 295 | + | |
| 296 | + | |
| 297 | + | |
| 298 | + | |
| 299 | + | |
| 300 | + | |
| 301 | + | |
185 | 302 | | |
186 | 303 | | |
187 | 304 | | |
| |||
0 commit comments