@slack/oauth
Advanced tools
Comparing version 2.4.0 to 2.5.0-rc.1
@@ -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); |
@@ -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
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
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
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
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
187596
82
2087
378
25
1
3