UNPKG

@donation-alerts/auth

Version:

Authentication provider for Donation Alerts API with ability to automatically refresh user tokens.

200 lines (199 loc) 9.18 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.RefreshingAuthProvider = void 0; const tslib_1 = require("tslib"); const typed_event_emitter_1 = require("@d-fischer/typed-event-emitter"); const api_call_1 = require("@donation-alerts/api-call"); const common_1 = require("@donation-alerts/common"); const shared_utils_1 = require("@stimulcross/shared-utils"); const access_token_1 = require("../access-token"); const errors_1 = require("../errors"); const helpers_1 = require("../helpers"); /** * An authentication provider that automatically refreshes user access tokens when they expire. */ let RefreshingAuthProvider = class RefreshingAuthProvider extends typed_event_emitter_1.EventEmitter { /** * Creates a new instance of the `RefreshingAuthProvider`. * * @param config The configuration object that defines client credentials and settings. */ constructor(config) { super(); this._registry = new Map(); this._newTokenPromises = new Map(); /** * Fires when a user's token is successfully refreshed. * * @param userId The ID of the user whose token was refreshed. * @param token The updated {@link AccessToken} object. */ this.onRefresh = this.registerEvent(); this._config = config; } get clientId() { return this._config.clientId; } /** * Checks whether the specified user is registered in this provider. * * @param user The ID of the user to check. */ hasUser(user) { return this._registry.has((0, common_1.extractUserId)(user)); } /** * Adds a user to this provider, associating them with the provided token data. * * @param user The ID of the user to add. * @param token The token data, including refresh and access tokens. * * @throws {@link InvalidTokenError} if the access or refresh tokens are invalid. * @throws {@link MissingScopeError} if the token does not match the required scopes. */ addUser(user, token) { const userId = (0, common_1.extractUserId)(user); this._validateToken(token, userId); if (token.scopes) { (0, helpers_1.compareScopes)(token.scopes, this._config.scopes, userId); } this._registry.set(userId, token); return { userId, ...token }; } /** * Determines the user ID from an access token and registers that user. * * If you already know the user's ID, using {@link addUser} might be preferable. * * @param token The token data, including refresh and access tokens. */ async addUserForToken(token) { this._validateToken(token); let accessToken = token; let isTokenRefreshed = false; if ((0, access_token_1.isAccessTokenExpired)(accessToken)) { accessToken = await (0, helpers_1.refreshAccessToken)(this._config.clientId, this._config.clientSecret, accessToken.refreshToken, this._config.scopes); isTokenRefreshed = true; } const user = await (0, api_call_1.callDonationAlertsApi)({ type: 'api', url: 'user/oauth' }, accessToken.accessToken); const userId = user.data.id; if (accessToken.scopes) { (0, helpers_1.compareScopes)(accessToken.scopes, this._config.scopes, userId); } this.addUser(userId, accessToken); if (isTokenRefreshed) { this.emit(this.onRefresh, userId, accessToken); } return { ...accessToken, userId }; } /** * Exchanges a grant authorization code for an access token and registers the user in this auth provider. * * @remarks * The `redirectUri` option must be specified in {@link RefreshingAuthProviderConfig} to complete * this flow successfully. * * @param code The authorization code. * @param scopes Optional scopes that the user granted when retrieving the code. These scopes will be compared * against the scopes specified in the constructor. */ async addUserForCode(code, scopes) { if (!this._config.redirectUri) { throw new Error('Exchanging authorization code requires "redirectUri" option to be specified'); } const token = await (0, helpers_1.getAccessToken)(this._config.clientId, this._config.clientSecret, this._config.redirectUri, code); const user = await (0, api_call_1.callDonationAlertsApi)({ type: 'api', url: 'user/oauth' }, token.accessToken); token.scopes = scopes; if (token.scopes) { (0, helpers_1.compareScopes)(token.scopes, this._config.scopes, user.data.id); } this.addUser(user.data.id, token); this.emit(this.onRefresh, user.data.id, token); return { ...token, userId: user.data.id }; } /** * Removes a user from this provider. * * @param user The ID of the user to remove. */ removeUser(user) { this._registry.delete((0, common_1.extractUserId)(user)); } getScopesForUser(user) { const userId = (0, common_1.extractUserId)(user); if (!this._registry.has(userId)) { throw new errors_1.UnregisteredUserError(userId, `User "${userId}" could not be located in the authentication provider registry. Please add the user first by using one of the following methods: addUser, addUserForToken, or addUserForCode`); } return this._registry.get(userId).scopes ?? []; } async getAccessTokenForUser(user, scopes) { const userId = (0, common_1.extractUserId)(user); if (this._newTokenPromises.has(userId)) { const token = (await this._newTokenPromises.get(userId)); return { ...token, userId }; } if (!this._registry.has(userId)) { throw new errors_1.UnregisteredUserError(userId, `User "${userId}" could not be located in the authentication provider registry. Please add the user first by using one of the following methods: addUser, addUserForToken, or addUserForCode. `); } const currentToken = this._registry.get(userId); if (currentToken.accessToken && !(0, access_token_1.isAccessTokenExpired)(currentToken)) { if (currentToken.scopes) { (0, helpers_1.compareScopes)(currentToken.scopes, scopes, userId); } return { ...currentToken, userId }; } const token = await this.refreshAccessTokenForUser(userId); token.scopes = currentToken.scopes; if (token.scopes) { (0, helpers_1.compareScopes)(token.scopes, scopes, userId); } return { ...token, userId }; } /** * Forces a token refresh for the specified user and updates the provider's registry accordingly. * * @param user The ID of the user to add. * * @throws {@link UnregisteredUserError} if the user is not registered in this provider. * @throws {@link InvalidTokenError} if the refresh token is missing or invalid. */ async refreshAccessTokenForUser(user) { const userId = (0, common_1.extractUserId)(user); if (!this._registry.has(userId)) { throw new errors_1.UnregisteredUserError(userId, `User "${userId}" could not be located in the authentication provider registry. Please add the user first by using one of the following methods: addUser, addUserForToken, or addUserForCode.`); } const currentToken = this._registry.get(userId); if (!currentToken.refreshToken) { throw new errors_1.InvalidTokenError(userId, `Unable to refresh access token for user "${userId}". Refresh token is not specified.`); } const newTokenPromise = (0, helpers_1.refreshAccessToken)(this._config.clientId, this._config.clientSecret, currentToken.refreshToken); this._newTokenPromises.set(userId, newTokenPromise); const token = await newTokenPromise; this._newTokenPromises.delete(userId); this._registry.set(userId, token); this.emit(this.onRefresh, userId, token); return { ...token, userId }; } _validateToken(token, userId) { if (!token.accessToken) { throw new errors_1.InvalidTokenError(userId ?? null, `The access token of user "${userId}" is invalid. Make sure it's a non-empty string.`); } if (!token.refreshToken) { throw new errors_1.InvalidTokenError(userId ?? null, `The refresh token of user "${userId}" is invalid. Make sure it's a non-empty string.`); } } }; exports.RefreshingAuthProvider = RefreshingAuthProvider; tslib_1.__decorate([ shared_utils_1.nonenumerable ], RefreshingAuthProvider.prototype, "_config", void 0); tslib_1.__decorate([ shared_utils_1.nonenumerable ], RefreshingAuthProvider.prototype, "_registry", void 0); tslib_1.__decorate([ shared_utils_1.nonenumerable ], RefreshingAuthProvider.prototype, "_newTokenPromises", void 0); exports.RefreshingAuthProvider = RefreshingAuthProvider = tslib_1.__decorate([ (0, common_1.ReadDocumentation)('events') ], RefreshingAuthProvider);