@coko/server
Version:
Reusable server for use by Coko's projects
167 lines • 7.55 kB
JavaScript
;
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