Skip to content

Commit

Permalink
Fix slackapi#1435 Proper use of state parameter for the OAuth CSRF pr…
Browse files Browse the repository at this point in the history
…otection

Co-authored-by: Yuji Nakayama <nkymyj@gmail.com>
  • Loading branch information
seratch and yujinakayama committed Mar 1, 2022
1 parent 7689fad commit 57ae345
Show file tree
Hide file tree
Showing 9 changed files with 610 additions and 72 deletions.
39 changes: 38 additions & 1 deletion examples/oauth-v2/app.js
Expand Up @@ -13,7 +13,7 @@ const slackEvents = createEventAdapter(process.env.SLACK_SIGNING_SECRET, {
// Set path to receive events
app.use('/slack/events', slackEvents.requestListener());

const scopes = ['channels:read', 'groups:read', 'channels:manage', 'chat:write', 'incoming-webhook'];
const scopes = ['app_mentions:read', 'channels:read', 'groups:read', 'channels:manage', 'chat:write', 'incoming-webhook'];
const userScopes = ['chat:write'];

const installer = new InstallProvider({
Expand Down Expand Up @@ -44,6 +44,17 @@ app.get('/slack/install', async (req, res, next) => {
}
});

// This works since @slack/oauth@2.5.0 or newer
/*
app.get('/slack/install', async (req, res) => {
await installer.handleInstallPath(req, res, {
scopes,
userScopes,
metadata: 'some_metadata',
});
});
*/

// example 1
// use default success and failure handlers
app.get('/slack/oauth_redirect', async (req, res) => {
Expand All @@ -65,6 +76,32 @@ app.get('/slack/oauth_redirect', async (req, res) => {
// await installer.handleCallback(req, res, callbackOptions);
// });

slackEvents.on('app_mention', async (event, body) => {
console.log(event);
let DBInstallData;
if (body.authorizations !== undefined && body.authorizations[0].is_enterprise_install) {
//org wide installation
DBInstallData = await installer.authorize({
enterpriseId: body.enterprise_id,
userId: event.user,
isEnterpriseInstall: true,
});
} else {
// non org wide installation
DBInstallData = await installer.authorize({
enterpriseId: body.enterprise_id,
teamId: body.team_id,
userId: event.user,
isEnterpriseInstall: false,
});
}
const web = new WebClient(DBInstallData.botToken);
await web.chat.postMessage({
channel: event.channel,
text: 'Hi there!',
});
});

// When a user navigates to the app home, grab the token from our database and publish a view
slackEvents.on('app_home_opened', async (event, body) => {
console.log(event);
Expand Down
55 changes: 47 additions & 8 deletions packages/oauth/src/callback-options.ts
@@ -1,5 +1,5 @@
import { IncomingMessage, ServerResponse } from 'http';
import { CodedError } from './errors';
import { CodedError, ErrorCode } from './errors';
import { InstallURLOptions } from './install-url-options';
import { Installation, OrgInstallation } from './installation';

Expand Down Expand Up @@ -47,25 +47,64 @@ export function defaultCallbackSuccess(
// does not change the workspace the slack client was last in
redirectUrl = 'slack://open';
}
let browserUrl = redirectUrl;
if (isNotOrgInstall(installation)) {
browserUrl = `https://app.slack.com/client/${installation.team.id}`;
}
const htmlResponse = `<html>
<head>
<meta http-equiv="refresh" content="0; URL=${redirectUrl}">
<style>
body {
padding: 10px 15px;
font-family: verdana;
text-align: center;
}
</style>
</head>
<body>
<h1>Success! Redirecting to the Slack App...</h1>
<button onClick="window.location = '${redirectUrl}'">Click here to redirect</button>
</body></html>`;
res.writeHead(200, { 'Content-Type': 'text/html' });
<h2>Thank you!</h2>
<p>Redirecting to the Slack App... click <a href="${redirectUrl}">here</a>. If you use the browser version of Slack, click <a href="${browserUrl}" target="_blank">this link</a> instead.</p>
</body>
</html>`;
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
res.end(htmlResponse);
}

// Default function to call when OAuth flow is unsuccessful
export function defaultCallbackFailure(
_error: CodedError,
error: CodedError,
_options: InstallURLOptions,
_req: IncomingMessage,
res: ServerResponse,
): void {
res.writeHead(500, { 'Content-Type': 'text/html' });
res.end('<html><body><h1>Oops, Something Went Wrong! Please Try Again or Contact the App Owner</h1></body></html>');
let httpStatus: number;
switch (error.code) {
case ErrorCode.MissingStateError:
case ErrorCode.InvalidStateError:
case ErrorCode.MissingCodeError:
httpStatus = 400;
break;
default:
httpStatus = 500;
}
res.writeHead(httpStatus, { 'Content-Type': 'text/html; charset=utf-8' });
const html = `<html>
<head>
<style>
body {
padding: 10px 15px;
font-family: verdana;
text-align: center;
}
</style>
</head>
<body>
<h2>Oops, Something Went Wrong!</h2>
<p>Please try again or contact the app owner (reason: ${error.code})</p>
</body>
</html>`;
res.end(html);
}

// ------------------------------------------
Expand Down
11 changes: 11 additions & 0 deletions packages/oauth/src/default-render-html-for-install-path.spec.js
@@ -0,0 +1,11 @@
require('mocha');
const { assert } = require('chai');
const { default: defaultRenderHtmlForInstallPath } = require('./default-render-html-for-install-path');

describe('defaultRenderHtmlForInstallPath', async () => {
it('should render an HTML text with a given URL', async () => {
const url = 'https://expected-url';
const html = defaultRenderHtmlForInstallPath(url);
assert.isTrue(html.includes(`<a href="${url}">`));
});
});
18 changes: 18 additions & 0 deletions packages/oauth/src/default-render-html-for-install-path.ts
@@ -0,0 +1,18 @@
export default function defaultRenderHtmlForInstallPath(addToSlackUrl: string): string {
return `<html>
<head>
<link rel="icon" href="data:,">
<style>
body {
padding: 10px 15px;
font-family: verdana;
text-align: center;
}
</style>
</head>
<body>
<h2>Slack App Installation</h2>
<p><a href="${addToSlackUrl}"><img alt=""Add to 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>`;
}
86 changes: 84 additions & 2 deletions packages/oauth/src/install-provider-options.ts
Expand Up @@ -2,17 +2,99 @@ import { Logger, LogLevel } from '@slack/logger';
import { WebClientOptions } from '@slack/web-api';
import { StateStore } from './state-stores';
import { InstallationStore } from './stores';
import { InstallURLOptions } from './install-url-options';

export interface InstallProviderOptions {

/**
* Client ID, which can be found under the Basic Information section of your application on https://api.slack.com/apps
*/
clientId: string;

/**
* Client Secret, which can be found under the Basic Information section of your application on https://api.slack.com/apps
*/
clientSecret: string;

/**
* Manages installation data, which can be called by both the OAuth flow and authorize() in event handling
*/
installationStore?: InstallationStore; // default MemoryInstallationStore

/**
* The slack.com authorize URL
*/
authorizationUrl?: string;

/**
* Stores state issued to authorization server
* and verifies the value returned at redirection during OAuth flow to prevent CSRF
*/
stateStore?: StateStore; // default ClearStateStore

/**
* The secret value used for generating the state parameter value
*/
stateSecret?: string; // required with default ClearStateStore

/**
* handleCallback() verifies the state parameter if true (default: true)
*/
stateVerification?: boolean; // default true, disables state verification when false
installationStore?: InstallationStore; // default MemoryInstallationStore

/**
* handleCallback() skips checking browser cookies if true (default: false)
* Enabling this option is not recommended.
* This is supposed to be used only for backward-compatibility with v2.4 and olders.
*/
legacyStateVerification?: boolean;

/**
* The cookie name used for setting state parameter value in cookies
*/
stateCookieName?: string;

/**
* The expiration time in seconds for the state parameter value stored via cookies
*/
stateCookieExpirationSeconds?: number;

/**
* The function for rendering the web page for the install path URL
*/
renderHtmlForInstallPath?: (url: string) => string;

/**
* The install path web page rendering will be skipped if true (default: false)
*/
directInstall?: boolean; // default false, disables rendering "Add to Slack" page for /slack/install when true

/**
* The default is "v2" (a.k.a. Granular Bot Permissions), different from "v1" (a.k.a. "Classic Apps").
* More details here:
* - https://medium.com/slack-developer-blog/more-precision-less-restrictions-a3550006f9c3
* - https://api.slack.com/authentication/migration
*/
authVersion?: 'v1' | 'v2'; // default 'v2'

/**
* The initialization options for the OAuth flow
*/
installUrlOptions?: InstallURLOptions;

/**
* @slack/logger logging used in this class
*/
logger?: Logger;

/**
* @slack/logger logging level used in this class
*/
logLevel?: LogLevel;

/**
* The customization options for WebClient
*/
clientOptions?: Omit<WebClientOptions, 'logLevel' | 'logger'>;
authorizationUrl?: string;

}

0 comments on commit 57ae345

Please sign in to comment.