-
Notifications
You must be signed in to change notification settings - Fork 662
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Fix #1435 Proper use of state parameter for the OAuth CSRF protection #1436
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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({ | ||
|
@@ -44,6 +44,17 @@ app.get('/slack/install', async (req, res, next) => { | |
} | ||
}); | ||
|
||
// This works since @slack/[email protected] 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) => { | ||
|
@@ -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); | ||
seratch marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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); | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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'; | ||
|
||
|
@@ -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> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Updated the HTML content to align with bolt-python and bolt-java. I'm sure that this one should be better for most people. |
||
<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) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Adjusted the HTTP status a bit |
||
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); | ||
} | ||
|
||
// ------------------------------------------ | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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}">`)); | ||
}); | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
export default function defaultRenderHtmlForInstallPath(addToSlackUrl: string): string { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ported from bolt-js. |
||
return `<html> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. same as above - this is copied from bolt-python / bolt-java |
||
<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/[email protected] 2x" /></a></p> | ||
</body> | ||
</html>`; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 { | ||
|
||
/** | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Feedback on these comments would be appreciated |
||
* 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; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added for backward-compatibility (most developers won't use this option but just in case) |
||
|
||
/** | ||
* The cookie name used for setting state parameter value in cookies | ||
*/ | ||
stateCookieName?: string; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. for customization - the default ones work fine for most developers, though |
||
|
||
/** | ||
* 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; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added this for better customization of |
||
|
||
/** | ||
* 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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added for customization of |
||
|
||
/** | ||
* 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; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This one works as the default but you can pass overridden one to |
||
|
||
/** | ||
* @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; | ||
|
||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is the new way. It's better in security but, moreover, it will be much simpler.