|
5 | 5 | // callback refusing forged/incomplete redirects, the sealed-session cookie |
6 | 6 | // actually authorizing the session API, and logout dropping the cookie. |
7 | 7 | import { expect } from "@effect/vitest"; |
8 | | -import { Effect } from "effect"; |
| 8 | +import { Effect, Encoding, Result, Schema } from "effect"; |
9 | 9 |
|
10 | 10 | import { scenario } from "../src/scenario"; |
11 | 11 | import { Api, Target } from "../src/services"; |
@@ -33,7 +33,18 @@ scenario( |
33 | 33 |
|
34 | 34 | const authorizeUrl = new URL(response.headers.get("location") ?? ""); |
35 | 35 | const state = authorizeUrl.searchParams.get("state") ?? ""; |
36 | | - expect(state, "the redirect carries an unguessable CSRF state").toMatch(/^[0-9a-f]{64}$/); |
| 36 | + // state = base64url(JSON { nonce, returnTo? }) — the nonce is the CSRF |
| 37 | + // secret; returnTo (absent here, no query was passed) rides beside it. |
| 38 | + const decoded = Schema.decodeUnknownOption( |
| 39 | + Schema.fromJsonString( |
| 40 | + Schema.Struct({ nonce: Schema.String, returnTo: Schema.optional(Schema.String) }), |
| 41 | + ), |
| 42 | + )(Result.getOrElse(Encoding.decodeBase64UrlString(state), () => "")); |
| 43 | + expect(decoded._tag, "the state decodes as our login-state envelope").toBe("Some"); |
| 44 | + expect( |
| 45 | + decoded._tag === "Some" ? decoded.value.nonce : "", |
| 46 | + "the state carries an unguessable CSRF nonce", |
| 47 | + ).toMatch(/^[0-9a-f]{64}$/); |
37 | 48 | expect( |
38 | 49 | authorizeUrl.searchParams.get("redirect_uri"), |
39 | 50 | "AuthKit is told to come back to this deployment's callback", |
|
0 commit comments