@bdelab/roar-firekit
Version:
A library to facilitate Firebase authentication and Cloud Firestore interaction for ROAR apps
915 lines • 134 kB
JavaScript
"use strict";
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 __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.RoarFirekit = void 0;
/* eslint-disable @typescript-eslint/no-non-null-assertion */
const get_1 = __importDefault(require("lodash/get"));
const set_1 = __importDefault(require("lodash/set"));
const isEmpty_1 = __importDefault(require("lodash/isEmpty"));
const nth_1 = __importDefault(require("lodash/nth"));
const union_1 = __importDefault(require("lodash/union"));
const auth_1 = require("firebase/auth");
const firestore_1 = require("firebase/firestore");
const functions_1 = require("firebase/functions");
const auth_2 = require("./auth");
const util_1 = require("./firestore/util");
const interfaces_1 = require("./interfaces");
const appkit_1 = require("./firestore/app/appkit");
const query_assessment_1 = require("./firestore/query-assessment");
const task_1 = require("./firestore/app/task");
var AuthProviderType;
(function (AuthProviderType) {
AuthProviderType["CLEVER"] = "clever";
AuthProviderType["CLASSLINK"] = "classlink";
AuthProviderType["GOOGLE"] = "google";
AuthProviderType["EMAIL"] = "email";
AuthProviderType["USERNAME"] = "username";
AuthProviderType["PASSWORD"] = "password";
})(AuthProviderType || (AuthProviderType = {}));
class RoarFirekit {
/**
* Create a RoarFirekit. This expects an object with keys `roarConfig`,
* where `roarConfig` is a [[RoarConfig]] object.
* @param {{roarConfig: RoarConfig }=} destructuredParam
* roarConfig: The ROAR firebase config object
*/
constructor({ roarConfig, verboseLogging = false, authPersistence = util_1.AuthPersistence.session, markRawConfig = {}, listenerUpdateCallback, }) {
this.roarConfig = roarConfig;
this._verboseLogging = verboseLogging;
this._authPersistence = authPersistence;
this._markRawConfig = markRawConfig;
this._initialized = false;
this._idTokens = {};
// eslint-disable-next-line @typescript-eslint/no-empty-function
this.listenerUpdateCallback = listenerUpdateCallback !== null && listenerUpdateCallback !== void 0 ? listenerUpdateCallback : (() => { });
}
_getProviderIds() {
return Object.assign(Object.assign({}, auth_1.ProviderId), { CLEVER: 'oidc.clever', CLASSLINK: 'oidc.classlink', ROAR_ADMIN_PROJECT: `oidc.${this.roarConfig.admin.projectId}` });
}
_scrubAuthProperties() {
this.userData = undefined;
this.roarAppUserInfo = undefined;
this._adminOrgs = undefined;
this._superAdmin = undefined;
this.currentAssignments = undefined;
this.oAuthAccessToken = undefined;
this._adminClaimsListener = undefined;
this._adminTokenListener = undefined;
this._appTokenListener = undefined;
this._idTokens = {};
}
init() {
return __awaiter(this, void 0, void 0, function* () {
this.app = yield (0, util_1.initializeFirebaseProject)(this.roarConfig.app, 'app', this._authPersistence, this._markRawConfig);
this.admin = yield (0, util_1.initializeFirebaseProject)(this.roarConfig.admin, 'admin', this._authPersistence, this._markRawConfig);
this._initialized = true;
(0, auth_1.onAuthStateChanged)(this.admin.auth, (user) => __awaiter(this, void 0, void 0, function* () {
this.verboseLog('onAuthStateChanged triggered for admin auth');
if (this.admin) {
if (user) {
this.verboseLog('admin firebase instance and user are defined');
this.admin.user = user;
this._adminClaimsListener = this._listenToClaims(this.admin);
this.verboseLog('adminClaimsListener instance set up using listenToClaims');
this._adminTokenListener = this._listenToTokenChange(this.admin, 'admin');
this.verboseLog('adminTokenListener instance set up using listenToClaims');
this.verboseLog('[admin] Attempting to fire user.getIdToken(), existing token is', this._idTokens.admin);
user.getIdToken().then((idToken) => {
this.verboseLog('in .then() for user.getIdToken() with new token', idToken);
this._idTokens.admin = idToken;
this.verboseLog(`Updated internal admin token to ${idToken}`);
});
this._roarUid = yield this.getRoarUid();
}
else {
this.verboseLog('User for admin is undefined.');
this.admin.user = undefined;
this._roarUid = undefined;
}
}
this.verboseLog('[admin] Call this.listenerUpdateCallback()');
this.listenerUpdateCallback();
}));
(0, auth_1.onAuthStateChanged)(this.app.auth, (user) => {
this.verboseLog('onAuthStateChanged triggered for assessment auth');
if (this.app) {
if (user) {
this.verboseLog('assessment firebase instance and user are defiend');
this.app.user = user;
this._appTokenListener = this._listenToTokenChange(this.app, 'app');
this.verboseLog('appTokenListener instance set up using listenToTokenChange');
this.verboseLog('[app] Attempting to fire user.getIdToken() from app , existing token is', this._idTokens.app);
user.getIdToken().then((idToken) => {
this.verboseLog('in .then() for user.getItToken() with new token', idToken);
this._idTokens.app = idToken;
this.verboseLog('Updated internal app token to', idToken);
});
}
else {
this.verboseLog('User for app is undefined');
this.app.user = undefined;
}
}
this.verboseLog('[app] Call this.listenerUpdateCallback()');
this.listenerUpdateCallback();
});
return this;
});
}
verboseLog(...logStatement) {
if (this._verboseLogging) {
console.log('[RoarFirekit] ', ...logStatement);
}
else
return;
}
get initialized() {
return this._initialized;
}
/**
* Verifies if the RoarFirekit instance has been initialized.
*
* This method checks if the RoarFirekit instance has been initialized by checking the `_initialized` property.
* If the instance has not been initialized, it throws an error with a descriptive message.
*
* @throws {Error} - If the RoarFirekit instance has not been initialized.
*/
_verifyInit() {
if (!this._initialized) {
throw new Error('RoarFirekit has not been initialized. Use the `init` method.');
}
}
// +--------------------------------+
// ----------| Begin Authentication Methods |----------
// +--------------------------------+
/**
* Verifies if the user is authenticated in both the admin and app Firebase projects.
*
* This method checks if the user is authenticated in both the admin and app Firebase projects.
* It returns a boolean value indicating whether the user is authenticated or not.
*
* @returns {boolean} - A boolean value indicating whether the user is authenticated or not.
* @throws {Error} - If the Firebase projects are not initialized.
*/
_isAuthenticated() {
this._verifyInit();
return !(this.admin.user === undefined || this.app.user === undefined);
}
/**
* Checks if the current user is an administrator.
*
* This method checks if the current user has administrative privileges in the application.
* It returns a boolean value indicating whether the user is an administrator or not.
*
* @returns {boolean} - A boolean value indicating whether the user is an administrator or not.
*
* @remarks
* - If the user is a super administrator, the method returns `true`.
* - If the user has no adminOrgs, the method returns `false`.
* - If the application is using the Levante platform, the method checks if the user is an administrator specifically for the Levante platform.
* - If the adminOrgs are empty, the method returns `false`.
* - If none of the above conditions are met, the method returns `true`.
*/
isAdmin() {
if (this.superAdmin)
return true;
if (this._adminOrgs === undefined)
return false;
if (this.roarConfig.admin.projectId.includes('levante') || this.roarConfig.app.projectId.includes('levante')) {
return this._admin;
}
if ((0, isEmpty_1.default)((0, union_1.default)(...Object.values(this._adminOrgs))))
return false;
return true;
}
_verifyAuthentication() {
this._verifyInit();
if (!this._isAuthenticated()) {
throw new Error('User is not authenticated.');
}
}
/**
* Verifies if the user is authenticated in the application.
*
* This method checks if the user is authenticated in both the admin and assessment Firebase projects.
* If the user is authenticated in both projects, the method returns without throwing an error.
* If the user is not authenticated in either project, the method throws an error with the message 'User is not authenticated.'
*
* @throws {Error} - Throws an error if the user is not authenticated.
*/
_verifyAdmin() {
this._verifyAuthentication();
if (!this.isAdmin()) {
throw new Error('User is not an administrator.');
}
}
/**
* Listens for changes in the user's custom claims and updates the internal state accordingly.
*
* This method sets up a snapshot listener on the user's custom claims document in the admin Firebase project.
* When the listener detects changes in the claims, it updates the internal state of the `RoarAuth` instance.
* It also refreshes the user's ID token if the claims have been updated.
*
* @param {FirebaseFirestore.Firestore} firekit.db - The Firestore database instance for the admin Firebase project.
* @param {FirebaseAuth.User} firekit.user - The user object for the admin Firebase project.
* @returns {FirebaseFirestore.Unsubscribe} - The unsubscribe function to stop listening for changes in the user's custom claims.
* @throws {FirebaseError} - If there is an error setting up the snapshot listener.
*/
_listenToClaims(firekit) {
this.verboseLog('entry point to listenToClaims');
this._verifyInit();
if (firekit.user) {
this.verboseLog('firekit.user is defined');
let unsubscribe;
this.verboseLog('About to try setting up the claims listener');
try {
this.verboseLog('Beginning onSnapshot definition');
unsubscribe = (0, firestore_1.onSnapshot)((0, firestore_1.doc)(firekit.db, 'userClaims', firekit.user.uid), (doc) => __awaiter(this, void 0, void 0, function* () {
var _a, _b, _c;
this.verboseLog('In onSnapshot call for listenToClaims');
const data = doc.data();
this._adminOrgs = (_a = data === null || data === void 0 ? void 0 : data.claims) === null || _a === void 0 ? void 0 : _a.adminOrgs;
this._superAdmin = (_b = data === null || data === void 0 ? void 0 : data.claims) === null || _b === void 0 ? void 0 : _b.super_admin;
if (this.roarConfig.admin.projectId.includes('levante') ||
this.roarConfig.app.projectId.includes('levante')) {
this._admin = ((_c = data === null || data === void 0 ? void 0 : data.claims) === null || _c === void 0 ? void 0 : _c.admin) || false;
}
this.verboseLog('data, adminOrgs, superAdmin are retrieved from doc.data()');
this.verboseLog('about to check for existance of data.lastUpdated');
if (data === null || data === void 0 ? void 0 : data.lastUpdated) {
this.verboseLog('lastUpdate exists.');
const lastUpdated = new Date(data.lastUpdated);
this.verboseLog('Checking for firekit.claimsLastUpdated existance or outdated (< lastUpdated from retrieved data)');
if (!firekit.claimsLastUpdated || lastUpdated > firekit.claimsLastUpdated) {
this.verboseLog("Firekit's last updated either does not exist or is outdated. Await getIdToken and update firekit's claimsLastUpdated field.");
// Update the user's ID token and refresh claimsLastUpdated.
yield (0, auth_1.getIdToken)(firekit.user, true);
firekit.claimsLastUpdated = lastUpdated;
}
}
this.verboseLog('Call listenerUpdateCallback from listenToClaims');
this.listenerUpdateCallback();
}), (error) => {
throw error;
});
// eslint-disable-next-line @typescript-eslint/no-explicit-any
}
catch (error) {
this.verboseLog('Attempt to set up claims listener failed. Error is', error);
if (error.code !== 'permission-denied') {
throw error;
}
}
return unsubscribe;
}
}
/**
* Forces a refresh of the ID token for the admin and app Firebase users.
*
* This method retrieves the ID tokens for the admin and app Firebase users
* and refreshes them. It ensures that the tokens are up-to-date and valid.
*
* @returns {Promise<void>} - A promise that resolves when the ID tokens are refreshed successfully.
* @throws {FirebaseError} - If an error occurs while refreshing the ID tokens.
*/
forceIdTokenRefresh() {
return __awaiter(this, void 0, void 0, function* () {
this.verboseLog('Entry point for forceIdTokenRefresh');
this._verifyAuthentication();
yield (0, auth_1.getIdToken)(this.admin.user, true);
yield (0, auth_1.getIdToken)(this.app.user, true);
});
}
/**
* Listens for changes in the ID token of the specified Firebase project and updates the corresponding token.
*
* This method sets up a listener to track changes in the ID token of the specified Firebase project (either admin or app).
* When the ID token changes, it retrieves the new ID token and updates the corresponding token in the `_idTokens` object.
* It also calls the `listenerUpdateCallback` function to notify any listeners of the token update.
*
* @param {FirebaseProject} firekit - The Firebase project to listen for token changes.
* @param {'admin' | 'app'} _type - The type of Firebase project ('admin' or 'app').
* @returns {firebase.Unsubscribe} - A function to unsubscribe from the listener.
* @private
*/
_listenToTokenChange(firekit, _type) {
this.verboseLog('Entry point for listenToTokenChange, called with', _type);
this._verifyInit();
this.verboseLog('Checking for existance of tokenListener with type', _type);
if ((!this._adminTokenListener && _type === 'admin') || (!this._appTokenListener && _type === 'app')) {
this.verboseLog('Token listener does not exist, create now.');
return (0, auth_1.onIdTokenChanged)(firekit.auth, (user) => __awaiter(this, void 0, void 0, function* () {
this.verboseLog('onIdTokenChanged body');
if (user) {
this.verboseLog('user exists, await user.getIdTokenResult(false)');
const idTokenResult = yield user.getIdTokenResult(false);
this.verboseLog('Returned with token', idTokenResult);
if (_type === 'admin') {
this.verboseLog('Type is admin, set idTokenRecieved flag');
this._idTokenReceived = true;
}
this.verboseLog(`Setting idTokens.${_type} to token`, idTokenResult.token);
this._idTokens[_type] = idTokenResult.token;
}
this.verboseLog('Calling listenerUpdateCallback from listenToTokenChange', _type);
this.listenerUpdateCallback();
}));
}
else if (_type === 'admin') {
this.verboseLog('Type is admin, invoking _adminTokenListener');
return this._adminTokenListener;
}
this.verboseLog('Type is app, invoking _appTokenListener');
return this._appTokenListener;
}
/**
* Sets the UID custom claims for the admin and assessment UIDs in the Firebase projects.
*
* This method is responsible for associating the admin and assessment UIDs in the Firebase projects.
* It calls the setUidClaims cloud function in the admin Firebase project.
* If the cloud function execution is successful, it refreshes the ID tokens for both projects.
*
* @returns {Promise<any>} - A promise that resolves with the result of the setUidClaims cloud function execution.
* @param {object} input - An object containing the required parameters
* @param {string} input.identityProviderId - The identity provider ID for the user (optional).
* @param {AuthProviderType} input.identityProviderType - The type of the identity provider (optional).
* @throws {Error} - If the setUidClaims cloud function execution fails, an Error is thrown.
*/
_setUidCustomClaims({ identityProviderId = undefined, identityProviderType = undefined, } = {}) {
return __awaiter(this, void 0, void 0, function* () {
this.verboseLog('Entry point to setUidCustomClaims');
this._verifyAuthentication();
this.verboseLog('Calling cloud function for setUidClaims');
const setUidClaims = (0, functions_1.httpsCallable)(this.admin.functions, 'setUidClaims');
const result = yield setUidClaims({
assessmentUid: this.app.user.uid,
identityProviderId,
identityProviderType,
});
this.verboseLog('setUidClaims returned with result', result);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
if ((0, get_1.default)(result.data, 'status') !== 'ok') {
this.verboseLog('Error in calling setUidClaims cloud function', result.data);
throw new Error('Failed to set UIDs in the admin and assessment Firebase projects.');
}
yield this.forceIdTokenRefresh();
this.verboseLog('Returning result from setUidCustomClaims', result);
return result;
});
}
/**
* Synchronizes Education Single Sign-On (SSO) user data.
*
* This method is responsible for synchronizing user data between the
* Education SSO platform (Clever or ClassLink) and the ROAR (Readiness
* Outcomes Assessment Reporting) system. It uses the provided OAuth
* access token to authenticate with the Education SSO platform and
* calls the appropriate cloud function to sync the user data.
*
* @param {string} oAuthAccessToken - The OAuth access token obtained from the Education SSO platform.
* @param {AuthProviderType} authProvider - The type of the Education SSO platform (Clever or ClassLink).
* @throws {Error} - If the required parameters are missing or invalid.
* @returns {Promise<void>} - A promise that resolves when the synchronization is complete.
*/
_syncEduSSOUser(oAuthAccessToken, authProvider) {
return __awaiter(this, void 0, void 0, function* () {
this.verboseLog('Entry point for syncEduSSOUser');
if (authProvider === AuthProviderType.CLASSLINK) {
this.verboseLog('Calling syncEduSSOUser cloud function [ClassLink]');
if (oAuthAccessToken === undefined) {
this.verboseLog('Not OAuth token provided.');
throw new Error('No OAuth access token provided.');
}
this._verifyAuthentication();
this.verboseLog('Calling syncClassLinkUser cloud function');
const syncClassLinkUser = (0, functions_1.httpsCallable)(this.admin.functions, 'syncClassLinkUser');
const adminResult = yield syncClassLinkUser({
assessmentUid: this.app.user.uid,
accessToken: oAuthAccessToken,
});
this.verboseLog('syncClassLinkUser cloud function returned with result', adminResult);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
if ((0, get_1.default)(adminResult.data, 'status') !== 'ok') {
this.verboseLog('There was an error with the cloud function syncClassLinkUser cloud function', adminResult.data);
throw new Error('Failed to sync ClassLink and ROAR data.');
}
}
});
}
/**
* Checks if the given username is available for a new user registration.
*
* This method verifies if the given username is not already associated with
* a user in the admin Firebase project. It returns a promise that resolves with
* a boolean value indicating whether the username is available or not.
*
* @param {string} username - The username to check.
* @returns {Promise<boolean>} - A promise that resolves with a boolean value indicating whether the username is available or not.
* @throws {FirebaseError} - If an error occurs while checking the username availability.
*/
isUsernameAvailable(username) {
return __awaiter(this, void 0, void 0, function* () {
this._verifyInit();
return (0, auth_2.isUsernameAvailable)(this.admin.auth, username);
});
}
/**
* Checks if the given email address is available for a new user registration.
*
* This method verifies if the given email address is not already associated with
* a user in the admin Firebase project. It returns a promise that resolves with
* a boolean value indicating whether the email address is available or not.
*
* @param {string} email - The email address to check.
* @returns {Promise<boolean>} - A promise that resolves with a boolean value indicating whether the email address is available or not.
* @throws {FirebaseError} - If an error occurs while checking the email availability.
*/
isEmailAvailable(email) {
return __awaiter(this, void 0, void 0, function* () {
this._verifyInit();
return (0, auth_2.isEmailAvailable)(this.admin.auth, email);
});
}
/**
* Fetches the list of providers associated with the given user's email address.
*
* This method retrieves the list of providers associated with the given user's email address
* from the admin Firebase project. The list of providers includes the authentication methods
* that the user has used to sign in with their email address.
*
* @param {string} email - The email address of the user.
* @returns {Promise<string[]>} - A promise that resolves with an array of provider IDs.
* @throws {FirebaseError} - If an error occurs while fetching the email authentication methods.
*/
fetchEmailAuthMethods(email) {
return __awaiter(this, void 0, void 0, function* () {
this._verifyInit();
return (0, auth_2.fetchEmailAuthMethods)(this.admin.auth, email);
});
}
/**
* Checks if the given email address belongs to a user in the ROAR authentication system.
*
* This method checks if the given email address is associated with a user in the admin Firebase project.
* It returns a boolean value indicating whether the email address belongs to a ROAR user or not.
*
* @param {string} email - The email address to check.
* @returns {boolean} - A boolean value indicating whether the email address belongs to a ROAR user or not.
*/
isRoarAuthEmail(email) {
this._verifyInit();
return (0, auth_2.isRoarAuthEmail)(email);
}
/**
* Registers a new user with the provided email and password.
*
* This method creates a new user in both the admin and assessment Firebase projects.
* It first creates the user in the admin project and then in the assessment project.
* After successful user creation, it sets the UID custom claims by calling the `_setUidCustomClaims` method.
*
* @param {object} params - The parameters for registering a new user.
* @param {string} params.email - The email address of the new user.
* @param {string} params.password - The password of the new user.
* @returns {Promise<void>} - A promise that resolves when the user registration is complete.
* @throws {AuthError} - If the user registration fails, the promise will be rejected with an AuthError.
*/
registerWithEmailAndPassword({ email, password }) {
return __awaiter(this, void 0, void 0, function* () {
this._verifyInit();
return (0, auth_1.createUserWithEmailAndPassword)(this.admin.auth, email, password)
.catch((error) => {
console.log('Error creating user', error);
console.log(error.code);
console.log(error.message);
throw error;
})
.then(() => {
this._identityProviderType = AuthProviderType.EMAIL;
return (0, auth_1.createUserWithEmailAndPassword)(this.app.auth, email, password).then(() => {
return this._setUidCustomClaims();
});
});
});
}
/**
* Initiates a login process using an email and password.
*
* This method signs in the user with the provided email and password in both the admin and assessment Firebase projects.
* It first signs in the user in the admin project and then in the assessment project. After successful sign-in, it sets
* the UID custom claims by calling the `_setUidCustomClaims` method.
*
* @param {object} params - The parameters for initiating the login process.
* @param {string} params.email - The email address of the user.
* @param {string} params.password - The password of the user.
* @returns {Promise<void>} - A promise that resolves when the login process is complete.
* @throws {AuthError} - If the login process fails, the promise will be rejected with an AuthError.
*/
logInWithEmailAndPassword({ email, password }) {
return __awaiter(this, void 0, void 0, function* () {
this._verifyInit();
return (0, auth_1.signInWithEmailAndPassword)(this.admin.auth, email, password)
.then((adminUserCredential) => __awaiter(this, void 0, void 0, function* () {
this._identityProviderType = AuthProviderType.EMAIL;
const roarProviderIds = this._getProviderIds();
const roarAdminProvider = new auth_1.OAuthProvider(roarProviderIds.ROAR_ADMIN_PROJECT);
const roarAdminIdToken = yield (0, auth_1.getIdToken)(adminUserCredential.user);
const roarAdminCredential = roarAdminProvider.credential({
idToken: roarAdminIdToken,
});
return (0, auth_1.signInWithCredential)(this.app.auth, roarAdminCredential);
}))
.then(() => {
return this._setUidCustomClaims();
})
.catch((error) => {
console.error('Error signing in', error);
throw error;
});
});
}
/**
* Initiates a login process using a username and password.
*
* This method constructs an email address from the provided username using the
* roarEmail() function and then calls the logInWithEmailAndPassword() method with
* the constructed email address and the provided password.
*
* @param {object} params - The parameters for initiating the login process.
* @param {string} params.username - The username to use for the login process.
* @param {string} params.password - The password to use for the login process.
* @returns {Promise<void>} - A promise that resolves when the login process is complete.
*/
logInWithUsernameAndPassword({ username, password }) {
return __awaiter(this, void 0, void 0, function* () {
const email = (0, auth_2.roarEmail)(username);
return this.logInWithEmailAndPassword({ email, password });
});
}
/**
* Link the current user with email and password credentials.
*
* This method creates a credential using the provided email and password, and then links the user's account with the current user in both the admin and app Firebase projects.
*
* @param {string} email - The email of the user to link.
* @param {string} password - The password of the user to link.
*
* @returns {Promise<void>} - A promise that resolves when the user is successfully linked with the specified authentication provider.
*/
linkEmailPasswordWithAuthProvider(email, password) {
return __awaiter(this, void 0, void 0, function* () {
this._verifyAuthentication();
const emailCredential = auth_1.EmailAuthProvider.credential(email, password);
return (0, auth_1.linkWithCredential)(this.admin.auth.currentUser, emailCredential).then(() => {
return (0, auth_1.linkWithCredential)(this.app.auth.currentUser, emailCredential);
});
});
}
/**
* Initiates the login process with an email link.
*
* This method sends a sign-in link to the specified email address. The user
* can click on the link to sign in to their account. The sign-in process is
* handled in a separate browser window or tab.
*
* @param {object} params - The parameters for initiating the login process.
* @param {string} params.email - The email address to send the sign-in link to.
* @param {string} params.redirectUrl - The URL to redirect the user to after they click on the sign-in link.
* @returns {Promise<void>} - A promise that resolves when the sign-in link is sent successfully.
*/
initiateLoginWithEmailLink({ email, redirectUrl }) {
return __awaiter(this, void 0, void 0, function* () {
this._verifyInit();
const actionCodeSettings = {
url: redirectUrl,
handleCodeInApp: true,
};
return (0, auth_1.sendSignInLinkToEmail)(this.admin.auth, email, actionCodeSettings);
});
}
/**
* Check if the given email link is a sign-in with email link.
*
* This method checks if the given email link is a valid sign-in with email link
* for the admin Firebase project. It returns a promise that resolves with a boolean
* value indicating whether the email link is valid or not.
*
* @param {string} emailLink - The email link to check.
* @returns {Promise<boolean>} - A promise that resolves with a boolean value indicating whether the email link is valid or not.
*/
isSignInWithEmailLink(emailLink) {
return __awaiter(this, void 0, void 0, function* () {
this._verifyInit();
return (0, auth_1.isSignInWithEmailLink)(this.admin.auth, emailLink);
});
}
signInWithEmailLink({ email, emailLink }) {
return __awaiter(this, void 0, void 0, function* () {
this._verifyInit();
return (0, auth_1.signInWithEmailLink)(this.admin.auth, email, emailLink)
.then((userCredential) => __awaiter(this, void 0, void 0, function* () {
this._identityProviderType = AuthProviderType.EMAIL;
const roarProviderIds = this._getProviderIds();
const roarAdminProvider = new auth_1.OAuthProvider(roarProviderIds.ROAR_ADMIN_PROJECT);
const roarAdminIdToken = yield (0, auth_1.getIdToken)(userCredential.user);
const roarAdminCredential = roarAdminProvider.credential({
idToken: roarAdminIdToken,
});
return roarAdminCredential;
}))
.then((credential) => {
if (credential) {
return (0, auth_1.signInWithCredential)(this.app.auth, credential);
}
})
.then((credential) => {
if (credential) {
return this._setUidCustomClaims();
}
});
});
}
/**
* Handle the sign-in process in a popup window.
*
* This method handles the sign-in process in a popup window from from an
* external identity provider. It retrieves the user's credentials from the
* popup result and authenticates the user to the admin Firebase project
* using these credentials.
*
* The identity provider token is generally mean to be one-time use only.
* Because of this, the external identity provider's credential cannot be
* reused in the assessment project. To authenticate into the assessment
* project, we ask the admin Firebase project itself to mint a new credential
* for the assessment project. Thus, the external identity providers are used
* only in the admin Firebase project. And the admin Firebase project acts as
* an "external" identity provider for the assessment project.
*
* Therefore, the workflow for this method is as follows:
* 1. Authenticate into the external provider using a popup window.
* 2. Retrieve the external identity provider's credential from the popup result.
* 3. Authenticate into the admin Firebase project with this credential.
* 4. Generate a new "external" credential from the admin Firebase project.
* 5. Authenticate into the assessment Firebase project with the admin project's "external" credential.
* 6. Set UID custom claims by calling setUidCustomClaims().
* 7. Sync Clever/Classlink user data by calling syncEduSSOUser().
*
* @param {AuthProviderType} provider - The authentication provider to use. It can be one of the following:
* - AuthProviderType.GOOGLE
* - AuthProviderType.CLEVER
* - AuthProviderType.CLASSLINK
*
* @returns {Promise<UserCredential | null>} - A promise that resolves with the user's credential or null.
*/
signInWithPopup(provider) {
return __awaiter(this, void 0, void 0, function* () {
this._verifyInit();
const allowedProviders = [AuthProviderType.GOOGLE, AuthProviderType.CLEVER, AuthProviderType.CLASSLINK];
let authProvider;
if (provider === AuthProviderType.GOOGLE) {
authProvider = new auth_1.GoogleAuthProvider();
}
else if (provider === AuthProviderType.CLEVER) {
const roarProviderIds = this._getProviderIds();
authProvider = new auth_1.OAuthProvider(roarProviderIds.CLEVER);
}
else if (provider === AuthProviderType.CLASSLINK) {
const roarProviderIds = this._getProviderIds();
authProvider = new auth_1.OAuthProvider(roarProviderIds.CLASSLINK);
}
else {
throw new Error(`provider must be one of ${allowedProviders.join(', ')}. Received ${provider} instead.`);
}
const allowedErrors = ['auth/cancelled-popup-request', 'auth/popup-closed-by-user'];
const swallowAllowedErrors = (error) => {
if (!allowedErrors.includes(error.code)) {
throw error;
}
};
let oAuthAccessToken;
return (0, auth_1.signInWithPopup)(this.admin.auth, authProvider)
.then((adminUserCredential) => __awaiter(this, void 0, void 0, function* () {
var _a;
this._identityProviderType = provider;
const roarProviderIds = this._getProviderIds();
if (provider === AuthProviderType.GOOGLE) {
const credential = auth_1.GoogleAuthProvider.credentialFromResult(adminUserCredential);
// This gives you a Google Access Token. You can use it to access Google APIs.
// TODO: Find a way to put this in the onAuthStateChanged handler
oAuthAccessToken = credential === null || credential === void 0 ? void 0 : credential.accessToken;
return credential;
}
else if ([AuthProviderType.CLEVER, AuthProviderType.CLASSLINK].includes(provider)) {
const credential = auth_1.OAuthProvider.credentialFromResult(adminUserCredential);
// This gives you a Clever/Classlink Access Token. You can use it to access Clever/Classlink APIs.
oAuthAccessToken = credential === null || credential === void 0 ? void 0 : credential.accessToken;
let providerId;
if (provider === AuthProviderType.CLEVER) {
providerId = roarProviderIds.CLEVER;
}
else if (provider === AuthProviderType.CLASSLINK) {
providerId = roarProviderIds.CLASSLINK;
}
this._identityProviderId = (_a = adminUserCredential.user.providerData.find((userInfo) => userInfo.providerId === providerId)) === null || _a === void 0 ? void 0 : _a.uid;
const roarAdminProvider = new auth_1.OAuthProvider(roarProviderIds.ROAR_ADMIN_PROJECT);
const roarAdminIdToken = yield (0, auth_1.getIdToken)(adminUserCredential.user);
const roarAdminCredential = roarAdminProvider.credential({
idToken: roarAdminIdToken,
});
return roarAdminCredential;
}
}))
.catch(swallowAllowedErrors)
.then((credential) => {
if (credential) {
return (0, auth_1.signInWithCredential)(this.app.auth, credential).catch(swallowAllowedErrors);
}
})
.then((credential) => {
if (credential) {
const claimsParams = {
identityProviderId: this._identityProviderId,
identityProviderType: this._identityProviderType,
};
return this._setUidCustomClaims(claimsParams);
}
})
.then((setClaimsResult) => {
if (setClaimsResult) {
this._syncEduSSOUser(oAuthAccessToken, provider);
}
});
});
}
/**
* Link the current user with the specified authentication provider using a popup window.
*
* This method opens a popup window to allow the user to sign in with the specified authentication provider.
* It then links the user's account with the current user in both the admin and app Firebase projects.
*
* @param {AuthProviderType} provider - The authentication provider to link with. It can be one of the following:
* - AuthProviderType.GOOGLE
* - AuthProviderType.CLEVER
* - AuthProviderType.CLASSLINK
*
* @returns {Promise<void>} - A promise that resolves when the user is successfully linked with the specified authentication provider.
*
* @throws {Error} - If the specified provider is not one of the allowed providers, an error is thrown.
*/
linkAuthProviderWithPopup(provider) {
return __awaiter(this, void 0, void 0, function* () {
this._verifyAuthentication();
const allowedProviders = [AuthProviderType.GOOGLE, AuthProviderType.CLEVER, AuthProviderType.CLASSLINK];
const roarProviderIds = this._getProviderIds();
let authProvider;
if (provider === AuthProviderType.GOOGLE) {
authProvider = new auth_1.GoogleAuthProvider();
}
else if (provider === AuthProviderType.CLEVER) {
authProvider = new auth_1.OAuthProvider(roarProviderIds.CLEVER);
}
else if (provider === AuthProviderType.CLASSLINK) {
authProvider = new auth_1.OAuthProvider(roarProviderIds.CLASSLINK);
}
else {
throw new Error(`provider must be one of ${allowedProviders.join(', ')}. Received ${provider} instead.`);
}
const allowedErrors = ['auth/cancelled-popup-request', 'auth/popup-closed-by-user'];
const swallowAllowedErrors = (error) => {
if (!allowedErrors.includes(error.code)) {
throw error;
}
};
let oAuthAccessToken;
return (0, auth_1.linkWithPopup)(this.admin.auth.currentUser, authProvider)
.then((adminUserCredential) => __awaiter(this, void 0, void 0, function* () {
var _a;
this._identityProviderType = provider;
if (provider === AuthProviderType.GOOGLE) {
const credential = auth_1.GoogleAuthProvider.credentialFromResult(adminUserCredential);
// This gives you a Google Access Token. You can use it to access Google APIs.
// TODO: Find a way to put this in the onAuthStateChanged handler
oAuthAccessToken = credential === null || credential === void 0 ? void 0 : credential.accessToken;
return credential;
}
else if ([AuthProviderType.CLEVER, AuthProviderType.CLASSLINK].includes(provider)) {
const credential = auth_1.OAuthProvider.credentialFromResult(adminUserCredential);
// This gives you a Clever/Classlink Access Token. You can use it to access Clever/Classlink APIs.
oAuthAccessToken = credential === null || credential === void 0 ? void 0 : credential.accessToken;
const roarProviderIds = this._getProviderIds();
let providerId;
if (provider === AuthProviderType.CLEVER) {
providerId = roarProviderIds.CLEVER;
}
else if (provider === AuthProviderType.CLASSLINK) {
providerId = roarProviderIds.CLASSLINK;
}
this._identityProviderId = (_a = adminUserCredential.user.providerData.find((userInfo) => userInfo.providerId === providerId)) === null || _a === void 0 ? void 0 : _a.uid;
const roarAdminProvider = new auth_1.OAuthProvider(roarProviderIds.ROAR_ADMIN_PROJECT);
const roarAdminIdToken = yield (0, auth_1.getIdToken)(adminUserCredential.user);
const roarAdminCredential = roarAdminProvider.credential({
idToken: roarAdminIdToken,
});
return roarAdminCredential;
}
}))
.catch(swallowAllowedErrors)
.then((credential) => {
if (credential) {
return (0, auth_1.linkWithCredential)(this.app.auth.currentUser, credential).catch(swallowAllowedErrors);
}
})
.then((credential) => {
if (credential) {
const claimsParams = {
identityProviderId: this._identityProviderId,
identityProviderType: this._identityProviderType,
};
return this._setUidCustomClaims(claimsParams);
}
})
.then((setClaimsResult) => {
if (setClaimsResult) {
this._syncEduSSOUser(oAuthAccessToken, provider);
}
});
});
}
/**
* Initiates a redirect sign-in flow with the specified authentication provider.
*
* This method triggers a redirect to the authentication provider's sign-in page.
* After the user successfully signs in, they will be redirected back to the application.
*
* If the linkToAuthenticatedUser parameter is set to true, an existing user
* must already be authenticated and the user's account will be linked with
* the new provider.
*
* @param {AuthProviderType} provider - The authentication provider to initiate the sign-in flow with.
* It can be one of the following: AuthProviderType.GOOGLE, AuthProviderType.CLEVER, AuthProviderType.CLASSLINK.
* @param {boolean} linkToAuthenticatedUser - Whether to link an authenticated user's account with the new provider.
*
* @returns {Promise<void>} - A promise that resolves when the redirect sign-in flow is initiated.
* @throws {Error} - If the specified provider is not one of the allowed providers, an error is thrown.
*/
initiateRedirect(provider, linkToAuthenticatedUser = false) {
return __awaiter(this, void 0, void 0, function* () {
this.verboseLog('Entry point for initiateRedirect');
this._verifyInit();
if (linkToAuthenticatedUser) {
this._verifyAuthentication();
}
const allowedProviders = [AuthProviderType.GOOGLE, AuthProviderType.CLEVER, AuthProviderType.CLASSLINK];
let authProvider;
this.verboseLog('Attempting sign in with AuthProvider', provider);
if (provider === AuthProviderType.GOOGLE) {
authProvider = new auth_1.GoogleAuthProvider();
this.verboseLog('Google AuthProvider object:', authProvider);
}
else if (provider === AuthProviderType.CLEVER) {
const roarProviderIds = this._getProviderIds();
this.verboseLog('Clever roarProviderIds', roarProviderIds);
authProvider = new auth_1.OAuthProvider(roarProviderIds.CLEVER);
this.verboseLog('Clever AuthProvider object:', authProvider);
}
else if (provider === AuthProviderType.CLASSLINK) {
const roarProviderIds = this._getProviderIds();
this.verboseLog('Classlink roarProviderIds', roarProviderIds);
// Use the partner-initiated flow for Classlink
authProvider = new auth_1.OAuthProvider(roarProviderIds.CLASSLINK);
this.verboseLog('Classlink AuthProvider object:', authProvider);
}
else {
throw new Error(`provider must be one of ${allowedProviders.join(', ')}. Received ${provider} instead.`);
}
this.verboseLog('Calling signInWithRedirect from initiateRedirect with provider', authProvider);
if (linkToAuthenticatedUser) {
return (0, auth_1.linkWithRedirect)(this.admin.auth.currentUser, authProvider);
}
return (0, auth_1.signInWithRedirect)(this.admin.auth, authProvider);
});
}
/**
* Handle the sign-in process from a redirect result.
*
* This method handles the sign-in process after a user has been redirected
* from an external identity provider. It retrieves the user's credentials
* from the redirect result and authenticates the user to the admin Firebase
* project using the credentials.
*
* The identity provider token is generally mean to be one-time use only.
* Because of this, the external identity provider's credential cannot be
* reused in the assessment project. To authenticate into the assessment
* project, we ask the admin Firebase project itself to mint a new credential
* for the assessment project. Thus, the external identity providers are used
* only in the admin Fi