@slack/oauth
Advanced tools
Comparing version 2.0.1 to 2.1.0-tokenRotationBeta.1
@@ -10,2 +10,4 @@ "use strict"; | ||
return function (d, b) { | ||
if (typeof b !== "function" && b !== null) | ||
throw new TypeError("Class extends value " + String(b) + " is not a constructor or null"); | ||
extendStatics(d, b); | ||
@@ -12,0 +14,0 @@ function __() { this.constructor = d; } |
/// <reference types="node" /> | ||
import { IncomingMessage, ServerResponse } from 'http'; | ||
import { WebClientOptions } from '@slack/web-api'; | ||
import { WebAPICallResult, WebClientOptions } from '@slack/web-api'; | ||
import { CodedError } from './errors'; | ||
@@ -28,6 +28,12 @@ import { Logger, LogLevel } from './logger'; | ||
/** | ||
* Fetches data from the installationStore for non Org Installations. | ||
* 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 a URL that is suitable for including in an Add to Slack button | ||
@@ -126,2 +132,4 @@ * Uses stateStore to generate a value for the state query param. | ||
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); | ||
@@ -132,2 +140,4 @@ id: string; | ||
token: string; | ||
refreshToken?: string; | ||
expiresAt?: number; | ||
scopes: string[]; | ||
@@ -159,2 +169,4 @@ id: string; | ||
authVersion?: AuthVersion; | ||
/** A string value that can be held in the state parameter in the OAuth flow. */ | ||
metadata?: string; | ||
} | ||
@@ -179,3 +191,7 @@ /** | ||
botToken?: string; | ||
botRefreshToken?: string; | ||
botTokenExpiresAt?: number; | ||
userToken?: string; | ||
userRefreshToken?: string; | ||
userTokenExpiresAt?: number; | ||
botId?: string; | ||
@@ -186,3 +202,54 @@ botUserId?: 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; | ||
response_metadata: {}; | ||
} | ||
export { Logger, LogLevel } from './logger'; | ||
//# sourceMappingURL=index.d.ts.map |
@@ -69,3 +69,3 @@ "use strict"; | ||
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.installationStore, installationStore = _d === void 0 ? new MemoryInstallationStore() : _d, _e = _a.authVersion, authVersion = _e === void 0 ? 'v2' : _e, _f = _a.logger, logger = _f === void 0 ? undefined : _f, _g = _a.logLevel, logLevel = _g === void 0 ? logger_1.LogLevel.INFO : _g, _h = _a.clientOptions, clientOptions = _h === void 0 ? {} : _h, _j = _a.authorizationUrl, authorizationUrl = _j === void 0 ? 'https://slack.com/oauth/v2/authorize' : _j; | ||
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.installationStore, installationStore = _d === void 0 ? new MemoryInstallationStore() : _d, _e = _a.authVersion, authVersion = _e === void 0 ? 'v2' : _e, _f = _a.logger, logger = _f === void 0 ? undefined : _f, _g = _a.logLevel, logLevel = _g === void 0 ? undefined : _g, _h = _a.clientOptions, clientOptions = _h === void 0 ? {} : _h, _j = _a.authorizationUrl, authorizationUrl = _j === void 0 ? 'https://slack.com/oauth/v2/authorize' : _j; | ||
if (clientId === undefined || clientSecret === undefined) { | ||
@@ -82,3 +82,3 @@ throw new errors_1.InstallerInitializationError('You must provide a valid clientId and clientSecret'); | ||
else { | ||
this.logger = logger_1.getLogger('OAuth:InstallProvider', logLevel, logger); | ||
this.logger = logger_1.getLogger('OAuth:InstallProvider', logLevel !== null && logLevel !== void 0 ? logLevel : logger_1.LogLevel.INFO, logger); | ||
} | ||
@@ -108,14 +108,15 @@ // Setup stateStore | ||
} | ||
this.clientOptions = __assign({ logLevel: this.logger.getLevel() }, clientOptions); | ||
this.clientOptions = __assign({ logger: logger, logLevel: this.logger.getLevel() }, clientOptions); | ||
} | ||
/** | ||
* Fetches data from the installationStore for non Org Installations. | ||
* Fetches data from the installationStore | ||
*/ | ||
InstallProvider.prototype.authorize = function (source) { | ||
return __awaiter(this, void 0, void 0, function () { | ||
var queryResult, authResult, error_1; | ||
return __generator(this, function (_a) { | ||
switch (_a.label) { | ||
var queryResult, authResult, currentUTCSec, tokensToRefresh, installationUpdates, refreshResponses, _i, refreshResponses_1, refreshResp, tokenType, updatedInstallation, error_1; | ||
var _a; | ||
return __generator(this, function (_b) { | ||
switch (_b.label) { | ||
case 0: | ||
_a.trys.push([0, 5, , 6]); | ||
_b.trys.push([0, 10, , 11]); | ||
queryResult = void 0; | ||
@@ -125,8 +126,8 @@ if (!source.isEnterpriseInstall) return [3 /*break*/, 2]; | ||
case 1: | ||
queryResult = _a.sent(); | ||
queryResult = _b.sent(); | ||
return [3 /*break*/, 4]; | ||
case 2: return [4 /*yield*/, this.installationStore.fetchInstallation(source, this.logger)]; | ||
case 3: | ||
queryResult = _a.sent(); | ||
_a.label = 4; | ||
queryResult = _b.sent(); | ||
_b.label = 4; | ||
case 4: | ||
@@ -158,8 +159,55 @@ if (queryResult === undefined) { | ||
authResult.botUserId = queryResult.bot.userId; | ||
// Token Rotation Enabled (Bot Token) | ||
if (queryResult.bot.refreshToken !== undefined) { | ||
authResult.botRefreshToken = queryResult.bot.refreshToken; | ||
authResult.botTokenExpiresAt = queryResult.bot.expiresAt; // utc, seconds | ||
} | ||
} | ||
return [2 /*return*/, authResult]; | ||
// Token Rotation Enabled (User Token) | ||
if (queryResult.user.refreshToken !== undefined) { | ||
authResult.userRefreshToken = queryResult.user.refreshToken; | ||
authResult.userTokenExpiresAt = queryResult.user.expiresAt; // utc, seconds | ||
} | ||
if (!(authResult.botRefreshToken !== undefined || authResult.userRefreshToken !== undefined)) 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: | ||
error_1 = _a.sent(); | ||
refreshResponses = _b.sent(); | ||
_i = 0, refreshResponses_1 = refreshResponses; | ||
_b.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), (_a = {}, _a[tokenType] = __assign(__assign({}, queryResult[tokenType]), installationUpdates[tokenType]), _a)); | ||
return [4 /*yield*/, this.installationStore.storeInstallation(updatedInstallation)]; | ||
case 7: | ||
_b.sent(); | ||
_b.label = 8; | ||
case 8: | ||
_i++; | ||
return [3 /*break*/, 6]; | ||
case 9: return [2 /*return*/, authResult]; | ||
case 10: | ||
error_1 = _b.sent(); | ||
throw new errors_1.AuthorizationError(error_1.message); | ||
case 6: return [2 /*return*/]; | ||
case 11: return [2 /*return*/]; | ||
} | ||
@@ -170,2 +218,30 @@ }); | ||
/** | ||
* 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 a URL that is suitable for including in an Add to Slack button | ||
@@ -234,7 +310,7 @@ * Uses stateStore to generate a value for the state query param. | ||
return __awaiter(this, void 0, void 0, function () { | ||
var parsedUrl, code, state, installOptions, client, installation, resp, v1Resp, v1Installation, authResult, botId, v2Resp, v2Installation, authResult, authResult, error_2; | ||
var parsedUrl, code, state, installOptions, client, installation, resp, v1Resp, v1Installation, authResult, botId, v2Resp, v2Installation, currentUTC, authResult, authResult, error_2; | ||
return __generator(this, function (_b) { | ||
switch (_b.label) { | ||
case 0: | ||
_b.trys.push([0, 17, , 18]); | ||
_b.trys.push([0, 16, , 17]); | ||
if (req.url !== undefined) { | ||
@@ -293,3 +369,3 @@ parsedUrl = url_1.parse(req.url, true); | ||
installation = v1Installation; | ||
return [3 /*break*/, 12]; | ||
return [3 /*break*/, 11]; | ||
case 5: return [4 /*yield*/, client.oauth.v2.access({ | ||
@@ -317,2 +393,3 @@ code: code, | ||
}; | ||
currentUTC = Math.floor(Date.now() / 1000); | ||
if (!(v2Resp.access_token !== undefined && v2Resp.scope !== undefined && v2Resp.bot_user_id !== undefined)) return [3 /*break*/, 8]; | ||
@@ -329,23 +406,29 @@ return [4 /*yield*/, runAuthTest(v2Resp.access_token, this.clientOptions)]; | ||
if (v2Resp.is_enterprise_install) { | ||
// if it is an org enterprise install, add the enterprise url | ||
v2Installation.enterpriseUrl = authResult.url; | ||
} | ||
return [3 /*break*/, 11]; | ||
// 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 = 8; | ||
case 8: | ||
if (!(v2Resp.authed_user.access_token !== undefined)) return [3 /*break*/, 10]; | ||
if (!(v2Resp.authed_user !== undefined && v2Resp.authed_user.access_token !== undefined)) return [3 /*break*/, 10]; | ||
return [4 /*yield*/, runAuthTest(v2Resp.authed_user.access_token, this.clientOptions)]; | ||
case 9: | ||
authResult = _b.sent(); | ||
if (v2Resp.is_enterprise_install) { | ||
if (v2Resp.is_enterprise_install && v2Installation.enterpriseUrl === undefined) { | ||
v2Installation.enterpriseUrl = authResult.url; | ||
} | ||
return [3 /*break*/, 11]; | ||
case 10: | ||
// TODO: make this a coded error | ||
throw new Error('The response from the authorization URL contained inconsistent information. Please file a bug.'); | ||
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 = 10; | ||
case 10: | ||
resp = v2Resp; | ||
installation = v2Installation; | ||
_b.label = 12; | ||
case 12: | ||
_b.label = 11; | ||
case 11: | ||
if (resp.incoming_webhook !== undefined) { | ||
@@ -359,12 +442,17 @@ installation.incomingWebhook = { | ||
} | ||
if (!installation.isEnterpriseInstall) return [3 /*break*/, 14]; | ||
if (installOptions !== undefined && 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*/, 13]; | ||
return [4 /*yield*/, this.installationStore.storeInstallation(installation, this.logger)]; | ||
case 13: | ||
case 12: | ||
_b.sent(); | ||
return [3 /*break*/, 16]; | ||
case 14: return [4 /*yield*/, this.installationStore.storeInstallation(installation, this.logger)]; | ||
return [3 /*break*/, 15]; | ||
case 13: return [4 /*yield*/, this.installationStore.storeInstallation(installation, this.logger)]; | ||
case 14: | ||
_b.sent(); | ||
_b.label = 15; | ||
case 15: | ||
_b.sent(); | ||
_b.label = 16; | ||
case 16: | ||
// Call the success callback | ||
@@ -379,4 +467,4 @@ if (options !== undefined && options.success !== undefined) { | ||
} | ||
return [3 /*break*/, 18]; | ||
case 17: | ||
return [3 /*break*/, 17]; | ||
case 16: | ||
error_2 = _b.sent(); | ||
@@ -393,4 +481,4 @@ this.logger.error(error_2); | ||
} | ||
return [3 /*break*/, 18]; | ||
case 18: return [2 /*return*/]; | ||
return [3 /*break*/, 17]; | ||
case 17: return [2 /*return*/]; | ||
} | ||
@@ -538,4 +626,26 @@ }); | ||
} | ||
/** | ||
* 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 !== undefined && authResult.botTokenExpiresAt !== undefined) { | ||
var botTokenExpiresIn = authResult.botTokenExpiresAt - currentUTCSec; | ||
if (botTokenExpiresIn <= EXPIRY_WINDOW) { | ||
tokensToRefresh.push(authResult.botRefreshToken); | ||
} | ||
} | ||
if (authResult.userRefreshToken !== undefined && authResult.userTokenExpiresAt !== undefined) { | ||
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; } }); | ||
//# sourceMappingURL=index.js.map |
{ | ||
"name": "@slack/oauth", | ||
"version": "2.0.1", | ||
"version": "2.1.0-tokenRotationBeta.1", | ||
"description": "Official library for interacting with Slack's Oauth endpoints", | ||
@@ -41,4 +41,4 @@ "author": "Slack Technologies, Inc.", | ||
"dependencies": { | ||
"@slack/logger": "^2.0.0", | ||
"@slack/web-api": "^5.7.0", | ||
"@slack/logger": "^3.0.0", | ||
"@slack/web-api": "feat-token-rotation", | ||
"@types/jsonwebtoken": "^8.3.7", | ||
@@ -68,2 +68,2 @@ "@types/node": ">=12", | ||
} | ||
} | ||
} |
# Slack OAuth | ||
[![build-ci](https://github.com/slackapi/node-slack-sdk/workflows/CI%20Build/badge.svg)](https://github.com/slackapi/node-slack-sdk/actions?query=workflow%3A%22CI+Build%22) | ||
<!-- TODO: per-flag badge https://docs.codecov.io/docs/flags#section-flag-badges-and-graphs --> | ||
[![codecov](https://codecov.io/gh/slackapi/node-slack-sdk/branch/master/graph/badge.svg)](https://codecov.io/gh/slackapi/node-slack-sdk) | ||
<!-- TODO: npm versions with scoped packages: https://github.com/rvagg/nodei.co/issues/24 --> | ||
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). | ||
@@ -9,0 +4,0 @@ |
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
New author
Supply chain riskA new npm collaborator published a version of the package for the first time. New collaborators are usually benign additions to a project, but do indicate a change to the security surface area of a package.
Found 1 instance in 1 package
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
Found 1 instance in 1 package
96269
1032
1
361
1
+ Added@slack/logger@3.0.0(transitive)
+ Added@slack/types@2.14.0(transitive)
+ Added@slack/web-api@6.2.4-tokenRotationBeta.1(transitive)
+ Addedis-electron@2.2.2(transitive)
- Removed@slack/logger@2.0.0(transitive)
- Removed@slack/types@1.10.0(transitive)
- Removed@slack/web-api@5.15.0(transitive)
Updated@slack/logger@^3.0.0