Skip to content

Commit 9f7a97f

Browse files
authored
Merge commit from fork
1 parent a63eee1 commit 9f7a97f

2 files changed

Lines changed: 88 additions & 4 deletions

File tree

packages/core/src/lib/actions/callback/oauth/checks.ts

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,12 @@ import type { WebAuthnProviderType } from "../../../../providers/webauthn.js"
1515

1616
interface CookiePayload {
1717
value: string
18+
/**
19+
* The id of the provider that created this cookie. Since the cookie names
20+
* are not provider-specific, the id is stored in the sealed payload and
21+
* checked on callback.
22+
*/
23+
provider?: string
1824
}
1925

2026
const COOKIE_TTL = 60 * 15 // 15 minutes
@@ -40,7 +46,10 @@ async function sealCookie(
4046
const encoded = await encode({
4147
...options.jwt,
4248
maxAge: COOKIE_TTL,
43-
token: { value: payload } satisfies CookiePayload,
49+
token: {
50+
value: payload,
51+
provider: options.provider.id,
52+
} satisfies CookiePayload,
4453
salt: cookie.name,
4554
})
4655
const cookieOptions = { ...cookie.options, expires }
@@ -62,8 +71,15 @@ async function parseCookie(
6271
token: value,
6372
salt: cookies[name].name,
6473
})
65-
if (parsed?.value) return parsed.value
66-
throw new Error("Invalid cookie")
74+
if (!parsed?.value) throw new Error("Invalid cookie")
75+
// The check must have been created by the provider currently handling
76+
// the callback.
77+
if (parsed.provider !== options.provider?.id) {
78+
throw new Error(
79+
`${name} cookie was created for a different provider than the one handling the callback`
80+
)
81+
}
82+
return parsed.value
6783
} catch (error) {
6884
throw new InvalidCheck(`${name} value could not be parsed`, {
6985
cause: error,

packages/core/test/actions/callback.test.ts

Lines changed: 69 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,11 @@ import Credentials from "../../src/providers/credentials.js"
66
import { logger, makeAuthRequest, testConfig } from "../utils.js"
77
import { skipCSRFCheck } from "../../src/index.js"
88
import { CredentialsSignin, InvalidCheck } from "../../src/errors.js"
9-
import { state } from "../../src/lib/actions/callback/oauth/checks.js"
9+
import {
10+
nonce,
11+
pkce,
12+
state,
13+
} from "../../src/lib/actions/callback/oauth/checks.js"
1014

1115
describe("assert GET callback action", () => {
1216
beforeEach(() => {
@@ -149,6 +153,70 @@ describe("assert GET callback action", () => {
149153
})
150154
})
151155

156+
describe("OAuth check cookies are bound to their provider", () => {
157+
const cookieOptions = { secure: true, sameSite: "lax" as const, httpOnly: true }
158+
const cookies = {
159+
state: { name: "authjs.state", options: cookieOptions },
160+
nonce: { name: "authjs.nonce", options: cookieOptions },
161+
pkceCodeVerifier: {
162+
name: "authjs.pkce.code_verifier",
163+
options: cookieOptions,
164+
},
165+
}
166+
167+
function optionsFor(id: string, checks: Array<"state" | "nonce" | "pkce">) {
168+
return {
169+
logger,
170+
provider: { id, checks },
171+
jwt: { secret: "secret" },
172+
cookies,
173+
} as any
174+
}
175+
176+
it("accepts a state cookie on the same provider's callback", async () => {
177+
const created = (await state.create(optionsFor("github", ["state"]))) as any
178+
const value = await state.use(
179+
{ [cookies.state.name]: created.cookie.value },
180+
[],
181+
optionsFor("github", ["state"])
182+
)
183+
expect(value).toBe(created.value)
184+
})
185+
186+
it("rejects a state cookie minted for a different provider", async () => {
187+
const created = (await state.create(optionsFor("github", ["state"]))) as any
188+
await expect(
189+
state.use(
190+
{ [cookies.state.name]: created.cookie.value },
191+
[],
192+
optionsFor("google", ["state"])
193+
)
194+
).rejects.toThrow(InvalidCheck)
195+
})
196+
197+
it("rejects a nonce cookie minted for a different provider", async () => {
198+
const created = (await nonce.create(optionsFor("github", ["nonce"]))) as any
199+
await expect(
200+
nonce.use(
201+
{ [cookies.nonce.name]: created.cookie.value },
202+
[],
203+
optionsFor("google", ["nonce"])
204+
)
205+
).rejects.toThrow(InvalidCheck)
206+
})
207+
208+
it("rejects a PKCE verifier cookie minted for a different provider", async () => {
209+
const created = (await pkce.create(optionsFor("github", ["pkce"]))) as any
210+
await expect(
211+
pkce.use(
212+
{ [cookies.pkceCodeVerifier.name]: created.cookie.value },
213+
[],
214+
optionsFor("google", ["pkce"])
215+
)
216+
).rejects.toThrow(InvalidCheck)
217+
})
218+
})
219+
152220
describe("assert POST callback action", () => {
153221
describe("Credentials provider", () => {
154222
it("should return error=CredentialSignin and code=credentials if authorize returns null", async () => {

0 commit comments

Comments
 (0)