UNPKG

@coko/server

Version:

Reusable server for use by Coko's projects

167 lines 7.55 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.invalidateProviderTokens = exports.invalidateProviderAccessToken = exports.hasValidRefreshToken = exports.getDefaultIdentity = exports.getUserIdentities = exports.createOAuthIdentity = void 0; const axios_1 = __importDefault(require("axios")); const dayjs_1 = __importDefault(require("dayjs")); const utc_1 = __importDefault(require("dayjs/plugin/utc")); const logger_1 = __importDefault(require("../../logger")); const pubsub_1 = __importDefault(require("../../graphql/pubsub")); const time_1 = require("../../utils/time"); const jobManager_1 = require("../../jobManager"); const user_controller_1 = require("../user/user.controller"); const identity_model_1 = __importDefault(require("./identity.model")); const constants_1 = require("../user/constants"); const constants_2 = require("./constants"); const config_1 = __importDefault(require("../../configManager/config")); const number_1 = require("../../utils/number"); dayjs_1.default.extend(utc_1.default); const { USER_UPDATED } = constants_1.subscriptions; const { IDENTITY_CONTROLLER } = constants_2.labels; const getUserIdentities = async (userId) => { return identity_model_1.default.find({ userId }); }; exports.getUserIdentities = getUserIdentities; const getDefaultIdentity = async (userId) => { return identity_model_1.default.findOne({ userId, isDefault: true, }); }; exports.getDefaultIdentity = getDefaultIdentity; const hasValidRefreshToken = (identity) => { const { oauthRefreshTokenExpiration, oauthRefreshToken } = identity; const UTCNowTimestamp = (0, dayjs_1.default)().utc().valueOf(); return (!!oauthRefreshToken && !!oauthRefreshTokenExpiration && oauthRefreshTokenExpiration.getTime() > UTCNowTimestamp); }; exports.hasValidRefreshToken = hasValidRefreshToken; /** * Authorise user OAuth. * Save OAuth access and refresh tokens. * Trigger subscription indicating the identity has changed. */ const createOAuthIdentity = async (userId, provider, sessionState, code) => { // Throw error if unable to acquire and then store authorisation try { let identity = await identity_model_1.default.findOne({ userId, provider }); if (identity && hasValidRefreshToken(identity)) { return identity; } const { ...authData } = await authorizeOAuth(provider, sessionState, code); const { email, given_name: givenNames, family_name: surname, sub: providerUserId, } = JSON.parse(Buffer.from(authData.oauthAccessToken.split('.')[1], 'base64').toString()); if (!identity) { identity = await identity_model_1.default.insert({ email, provider, userId, profileData: { givenNames, surname, providerUserId, }, ...authData, }); } else { identity = await identity_model_1.default.patchAndFetchById(identity.id, { ...authData }); } const { oauthRefreshTokenExpiration } = authData; if (oauthRefreshTokenExpiration.getTime() !== time_1.foreverDate.getTime()) { const expiresIn = (oauthRefreshTokenExpiration.getTime() - (0, dayjs_1.default)().utc().valueOf()) / 1000; await jobManager_1.jobManager.sendToQueue(jobManager_1.defaultJobQueueNames.REFRESH_TOKEN_EXPIRED, { userId, providerLabel: provider }, { startAfter: Math.round(expiresIn) }); } return identity; } catch (e) { logger_1.default.error(`${IDENTITY_CONTROLLER} createOAuthIdentity: ${e.message}`); throw e; } }; exports.createOAuthIdentity = createOAuthIdentity; /** authorizeOAuth * Send an Oauth2 authorisation code requesting access and refresh tokens. * Return the validated tokens or throw an error. */ const authorizeOAuth = async (provider, sessionState, code) => { const integrations = config_1.default.get('integrations'); const providerIntegration = integrations.find(i => i.name === provider); const { tokenUrl, clientId, redirectUri } = providerIntegration; const postData = { code, grant_type: 'authorization_code', session_state: sessionState, client_id: clientId, redirect_uri: redirectUri, }; const params = new URLSearchParams(postData); // Get tokens const { data } = await (0, axios_1.default)({ method: 'POST', url: tokenUrl, data: params.toString(), headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, }); if (data.token_type?.toLowerCase() !== 'bearer') { throw new Error(`Invalid "token_type": ${data.token_type}`); } if (data.session_state !== sessionState) { throw new Error(`Invalid "session_state": ${data.session_state}`); } /* eslint-disable camelcase */ const { access_token, expires_in, refresh_token, refresh_expires_in } = data; if (!access_token) { throw new Error('Missing access_token from response!'); } if (!(0, number_1.isValidPositiveIntegerOrZero)(expires_in)) { throw new Error('Missing expires_in from response!'); } if (!refresh_token) { throw new Error('Missing refresh_token from response!'); } if (!(0, number_1.isValidPositiveIntegerOrZero)(refresh_expires_in)) { throw new Error('Missing refresh_expires_in from response!'); } return { oauthAccessToken: access_token, oauthRefreshToken: refresh_token, oauthAccessTokenExpiration: expires_in === 0 ? time_1.foreverDate : (0, time_1.getExpirationTime)(expires_in), oauthRefreshTokenExpiration: refresh_expires_in === 0 ? time_1.foreverDate : (0, time_1.getExpirationTime)(refresh_expires_in), }; /* eslint-enable camelcase */ }; const invalidateProviderAccessToken = async (userId, providerLabel) => { const providerUserIdentity = await identity_model_1.default.findOne({ userId, provider: providerLabel, }); await identity_model_1.default.patchAndFetchById(providerUserIdentity.id, { oauthAccessTokenExpiration: (0, dayjs_1.default)().utc().toDate(), }); logger_1.default.info(`access token for provider ${providerLabel} became invalid, trying to get a new one via the refresh token`); }; exports.invalidateProviderAccessToken = invalidateProviderAccessToken; const invalidateProviderTokens = async (userId, providerLabel) => { const updatedUser = await (0, user_controller_1.getUser)(userId); const providerUserIdentity = await identity_model_1.default.findOne({ userId, provider: providerLabel, }); await identity_model_1.default.patchAndFetchById(providerUserIdentity.id, { oauthAccessTokenExpiration: (0, dayjs_1.default)().utc().toDate(), oauthRefreshTokenExpiration: (0, dayjs_1.default)().utc().toDate(), }); pubsub_1.default.publish(USER_UPDATED, { userUpdated: updatedUser, }); logger_1.default.error(`refresh token for provider ${providerLabel} became invalid, authorization flow (provider login) should be followed by the user`); }; exports.invalidateProviderTokens = invalidateProviderTokens; //# sourceMappingURL=identity.controller.js.map