Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

@slack/oauth

Package Overview
Dependencies
Maintainers
12
Versions
28
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@slack/oauth - npm Package Compare versions

Comparing version 2.4.0 to 2.5.0-rc.1

dist/authorize-result.d.ts

4

dist/errors.d.ts

@@ -12,2 +12,3 @@ export interface CodedError extends Error {

MissingStateError = "slack_oauth_missing_state",
InvalidStateError = "slack_oauth_invalid_state",
MissingCodeError = "slack_oauth_missing_code",

@@ -25,2 +26,5 @@ UnknownError = "slack_oauth_unknown_error"

}
export declare class InvalidStateError extends Error implements CodedError {
code: ErrorCode;
}
export declare class MissingCodeError extends Error implements CodedError {

@@ -27,0 +31,0 @@ code: ErrorCode;

@@ -18,3 +18,3 @@ "use strict";

Object.defineProperty(exports, "__esModule", { value: true });
exports.AuthorizationError = exports.UnknownError = exports.MissingCodeError = exports.MissingStateError = exports.GenerateInstallUrlError = exports.InstallerInitializationError = exports.ErrorCode = void 0;
exports.AuthorizationError = exports.UnknownError = exports.MissingCodeError = exports.InvalidStateError = exports.MissingStateError = exports.GenerateInstallUrlError = exports.InstallerInitializationError = exports.ErrorCode = void 0;
/**

@@ -29,2 +29,3 @@ * A dictionary of codes for errors produced by this package.

ErrorCode["MissingStateError"] = "slack_oauth_missing_state";
ErrorCode["InvalidStateError"] = "slack_oauth_invalid_state";
ErrorCode["MissingCodeError"] = "slack_oauth_missing_code";

@@ -63,2 +64,12 @@ ErrorCode["UnknownError"] = "slack_oauth_unknown_error";

exports.MissingStateError = MissingStateError;
var InvalidStateError = /** @class */ (function (_super) {
__extends(InvalidStateError, _super);
function InvalidStateError() {
var _this = _super !== null && _super.apply(this, arguments) || this;
_this.code = ErrorCode.InvalidStateError;
return _this;
}
return InvalidStateError;
}(Error));
exports.InvalidStateError = InvalidStateError;
var MissingCodeError = /** @class */ (function (_super) {

@@ -65,0 +76,0 @@ __extends(MissingCodeError, _super);

266

dist/index.d.ts

@@ -1,258 +0,12 @@

/// <reference types="node" />
import { IncomingMessage, ServerResponse } from 'http';
import { WebAPICallResult, WebClientOptions } from '@slack/web-api';
import { CodedError } from './errors';
import { Logger, LogLevel } from './logger';
/**
* InstallProvider Class.
* @param clientId - Your apps client ID
* @param clientSecret - Your apps client Secret
* @param stateSecret - Used to sign and verify the generated state when using the built-in `stateStore`
* @param stateStore - Replacement function for the built-in `stateStore`
* @param stateVerification - Pass in false to disable state parameter verification
* @param installationStore - Interface to store and retrieve installation data from the database
* @param authVersion - Can be either `v1` or `v2`. Determines which slack Oauth URL and method to use
* @param logger - Pass in your own Logger if you don't want to use the built-in one
* @param logLevel - Pass in the log level you want (ERROR, WARN, INFO, DEBUG). Default is INFO
*/
export declare class InstallProvider {
stateStore?: StateStore;
installationStore: InstallationStore;
private clientId;
private clientSecret;
private authVersion;
private logger;
private clientOptions;
private authorizationUrl;
private stateVerification;
constructor({ clientId, clientSecret, stateSecret, stateStore, stateVerification, installationStore, authVersion, logger, logLevel, clientOptions, authorizationUrl, }: InstallProviderOptions);
/**
* Fetches data from the installationStore
*/
authorize(source: InstallationQuery<boolean>): Promise<AuthorizeResult>;
/**
* refreshExpiringTokens refreshes expired access tokens using the `oauth.v2.access` endpoint.
*
* The return value is an Array of Promises made up of the resolution of each token refresh attempt.
*/
private refreshExpiringTokens;
/**
* Returns search params from a URL and ignores protocol / hostname as those
* aren't guaranteed to be accurate e.g. in x-forwarded- scenarios
*/
private static extractSearchParams;
/**
* Returns a URL that is suitable for including in an Add to Slack button
* Uses stateStore to generate a value for the state query param.
*/
generateInstallUrl(options: InstallURLOptions, stateVerification?: boolean): Promise<string>;
/**
* This method handles the incoming request to the callback URL.
* It can be used as a RequestListener in almost any HTTP server
* framework.
*
* Verifies the state using the stateStore, exchanges the grant in the
* query params for an access token, and stores token and associated data
* in the installationStore.
*/
handleCallback(req: IncomingMessage, res: ServerResponse, options?: CallbackOptions, installOptions?: InstallURLOptions): Promise<void>;
}
export interface InstallProviderOptions {
clientId: string;
clientSecret: string;
stateStore?: StateStore;
stateSecret?: string;
stateVerification?: boolean;
installationStore?: InstallationStore;
authVersion?: 'v1' | 'v2';
logger?: Logger;
logLevel?: LogLevel;
clientOptions?: Omit<WebClientOptions, 'logLevel' | 'logger'>;
authorizationUrl?: string;
}
export interface InstallURLOptions {
scopes: string | string[];
teamId?: string;
redirectUri?: string;
userScopes?: string | string[];
metadata?: string;
}
export interface CallbackOptions {
success?: (installation: Installation | OrgInstallation, options: InstallURLOptions, callbackReq: IncomingMessage, callbackRes: ServerResponse) => void;
failure?: (error: CodedError, options: InstallURLOptions, callbackReq: IncomingMessage, callbackRes: ServerResponse) => void;
}
export interface StateStore {
generateStateParam: (installOptions: InstallURLOptions, now: Date) => Promise<string>;
verifyStateParam: (now: Date, state: string) => Promise<InstallURLOptions>;
}
export interface InstallationStore {
storeInstallation<AuthVersion extends 'v1' | 'v2'>(installation: Installation<AuthVersion, boolean>, logger?: Logger): Promise<void>;
fetchInstallation: (query: InstallationQuery<boolean>, logger?: Logger) => Promise<Installation<'v1' | 'v2', boolean>>;
deleteInstallation?: (query: InstallationQuery<boolean>, logger?: Logger) => Promise<void>;
}
/**
* An individual installation of the Slack app.
*
* This interface creates a representation for installations that normalizes the responses from OAuth grant exchanges
* across auth versions (responses from the Web API methods `oauth.v2.access` and `oauth.access`). It describes some of
* these differences using the `AuthVersion` generic placeholder type.
*
* This interface also represents both installations which occur on individual Slack workspaces and on Slack enterprise
* organizations. The `IsEnterpriseInstall` generic placeholder type is used to describe some of those differences.
*
* This representation is designed to be used both when producing data that should be stored by an InstallationStore,
* and when consuming data that is fetched from an InstallationStore. Most often, InstallationStore implementations
* are a database. If you are going to implement an InstallationStore, it's advised that you **store as much of the
* data in these objects as possible so that you can return as much as possible inside `fetchInstallation()`**.
*
* A few properties are synthesized with a default value if they are not present when returned from
* `fetchInstallation()`. These properties are optional in the interface so that stored installations from previous
* versions of this library (from before those properties were introduced) continue to work without requiring a breaking
* change. However the synthesized default values are not always perfect and are based on some assumptions, so this is
* why it's recommended to store as much of that data as possible in any InstallationStore.
*
* Some of the properties (e.g. `team.name`) can change between when the installation occurred and when it is fetched
* from the InstallationStore. This can be seen as a reason not to store those properties. In most workspaces these
* properties rarely change, and for most Slack apps having a slightly out of date value has no impact. However if your
* app uses these values in a way where it must be up to date, it's recommended to implement a caching strategy in the
* InstallationStore to fetch the latest data from the Web API (using methods such as `auth.test`, `teams.info`, etc.)
* as often as it makes sense for your Slack app.
*
* TODO: IsEnterpriseInstall is always false when AuthVersion is v1
*/
export interface Installation<AuthVersion extends ('v1' | 'v2') = ('v1' | 'v2'), IsEnterpriseInstall extends boolean = boolean> {
/**
* TODO: when performing a “single workspace” install with the admin scope on the enterprise,
* is the team property returned from oauth.access?
*/
team: IsEnterpriseInstall extends true ? undefined : {
id: string;
/** Left as undefined when not returned from fetch. */
name?: string;
};
/**
* When the installation is an enterprise install or when the installation occurs on the org to acquire `admin` scope,
* the name and ID of the enterprise org.
*/
enterprise: IsEnterpriseInstall extends true ? EnterpriseInfo : (EnterpriseInfo | undefined);
user: {
token: AuthVersion extends 'v1' ? string : (string | undefined);
refreshToken?: AuthVersion extends 'v1' ? never : (string | undefined);
expiresAt?: AuthVersion extends 'v1' ? never : (number | undefined);
scopes: AuthVersion extends 'v1' ? string[] : (string[] | undefined);
id: string;
};
bot?: {
token: string;
refreshToken?: string;
expiresAt?: number;
scopes: string[];
id: string;
userId: string;
};
incomingWebhook?: {
url: string;
/** Left as undefined when not returned from fetch. */
channel?: string;
/** Left as undefined when not returned from fetch. */
channelId?: string;
/** Left as undefined when not returned from fetch. */
configurationUrl?: string;
};
/** The App ID, which does not vary per installation. Left as undefined when not returned from fetch. */
appId?: AuthVersion extends 'v2' ? string : undefined;
/** When the installation contains a bot user, the token type. Left as undefined when not returned from fetch. */
tokenType?: 'bot';
/**
* When the installation is an enterprise org install, the URL of the landing page for all workspaces in the org.
* Left as undefined when not returned from fetch.
*/
enterpriseUrl?: AuthVersion extends 'v2' ? string : undefined;
/** Whether the installation was performed on an enterprise org. Synthesized as `false` when not present. */
isEnterpriseInstall?: IsEnterpriseInstall;
/** The version of Slack's auth flow that produced this installation. Synthesized as `v2` when not present. */
authVersion?: AuthVersion;
/** A string value that can be held in the state parameter in the OAuth flow. */
metadata?: string;
}
/**
* A type to describe enterprise organization installations.
*/
export declare type OrgInstallation = Installation<'v2', true>;
interface EnterpriseInfo {
id: string;
name?: string;
}
export interface InstallationQuery<isEnterpriseInstall extends boolean> {
teamId: isEnterpriseInstall extends false ? string : undefined;
enterpriseId: isEnterpriseInstall extends true ? string : (string | undefined);
userId?: string;
conversationId?: string;
isEnterpriseInstall: isEnterpriseInstall;
}
export declare type OrgInstallationQuery = InstallationQuery<true>;
export interface AuthorizeResult {
botToken?: string;
botRefreshToken?: string;
botTokenExpiresAt?: number;
userToken?: string;
userRefreshToken?: string;
userTokenExpiresAt?: number;
botId?: string;
botUserId?: string;
teamId?: string;
enterpriseId?: string;
}
export interface OAuthV2Response extends WebAPICallResult {
app_id: string;
authed_user: {
id: string;
scope?: string;
access_token?: string;
token_type?: string;
refresh_token?: string;
expires_in?: number;
};
scope?: string;
token_type?: 'bot';
access_token?: string;
refresh_token?: string;
expires_in?: number;
bot_user_id?: string;
team: {
id: string;
name: string;
} | null;
enterprise: {
name: string;
id: string;
} | null;
is_enterprise_install: boolean;
incoming_webhook?: {
url: string;
channel: string;
channel_id: string;
configuration_url: string;
};
}
export interface OAuthV2TokenRefreshResponse extends WebAPICallResult {
app_id: string;
scope: string;
token_type: 'bot' | 'user';
access_token: string;
refresh_token: string;
expires_in: number;
bot_user_id?: string;
team: {
id: string;
name: string;
};
enterprise: {
name: string;
id: string;
} | null;
is_enterprise_install: boolean;
}
export * from './errors';
export { Logger, LogLevel } from './logger';
export * from './stores';
export { AuthorizeResult } from './authorize-result';
export { CallbackOptions } from './callback-options';
export { InstallProvider, OAuthV2TokenRefreshResponse, OAuthV2Response, } from './install-provider';
export { Installation, OrgInstallation } from './installation';
export { InstallationQuery, OrgInstallationQuery } from './installation-query';
export { InstallProviderOptions } from './install-provider-options';
export { InstallURLOptions } from './install-url-options';
export * from './installation-stores';
export * from './state-stores';
//# sourceMappingURL=index.d.ts.map
"use strict";
var __assign = (this && this.__assign) || function () {
__assign = Object.assign || function(t) {
for (var s, i = 1, n = arguments.length; i < n; i++) {
s = arguments[i];
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
t[p] = s[p];
}
return t;
};
return __assign.apply(this, arguments);
};
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {

@@ -23,614 +12,11 @@ if (k2 === undefined) k2 = k;

};
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __generator = (this && this.__generator) || function (thisArg, body) {
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
function verb(n) { return function (v) { return step([n, v]); }; }
function step(op) {
if (f) throw new TypeError("Generator is already executing.");
while (_) try {
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
if (y = 0, t) op = [op[0] & 2, t.value];
switch (op[0]) {
case 0: case 1: t = op; break;
case 4: _.label++; return { value: op[1], done: false };
case 5: _.label++; y = op[1]; op = [0]; continue;
case 7: op = _.ops.pop(); _.trys.pop(); continue;
default:
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
if (t[2]) _.ops.pop();
_.trys.pop(); continue;
}
op = body.call(thisArg, _);
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
}
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.LogLevel = exports.InstallProvider = void 0;
var url_1 = require("url");
var jsonwebtoken_1 = require("jsonwebtoken");
var web_api_1 = require("@slack/web-api");
var errors_1 = require("./errors");
exports.InstallProvider = exports.LogLevel = void 0;
__exportStar(require("./errors"), exports);
var logger_1 = require("./logger");
var stores_1 = require("./stores");
// default implementation of StateStore
var ClearStateStore = /** @class */ (function () {
function ClearStateStore(stateSecret) {
this.stateSecret = stateSecret;
}
ClearStateStore.prototype.generateStateParam = function (installOptions, now) {
return __awaiter(this, void 0, void 0, function () {
return __generator(this, function (_a) {
return [2 /*return*/, (0, jsonwebtoken_1.sign)({ installOptions: installOptions, now: now.toJSON() }, this.stateSecret)];
});
});
};
ClearStateStore.prototype.verifyStateParam = function (_now, state) {
return __awaiter(this, void 0, void 0, function () {
var decoded;
return __generator(this, function (_a) {
decoded = (0, jsonwebtoken_1.verify)(state, this.stateSecret);
// return installOptions
return [2 /*return*/, decoded.installOptions];
});
});
};
return ClearStateStore;
}());
/**
* InstallProvider Class.
* @param clientId - Your apps client ID
* @param clientSecret - Your apps client Secret
* @param stateSecret - Used to sign and verify the generated state when using the built-in `stateStore`
* @param stateStore - Replacement function for the built-in `stateStore`
* @param stateVerification - Pass in false to disable state parameter verification
* @param installationStore - Interface to store and retrieve installation data from the database
* @param authVersion - Can be either `v1` or `v2`. Determines which slack Oauth URL and method to use
* @param logger - Pass in your own Logger if you don't want to use the built-in one
* @param logLevel - Pass in the log level you want (ERROR, WARN, INFO, DEBUG). Default is INFO
*/
var InstallProvider = /** @class */ (function () {
function InstallProvider(_a) {
var clientId = _a.clientId, clientSecret = _a.clientSecret, _b = _a.stateSecret, stateSecret = _b === void 0 ? undefined : _b, _c = _a.stateStore, stateStore = _c === void 0 ? undefined : _c, _d = _a.stateVerification, stateVerification = _d === void 0 ? true : _d, _e = _a.installationStore, installationStore = _e === void 0 ? new stores_1.MemoryInstallationStore() : _e, _f = _a.authVersion, authVersion = _f === void 0 ? 'v2' : _f, _g = _a.logger, logger = _g === void 0 ? undefined : _g, _h = _a.logLevel, logLevel = _h === void 0 ? undefined : _h, _j = _a.clientOptions, clientOptions = _j === void 0 ? {} : _j, _k = _a.authorizationUrl, authorizationUrl = _k === void 0 ? 'https://slack.com/oauth/v2/authorize' : _k;
if (clientId === undefined || clientSecret === undefined) {
throw new errors_1.InstallerInitializationError('You must provide a valid clientId and clientSecret');
}
// Setup the logger
if (typeof logger !== 'undefined') {
this.logger = logger;
if (typeof logLevel !== 'undefined') {
this.logger.debug('The logLevel given to OAuth was ignored as you also gave logger');
}
}
else {
this.logger = (0, logger_1.getLogger)('OAuth:InstallProvider', logLevel !== null && logLevel !== void 0 ? logLevel : logger_1.LogLevel.INFO, logger);
}
this.stateVerification = stateVerification;
if (!stateVerification) {
this.logger.warn("You've set InstallProvider#stateVerification to false. This flag is intended to enable org-wide app installations from admin pages. If this isn't your scenario, we recommend setting stateVerification to true and starting your OAuth flow from the provided `/slack/install` or your own starting endpoint.");
}
// Setup stateStore
if (stateStore !== undefined) {
this.stateStore = stateStore;
}
else if (this.stateVerification) {
// if state verification is disabled, state store is not necessary
if (stateSecret !== undefined) {
this.stateStore = new ClearStateStore(stateSecret);
}
else {
throw new errors_1.InstallerInitializationError('To use the built-in state store you must provide a State Secret');
}
}
this.installationStore = installationStore;
this.clientId = clientId;
this.clientSecret = clientSecret;
this.handleCallback = this.handleCallback.bind(this);
this.authorize = this.authorize.bind(this);
this.authVersion = authVersion;
this.authorizationUrl = authorizationUrl;
if (authorizationUrl !== 'https://slack.com/oauth/v2/authorize' && authVersion === 'v1') {
this.logger.info('You provided both an authorizationUrl and an authVersion! The authVersion will be ignored in favor of the authorizationUrl.');
}
else if (authVersion === 'v1') {
this.authorizationUrl = 'https://slack.com/oauth/authorize';
}
this.clientOptions = __assign({ logger: logger, logLevel: this.logger.getLevel() }, clientOptions);
}
/**
* Fetches data from the installationStore
*/
InstallProvider.prototype.authorize = function (source) {
var _a, _b, _c, _d;
return __awaiter(this, void 0, void 0, function () {
var queryResult, authResult, currentUTCSec, tokensToRefresh, installationUpdates, refreshResponses, _i, refreshResponses_1, refreshResp, tokenType, updatedInstallation, error_1;
var _e;
return __generator(this, function (_f) {
switch (_f.label) {
case 0:
_f.trys.push([0, 10, , 11]);
queryResult = void 0;
if (!source.isEnterpriseInstall) return [3 /*break*/, 2];
return [4 /*yield*/, this.installationStore.fetchInstallation(source, this.logger)];
case 1:
queryResult = _f.sent();
return [3 /*break*/, 4];
case 2: return [4 /*yield*/, this.installationStore.fetchInstallation(source, this.logger)];
case 3:
queryResult = _f.sent();
_f.label = 4;
case 4:
if (queryResult === undefined || queryResult === null) {
throw new Error('Failed fetching data from the Installation Store');
}
authResult = {};
if (queryResult.user) {
authResult.userToken = queryResult.user.token;
}
if ((_a = queryResult.team) === null || _a === void 0 ? void 0 : _a.id) {
authResult.teamId = queryResult.team.id;
}
else if (source === null || source === void 0 ? void 0 : source.teamId) {
/**
* Since queryResult is a org installation, it won't have team.id.
* If one was passed in via source, we should add it to the authResult.
*/
authResult.teamId = source.teamId;
}
if (((_b = queryResult === null || queryResult === void 0 ? void 0 : queryResult.enterprise) === null || _b === void 0 ? void 0 : _b.id) || (source === null || source === void 0 ? void 0 : source.enterpriseId)) {
authResult.enterpriseId = ((_c = queryResult === null || queryResult === void 0 ? void 0 : queryResult.enterprise) === null || _c === void 0 ? void 0 : _c.id) || (source === null || source === void 0 ? void 0 : source.enterpriseId);
}
if (queryResult.bot) {
authResult.botToken = queryResult.bot.token;
authResult.botId = queryResult.bot.id;
authResult.botUserId = queryResult.bot.userId;
// Token Rotation Enabled (Bot Token)
if (queryResult.bot.refreshToken) {
authResult.botRefreshToken = queryResult.bot.refreshToken;
authResult.botTokenExpiresAt = queryResult.bot.expiresAt; // utc, seconds
}
}
// Token Rotation Enabled (User Token)
if ((_d = queryResult.user) === null || _d === void 0 ? void 0 : _d.refreshToken) {
authResult.userRefreshToken = queryResult.user.refreshToken;
authResult.userTokenExpiresAt = queryResult.user.expiresAt; // utc, seconds
}
if (!(authResult.botRefreshToken || authResult.userRefreshToken)) return [3 /*break*/, 9];
currentUTCSec = Math.floor(Date.now() / 1000);
tokensToRefresh = detectExpiredOrExpiringTokens(authResult, currentUTCSec);
if (!(tokensToRefresh.length > 0)) return [3 /*break*/, 9];
installationUpdates = __assign({}, queryResult);
return [4 /*yield*/, this.refreshExpiringTokens(tokensToRefresh)];
case 5:
refreshResponses = _f.sent();
_i = 0, refreshResponses_1 = refreshResponses;
_f.label = 6;
case 6:
if (!(_i < refreshResponses_1.length)) return [3 /*break*/, 9];
refreshResp = refreshResponses_1[_i];
tokenType = refreshResp.token_type;
// Update Authorization
if (tokenType === 'bot') {
authResult.botToken = refreshResp.access_token;
authResult.botRefreshToken = refreshResp.refresh_token;
authResult.botTokenExpiresAt = currentUTCSec + refreshResp.expires_in;
}
if (tokenType === 'user') {
authResult.userToken = refreshResp.access_token;
authResult.userRefreshToken = refreshResp.refresh_token;
authResult.userTokenExpiresAt = currentUTCSec + refreshResp.expires_in;
}
// Update Installation
installationUpdates[tokenType].token = refreshResp.access_token;
installationUpdates[tokenType].refreshToken = refreshResp.refresh_token;
installationUpdates[tokenType].expiresAt = currentUTCSec + refreshResp.expires_in;
updatedInstallation = __assign(__assign({}, installationUpdates), (_e = {}, _e[tokenType] = __assign(__assign({}, queryResult[tokenType]), installationUpdates[tokenType]), _e));
// TODO: related to the above TODO comment as well
// eslint-disable-next-line no-await-in-loop
return [4 /*yield*/, this.installationStore.storeInstallation(updatedInstallation)];
case 7:
// TODO: related to the above TODO comment as well
// eslint-disable-next-line no-await-in-loop
_f.sent();
_f.label = 8;
case 8:
_i++;
return [3 /*break*/, 6];
case 9: return [2 /*return*/, authResult];
case 10:
error_1 = _f.sent();
throw new errors_1.AuthorizationError(error_1.message);
case 11: return [2 /*return*/];
}
});
});
};
/**
* refreshExpiringTokens refreshes expired access tokens using the `oauth.v2.access` endpoint.
*
* The return value is an Array of Promises made up of the resolution of each token refresh attempt.
*/
InstallProvider.prototype.refreshExpiringTokens = function (tokensToRefresh) {
return __awaiter(this, void 0, void 0, function () {
var client, refreshPromises;
var _this = this;
return __generator(this, function (_a) {
client = new web_api_1.WebClient(undefined, this.clientOptions);
refreshPromises = tokensToRefresh.map(function (refreshToken) { return __awaiter(_this, void 0, void 0, function () {
return __generator(this, function (_a) {
switch (_a.label) {
case 0: return [4 /*yield*/, client.oauth.v2.access({
client_id: this.clientId,
client_secret: this.clientSecret,
grant_type: 'refresh_token',
refresh_token: refreshToken,
}).catch(function (e) { return e; })];
case 1: return [2 /*return*/, _a.sent()];
}
});
}); });
return [2 /*return*/, Promise.all(refreshPromises)];
});
});
};
/**
* Returns search params from a URL and ignores protocol / hostname as those
* aren't guaranteed to be accurate e.g. in x-forwarded- scenarios
*/
InstallProvider.extractSearchParams = function (req) {
var searchParams = new url_1.URL(req.url, "https://".concat(req.headers.host)).searchParams;
return searchParams;
};
/**
* Returns a URL that is suitable for including in an Add to Slack button
* Uses stateStore to generate a value for the state query param.
*/
InstallProvider.prototype.generateInstallUrl = function (options, stateVerification) {
if (stateVerification === void 0) { stateVerification = true; }
return __awaiter(this, void 0, void 0, function () {
var slackURL, scopes, params, state, userScopes;
return __generator(this, function (_a) {
switch (_a.label) {
case 0:
slackURL = new url_1.URL(this.authorizationUrl);
if (options.scopes === undefined || options.scopes === null) {
throw new errors_1.GenerateInstallUrlError('You must provide a scope parameter when calling generateInstallUrl');
}
if (options.scopes instanceof Array) {
scopes = options.scopes.join(',');
}
else {
scopes = options.scopes;
}
params = new url_1.URLSearchParams("scope=".concat(scopes));
if (!(stateVerification && this.stateStore)) return [3 /*break*/, 2];
return [4 /*yield*/, this.stateStore.generateStateParam(options, new Date())];
case 1:
state = _a.sent();
params.append('state', state);
_a.label = 2;
case 2:
// client id
params.append('client_id', this.clientId);
// redirect uri
if (options.redirectUri !== undefined) {
params.append('redirect_uri', options.redirectUri);
}
// team id
if (options.teamId !== undefined) {
params.append('team', options.teamId);
}
// user scope, only available for OAuth v2
if (options.userScopes !== undefined && this.authVersion === 'v2') {
userScopes = void 0;
if (options.userScopes instanceof Array) {
userScopes = options.userScopes.join(',');
}
else {
userScopes = options.userScopes;
}
params.append('user_scope', userScopes);
}
slackURL.search = params.toString();
return [2 /*return*/, slackURL.toString()];
}
});
});
};
/**
* This method handles the incoming request to the callback URL.
* It can be used as a RequestListener in almost any HTTP server
* framework.
*
* Verifies the state using the stateStore, exchanges the grant in the
* query params for an access token, and stores token and associated data
* in the installationStore.
*/
InstallProvider.prototype.handleCallback = function (req, res, options, installOptions) {
var _a;
return __awaiter(this, void 0, void 0, function () {
var code, flowError, state, searchParams, emptyInstallOptions, client, installation, resp, v1Resp, v1Installation, authResult, botId, v2Resp, v2Installation, currentUTC, authResult, authResult, error_2, emptyInstallOptions;
return __generator(this, function (_b) {
switch (_b.label) {
case 0:
_b.trys.push([0, 18, , 19]);
if (req.url !== undefined) {
searchParams = InstallProvider.extractSearchParams(req);
flowError = searchParams.get('error');
if (flowError === 'access_denied') {
throw new errors_1.AuthorizationError('User cancelled the OAuth installation flow!');
}
code = searchParams.get('code');
state = searchParams.get('state');
if (!code) {
throw new errors_1.MissingCodeError('Redirect url is missing the required code query parameter');
}
if (this.stateVerification && !state) {
throw new errors_1.MissingStateError('Redirect url is missing the state query parameter. If this is intentional, see options for disabling default state verification.');
}
}
else {
throw new errors_1.UnknownError('Something went wrong');
}
if (!(this.stateVerification && this.stateStore)) return [3 /*break*/, 2];
return [4 /*yield*/, this.stateStore.verifyStateParam(new Date(), state)];
case 1:
// eslint-disable-next-line no-param-reassign
installOptions = _b.sent();
_b.label = 2;
case 2:
if (!installOptions) {
emptyInstallOptions = { scopes: [] };
// eslint-disable-next-line no-param-reassign
installOptions = emptyInstallOptions;
}
client = new web_api_1.WebClient(undefined, this.clientOptions);
installation = void 0;
resp = void 0;
if (!(this.authVersion === 'v1')) return [3 /*break*/, 6];
return [4 /*yield*/, client.oauth.access({
code: code,
client_id: this.clientId,
client_secret: this.clientSecret,
redirect_uri: installOptions.redirectUri,
})];
case 3:
v1Resp = _b.sent();
v1Installation = {
team: { id: v1Resp.team_id, name: v1Resp.team_name },
enterprise: v1Resp.enterprise_id === null ? undefined : { id: v1Resp.enterprise_id },
user: {
token: v1Resp.access_token,
scopes: v1Resp.scope.split(','),
id: v1Resp.user_id,
},
// synthesized properties: enterprise installation is unsupported in v1 auth
isEnterpriseInstall: false,
authVersion: 'v1',
};
if (!(v1Resp.bot !== undefined)) return [3 /*break*/, 5];
return [4 /*yield*/, runAuthTest(v1Resp.bot.bot_access_token, this.clientOptions)];
case 4:
authResult = _b.sent();
botId = authResult.bot_id;
v1Installation.bot = {
id: botId,
scopes: ['bot'],
token: v1Resp.bot.bot_access_token,
userId: v1Resp.bot.bot_user_id,
};
_b.label = 5;
case 5:
resp = v1Resp;
installation = v1Installation;
return [3 /*break*/, 13];
case 6: return [4 /*yield*/, client.oauth.v2.access({
code: code,
client_id: this.clientId,
client_secret: this.clientSecret,
redirect_uri: installOptions.redirectUri,
})];
case 7:
v2Resp = _b.sent();
v2Installation = {
team: v2Resp.team === null ? undefined : v2Resp.team,
enterprise: v2Resp.enterprise == null ? undefined : v2Resp.enterprise,
user: {
token: v2Resp.authed_user.access_token,
scopes: (_a = v2Resp.authed_user.scope) === null || _a === void 0 ? void 0 : _a.split(','),
id: v2Resp.authed_user.id,
},
tokenType: v2Resp.token_type,
isEnterpriseInstall: v2Resp.is_enterprise_install,
appId: v2Resp.app_id,
// synthesized properties
authVersion: 'v2',
};
currentUTC = Math.floor(Date.now() / 1000);
if (!(v2Resp.access_token !== undefined && v2Resp.scope !== undefined && v2Resp.bot_user_id !== undefined)) return [3 /*break*/, 9];
return [4 /*yield*/, runAuthTest(v2Resp.access_token, this.clientOptions)];
case 8:
authResult = _b.sent();
v2Installation.bot = {
scopes: v2Resp.scope.split(','),
token: v2Resp.access_token,
userId: v2Resp.bot_user_id,
id: authResult.bot_id,
};
if (v2Resp.is_enterprise_install) {
v2Installation.enterpriseUrl = authResult.url;
}
// Token Rotation is Enabled
if (v2Resp.refresh_token !== undefined && v2Resp.expires_in !== undefined) {
v2Installation.bot.refreshToken = v2Resp.refresh_token;
v2Installation.bot.expiresAt = currentUTC + v2Resp.expires_in; // utc, seconds
}
_b.label = 9;
case 9:
if (!(v2Resp.authed_user !== undefined && v2Resp.authed_user.access_token !== undefined)) return [3 /*break*/, 12];
if (!(v2Resp.is_enterprise_install && v2Installation.enterpriseUrl === undefined)) return [3 /*break*/, 11];
return [4 /*yield*/, runAuthTest(v2Resp.authed_user.access_token, this.clientOptions)];
case 10:
authResult = _b.sent();
v2Installation.enterpriseUrl = authResult.url;
_b.label = 11;
case 11:
// Token Rotation is Enabled
if (v2Resp.authed_user.refresh_token !== undefined && v2Resp.authed_user.expires_in !== undefined) {
v2Installation.user.refreshToken = v2Resp.authed_user.refresh_token;
v2Installation.user.expiresAt = currentUTC + v2Resp.authed_user.expires_in; // utc, seconds
}
_b.label = 12;
case 12:
resp = v2Resp;
installation = v2Installation;
_b.label = 13;
case 13:
if (resp.incoming_webhook !== undefined) {
installation.incomingWebhook = {
url: resp.incoming_webhook.url,
channel: resp.incoming_webhook.channel,
channelId: resp.incoming_webhook.channel_id,
configurationUrl: resp.incoming_webhook.configuration_url,
};
}
if (installOptions && installOptions.metadata !== undefined) {
// Pass the metadata in state parameter if exists.
// Developers can use the value for additional/custom data associated with the installation.
installation.metadata = installOptions.metadata;
}
if (!installation.isEnterpriseInstall) return [3 /*break*/, 15];
return [4 /*yield*/, this.installationStore.storeInstallation(installation, this.logger)];
case 14:
_b.sent();
return [3 /*break*/, 17];
case 15: return [4 /*yield*/, this.installationStore.storeInstallation(installation, this.logger)];
case 16:
_b.sent();
_b.label = 17;
case 17:
// Call the success callback
if (options !== undefined && options.success !== undefined) {
this.logger.debug('calling passed in options.success');
options.success(installation, installOptions, req, res);
}
else {
this.logger.debug('run built-in success function');
callbackSuccess(installation, installOptions, req, res);
}
return [3 /*break*/, 19];
case 18:
error_2 = _b.sent();
this.logger.error(error_2);
if (!installOptions) {
emptyInstallOptions = { scopes: [] };
// eslint-disable-next-line no-param-reassign
installOptions = emptyInstallOptions;
}
// Call the failure callback
if (options !== undefined && options.failure !== undefined) {
this.logger.debug('calling passed in options.failure');
options.failure(error_2, installOptions, req, res);
}
else {
this.logger.debug('run built-in failure function');
callbackFailure(error_2, installOptions, req, res);
}
return [3 /*break*/, 19];
case 19: return [2 /*return*/];
}
});
});
};
return InstallProvider;
}());
exports.InstallProvider = InstallProvider;
// Default function to call when OAuth flow is successful
function callbackSuccess(installation, _options, _req, res) {
var redirectUrl;
if (isNotOrgInstall(installation) && installation.appId !== undefined) {
// redirect back to Slack native app
// Changes to the workspace app was installed to, to the app home
redirectUrl = "slack://app?team=".concat(installation.team.id, "&id=").concat(installation.appId);
}
else if (isOrgInstall(installation)) {
// redirect to Slack app management dashboard
redirectUrl = "".concat(installation.enterpriseUrl, "manage/organization/apps/profile/").concat(installation.appId, "/workspaces/add");
}
else {
// redirect back to Slack native app
// does not change the workspace the slack client was last in
redirectUrl = 'slack://open';
}
var htmlResponse = "<html>\n <meta http-equiv=\"refresh\" content=\"0; URL=".concat(redirectUrl, "\">\n <body>\n <h1>Success! Redirecting to the Slack App...</h1>\n <button onClick=\"window.location = '").concat(redirectUrl, "'\">Click here to redirect</button>\n </body></html>");
res.writeHead(200, { 'Content-Type': 'text/html' });
res.end(htmlResponse);
}
// Default function to call when OAuth flow is unsuccessful
function callbackFailure(_error, _options, _req, res) {
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>');
}
// Gets the bot_id using the `auth.test` method.
function runAuthTest(token, clientOptions) {
return __awaiter(this, void 0, void 0, function () {
var client, authResult;
return __generator(this, function (_a) {
switch (_a.label) {
case 0:
client = new web_api_1.WebClient(token, clientOptions);
return [4 /*yield*/, client.auth.test()];
case 1:
authResult = _a.sent();
return [2 /*return*/, authResult];
}
});
});
}
// Type guard to narrow Installation type to OrgInstallation
function isOrgInstall(installation) {
return installation.isEnterpriseInstall || false;
}
function isNotOrgInstall(installation) {
return !(isOrgInstall(installation));
}
/**
* detectExpiredOrExpiringTokens determines access tokens' eligibility for refresh.
*
* The return value is an Array of expired or soon-to-expire access tokens.
*/
function detectExpiredOrExpiringTokens(authResult, currentUTCSec) {
var tokensToRefresh = [];
var EXPIRY_WINDOW = 7200; // 2 hours
if (authResult.botRefreshToken &&
(authResult.botTokenExpiresAt !== undefined && authResult.botTokenExpiresAt !== null)) {
var botTokenExpiresIn = authResult.botTokenExpiresAt - currentUTCSec;
if (botTokenExpiresIn <= EXPIRY_WINDOW) {
tokensToRefresh.push(authResult.botRefreshToken);
}
}
if (authResult.userRefreshToken &&
(authResult.userTokenExpiresAt !== undefined && authResult.userTokenExpiresAt !== null)) {
var userTokenExpiresIn = authResult.userTokenExpiresAt - currentUTCSec;
if (userTokenExpiresIn <= EXPIRY_WINDOW) {
tokensToRefresh.push(authResult.userRefreshToken);
}
}
return tokensToRefresh;
}
var logger_2 = require("./logger");
Object.defineProperty(exports, "LogLevel", { enumerable: true, get: function () { return logger_2.LogLevel; } });
__exportStar(require("./stores"), exports);
Object.defineProperty(exports, "LogLevel", { enumerable: true, get: function () { return logger_1.LogLevel; } });
var install_provider_1 = require("./install-provider");
Object.defineProperty(exports, "InstallProvider", { enumerable: true, get: function () { return install_provider_1.InstallProvider; } });
__exportStar(require("./installation-stores"), exports);
__exportStar(require("./state-stores"), exports);
//# sourceMappingURL=index.js.map
{
"name": "@slack/oauth",
"version": "2.4.0",
"version": "2.5.0-rc.1",
"description": "Official library for interacting with Slack's Oauth endpoints",

@@ -34,5 +34,5 @@ "author": "Slack Technologies, LLC",

"build:clean": "shx rm -rf ./dist ./coverage ./.nyc_output",
"lint": "eslint --ext .ts src",
"lint": "eslint --fix --ext .ts src",
"test": "npm run lint && npm run test:mocha",
"test:mocha": "nyc mocha --config .mocharc.json src/*.spec.js",
"test:mocha": "nyc mocha --config .mocharc.json src/*.spec.js src/**/*.spec.js src/*.spec.ts src/**/*.spec.ts",
"coverage": "codecov -F oauthhelper --root=$PWD",

@@ -51,4 +51,6 @@ "ref-docs:model": "api-extractor run",

"devDependencies": {
"@microsoft/api-extractor": "^7.19.4",
"@types/chai": "^4.2.11",
"@microsoft/api-extractor": "^7.3.4",
"@types/mocha": "^9.1.0",
"@types/sinon": "^10.0.11",
"@typescript-eslint/eslint-plugin": "^4.4.1",

@@ -64,5 +66,5 @@ "@typescript-eslint/parser": "^4.4.0",

"eslint-plugin-node": "^11.1.0",
"mocha": "^9.1.0",
"mocha": "^9.2.1",
"nop": "^1.0.0",
"nyc": "^14.1.1",
"nyc": "^15.1.0",
"rewiremock": "^3.13.9",

@@ -69,0 +71,0 @@ "shx": "^0.3.2",

# Slack OAuth
The `@slack/oauth` package makes it simple to setup the OAuth flow for Slack apps. It supports [V2 OAuth](https://api.slack.com/authentication/oauth-v2) for Slack Apps as well as [V1 OAuth](https://api.slack.com/docs/oauth) for [Classic Slack apps](https://api.slack.com/authentication/quickstart). Slack apps that are installed in multiple workspaces, like in the App Directory or in an Enterprise Grid, will need to implement OAuth and store information about each of those installations (such as access tokens).
The `@slack/oauth` package makes it simple to setup the OAuth flow for Slack apps. It supports [V2 OAuth](https://api.slack.com/authentication/oauth-v2) for Slack Apps as well as [V1 OAuth](https://api.slack.com/docs/oauth) for [Classic Slack apps](https://api.slack.com/authentication/quickstart). Slack apps that are installed in multiple workspaces, like in the App Directory or in an Enterprise Grid, will need to implement OAuth and store information about each of those installations (such as access tokens).

@@ -66,3 +66,3 @@ The package handles URL generation, state verification, and authorization code exchange for access tokens. It also provides an interface for easily plugging in your own database for saving and retrieving installation data.

The `installProvider.generateInstallUrl()` method will create an installation URL for you. It takes in an options argument which at a minimum contains a `scopes` property. `installProvider.generateInstallUrl()` options argument also supports `metadata`, `teamId`, `redirectUri` and `userScopes` properties.
The `installProvider.generateInstallUrl()` method will create an installation URL for you. It takes in an options argument which at a minimum contains a `scopes` property. `installProvider.generateInstallUrl()` options argument also supports `metadata`, `teamId`, `redirectUri` and `userScopes` properties.

@@ -80,3 +80,3 @@ ```javascript

You might want to present an "Add to Slack" button while the user is in the middle of some other tasks (e.g. linking their Slack account to your service). In these situations, you want to bring the user back to where they left off after the app installation is complete. Custom metadata can be used to capture partial (incomplete) information about the task (like which page they were on or inputs to form elements the user began to fill out) in progress. Then when the installation is complete, that custom metadata will be available for your app to recreate exactly where they left off. You must also use a [custom success handler when handling the OAuth redirect](#handling-the-oauth-redirect) to read the custom metadata after the installation is complete.
You might want to present an "Add to Slack" button while the user is in the middle of some other tasks (e.g. linking their Slack account to your service). In these situations, you want to bring the user back to where they left off after the app installation is complete. Custom metadata can be used to capture partial (incomplete) information about the task (like which page they were on or inputs to form elements the user began to fill out) in progress. Then when the installation is complete, that custom metadata will be available for your app to recreate exactly where they left off. You must also use a [custom success handler when handling the OAuth redirect](#handling-the-oauth-redirect) to read the custom metadata after the installation is complete.

@@ -137,9 +137,11 @@ ```javascript

const callbackOptions = {
success: (installation, metadata, req, res) => {
success: (installation, installOptions, req, res) => {
// Do custom success logic here
// tip: you can add javascript and css in the htmlResponse using the <script> and <style> tags
// Tips:
// - Inspect the metadata with `installOptions.metadata`
// - Add javascript and css in the htmlResponse using the <script> and <style> tags
const htmlResponse = `<html><body>Success!</body></html>`
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
res.end(htmlResponse);
},
},
failure: (error, installOptions , req, res) => {

@@ -162,3 +164,3 @@ // Do custom failure logic here

An installation store is an object that provides two methods: `storeInstallation`, and `fetchInstallation`. `storeInstallation` takes an `installation` as an argument, which is an object that contains all installation related data (like tokens, teamIds, enterpriseIds, etc). `fetchInstallation` takes in a `installQuery`, which is used to query the database. The `installQuery` can contain `teamId`, `enterpriseId`, `userId`, `conversationId` and `isEnterpriseInstall`.
An installation store is an object that provides two methods: `storeInstallation`, and `fetchInstallation`. `storeInstallation` takes an `installation` as an argument, which is an object that contains all installation related data (like tokens, teamIds, enterpriseIds, etc). `fetchInstallation` takes in a `installQuery`, which is used to query the database. The `installQuery` can contain `teamId`, `enterpriseId`, `userId`, `conversationId` and `isEnterpriseInstall`.

@@ -246,3 +248,3 @@ In the following example, the `installationStore` option is used and the object is defined in line. The methods are implemented by calling an example database library with simple get and set operations.

The `installer.authorize()` method only returns a subset of the installation data returned by the installation store. To fetch the entire saved installation, use the `installer.installationStore.fetchInstallation()` method.
The `installer.authorize()` method only returns a subset of the installation data returned by the installation store. To fetch the entire saved installation, use the `installer.installationStore.fetchInstallation()` method.

@@ -249,0 +251,0 @@ ```javascript

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc