Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 90 additions & 15 deletions lib/strategy.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
var util = require('util')
, OAuth2Strategy = require('passport-oauth2').Strategy;

var providerName = 'slack';

/**
* `Strategy` constructor.
Expand All @@ -20,7 +21,7 @@ var util = require('util')
* - `clientID` your Slack application's client id
* - `clientSecret` your Slack application's client secret
* - `callbackURL` URL to which Slack will redirect the user after granting authorization
* - `scope` array of permission scopes to request defaults to:
* - `scope` array of permission scopes to request bot access
* ['identity.basic', 'identity.email', 'identity.avatar', 'identity.team']
* full set of scopes: https://api.slack.com/docs/oauth-scopes
*
Expand All @@ -45,23 +46,33 @@ var util = require('util')
*/
function Strategy(options, verify) {
options = options || {};
options.tokenURL = options.tokenURL || 'https://slack.com/api/oauth.access';
options.authorizationURL = options.authorizationURL || 'https://slack.com/oauth/authorize';
options.scope = options.scope || ['identity.basic', 'identity.email', 'identity.avatar', 'identity.team'];
options.clientID = options.clientID || options.clientId;
options.tokenURL = options.tokenURL || options.tokenUrl || 'https://slack.com/api/oauth.v2.access';
options.authorizationURL = options.authorizationURL || options.authorizationUrl || 'https://slack.com/oauth/v2/authorize';

var defaultProfileUrl = "https://slack.com/api/users.identity" // requires 'identity.basic' scope
this.profileUrl = options.profileURL || options.profileUrl || defaultProfileUrl;
this._team = options.team;
this._user_scope = options.user_scope;
// bot scope
options.scope = options.scope || ['users:read'];
// user scope
options.user_scope = options.user_scope || ['identity.basic', 'identity.email', 'identity.team', 'identity.avatar'];

OAuth2Strategy.call(this, options, verify);
this.name = options.name || 'Slack';
if ((!options.scope || options.scope.length == 0) && (!options.user_scope || options.user_scope.length == 0)) {
throw new TypeError('SlackStrategy requires at least one scope or user_scope');
}

this._team = options.team || options.teamId;

// warn is not enough scope
// Details on Slack's identity scope - https://api.slack.com/methods/users.identity
if(!this._skipUserProfile && this.profileUrl === defaultProfileUrl && this._scope.indexOf('identity.basic') === -1){
console.warn("Scope 'identity.basic' is required to retrieve Slack user profile");
this.profileUrl = options.profileURL || options.profileUrl;
this._hasCustomProfileUrl = !!this.profileUrl;

if( !this._skipUserProfile &&
!~options.user_scope.indexOf('identity.basic') &&
!~options.scope.indexOf('users:read') ){

console.warn("Scope 'users:read' or UserScope 'identity.basic' is required to retrieve Slack profile");
}

OAuth2Strategy.call(this, options, verify);
this.name = options.name || providerName;
}

/**
Expand All @@ -86,6 +97,7 @@ Strategy.prototype.userProfile = function(accessToken, done) {
var header = {
Authorization: 'Bearer ' + accessToken
};

this.get(this.profileUrl, header, function (err, body, res) {
if (err) {
return done(err);
Expand All @@ -98,7 +110,7 @@ Strategy.prototype.userProfile = function(accessToken, done) {
} else {
delete profile.ok;

profile.provider = 'Slack';
profile.provider = providerName;
profile.id = profile.user.id;
profile.displayName = profile.user.name;

Expand All @@ -118,6 +130,69 @@ Strategy.prototype.get = function(url, header, callback) {
this._oauth2._request("GET", url, header, "", "", callback );
};

/**
* Passport's goal is User Authentication.
* Slack's OAuth2 v2 implementation supports simultaneous bot & user
* authentication. Slack's OAuth2 v2 token response can return multiple tokens
* within a single response. OAuth2 does not support issues multiple tokens
* as a set or array of responses. Therefore, in order for Slack to achieve
* multiple token responses, the user token response is nested inside of the
* bot token response.
* In order to align with Passport's User Authentication Goal, Slack's
* OAuth2 v2 token response needs to be reformatted to life the user token
* and nest the bot token. See https://github.com/nmaves/passport-slack-oauth2/issues/9#issuecomment-1544231819
*
* @param {String:null} accessToken
* @param {String:null} refreshToken
* @param {Object} params
* @return {Object}
*/
Strategy.prototype.handleOAuthAccessTokenResponse = function (accessToken, refreshToken, params, next) {
try {
// deep copy
var tokenResponse = JSON.parse(JSON.stringify(params));
delete tokenResponse.bot_user_id;
tokenResponse.authed_bot = {};

if (params.token_type === 'bot') {
// copy bot profile & tokens to authed_bot
tokenResponse.authed_bot.id = params.bot_user_id;
tokenResponse.authed_bot.scope = params.scope;
tokenResponse.authed_bot.token_type = params.token_type;
tokenResponse.authed_bot.access_token = params.access_token;
tokenResponse.authed_bot.refresh_token = params.refresh_token;

if (!(this._skipUserProfile || this._hasCustomProfileUrl)) {
this.profileUrl = 'https://slack.com/api/users.info?user=' + params.bot_user_id;
}
}

if (params.authed_user.token_type === 'user') {
// copy the authed_user tokens
accessToken = params.authed_user.access_token;
refreshToken = params.authed_user.refresh_token;

// copy the authed_user profile & tokens to root
tokenResponse.id = params.authed_user.id;
tokenResponse.scope = params.authed_user.scope;
tokenResponse.token_type = params.authed_user.token_type;
tokenResponse.access_token = params.authed_user.access_token;
tokenResponse.refresh_token = params.authed_user.refresh_token;

if (!this._skipUserProfile || !this._hasCustomProfileUrl) {
this.profileUrl = 'https://slack.com/api/users.identity';
}
} else {
// normalize id
tokenResponse.id = params.bot_user_id;
}

next(null, accessToken, refreshToken, tokenResponse);
} catch (ex) {
return next(ex);
}
};



/**
Expand Down
Loading