Skip to content

Commit

Permalink
Fix #1296 Add "Sign in with Slack (OpenID Connect)" support (#1301)
Browse files Browse the repository at this point in the history
  • Loading branch information
seratch authored Aug 6, 2021
1 parent 74149a5 commit 15cd74c
Show file tree
Hide file tree
Showing 29 changed files with 524 additions and 173 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
node_modules
/package-lock.json
scripts/package-lock.json
examples/*/package-lock.json

npm-debug.log
lerna-debug.log
tmp/
.env*
70 changes: 70 additions & 0 deletions examples/openid-connect/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# OpenID Connect Example

This repo contains a sample app for implementing Sign in with Slack (OpenID Connect compatible). Checkout `app.js`. The code includes a few different options which have been commented out. As you play around with the app, you can uncomment some of these options to get a deeper understanding of how to use this library.

Local development requires a public URL where Slack can send requests. In this guide, we'll be using [`ngrok`](https://ngrok.com/download). Checkout [this guide](https://api.slack.com/tutorials/tunneling-with-ngrok) for setting it up.

Before we get started, make sure you have a development workspace where you have permissions to install apps. If you don’t have one setup, go ahead and [create one](https://slack.com/create). You also need to [create a new app](https://api.slack.com/apps?new_app=1) with the following App Manifest if you haven’t already.

```yaml
_metadata:
major_version: 1
minor_version: 1
display_information:
name: my-openid-connect-app
oauth_config:
redirect_urls:
# TODO: Replace the URL with our own one
- https://your-own-domain/slack/oauth_redirect
scopes:
user:
- openid
- email
- profile
```
## Install Dependencies
```
npm install
```

## Setup Environment Variables

This app requires you setup a few environment variables. You can get these values by navigating to your app's [**BASIC INFORMATION** Page](https://api.slack.com/apps).

```
export SLACK_CLIENT_ID=YOUR_SLACK_CLIENT_ID
export SLACK_CLIENT_SECRET=YOUR_SLACK_CLIENT_SECRET
export SLACK_REDIRECT_URI=https://{your-public-domain}/slack/oauth_redirect
```

## Run the App

Start the app with the following command:

```
npm start
```

This will start the app on port `3000`.

Now lets start `ngrok` so we can access the app on an external network and create a `redirect url` for OAuth.

```
ngrok http 3000
```

This should output a forwarding address for `http` and `https`. Take note of the `https` one. It should look something like the following:

```
Forwarding https://3cb89939.ngrok.io -> http://localhost:3000
```

Go to your app on https://api.slack.com/apps and navigate to your apps **OAuth & Permissions** page. Under **Redirect URLs**, add your `ngrok` forwarding address with the `/slack/oauth_redirect` path appended. ex:

```
https://3cb89939.ngrok.io/slack/oauth_redirect
```

Everything is now setup. In your browser, navigate to http://localhost:3000/slack/install to initiate the oAuth flow. Once you install the app, it should receive the OpenID Connect claims and an OAuth access token for the connected Slack account.
133 changes: 133 additions & 0 deletions examples/openid-connect/app.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
const Koa = require("koa");
const Router = require("@koa/router");
const { WebClient } = require("@slack/web-api"); // requires v6.4 or higher
const jwt = require("jsonwebtoken");
const uuid = require("uuid");

const app = new Koa();
const router = new Router();

router.get("/", async (ctx) => {
ctx.redirect("/slack/install");
});

const clientId = process.env.SLACK_CLIENT_ID;
const clientSecret = process.env.SLACK_CLIENT_SECRET;
const oidcScopes = "openid,email,profile"; // openid is required at least
const redirectUri = process.env.SLACK_REDIRECT_URI;

class MyStateStore {
constructor() {
this.activeStates = {};
}
async generate() {
const newValue = uuid.v4();
this.activeStates[newValue] = Date.now() + 10 * 60 * 1000; // 10 minutes
return newValue;
}
async validate(state) {
const expiresAt = this.activeStates[state];
if (expiresAt && Date.now() <= expiresAt) {
delete this.activeStates[state];
return true;
}
return false;
}
}
const myStateStore = new MyStateStore();

router.get("/slack/install", async (ctx) => {
const state = await myStateStore.generate();
// (optional) you can pass nonce parameter as well
// refer to https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest for details
const nonce = "your-own-nonce-value";
const url = `https://slack.com/openid/connect/authorize?response_type=code&state=${state}&client_id=${clientId}&scope=${oidcScopes}&redirect_uri=${redirectUri}&nonce=${nonce}`;

ctx.headers["content-type"] = "text/html; charset=utf-8";
ctx.body = `<html>
<head><style>body {padding: 10px 15px;font-family: verdana;text-align: center;}</style></head>
<body>
<h2>Slack OpenID Connect</h2>
<p><a href="${url}"><img alt="Sign in with Slack" height="40" width="139" src="https://platform.slack-edge.com/img/add_to_slack.png" srcset="https://platform.slack-edge.com/img/add_to_slack.png 1x, https://platform.slack-edge.com/img/add_to_slack@2x.png 2x" /></a></p>
</body>
</html>`;
});

const client = new WebClient();

router.get("/slack/oauth_redirect", async (ctx) => {
if (!(await myStateStore.validate(ctx.query.state))) {
ctx.status = 400;
ctx.headers["content-type"] = "text/html; charset=utf-8";
ctx.body = `<html>
<head><style>body {padding: 10px 15px;font-family: verdana;text-align: center;}</style></head>
<body>
<h2>Invalid request</h2>
<p>Try again from <a href="./install">here</a></p>
</body>
</html>`;
return;
}

const token = await client.openid.connect.token({
client_id: clientId,
client_secret: clientSecret,
grant_type: "authorization_code",
code: ctx.query.code,
});
console.log(
`openid.connect.token response: ${JSON.stringify(token, null, 2)}`
);

let userAccessToken = token.access_token;

if (token.refresh_token) {
// token.refresh_token can exist if the token rotation is enabled.
// The following lines of code demonstrate how to refresh the token.
// If you don't enable token rotation, you can safely remove this part.
const refreshedToken = await client.openid.connect.token({
client_id: clientId,
client_secret: clientSecret,
grant_type: "refresh_token",
refresh_token: token.refresh_token,
});
console.log(
`openid.connect.token (refresh) response: ${JSON.stringify(
refreshedToken,
null,
2
)}`
);
userAccessToken = refreshedToken.access_token;
}

// You can save the id_token (JWT string) as-is but you can decode the value this way:
const claims = jwt.decode(token.id_token);

// TODO: you can do something meaningful here
// (e.g., storing the data in database + navigating the user to your service top paeg)

// The is a quick example demonstrating how to perform openid.connect.userInfo with the given access token.
// You don't need to do this here.
const tokenWiredClient = new WebClient(userAccessToken);
const userInfo = await tokenWiredClient.openid.connect.userInfo();
console.log(
`openid.connect.userInfo response: ${JSON.stringify(userInfo, null, 2)}`
);

ctx.headers["content-type"] = "text/html; charset=utf-8";
ctx.body = `<html>
<head><style>body h2 {padding: 10px 15px;font-family: verdana;text-align: center;}</style></head>
<body>
<h2>OpenID Connect Claims</h2>
<pre>${JSON.stringify(claims, null, 2)}</pre>
<h2>openid.connect.userInfo response</h2>
<pre>${JSON.stringify(userInfo, null, 2)}</pre>
</body>
</html>`;
});

// Enable the routes
app.use(router.routes()).use(router.allowedMethods());
// Start the web app, which is available at http://localhost:3000/slack/*
app.listen(3000);
14 changes: 14 additions & 0 deletions examples/openid-connect/app_manifest.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
_metadata:
major_version: 1
minor_version: 1
display_information:
name: my-openid-connect-app
oauth_config:
redirect_urls:
# TODO: Replace the URL with our own one
- https://your-own-domain/slack/oauth_redirect
scopes:
user:
- openid
- email
- profile
11 changes: 11 additions & 0 deletions examples/openid-connect/link.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
#!/bin/bash

current_dir=`dirname $0`
cd ${current_dir}
npm unlink @slack/web-api \
&& npm i \
&& cd ../../packages/web-api \
&& npm link \
&& cd - \
&& npm i \
&& npm link @slack/web-api
18 changes: 18 additions & 0 deletions examples/openid-connect/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"name": "openid-connect-example",
"version": "1.0.0",
"description": "",
"main": "app.js",
"scripts": {
"start": "node app.js"
},
"author": "Slack Technologies, Inc.",
"license": "MIT",
"repository": "slackapi/node-slack-sdk",
"dependencies": {
"@koa/router": "^10.1.0",
"jsonwebtoken": "^8.5.1",
"koa": "^2.13.1",
"uuid": "^8.3.2"
}
}
4 changes: 2 additions & 2 deletions packages/web-api/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@slack/web-api",
"version": "6.3.0",
"version": "6.4.0",
"description": "Official library for using the Slack Platform's Web API",
"author": "Slack Technologies, Inc.",
"license": "MIT",
Expand Down Expand Up @@ -81,4 +81,4 @@
"tsd": {
"directory": "test/types"
}
}
}
24 changes: 24 additions & 0 deletions packages/web-api/src/methods.ts
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,8 @@ import { AdminUsersSessionSetSettingsResponse } from './response/AdminUsersSessi
import { AdminUsersSessionClearSettingsResponse } from './response/AdminUsersSessionClearSettingsResponse';
import { AdminAppsClearResolutionResponse } from './response/AdminAppsClearResolutionResponse';
import { AdminAppsUninstallResponse } from './response/AdminAppsUninstallResponse';
import { OpenIDConnectTokenResponse } from './response/OpenIDConnectTokenResponse';
import { OpenIDConnectUserInfoResponse } from './response/OpenIDConnectUserInfoResponse';

// NOTE: could create a named type alias like data types like `SlackUserID: string`

Expand Down Expand Up @@ -526,6 +528,13 @@ export abstract class Methods extends EventEmitter<WebClientEvent> {
},
};

public readonly openid = {
connect: {
token: bindApiCall<OpenIDConnectTokenArguments, OpenIDConnectTokenResponse>(this, 'openid.connect.token'),
userInfo: bindApiCall<OpenIDConnectUserInfoArguments, OpenIDConnectUserInfoResponse>(this, 'openid.connect.userInfo'),
},
};

public readonly pins = {
add: bindApiCall<PinsAddArguments, PinsAddResponse>(this, 'pins.add'),
list: bindApiCall<PinsListArguments, PinsListResponse>(this, 'pins.list'),
Expand Down Expand Up @@ -1669,6 +1678,21 @@ export interface OAuthV2ExchangeArguments extends WebAPICallOptions {
grant_type: string;
refresh_token: string;
}

/*
* `openid.connect.*`
*/
export interface OpenIDConnectTokenArguments extends WebAPICallOptions {
client_id: string;
client_secret: string;
code?: string;
redirect_uri?: string;
grant_type?: 'authorization_code' | 'refresh_token';
refresh_token?: string;
}
export interface OpenIDConnectUserInfoArguments extends WebAPICallOptions {
}

