Skip to content

Commit

Permalink
Add Slack OAuth provider (#1008)
Browse files Browse the repository at this point in the history
  • Loading branch information
pilcrowonpaper authored Aug 22, 2023
1 parent a03e7d2 commit 30a114d
Show file tree
Hide file tree
Showing 6 changed files with 285 additions and 8 deletions.
6 changes: 6 additions & 0 deletions .auri/$zr0k9t3z.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
package: "@lucia-auth/oauth" # package name
type: "minor" # "major", "minor", "patch"
---

Add Slack OAuth providers
14 changes: 7 additions & 7 deletions documentation/content/oauth/providers/google.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,13 @@ const google: (

##### Parameters

| name | type | description | optional | default |
| ---------------------- | ------------------------------------------ | ------------------------------ | :------: | :-----: |
| `auth` | [`Auth`](/reference/lucia/interfaces/auth) | Lucia instance | | |
| `configs.clientId` | `string` | Google OAuth app client id | | |
| `configs.clientSecret` | `string` | Google OAuth app client secret | | |
| `configs.redirectUri` | `string` | an authorized redirect URI | | |
| `configs.scope` | `string[]` | an array of scopes || |
| name | type | description | optional |
| ---------------------- | ------------------------------------------ | ------------------------------ | :------: |
| `auth` | [`Auth`](/reference/lucia/interfaces/auth) | Lucia instance | |
| `configs.clientId` | `string` | Google OAuth app client id | |
| `configs.clientSecret` | `string` | Google OAuth app client secret | |
| `configs.redirectUri` | `string` | an authorized redirect URI | |
| `configs.scope` | `string[]` | an array of scopes ||

##### Returns

Expand Down
134 changes: 134 additions & 0 deletions documentation/content/oauth/providers/slack.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
---
title: "Slack"
description: "Learn how to use the Salck OAuth provider"
---

OAuth integration for Slack. Provider id is `slack`.

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

const slackAuth = slack(auth, configs);
```

## `slack()`

Scopes `oidc` and `profile` are always included.

```ts
const slack: (
auth: Auth,
configs: {
clientId: string;
clientSecret: string;
redirectUri: string;
scope?: string[];
}
) => SlackProvider;
```

##### Parameters

| name | type | description | optional |
| ---------------------- | ------------------------------------------ | ------------------------------------------ | :------: |
| `auth` | [`Auth`](/reference/lucia/interfaces/auth) | Lucia instance | |
| `configs.clientId` | `string` | Slack OAuth app client id | |
| `configs.clientSecret` | `string` | Slack OAuth app client secret | |
| `configs.redirectUri` | `string` | an authorized redirect URI (must be HTTPS) | |
| `configs.scope` | `string[]` | an array of scopes ||

##### Returns

| type | description |
| --------------------------------- | -------------- |
| [`SlackProvider`](#slackprovider) | Slack provider |

## Interfaces

### `SlackAuth`

See [`OAuth2ProviderAuth`](/reference/oauth/interfaces/oauth2providerauth).

```ts
// implements OAuth2ProviderAuth<SlackAuth<_Auth>>
interface SlackAuth<_Auth extends Auth> {
getAuthorizationUrl: () => Promise<readonly [url: URL, state: string]>;
validateCallback: (code: string) => Promise<SlackUserAuth<_Auth>>;
}
```

| type |
| --------------------------------- |
| [`SlackUserAuth`](#slackuserauth) |

##### Generics

| name | extends | default |
| ------- | ---------- | ------- |
| `_Auth` | [`Auth`]() | `Auth` |

### `SlackTokens`

```ts
type SlackTokens = {
accessToken: string;
idToken: string;
};
```

### `SlackUser`

```ts
type SlackUser = {
sub: string;
"https://slack.com/user_id": string;
"https://slack.com/team_id": string;
email?: string;
email_verified: boolean;
date_email_verified: number;
name: string;
picture: string;
given_name: string;
family_name: string;
locale: string;
"https://slack.com/team_name": string;
"https://slack.com/team_domain": string;
"https://slack.com/user_image_24": string;
"https://slack.com/user_image_32": string;
"https://slack.com/user_image_48": string;
"https://slack.com/user_image_72": string;
"https://slack.com/user_image_192": string;
"https://slack.com/user_image_512": string;
"https://slack.com/team_image_34": string;
"https://slack.com/team_image_44": string;
"https://slack.com/team_image_68": string;
"https://slack.com/team_image_88": string;
"https://slack.com/team_image_102": string;
"https://slack.com/team_image_132": string;
"https://slack.com/team_image_230": string;
"https://slack.com/team_image_default": true;
};
```

### `SlackUserAuth`

Extends [`ProviderUserAuth`](/reference/oauth/interfaces/provideruserauth).

```ts
interface Auth0UserAuth<_Auth extends Auth> extends ProviderUserAuth<_Auth> {
slackUser: SlackUser;
slackTokens: SlackTokens;
}
```

| properties | type | description |
| ------------- | ----------------------------- | ----------------- |
| `slackUser` | [`SlackUser`](#slackuser) | Slack user |
| `slackTokens` | [`SlackTokens`](#slacktokens) | Access tokens etc |

##### Generics

| name | extends |
| ------- | ---------- |
| `_Auth` | [`Auth`]() |
1 change: 1 addition & 0 deletions documentation/src/components/menus/OAuthMenu.astro
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import Menu from "./Menu.astro";
["osu!", "/oauth/providers/osu"],
["Patreon", "/oauth/providers/patreon"],
["Reddit", "/oauth/providers/reddit"],
["Slack", "/oauth/providers/slack"],
["Spotify", "/oauth/providers/spotify"],
["Twitch", "/oauth/providers/twitch"],
["Twitter", "/oauth/providers/twitter"]
Expand Down
2 changes: 1 addition & 1 deletion packages/oauth/src/providers/azure-ad.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ export class AzureADAuth<
redirectUri: this.config.redirectUri
}
);
url.searchParams.set("nonce", generateRandomString(32));
url.searchParams.set("nonce", generateRandomString(40));
return [url, codeVerifier, state];
};

Expand Down
136 changes: 136 additions & 0 deletions packages/oauth/src/providers/slack.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
import { generateRandomString } from "lucia/utils";
import {
OAuth2ProviderAuth,
createOAuth2AuthorizationUrl,
validateOAuth2AuthorizationCode
} from "../core/oauth2.js";
import { ProviderUserAuth } from "../core/provider.js";
import { handleRequest, authorizationHeader } from "../utils/request.js";

import type { Auth } from "lucia";

type Config = {
clientId: string;
clientSecret: string;
redirectUri: string;
scope?: string[];
};

const PROVIDER_ID = "slack";

export const slack = <_Auth extends Auth = Auth>(
auth: _Auth,
config: Config
): SlackAuth<_Auth> => {
return new SlackAuth(auth, config);
};

export class SlackAuth<_Auth extends Auth = Auth> extends OAuth2ProviderAuth<
SlackUserAuth<_Auth>
> {
private config: Config;

constructor(auth: _Auth, config: Config) {
super(auth);

this.config = config;
}

public getAuthorizationUrl = async (): Promise<
readonly [url: URL, state: string]
> => {
const scopeConfig = this.config.scope ?? [];
const [url, state] = await createOAuth2AuthorizationUrl(
"https://slack.com/openid/connect/authorize",
{
clientId: this.config.clientId,
scope: ["oidc", "profile", ...scopeConfig],
redirectUri: this.config.redirectUri
}
);
url.searchParams.set("nonce", generateRandomString(40));
return [url, state];
};

public validateCallback = async (
code: string
): Promise<SlackUserAuth<_Auth>> => {
const slackTokens = await this.validateAuthorizationCode(code);
const slackUserRequest = new Request(
"https://slack.com/api/openid.connect.userInfo",
{
headers: {
Authorization: authorizationHeader("bearer", slackTokens.accessToken)
}
}
);
const slackUser = await handleRequest<SlackUser>(slackUserRequest);
return new SlackUserAuth(this.auth, slackUser, slackTokens);
};

private validateAuthorizationCode = async (
code: string
): Promise<SlackTokens> => {
const tokens = await validateOAuth2AuthorizationCode<{
access_token: string;
id_token: string;
}>(code, "https://slack.com/api/openid.connect.token", {
clientId: this.config.clientId,
clientPassword: {
clientSecret: this.config.clientSecret,
authenticateWith: "client_secret"
}
});
return {
accessToken: tokens.access_token,
idToken: tokens.id_token
};
};
}

export class SlackUserAuth<_Auth extends Auth> extends ProviderUserAuth<_Auth> {
public slackTokens: SlackTokens;
public slackUser: SlackUser;

constructor(auth: _Auth, slackUser: SlackUser, slackTokens: SlackTokens) {
super(auth, PROVIDER_ID, slackUser.sub);

this.slackTokens = slackTokens;
this.slackUser = slackUser;
}
}

export type SlackTokens = {
accessToken: string;
idToken: string;
};

export type SlackUser = {
sub: string;
"https://slack.com/user_id": string;
"https://slack.com/team_id": string;
email?: string;
email_verified: boolean;
date_email_verified: number;
name: string;
picture: string;
given_name: string;
family_name: string;
locale: string;
"https://slack.com/team_name": string;
"https://slack.com/team_domain": string;
"https://slack.com/user_image_24": string;
"https://slack.com/user_image_32": string;
"https://slack.com/user_image_48": string;
"https://slack.com/user_image_72": string;
"https://slack.com/user_image_192": string;
"https://slack.com/user_image_512": string;
"https://slack.com/team_image_34": string;
"https://slack.com/team_image_44": string;
"https://slack.com/team_image_68": string;
"https://slack.com/team_image_88": string;
"https://slack.com/team_image_102": string;
"https://slack.com/team_image_132": string;
"https://slack.com/team_image_230": string;
"https://slack.com/team_image_default": true;
};

0 comments on commit 30a114d

Please sign in to comment.