Skip to content

Commit

Permalink
Lichess OAuth provider (#542)
Browse files Browse the repository at this point in the history
Co-authored-by: pilcrowOnPaper <[email protected]>
  • Loading branch information
gtim and pilcrowonpaper authored Jun 23, 2023
1 parent c520b5d commit b39f868
Show file tree
Hide file tree
Showing 6 changed files with 271 additions and 18 deletions.
127 changes: 127 additions & 0 deletions documentation-v2/content/oauth/providers/lichess.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
---
order:: 0
title: "Lichess"
description: "Learn about using the Lichess provider in Lucia OAuth integration"
---

OAuth integration for Lichess. Provider id is `lichess`.

```ts
import { lichess } from "@lucia-auth/oauth/providers";
import { auth } from "./lucia.js";

const lichessAuth = lichess(auth, config);
```

## `lichess()`

```ts
const lichess: (
auth: Auth,
config: {
clientId: string;
redirectUri: string;
scope?: string[];
}
) => LichessProvider;
```

##### Parameter

| name | type | description | optional |
| ------------------ | ------------------------------------ | --------------------------------------- | :------: |
| auth | [`Auth`](/reference/lucia-auth/auth) | Lucia instance | |
| config.clientId | `string` | client id - choose any unique client id | |
| config.redirectUri | `string` | redirect URI | |
| config.scope | `string[]` | an array of scopes ||

##### Returns

| type | description |
| ------------------------------------- | ---------------- |
| [`LichessProvider`](#lichessprovider) | Lichess provider |

## Interfaces

### `LichessProvider`

Satisfies [`LichessProvider`](/reference/oauth/oauthprovider).

#### `getAuthorizationUrl()`

Returns the authorization url for user redirection, a state and PKCE code verifier. The state and code verifier should be stored in a cookie and validated on callback.

```ts
const getAuthorizationUrl: () => Promise<
[url: URL, state: string, codeVerifier: string]
>;
```

##### Returns

| name | type | description |
| -------------- | -------- | -------------------- |
| `url` | `URL` | authorization url |
| `state` | `string` | state parameter used |
| `codeVerifier` | `string` | PKCE code verifier |

#### `validateCallback()`

Validates the callback code. Requires the PKCE code verifier generated with `getAuthorizationUrl()`.

```ts
const validateCallback: (
code: string,
codeVerifier: string
) => Promise<ProviderSession>;
```

##### Parameter

| name | type | description |
| -------------- | -------- | --------------------------------------------------------- |
| `code` | `string` | authorization code from callback |
| `codeVerifier` | `string` | PKCE code verifier generated with `getAuthorizationUrl()` |

##### Returns

| type |
| ------------------------------------- |
| [`LichessUserAuth`](#lichessuserauth) |

##### Errors

Request errors are thrown as [`OAuthRequestError`](/reference/oauth/interfaces#oauthrequesterror).

### `LichessUserAuth`

```ts
type LichessUserAuth = ProviderUserAuth & {
lichessUser: LichessUser;
lichessTokens: LichessTokens;
};
```

| type |
| ------------------------------------------------------------------ |
| [`ProviderUserAuth`](/reference/oauth/interfaces#provideruserauth) |
| [`LichessUser`](#lichessuser) |
| [`LichessTokens`](#lichesstokens) |

### `LichessTokens`

```ts
type LichessTokens = {
accessToken: string;
accessTokenExpiresIn: string;
};
```

## `LichessUser`

```ts
type LichessUser = {
id: string;
username: string;
};
```
4 changes: 4 additions & 0 deletions documentation-v2/content/reference/oauth/providers.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ See [Github](/oauth/providers/github) provider.

See [Google](/oauth/providers/google) provider.

## `lichess()`

See [Lichess](/oauth/providers/lichess) provider.

## `linkedin()`

See [Linkedin](/oauth/providers/linkedin) provider.
Expand Down
17 changes: 17 additions & 0 deletions packages/oauth/src/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,23 @@ export const generateState = () => {
return generateRandomString(43);
};

export const encodeBase64 = (s: string) => {
// ORDER IS IMPORTANT!!
// Buffer API EXISTS IN DENO!!
if (typeof window !== "undefined" && "Deno" in window) {
// deno
return btoa(s);
}
if (typeof Buffer === "function") {
// node
return Buffer.from(s).toString("base64");
}

// standard API
// IGNORE WARNING
return btoa(s);
};

export const scope = (base: string[], config: string[] = []) => {
return [...base, ...(config ?? [])].join(" ");
};
Expand Down
1 change: 1 addition & 0 deletions packages/oauth/src/providers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export { discord, type DiscordUser } from "./discord.js";
export { patreon, type PatreonUser } from "./patreon.js";
export { twitch, type TwitchUser } from "./twitch.js";
export { reddit, type RedditUser } from "./reddit.js";
export { lichess, type LichessUser } from "./lichess.js";
export { linkedin, type LinkedinUser } from "./linkedin.js";
export { auth0, type Auth0User } from "./auth0.js";
export { facebook, type FacebookUser } from "./facebook.js";
116 changes: 116 additions & 0 deletions packages/oauth/src/providers/lichess.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import { createUrl, handleRequest, authorizationHeaders } from "../request.js";
import {
scope,
generateState,
providerUserAuth,
encodeBase64
} from "../core.js";
import { generateRandomString } from "lucia/utils";

import type { Auth } from "lucia";
import type { OAuthConfig, OAuthProvider } from "../core.js";

type Config = OAuthConfig & {
redirectUri: string;
};

const PROVIDER_ID = "lichess";

export const lichess = <_Auth extends Auth>(auth: _Auth, config: Config) => {
const getAuthorizationUrl = async () => {
const state = generateState();
// PKCE code verifier length and alphabet defined in RFC 7636 section 4.1
const code_verifier = generateRandomString(
96,
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890-_.~"
);
const url = createUrl("https://lichess.org/oauth", {
response_type: "code",
client_id: config.clientId,
code_challenge_method: "S256",
code_challenge: pkceBase64urlEncode(
await pkceCodeChallenge(code_verifier)
),
scope: scope([], config.scope),
redirect_uri: config.redirectUri,
state
});
return [url, state, code_verifier] as const;
};

const getLichessTokens = async (code: string, codeVerifier: string) => {
// Not using createUrl since we need to POST
const request = new Request("https://lichess.org/api/token", {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded"
},
body: new URLSearchParams({
client_id: config.clientId,
grant_type: "authorization_code",
redirect_uri: config.redirectUri,
code_verifier: codeVerifier,
code
}).toString()
});
const tokens = await handleRequest<{
access_token: string;
expires_in: number;
}>(request);

return {
accessToken: tokens.access_token,
accessTokenExpiresIn: tokens.expires_in
};
};

const getLichessUser = async (accessToken: string) => {
const request = new Request("https://lichess.org/api/account", {
headers: authorizationHeaders("bearer", accessToken)
});
const lichessUser = await handleRequest<LichessUser>(request);
return lichessUser;
};

const validateCallback = async (code: string, code_verifier: string) => {
const lichessTokens = await getLichessTokens(code, code_verifier);
const lichessUser = await getLichessUser(lichessTokens.accessToken);
const providerUserId = lichessUser.id;
const lichessUserAuth = await providerUserAuth(
auth,
PROVIDER_ID,
providerUserId
);
return {
...lichessUserAuth,
lichessUser,
lichessTokens
};
};

return {
getAuthorizationUrl,
validateCallback
} as const satisfies OAuthProvider;
};

export type LichessUser = {
id: string;
username: string;
};

// Base64url-encode as specified in RFC 7636 (OAuth PKCE).
const pkceBase64urlEncode = (arg: string) => {
return encodeBase64(arg)
.split("=")[0]
.replace(/\+/g, "-")
.replace(/\//g, "_");
};

// Generates code_challenge from code_verifier, as specified in RFC 7636.
const pkceCodeChallenge = async (verifier: string) => {
const verifierBuffer = new TextEncoder().encode(verifier);
const challengeBuffer = await crypto.subtle.digest("SHA-256", verifierBuffer);
const challengeArray = Array.from(new Uint8Array(challengeBuffer));
return String.fromCharCode(...challengeArray);
};
24 changes: 6 additions & 18 deletions packages/oauth/src/providers/reddit.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import { createUrl, handleRequest, authorizationHeaders } from "../request.js";
import { scope, generateState, providerUserAuth } from "../core.js";
import {
scope,
generateState,
providerUserAuth,
encodeBase64
} from "../core.js";

import type { Auth } from "lucia";
import type { OAuthConfig, OAuthProvider } from "../core.js";
Expand Down Expand Up @@ -73,23 +78,6 @@ export const reddit = <_Auth extends Auth>(auth: _Auth, config: Config) => {
} as const satisfies OAuthProvider;
};

const encodeBase64 = (s: string) => {
// ORDER IS IMPORTANT!!
// Buffer API EXISTS IN DENO!!
if (typeof window !== "undefined" && "Deno" in window) {
// deno
return btoa(s);
}
if (typeof Buffer === "function") {
// node
return Buffer.from(s).toString("base64");
}

// standard API
// IGNORE WARNING
return btoa(s);
};

export type RedditUser = {
is_employee: boolean;
seen_layout_switch: boolean;
Expand Down

0 comments on commit b39f868

Please sign in to comment.