/*
* `pins.*`
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ export interface Attachment {
thumb_url?: string;
thumb_width?: number;
thumb_height?: number;
video_url?: string;
video_html?: string;
video_html_width?: number;
video_html_height?: number;
Expand Down
15 changes: 8 additions & 7 deletions packages/web-api/src/response/DndSetSnoozeResponse.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
/* tslint:disable */
import { WebAPICallResult } from '../WebClient';
export type DndSetSnoozeResponse = WebAPICallResult & {
ok?: boolean;
error?: string;
snooze_enabled?: boolean;
snooze_endtime?: number;
snooze_remaining?: number;
needed?: string;
provided?: string;
ok?: boolean;
error?: string;
snooze_enabled?: boolean;
snooze_endtime?: number;
snooze_remaining?: number;
snooze_is_indefinite?: boolean;
needed?: string;
provided?: string;
};
1 change: 1 addition & 0 deletions packages/web-api/src/response/FilesInfoResponse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ export interface File {
original_w?: number;
original_h?: number;
thumb_tiny?: string;
media_display_type?: string;
}

export interface Shares {
Expand Down
1 change: 1 addition & 0 deletions packages/web-api/src/response/FilesListResponse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ export interface File {
thumb_1024_w?: number;
thumb_1024_h?: number;
thumb_video?: string;
media_display_type?: string;
}

export interface Paging {
Expand Down
Loading

0 comments on commit 15cd74c

Please sign in to comment.