diff --git a/docs/pages/guides/oauth2-pkce.md b/docs/pages/guides/oauth2-pkce.md index c444c6bc..f2899ebe 100644 --- a/docs/pages/guides/oauth2-pkce.md +++ b/docs/pages/guides/oauth2-pkce.md @@ -14,39 +14,57 @@ const google = new Google(clientId, clientSecret, redirectURI); ### Create authorization URL -Generate a code verifier using `generateCodeVerifier()` and store it as a cookie. Use it to create an authorization URL with `createAuthorizationURL()` and redirect the user to it. +Generate a state and code verifier using `generateState()` and `generateCodeVerifier()`. Use them to create an authorization URL with `createAuthorizationURL()`, store the state and code verifier as cookies, and redirect the user to the authorization url. You may optionally pass `scopes`. For providers that implement OpenID Connect, `openid` is always included. There may be more options depending on the provider. ```ts -import { generateCodeVerifier } from "arctic"; +import { generateCodeVerifier, generateState } from "arctic"; +const state = generateState(); const codeVerifier = generateCodeVerifier(); -const url = await github.createAuthorizationURL(state, codeVerifier); +const url = await google.createAuthorizationURL(state, codeVerifier); + +// store state verifier as cookie +setCookie("state", state, { + secure: true, // set to false in localhost + path: "/", + httpOnly: true, + maxAge: 60 * 10 // 10 min +}); // store code verifier as cookie -setCookie("code_verifier", state, { +setCookie("code_verifier", codeVerifier, { secure: true, // set to false in localhost path: "/", httpOnly: true, maxAge: 60 * 10 // 10 min }); + return redirect(url); ``` ### Validate authorization code -Use `validateAuthorizationCode()` to validate the authorization code with the code verifier. This returns an object with an access token, and a refresh token if requested. If the code is invalid, it will throw an [`OAuth2RequestError`](https://oslo.js.org/reference/oauth2/OAuth2RequestError/). +Compare the state, and use `validateAuthorizationCode()` to validate the authorization code with the code verifier. This returns an object with an access token, and a refresh token if requested. If the code is invalid, it will throw an [`OAuth2RequestError`](https://oslo.js.org/reference/oauth2/OAuth2RequestError/). ```ts import { OAuth2RequestError } from "arctic"; const code = request.url.searchParams.get("code"); -const codeVerifier = request.url.searchParams.get("code_verifier"); +const state = request.url.searchParams.get("state"); + +const storedState = getCookie("state"); +const storedCodeVerifier = getCookie("code_verifier"); + +if (!code || !storedState || !storedCodeVerifier || state !== storedState) { + // 400 + throw new Error("Invalid request"); +} try { - const tokens = await github.validateAuthorizationCode(code, codeVerifier); + const tokens = await google.validateAuthorizationCode(code, codeVerifier); } catch (e) { if (e instanceof OAuth2RequestError) { // see https://oslo.js.org/reference/oauth2/OAuth2RequestError/ diff --git a/docs/pages/providers/google.md b/docs/pages/providers/google.md index aab16795..fdade659 100644 --- a/docs/pages/providers/google.md +++ b/docs/pages/providers/google.md @@ -15,7 +15,7 @@ const google = new Google(clientId, clientSecret, redirectURI˝); ``` ```ts -const url: URL = await google.createAuthorizationURL(codeVerifier, { +const url: URL = await google.createAuthorizationURL(state, codeVerifier, { // optional scopes // "openid" always included }); diff --git a/docs/pages/providers/keycloak.md b/docs/pages/providers/keycloak.md index f4baad5d..9d521827 100644 --- a/docs/pages/providers/keycloak.md +++ b/docs/pages/providers/keycloak.md @@ -17,7 +17,7 @@ const keycloak = new Keycloak(realmURL, clientId, clientSecret, redirectURI); ``` ```ts -const url: URL = await keycloak.createAuthorizationURL(state, { +const url: URL = await keycloak.createAuthorizationURL(state, codeVerifier, { // optional scopes }); diff --git a/docs/pages/providers/line.md b/docs/pages/providers/line.md index 01b849e9..0681f198 100644 --- a/docs/pages/providers/line.md +++ b/docs/pages/providers/line.md @@ -15,7 +15,7 @@ const line = new Line(clientId, clientSecret, redirectURI); ``` ```ts -const url: URL = await line.createAuthorizationURL(codeVerifier, { +const url: URL = await line.createAuthorizationURL(state, codeVerifier, { // optional scopes // "openid" always included }); diff --git a/docs/pages/providers/microsoft-entra-id.md b/docs/pages/providers/microsoft-entra-id.md index 353f870d..271d3b95 100644 --- a/docs/pages/providers/microsoft-entra-id.md +++ b/docs/pages/providers/microsoft-entra-id.md @@ -15,7 +15,7 @@ const entraId = new MicrosoftEntraID(clientId, clientSecret, redirectURI); ``` ```ts -const url: URL = await entraId.createAuthorizationURL(codeVerifier, { +const url: URL = await entraId.createAuthorizationURL(state, codeVerifier, { // optional scopes // "openid" always included }); diff --git a/docs/pages/providers/twitter.md b/docs/pages/providers/twitter.md index da2cb6f6..ff88d7e3 100644 --- a/docs/pages/providers/twitter.md +++ b/docs/pages/providers/twitter.md @@ -15,7 +15,7 @@ const twitter = new Twitter(clientId, clientSecret, redirectURI); ``` ```ts -const url: URL = await twitter.createAuthorizationURL(codeVerifier, { +const url: URL = await twitter.createAuthorizationURL(state, codeVerifier, { // optional scopes }); diff --git a/src/index.ts b/src/index.ts index c7b5d5ec..6c7167c3 100644 --- a/src/index.ts +++ b/src/index.ts @@ -56,7 +56,7 @@ export interface OAuth2Provider { } export interface OAuth2ProviderWithPKCE { - createAuthorizationURL(codeVerifier: string): Promise; + createAuthorizationURL(state: string, codeVerifier: string): Promise; validateAuthorizationCode(code: string, codeVerifier: string): Promise; refreshAccessToken?(refreshToken: string): Promise; } diff --git a/src/providers/atlassian.ts b/src/providers/atlassian.ts index 21aea9b3..3e88cb43 100644 --- a/src/providers/atlassian.ts +++ b/src/providers/atlassian.ts @@ -18,15 +18,15 @@ export class Atlassian implements OAuth2Provider { } public async createAuthorizationURL( - codeVerifier: string, + state: string, options?: { scopes?: string[]; } ): Promise { const url = await this.client.createAuthorizationURL({ - codeVerifier, scopes: options?.scopes }); + url.searchParams.set("state", state); url.searchParams.set("audience", "api.atlassian.com"); url.searchParams.set("prompt", "consent"); return url; diff --git a/src/providers/google.ts b/src/providers/google.ts index fbcc58c0..0dcb7719 100644 --- a/src/providers/google.ts +++ b/src/providers/google.ts @@ -18,6 +18,7 @@ export class Google implements OAuth2ProviderWithPKCE { } public async createAuthorizationURL( + state: string, codeVerifier: string, options?: { scopes?: string[]; @@ -28,6 +29,7 @@ export class Google implements OAuth2ProviderWithPKCE { codeVerifier, scopes: [...scopes, "openid"] }); + url.searchParams.set("state", state); url.searchParams.set("nonce", "_"); return url; } diff --git a/src/providers/keycloak.ts b/src/providers/keycloak.ts index aebe1a60..e599ecce 100644 --- a/src/providers/keycloak.ts +++ b/src/providers/keycloak.ts @@ -19,6 +19,7 @@ export class Keycloak implements OAuth2ProviderWithPKCE { } public async createAuthorizationURL( + state: string, codeVerifier: string, options?: { scopes?: string[]; @@ -29,6 +30,7 @@ export class Keycloak implements OAuth2ProviderWithPKCE { codeVerifier, scopes: [...scopes, "openid"] }); + url.searchParams.set("state", state); return url; } public async validateAuthorizationCode( diff --git a/src/providers/line.ts b/src/providers/line.ts index 1abc7e0b..f71b1841 100644 --- a/src/providers/line.ts +++ b/src/providers/line.ts @@ -19,6 +19,7 @@ export class Line implements OAuth2ProviderWithPKCE { } public async createAuthorizationURL( + state: string, codeVerifier: string, options?: { scopes?: string[]; @@ -29,7 +30,8 @@ export class Line implements OAuth2ProviderWithPKCE { codeVerifier, scopes: [...scopes, "openid"] }); - url.searchParams.set("state", generateState()); + if (!state) state = generateState(); + url.searchParams.set("state", state); return url; } diff --git a/src/providers/microsoft-entra-id.ts b/src/providers/microsoft-entra-id.ts index 0f15c872..83a5590f 100644 --- a/src/providers/microsoft-entra-id.ts +++ b/src/providers/microsoft-entra-id.ts @@ -17,6 +17,7 @@ export class MicrosoftEntraId implements OAuth2ProviderWithPKCE { } public async createAuthorizationURL( + state: string, codeVerifier: string, options?: { scopes?: string[]; @@ -27,6 +28,7 @@ export class MicrosoftEntraId implements OAuth2ProviderWithPKCE { codeVerifier, scopes: [...scopes, "openid"] }); + url.searchParams.set("state", state); url.searchParams.set("nonce", "_"); return url; } diff --git a/src/providers/twitter.ts b/src/providers/twitter.ts index b826a245..9a04944f 100644 --- a/src/providers/twitter.ts +++ b/src/providers/twitter.ts @@ -1,4 +1,4 @@ -import { OAuth2Client, generateState } from "oslo/oauth2"; +import { OAuth2Client } from "oslo/oauth2"; import type { OAuth2ProviderWithPKCE } from "../index.js"; @@ -17,6 +17,7 @@ export class Twitter implements OAuth2ProviderWithPKCE { } public async createAuthorizationURL( + state: string, codeVerifier: string, options?: { scopes?: string[]; @@ -26,7 +27,7 @@ export class Twitter implements OAuth2ProviderWithPKCE { codeVerifier, scopes: options?.scopes }); - url.searchParams.set("state", generateState()); + url.searchParams.set("state", state); return url; }