Skip to content

Commit

Permalink
Merge pull request #28 from LinusOP/pkce-state
Browse files Browse the repository at this point in the history
Optional state for PKCE clients
  • Loading branch information
pilcrowonpaper authored Dec 11, 2023
2 parents aaec920 + 838c14c commit b79fc1d
Show file tree
Hide file tree
Showing 13 changed files with 45 additions and 18 deletions.
32 changes: 25 additions & 7 deletions docs/pages/guides/oauth2-pkce.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/
Expand Down
2 changes: 1 addition & 1 deletion docs/pages/providers/google.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
});
Expand Down
2 changes: 1 addition & 1 deletion docs/pages/providers/keycloak.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
});
Expand Down
2 changes: 1 addition & 1 deletion docs/pages/providers/line.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
});
Expand Down
2 changes: 1 addition & 1 deletion docs/pages/providers/microsoft-entra-id.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
});
Expand Down
2 changes: 1 addition & 1 deletion docs/pages/providers/twitter.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
});
Expand Down
2 changes: 1 addition & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ export interface OAuth2Provider {
}

export interface OAuth2ProviderWithPKCE {
createAuthorizationURL(codeVerifier: string): Promise<URL>;
createAuthorizationURL(state: string, codeVerifier: string): Promise<URL>;
validateAuthorizationCode(code: string, codeVerifier: string): Promise<Tokens>;
refreshAccessToken?(refreshToken: string): Promise<Tokens>;
}
Expand Down
4 changes: 2 additions & 2 deletions src/providers/atlassian.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,15 @@ export class Atlassian implements OAuth2Provider {
}

public async createAuthorizationURL(
codeVerifier: string,
state: string,
options?: {
scopes?: string[];
}
): Promise<URL> {
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;
Expand Down
2 changes: 2 additions & 0 deletions src/providers/google.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export class Google implements OAuth2ProviderWithPKCE {
}

public async createAuthorizationURL(
state: string,
codeVerifier: string,
options?: {
scopes?: string[];
Expand All @@ -28,6 +29,7 @@ export class Google implements OAuth2ProviderWithPKCE {
codeVerifier,
scopes: [...scopes, "openid"]
});
url.searchParams.set("state", state);
url.searchParams.set("nonce", "_");
return url;
}
Expand Down
2 changes: 2 additions & 0 deletions src/providers/keycloak.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export class Keycloak implements OAuth2ProviderWithPKCE {
}

public async createAuthorizationURL(
state: string,
codeVerifier: string,
options?: {
scopes?: string[];
Expand All @@ -29,6 +30,7 @@ export class Keycloak implements OAuth2ProviderWithPKCE {
codeVerifier,
scopes: [...scopes, "openid"]
});
url.searchParams.set("state", state);
return url;
}
public async validateAuthorizationCode(
Expand Down
4 changes: 3 additions & 1 deletion src/providers/line.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export class Line implements OAuth2ProviderWithPKCE {
}

public async createAuthorizationURL(
state: string,
codeVerifier: string,
options?: {
scopes?: string[];
Expand All @@ -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;
}

Expand Down
2 changes: 2 additions & 0 deletions src/providers/microsoft-entra-id.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export class MicrosoftEntraId implements OAuth2ProviderWithPKCE {
}

public async createAuthorizationURL(
state: string,
codeVerifier: string,
options?: {
scopes?: string[];
Expand All @@ -27,6 +28,7 @@ export class MicrosoftEntraId implements OAuth2ProviderWithPKCE {
codeVerifier,
scopes: [...scopes, "openid"]
});
url.searchParams.set("state", state);
url.searchParams.set("nonce", "_");
return url;
}
Expand Down
5 changes: 3 additions & 2 deletions src/providers/twitter.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { OAuth2Client, generateState } from "oslo/oauth2";
import { OAuth2Client } from "oslo/oauth2";

import type { OAuth2ProviderWithPKCE } from "../index.js";

Expand All @@ -17,6 +17,7 @@ export class Twitter implements OAuth2ProviderWithPKCE {
}

public async createAuthorizationURL(
state: string,
codeVerifier: string,
options?: {
scopes?: string[];
Expand All @@ -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;
}

Expand Down

0 comments on commit b79fc1d

Please sign in to comment.