UNPKG

@bdelab/roar-firekit

Version:

A library to facilitate Firebase authentication and Cloud Firestore interaction for ROAR apps

915 lines 134 kB
"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