@donation-alerts/auth
Version:
Authentication provider for Donation Alerts API with ability to automatically refresh user tokens.
200 lines (199 loc) • 9.18 kB
JavaScript
;
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);