UNPKG

firebase-admin

Version:
1,081 lines 90.7 kB
/*! firebase-admin v13.2.0 */ "use strict"; /*! * @license * Copyright 2017 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ Object.defineProperty(exports, "__esModule", { value: true }); exports.TenantAwareAuthRequestHandler = exports.AuthRequestHandler = exports.AbstractAuthRequestHandler = exports.FIREBASE_AUTH_SIGN_UP_NEW_USER = exports.FIREBASE_AUTH_SET_ACCOUNT_INFO = exports.FIREBASE_AUTH_BATCH_DELETE_ACCOUNTS = exports.FIREBASE_AUTH_DELETE_ACCOUNT = exports.FIREBASE_AUTH_GET_ACCOUNTS_INFO = exports.FIREBASE_AUTH_GET_ACCOUNT_INFO = exports.FIREBASE_AUTH_DOWNLOAD_ACCOUNT = exports.FIREBASE_AUTH_UPLOAD_ACCOUNT = exports.FIREBASE_AUTH_CREATE_SESSION_COOKIE = exports.EMAIL_ACTION_REQUEST_TYPES = exports.RESERVED_CLAIMS = void 0; exports.useEmulator = useEmulator; const validator = require("../utils/validator"); const deep_copy_1 = require("../utils/deep-copy"); const error_1 = require("../utils/error"); const api_request_1 = require("../utils/api-request"); const utils = require("../utils/index"); const user_import_builder_1 = require("./user-import-builder"); const action_code_settings_builder_1 = require("./action-code-settings-builder"); const tenant_1 = require("./tenant"); const identifier_1 = require("./identifier"); const auth_config_1 = require("./auth-config"); const project_config_1 = require("./project-config"); /** Firebase Auth request header. */ const FIREBASE_AUTH_HEADERS = { 'X-Client-Version': `Node/Admin/${utils.getSdkVersion()}`, }; /** Firebase Auth request timeout duration in milliseconds. */ const FIREBASE_AUTH_TIMEOUT = 25000; /** List of reserved claims which cannot be provided when creating a custom token. */ exports.RESERVED_CLAIMS = [ 'acr', 'amr', 'at_hash', 'aud', 'auth_time', 'azp', 'cnf', 'c_hash', 'exp', 'iat', 'iss', 'jti', 'nbf', 'nonce', 'sub', 'firebase', ]; /** List of supported email action request types. */ exports.EMAIL_ACTION_REQUEST_TYPES = [ 'PASSWORD_RESET', 'VERIFY_EMAIL', 'EMAIL_SIGNIN', 'VERIFY_AND_CHANGE_EMAIL', ]; /** Maximum allowed number of characters in the custom claims payload. */ const MAX_CLAIMS_PAYLOAD_SIZE = 1000; /** Maximum allowed number of users to batch download at one time. */ const MAX_DOWNLOAD_ACCOUNT_PAGE_SIZE = 1000; /** Maximum allowed number of users to batch upload at one time. */ const MAX_UPLOAD_ACCOUNT_BATCH_SIZE = 1000; /** Maximum allowed number of users to batch get at one time. */ const MAX_GET_ACCOUNTS_BATCH_SIZE = 100; /** Maximum allowed number of users to batch delete at one time. */ const MAX_DELETE_ACCOUNTS_BATCH_SIZE = 1000; /** Minimum allowed session cookie duration in seconds (5 minutes). */ const MIN_SESSION_COOKIE_DURATION_SECS = 5 * 60; /** Maximum allowed session cookie duration in seconds (2 weeks). */ const MAX_SESSION_COOKIE_DURATION_SECS = 14 * 24 * 60 * 60; /** Maximum allowed number of provider configurations to batch download at one time. */ const MAX_LIST_PROVIDER_CONFIGURATION_PAGE_SIZE = 100; /** The Firebase Auth backend base URL format. */ const FIREBASE_AUTH_BASE_URL_FORMAT = 'https://identitytoolkit.googleapis.com/{version}/projects/{projectId}{api}'; /** Firebase Auth base URlLformat when using the auth emultor. */ const FIREBASE_AUTH_EMULATOR_BASE_URL_FORMAT = 'http://{host}/identitytoolkit.googleapis.com/{version}/projects/{projectId}{api}'; /** The Firebase Auth backend multi-tenancy base URL format. */ const FIREBASE_AUTH_TENANT_URL_FORMAT = FIREBASE_AUTH_BASE_URL_FORMAT.replace('projects/{projectId}', 'projects/{projectId}/tenants/{tenantId}'); /** Firebase Auth base URL format when using the auth emultor with multi-tenancy. */ const FIREBASE_AUTH_EMULATOR_TENANT_URL_FORMAT = FIREBASE_AUTH_EMULATOR_BASE_URL_FORMAT.replace('projects/{projectId}', 'projects/{projectId}/tenants/{tenantId}'); /** Maximum allowed number of tenants to download at one time. */ const MAX_LIST_TENANT_PAGE_SIZE = 1000; /** * Enum for the user write operation type. */ var WriteOperationType; (function (WriteOperationType) { WriteOperationType["Create"] = "create"; WriteOperationType["Update"] = "update"; WriteOperationType["Upload"] = "upload"; })(WriteOperationType || (WriteOperationType = {})); /** Defines a base utility to help with resource URL construction. */ class AuthResourceUrlBuilder { /** * The resource URL builder constructor. * * @param projectId - The resource project ID. * @param version - The endpoint API version. * @constructor */ constructor(app, version = 'v1') { this.app = app; this.version = version; if (useEmulator()) { this.urlFormat = utils.formatString(FIREBASE_AUTH_EMULATOR_BASE_URL_FORMAT, { host: emulatorHost() }); } else { this.urlFormat = FIREBASE_AUTH_BASE_URL_FORMAT; } } /** * Returns the resource URL corresponding to the provided parameters. * * @param api - The backend API name. * @param params - The optional additional parameters to substitute in the * URL path. * @returns The corresponding resource URL. */ getUrl(api, params) { return this.getProjectId() .then((projectId) => { const baseParams = { version: this.version, projectId, api: api || '', }; const baseUrl = utils.formatString(this.urlFormat, baseParams); // Substitute additional api related parameters. return utils.formatString(baseUrl, params || {}); }); } getProjectId() { if (this.projectId) { return Promise.resolve(this.projectId); } return utils.findProjectId(this.app) .then((projectId) => { if (!validator.isNonEmptyString(projectId)) { throw new error_1.FirebaseAuthError(error_1.AuthClientErrorCode.INVALID_CREDENTIAL, 'Failed to determine project ID for Auth. Initialize the ' + 'SDK with service account credentials or set project ID as an app option. ' + 'Alternatively set the GOOGLE_CLOUD_PROJECT environment variable.'); } this.projectId = projectId; return projectId; }); } } /** Tenant aware resource builder utility. */ class TenantAwareAuthResourceUrlBuilder extends AuthResourceUrlBuilder { /** * The tenant aware resource URL builder constructor. * * @param projectId - The resource project ID. * @param version - The endpoint API version. * @param tenantId - The tenant ID. * @constructor */ constructor(app, version, tenantId) { super(app, version); this.app = app; this.version = version; this.tenantId = tenantId; if (useEmulator()) { this.urlFormat = utils.formatString(FIREBASE_AUTH_EMULATOR_TENANT_URL_FORMAT, { host: emulatorHost() }); } else { this.urlFormat = FIREBASE_AUTH_TENANT_URL_FORMAT; } } /** * Returns the resource URL corresponding to the provided parameters. * * @param api - The backend API name. * @param params - The optional additional parameters to substitute in the * URL path. * @returns The corresponding resource URL. */ getUrl(api, params) { return super.getUrl(api, params) .then((url) => { return utils.formatString(url, { tenantId: this.tenantId }); }); } } /** * Auth-specific HTTP client which uses the special "owner" token * when communicating with the Auth Emulator. */ class AuthHttpClient extends api_request_1.AuthorizedHttpClient { getToken() { if (useEmulator()) { return Promise.resolve('owner'); } return super.getToken(); } } /** * Validates an AuthFactorInfo object. All unsupported parameters * are removed from the original request. If an invalid field is passed * an error is thrown. * * @param request - The AuthFactorInfo request object. */ function validateAuthFactorInfo(request) { const validKeys = { mfaEnrollmentId: true, displayName: true, phoneInfo: true, enrolledAt: true, }; // Remove unsupported keys from the original request. for (const key in request) { if (!(key in validKeys)) { delete request[key]; } } // No enrollment ID is available for signupNewUser. Use another identifier. const authFactorInfoIdentifier = request.mfaEnrollmentId || request.phoneInfo || JSON.stringify(request); // Enrollment uid may or may not be specified for update operations. if (typeof request.mfaEnrollmentId !== 'undefined' && !validator.isNonEmptyString(request.mfaEnrollmentId)) { throw new error_1.FirebaseAuthError(error_1.AuthClientErrorCode.INVALID_UID, 'The second factor "uid" must be a valid non-empty string.'); } if (typeof request.displayName !== 'undefined' && !validator.isString(request.displayName)) { throw new error_1.FirebaseAuthError(error_1.AuthClientErrorCode.INVALID_DISPLAY_NAME, `The second factor "displayName" for "${authFactorInfoIdentifier}" must be a valid string.`); } // enrolledAt must be a valid UTC date string. if (typeof request.enrolledAt !== 'undefined' && !validator.isISODateString(request.enrolledAt)) { throw new error_1.FirebaseAuthError(error_1.AuthClientErrorCode.INVALID_ENROLLMENT_TIME, `The second factor "enrollmentTime" for "${authFactorInfoIdentifier}" must be a valid ` + 'UTC date string.'); } // Validate required fields depending on second factor type. if (typeof request.phoneInfo !== 'undefined') { // phoneNumber should be a string and a valid phone number. if (!validator.isPhoneNumber(request.phoneInfo)) { throw new error_1.FirebaseAuthError(error_1.AuthClientErrorCode.INVALID_PHONE_NUMBER, `The second factor "phoneNumber" for "${authFactorInfoIdentifier}" must be a non-empty ` + 'E.164 standard compliant identifier string.'); } } else { // Invalid second factor. For example, a phone second factor may have been provided without // a phone number. A TOTP based second factor may require a secret key, etc. throw new error_1.FirebaseAuthError(error_1.AuthClientErrorCode.INVALID_ENROLLED_FACTORS, 'MFAInfo object provided is invalid.'); } } /** * Validates a providerUserInfo object. All unsupported parameters * are removed from the original request. If an invalid field is passed * an error is thrown. * * @param request - The providerUserInfo request object. */ function validateProviderUserInfo(request) { const validKeys = { rawId: true, providerId: true, email: true, displayName: true, photoUrl: true, }; // Remove invalid keys from original request. for (const key in request) { if (!(key in validKeys)) { delete request[key]; } } if (!validator.isNonEmptyString(request.providerId)) { throw new error_1.FirebaseAuthError(error_1.AuthClientErrorCode.INVALID_PROVIDER_ID); } if (typeof request.displayName !== 'undefined' && typeof request.displayName !== 'string') { throw new error_1.FirebaseAuthError(error_1.AuthClientErrorCode.INVALID_DISPLAY_NAME, `The provider "displayName" for "${request.providerId}" must be a valid string.`); } if (!validator.isNonEmptyString(request.rawId)) { // This is called localId on the backend but the developer specifies this as // uid externally. So the error message should use the client facing name. throw new error_1.FirebaseAuthError(error_1.AuthClientErrorCode.INVALID_UID, `The provider "uid" for "${request.providerId}" must be a valid non-empty string.`); } // email should be a string and a valid email. if (typeof request.email !== 'undefined' && !validator.isEmail(request.email)) { throw new error_1.FirebaseAuthError(error_1.AuthClientErrorCode.INVALID_EMAIL, `The provider "email" for "${request.providerId}" must be a valid email string.`); } // photoUrl should be a URL. if (typeof request.photoUrl !== 'undefined' && !validator.isURL(request.photoUrl)) { // This is called photoUrl on the backend but the developer specifies this as // photoURL externally. So the error message should use the client facing name. throw new error_1.FirebaseAuthError(error_1.AuthClientErrorCode.INVALID_PHOTO_URL, `The provider "photoURL" for "${request.providerId}" must be a valid URL string.`); } } /** * Validates a create/edit request object. All unsupported parameters * are removed from the original request. If an invalid field is passed * an error is thrown. * * @param request - The create/edit request object. * @param writeOperationType - The write operation type. */ function validateCreateEditRequest(request, writeOperationType) { const uploadAccountRequest = writeOperationType === WriteOperationType.Upload; // Hash set of whitelisted parameters. const validKeys = { displayName: true, localId: true, email: true, password: true, rawPassword: true, emailVerified: true, photoUrl: true, disabled: true, disableUser: true, deleteAttribute: true, deleteProvider: true, sanityCheck: true, phoneNumber: true, customAttributes: true, validSince: true, // Pass linkProviderUserInfo only for updates (i.e. not for uploads.) linkProviderUserInfo: !uploadAccountRequest, // Pass tenantId only for uploadAccount requests. tenantId: uploadAccountRequest, passwordHash: uploadAccountRequest, salt: uploadAccountRequest, createdAt: uploadAccountRequest, lastLoginAt: uploadAccountRequest, providerUserInfo: uploadAccountRequest, mfaInfo: uploadAccountRequest, // Only for non-uploadAccount requests. mfa: !uploadAccountRequest, }; // Remove invalid keys from original request. for (const key in request) { if (!(key in validKeys)) { delete request[key]; } } if (typeof request.tenantId !== 'undefined' && !validator.isNonEmptyString(request.tenantId)) { throw new error_1.FirebaseAuthError(error_1.AuthClientErrorCode.INVALID_TENANT_ID); } // For any invalid parameter, use the external key name in the error description. // displayName should be a string. if (typeof request.displayName !== 'undefined' && !validator.isString(request.displayName)) { throw new error_1.FirebaseAuthError(error_1.AuthClientErrorCode.INVALID_DISPLAY_NAME); } if ((typeof request.localId !== 'undefined' || uploadAccountRequest) && !validator.isUid(request.localId)) { // This is called localId on the backend but the developer specifies this as // uid externally. So the error message should use the client facing name. throw new error_1.FirebaseAuthError(error_1.AuthClientErrorCode.INVALID_UID); } // email should be a string and a valid email. if (typeof request.email !== 'undefined' && !validator.isEmail(request.email)) { throw new error_1.FirebaseAuthError(error_1.AuthClientErrorCode.INVALID_EMAIL); } // phoneNumber should be a string and a valid phone number. if (typeof request.phoneNumber !== 'undefined' && !validator.isPhoneNumber(request.phoneNumber)) { throw new error_1.FirebaseAuthError(error_1.AuthClientErrorCode.INVALID_PHONE_NUMBER); } // password should be a string and a minimum of 6 chars. if (typeof request.password !== 'undefined' && !validator.isPassword(request.password)) { throw new error_1.FirebaseAuthError(error_1.AuthClientErrorCode.INVALID_PASSWORD); } // rawPassword should be a string and a minimum of 6 chars. if (typeof request.rawPassword !== 'undefined' && !validator.isPassword(request.rawPassword)) { // This is called rawPassword on the backend but the developer specifies this as // password externally. So the error message should use the client facing name. throw new error_1.FirebaseAuthError(error_1.AuthClientErrorCode.INVALID_PASSWORD); } // emailVerified should be a boolean. if (typeof request.emailVerified !== 'undefined' && typeof request.emailVerified !== 'boolean') { throw new error_1.FirebaseAuthError(error_1.AuthClientErrorCode.INVALID_EMAIL_VERIFIED); } // photoUrl should be a URL. if (typeof request.photoUrl !== 'undefined' && !validator.isURL(request.photoUrl)) { // This is called photoUrl on the backend but the developer specifies this as // photoURL externally. So the error message should use the client facing name. throw new error_1.FirebaseAuthError(error_1.AuthClientErrorCode.INVALID_PHOTO_URL); } // disabled should be a boolean. if (typeof request.disabled !== 'undefined' && typeof request.disabled !== 'boolean') { throw new error_1.FirebaseAuthError(error_1.AuthClientErrorCode.INVALID_DISABLED_FIELD); } // validSince should be a number. if (typeof request.validSince !== 'undefined' && !validator.isNumber(request.validSince)) { throw new error_1.FirebaseAuthError(error_1.AuthClientErrorCode.INVALID_TOKENS_VALID_AFTER_TIME); } // createdAt should be a number. if (typeof request.createdAt !== 'undefined' && !validator.isNumber(request.createdAt)) { throw new error_1.FirebaseAuthError(error_1.AuthClientErrorCode.INVALID_CREATION_TIME); } // lastSignInAt should be a number. if (typeof request.lastLoginAt !== 'undefined' && !validator.isNumber(request.lastLoginAt)) { throw new error_1.FirebaseAuthError(error_1.AuthClientErrorCode.INVALID_LAST_SIGN_IN_TIME); } // disableUser should be a boolean. if (typeof request.disableUser !== 'undefined' && typeof request.disableUser !== 'boolean') { // This is called disableUser on the backend but the developer specifies this as // disabled externally. So the error message should use the client facing name. throw new error_1.FirebaseAuthError(error_1.AuthClientErrorCode.INVALID_DISABLED_FIELD); } // customAttributes should be stringified JSON with no blacklisted claims. // The payload should not exceed 1KB. if (typeof request.customAttributes !== 'undefined') { let developerClaims; try { developerClaims = JSON.parse(request.customAttributes); } catch (error) { // JSON parsing error. This should never happen as we stringify the claims internally. // However, we still need to check since setAccountInfo via edit requests could pass // this field. throw new error_1.FirebaseAuthError(error_1.AuthClientErrorCode.INVALID_CLAIMS, error.message); } const invalidClaims = []; // Check for any invalid claims. exports.RESERVED_CLAIMS.forEach((blacklistedClaim) => { if (Object.prototype.hasOwnProperty.call(developerClaims, blacklistedClaim)) { invalidClaims.push(blacklistedClaim); } }); // Throw an error if an invalid claim is detected. if (invalidClaims.length > 0) { throw new error_1.FirebaseAuthError(error_1.AuthClientErrorCode.FORBIDDEN_CLAIM, invalidClaims.length > 1 ? `Developer claims "${invalidClaims.join('", "')}" are reserved and cannot be specified.` : `Developer claim "${invalidClaims[0]}" is reserved and cannot be specified.`); } // Check claims payload does not exceed maxmimum size. if (request.customAttributes.length > MAX_CLAIMS_PAYLOAD_SIZE) { throw new error_1.FirebaseAuthError(error_1.AuthClientErrorCode.CLAIMS_TOO_LARGE, `Developer claims payload should not exceed ${MAX_CLAIMS_PAYLOAD_SIZE} characters.`); } } // passwordHash has to be a base64 encoded string. if (typeof request.passwordHash !== 'undefined' && !validator.isString(request.passwordHash)) { throw new error_1.FirebaseAuthError(error_1.AuthClientErrorCode.INVALID_PASSWORD_HASH); } // salt has to be a base64 encoded string. if (typeof request.salt !== 'undefined' && !validator.isString(request.salt)) { throw new error_1.FirebaseAuthError(error_1.AuthClientErrorCode.INVALID_PASSWORD_SALT); } // providerUserInfo has to be an array of valid UserInfo requests. if (typeof request.providerUserInfo !== 'undefined' && !validator.isArray(request.providerUserInfo)) { throw new error_1.FirebaseAuthError(error_1.AuthClientErrorCode.INVALID_PROVIDER_DATA); } else if (validator.isArray(request.providerUserInfo)) { request.providerUserInfo.forEach((providerUserInfoEntry) => { validateProviderUserInfo(providerUserInfoEntry); }); } // linkProviderUserInfo must be a (single) UserProvider value. if (typeof request.linkProviderUserInfo !== 'undefined') { validateProviderUserInfo(request.linkProviderUserInfo); } // mfaInfo is used for importUsers. // mfa.enrollments is used for setAccountInfo. // enrollments has to be an array of valid AuthFactorInfo requests. let enrollments = null; if (request.mfaInfo) { enrollments = request.mfaInfo; } else if (request.mfa && request.mfa.enrollments) { enrollments = request.mfa.enrollments; } if (enrollments) { if (!validator.isArray(enrollments)) { throw new error_1.FirebaseAuthError(error_1.AuthClientErrorCode.INVALID_ENROLLED_FACTORS); } enrollments.forEach((authFactorInfoEntry) => { validateAuthFactorInfo(authFactorInfoEntry); }); } } /** * Instantiates the createSessionCookie endpoint settings. * * @internal */ exports.FIREBASE_AUTH_CREATE_SESSION_COOKIE = new api_request_1.ApiSettings(':createSessionCookie', 'POST') // Set request validator. .setRequestValidator((request) => { // Validate the ID token is a non-empty string. if (!validator.isNonEmptyString(request.idToken)) { throw new error_1.FirebaseAuthError(error_1.AuthClientErrorCode.INVALID_ID_TOKEN); } // Validate the custom session cookie duration. if (!validator.isNumber(request.validDuration) || request.validDuration < MIN_SESSION_COOKIE_DURATION_SECS || request.validDuration > MAX_SESSION_COOKIE_DURATION_SECS) { throw new error_1.FirebaseAuthError(error_1.AuthClientErrorCode.INVALID_SESSION_COOKIE_DURATION); } }) // Set response validator. .setResponseValidator((response) => { // Response should always contain the session cookie. if (!validator.isNonEmptyString(response.sessionCookie)) { throw new error_1.FirebaseAuthError(error_1.AuthClientErrorCode.INTERNAL_ERROR); } }); /** * Instantiates the uploadAccount endpoint settings. * * @internal */ exports.FIREBASE_AUTH_UPLOAD_ACCOUNT = new api_request_1.ApiSettings('/accounts:batchCreate', 'POST'); /** * Instantiates the downloadAccount endpoint settings. * * @internal */ exports.FIREBASE_AUTH_DOWNLOAD_ACCOUNT = new api_request_1.ApiSettings('/accounts:batchGet', 'GET') // Set request validator. .setRequestValidator((request) => { // Validate next page token. if (typeof request.nextPageToken !== 'undefined' && !validator.isNonEmptyString(request.nextPageToken)) { throw new error_1.FirebaseAuthError(error_1.AuthClientErrorCode.INVALID_PAGE_TOKEN); } // Validate max results. if (!validator.isNumber(request.maxResults) || request.maxResults <= 0 || request.maxResults > MAX_DOWNLOAD_ACCOUNT_PAGE_SIZE) { throw new error_1.FirebaseAuthError(error_1.AuthClientErrorCode.INVALID_ARGUMENT, 'Required "maxResults" must be a positive integer that does not exceed ' + `${MAX_DOWNLOAD_ACCOUNT_PAGE_SIZE}.`); } }); /** * Instantiates the getAccountInfo endpoint settings. * * @internal */ exports.FIREBASE_AUTH_GET_ACCOUNT_INFO = new api_request_1.ApiSettings('/accounts:lookup', 'POST') // Set request validator. .setRequestValidator((request) => { if (!request.localId && !request.email && !request.phoneNumber && !request.federatedUserId) { throw new error_1.FirebaseAuthError(error_1.AuthClientErrorCode.INTERNAL_ERROR, 'INTERNAL ASSERT FAILED: Server request is missing user identifier'); } }) // Set response validator. .setResponseValidator((response) => { if (!response.users || !response.users.length) { throw new error_1.FirebaseAuthError(error_1.AuthClientErrorCode.USER_NOT_FOUND); } }); /** * Instantiates the getAccountInfo endpoint settings for use when fetching info * for multiple accounts. * * @internal */ exports.FIREBASE_AUTH_GET_ACCOUNTS_INFO = new api_request_1.ApiSettings('/accounts:lookup', 'POST') // Set request validator. .setRequestValidator((request) => { if (!request.localId && !request.email && !request.phoneNumber && !request.federatedUserId) { throw new error_1.FirebaseAuthError(error_1.AuthClientErrorCode.INTERNAL_ERROR, 'INTERNAL ASSERT FAILED: Server request is missing user identifier'); } }); /** * Instantiates the deleteAccount endpoint settings. * * @internal */ exports.FIREBASE_AUTH_DELETE_ACCOUNT = new api_request_1.ApiSettings('/accounts:delete', 'POST') // Set request validator. .setRequestValidator((request) => { if (!request.localId) { throw new error_1.FirebaseAuthError(error_1.AuthClientErrorCode.INTERNAL_ERROR, 'INTERNAL ASSERT FAILED: Server request is missing user identifier'); } }); /** * @internal */ exports.FIREBASE_AUTH_BATCH_DELETE_ACCOUNTS = new api_request_1.ApiSettings('/accounts:batchDelete', 'POST') .setRequestValidator((request) => { if (!request.localIds) { throw new error_1.FirebaseAuthError(error_1.AuthClientErrorCode.INTERNAL_ERROR, 'INTERNAL ASSERT FAILED: Server request is missing user identifiers'); } if (typeof request.force === 'undefined' || request.force !== true) { throw new error_1.FirebaseAuthError(error_1.AuthClientErrorCode.INTERNAL_ERROR, 'INTERNAL ASSERT FAILED: Server request is missing force=true field'); } }) .setResponseValidator((response) => { const errors = response.errors || []; errors.forEach((batchDeleteErrorInfo) => { if (typeof batchDeleteErrorInfo.index === 'undefined') { throw new error_1.FirebaseAuthError(error_1.AuthClientErrorCode.INTERNAL_ERROR, 'INTERNAL ASSERT FAILED: Server BatchDeleteAccountResponse is missing an errors.index field'); } if (!batchDeleteErrorInfo.localId) { throw new error_1.FirebaseAuthError(error_1.AuthClientErrorCode.INTERNAL_ERROR, 'INTERNAL ASSERT FAILED: Server BatchDeleteAccountResponse is missing an errors.localId field'); } // Allow the (error) message to be missing/undef. }); }); /** * Instantiates the setAccountInfo endpoint settings for updating existing accounts. * * @internal */ exports.FIREBASE_AUTH_SET_ACCOUNT_INFO = new api_request_1.ApiSettings('/accounts:update', 'POST') // Set request validator. .setRequestValidator((request) => { // localId is a required parameter. if (typeof request.localId === 'undefined') { throw new error_1.FirebaseAuthError(error_1.AuthClientErrorCode.INTERNAL_ERROR, 'INTERNAL ASSERT FAILED: Server request is missing user identifier'); } // Throw error when tenantId is passed in POST body. if (typeof request.tenantId !== 'undefined') { throw new error_1.FirebaseAuthError(error_1.AuthClientErrorCode.INVALID_ARGUMENT, '"tenantId" is an invalid "UpdateRequest" property.'); } validateCreateEditRequest(request, WriteOperationType.Update); }) // Set response validator. .setResponseValidator((response) => { // If the localId is not returned, then the request failed. if (!response.localId) { throw new error_1.FirebaseAuthError(error_1.AuthClientErrorCode.USER_NOT_FOUND); } }); /** * Instantiates the signupNewUser endpoint settings for creating a new user with or without * uid being specified. The backend will create a new one if not provided and return it. * * @internal */ exports.FIREBASE_AUTH_SIGN_UP_NEW_USER = new api_request_1.ApiSettings('/accounts', 'POST') // Set request validator. .setRequestValidator((request) => { // signupNewUser does not support customAttributes. if (typeof request.customAttributes !== 'undefined') { throw new error_1.FirebaseAuthError(error_1.AuthClientErrorCode.INVALID_ARGUMENT, '"customAttributes" cannot be set when creating a new user.'); } // signupNewUser does not support validSince. if (typeof request.validSince !== 'undefined') { throw new error_1.FirebaseAuthError(error_1.AuthClientErrorCode.INVALID_ARGUMENT, '"validSince" cannot be set when creating a new user.'); } // Throw error when tenantId is passed in POST body. if (typeof request.tenantId !== 'undefined') { throw new error_1.FirebaseAuthError(error_1.AuthClientErrorCode.INVALID_ARGUMENT, '"tenantId" is an invalid "CreateRequest" property.'); } validateCreateEditRequest(request, WriteOperationType.Create); }) // Set response validator. .setResponseValidator((response) => { // If the localId is not returned, then the request failed. if (!response.localId) { throw new error_1.FirebaseAuthError(error_1.AuthClientErrorCode.INTERNAL_ERROR, 'INTERNAL ASSERT FAILED: Unable to create new user'); } }); const FIREBASE_AUTH_GET_OOB_CODE = new api_request_1.ApiSettings('/accounts:sendOobCode', 'POST') // Set request validator. .setRequestValidator((request) => { if (!validator.isEmail(request.email)) { throw new error_1.FirebaseAuthError(error_1.AuthClientErrorCode.INVALID_EMAIL); } if (typeof request.newEmail !== 'undefined' && !validator.isEmail(request.newEmail)) { throw new error_1.FirebaseAuthError(error_1.AuthClientErrorCode.INVALID_NEW_EMAIL); } if (exports.EMAIL_ACTION_REQUEST_TYPES.indexOf(request.requestType) === -1) { throw new error_1.FirebaseAuthError(error_1.AuthClientErrorCode.INVALID_ARGUMENT, `"${request.requestType}" is not a supported email action request type.`); } }) // Set response validator. .setResponseValidator((response) => { // If the oobLink is not returned, then the request failed. if (!response.oobLink) { throw new error_1.FirebaseAuthError(error_1.AuthClientErrorCode.INTERNAL_ERROR, 'INTERNAL ASSERT FAILED: Unable to create the email action link'); } }); /** * Instantiates the retrieve OIDC configuration endpoint settings. * * @internal */ const GET_OAUTH_IDP_CONFIG = new api_request_1.ApiSettings('/oauthIdpConfigs/{providerId}', 'GET') // Set response validator. .setResponseValidator((response) => { // Response should always contain the OIDC provider resource name. if (!validator.isNonEmptyString(response.name)) { throw new error_1.FirebaseAuthError(error_1.AuthClientErrorCode.INTERNAL_ERROR, 'INTERNAL ASSERT FAILED: Unable to get OIDC configuration'); } }); /** * Instantiates the delete OIDC configuration endpoint settings. * * @internal */ const DELETE_OAUTH_IDP_CONFIG = new api_request_1.ApiSettings('/oauthIdpConfigs/{providerId}', 'DELETE'); /** * Instantiates the create OIDC configuration endpoint settings. * * @internal */ const CREATE_OAUTH_IDP_CONFIG = new api_request_1.ApiSettings('/oauthIdpConfigs?oauthIdpConfigId={providerId}', 'POST') // Set response validator. .setResponseValidator((response) => { // Response should always contain the OIDC provider resource name. if (!validator.isNonEmptyString(response.name)) { throw new error_1.FirebaseAuthError(error_1.AuthClientErrorCode.INTERNAL_ERROR, 'INTERNAL ASSERT FAILED: Unable to create new OIDC configuration'); } }); /** * Instantiates the update OIDC configuration endpoint settings. * * @internal */ const UPDATE_OAUTH_IDP_CONFIG = new api_request_1.ApiSettings('/oauthIdpConfigs/{providerId}?updateMask={updateMask}', 'PATCH') // Set response validator. .setResponseValidator((response) => { // Response should always contain the configuration resource name. if (!validator.isNonEmptyString(response.name)) { throw new error_1.FirebaseAuthError(error_1.AuthClientErrorCode.INTERNAL_ERROR, 'INTERNAL ASSERT FAILED: Unable to update OIDC configuration'); } }); /** * Instantiates the list OIDC configuration endpoint settings. * * @internal */ const LIST_OAUTH_IDP_CONFIGS = new api_request_1.ApiSettings('/oauthIdpConfigs', 'GET') // Set request validator. .setRequestValidator((request) => { // Validate next page token. if (typeof request.pageToken !== 'undefined' && !validator.isNonEmptyString(request.pageToken)) { throw new error_1.FirebaseAuthError(error_1.AuthClientErrorCode.INVALID_PAGE_TOKEN); } // Validate max results. if (!validator.isNumber(request.pageSize) || request.pageSize <= 0 || request.pageSize > MAX_LIST_PROVIDER_CONFIGURATION_PAGE_SIZE) { throw new error_1.FirebaseAuthError(error_1.AuthClientErrorCode.INVALID_ARGUMENT, 'Required "maxResults" must be a positive integer that does not exceed ' + `${MAX_LIST_PROVIDER_CONFIGURATION_PAGE_SIZE}.`); } }); /** * Instantiates the retrieve SAML configuration endpoint settings. * * @internal */ const GET_INBOUND_SAML_CONFIG = new api_request_1.ApiSettings('/inboundSamlConfigs/{providerId}', 'GET') // Set response validator. .setResponseValidator((response) => { // Response should always contain the SAML provider resource name. if (!validator.isNonEmptyString(response.name)) { throw new error_1.FirebaseAuthError(error_1.AuthClientErrorCode.INTERNAL_ERROR, 'INTERNAL ASSERT FAILED: Unable to get SAML configuration'); } }); /** * Instantiates the delete SAML configuration endpoint settings. * * @internal */ const DELETE_INBOUND_SAML_CONFIG = new api_request_1.ApiSettings('/inboundSamlConfigs/{providerId}', 'DELETE'); /** * Instantiates the create SAML configuration endpoint settings. * * @internal */ const CREATE_INBOUND_SAML_CONFIG = new api_request_1.ApiSettings('/inboundSamlConfigs?inboundSamlConfigId={providerId}', 'POST') // Set response validator. .setResponseValidator((response) => { // Response should always contain the SAML provider resource name. if (!validator.isNonEmptyString(response.name)) { throw new error_1.FirebaseAuthError(error_1.AuthClientErrorCode.INTERNAL_ERROR, 'INTERNAL ASSERT FAILED: Unable to create new SAML configuration'); } }); /** * Instantiates the update SAML configuration endpoint settings. * * @internal */ const UPDATE_INBOUND_SAML_CONFIG = new api_request_1.ApiSettings('/inboundSamlConfigs/{providerId}?updateMask={updateMask}', 'PATCH') // Set response validator. .setResponseValidator((response) => { // Response should always contain the configuration resource name. if (!validator.isNonEmptyString(response.name)) { throw new error_1.FirebaseAuthError(error_1.AuthClientErrorCode.INTERNAL_ERROR, 'INTERNAL ASSERT FAILED: Unable to update SAML configuration'); } }); /** * Instantiates the list SAML configuration endpoint settings. * * @internal */ const LIST_INBOUND_SAML_CONFIGS = new api_request_1.ApiSettings('/inboundSamlConfigs', 'GET') // Set request validator. .setRequestValidator((request) => { // Validate next page token. if (typeof request.pageToken !== 'undefined' && !validator.isNonEmptyString(request.pageToken)) { throw new error_1.FirebaseAuthError(error_1.AuthClientErrorCode.INVALID_PAGE_TOKEN); } // Validate max results. if (!validator.isNumber(request.pageSize) || request.pageSize <= 0 || request.pageSize > MAX_LIST_PROVIDER_CONFIGURATION_PAGE_SIZE) { throw new error_1.FirebaseAuthError(error_1.AuthClientErrorCode.INVALID_ARGUMENT, 'Required "maxResults" must be a positive integer that does not exceed ' + `${MAX_LIST_PROVIDER_CONFIGURATION_PAGE_SIZE}.`); } }); /** * Class that provides the mechanism to send requests to the Firebase Auth backend endpoints. * * @internal */ class AbstractAuthRequestHandler { /** * @param response - The response to check for errors. * @returns The error code if present; null otherwise. */ static getErrorCode(response) { return (validator.isNonNullObject(response) && response.error && response.error.message) || null; } static addUidToRequest(id, request) { if (!validator.isUid(id.uid)) { throw new error_1.FirebaseAuthError(error_1.AuthClientErrorCode.INVALID_UID); } request.localId ? request.localId.push(id.uid) : request.localId = [id.uid]; return request; } static addEmailToRequest(id, request) { if (!validator.isEmail(id.email)) { throw new error_1.FirebaseAuthError(error_1.AuthClientErrorCode.INVALID_EMAIL); } request.email ? request.email.push(id.email) : request.email = [id.email]; return request; } static addPhoneToRequest(id, request) { if (!validator.isPhoneNumber(id.phoneNumber)) { throw new error_1.FirebaseAuthError(error_1.AuthClientErrorCode.INVALID_PHONE_NUMBER); } request.phoneNumber ? request.phoneNumber.push(id.phoneNumber) : request.phoneNumber = [id.phoneNumber]; return request; } static addProviderToRequest(id, request) { if (!validator.isNonEmptyString(id.providerId)) { throw new error_1.FirebaseAuthError(error_1.AuthClientErrorCode.INVALID_PROVIDER_ID); } if (!validator.isNonEmptyString(id.providerUid)) { throw new error_1.FirebaseAuthError(error_1.AuthClientErrorCode.INVALID_PROVIDER_UID); } const federatedUserId = { providerId: id.providerId, rawId: id.providerUid, }; request.federatedUserId ? request.federatedUserId.push(federatedUserId) : request.federatedUserId = [federatedUserId]; return request; } /** * @param app - The app used to fetch access tokens to sign API requests. * @constructor */ constructor(app) { this.app = app; if (typeof app !== 'object' || app === null || !('options' in app)) { throw new error_1.FirebaseAuthError(error_1.AuthClientErrorCode.INVALID_ARGUMENT, 'First argument passed to admin.auth() must be a valid Firebase app instance.'); } this.httpClient = new AuthHttpClient(app); } /** * Creates a new Firebase session cookie with the specified duration that can be used for * session management (set as a server side session cookie with custom cookie policy). * The session cookie JWT will have the same payload claims as the provided ID token. * * @param idToken - The Firebase ID token to exchange for a session cookie. * @param expiresIn - The session cookie duration in milliseconds. * * @returns A promise that resolves on success with the created session cookie. */ createSessionCookie(idToken, expiresIn) { const request = { idToken, // Convert to seconds. validDuration: Math.floor(expiresIn / 1000), }; return this.invokeRequestHandler(this.getAuthUrlBuilder(), exports.FIREBASE_AUTH_CREATE_SESSION_COOKIE, request) .then((response) => response.sessionCookie); } /** * Looks up a user by uid. * * @param uid - The uid of the user to lookup. * @returns A promise that resolves with the user information. */ getAccountInfoByUid(uid) { if (!validator.isUid(uid)) { return Promise.reject(new error_1.FirebaseAuthError(error_1.AuthClientErrorCode.INVALID_UID)); } const request = { localId: [uid], }; return this.invokeRequestHandler(this.getAuthUrlBuilder(), exports.FIREBASE_AUTH_GET_ACCOUNT_INFO, request); } /** * Looks up a user by email. * * @param email - The email of the user to lookup. * @returns A promise that resolves with the user information. */ getAccountInfoByEmail(email) { if (!validator.isEmail(email)) { return Promise.reject(new error_1.FirebaseAuthError(error_1.AuthClientErrorCode.INVALID_EMAIL)); } const request = { email: [email], }; return this.invokeRequestHandler(this.getAuthUrlBuilder(), exports.FIREBASE_AUTH_GET_ACCOUNT_INFO, request); } /** * Looks up a user by phone number. * * @param phoneNumber - The phone number of the user to lookup. * @returns A promise that resolves with the user information. */ getAccountInfoByPhoneNumber(phoneNumber) { if (!validator.isPhoneNumber(phoneNumber)) { return Promise.reject(new error_1.FirebaseAuthError(error_1.AuthClientErrorCode.INVALID_PHONE_NUMBER)); } const request = { phoneNumber: [phoneNumber], }; return this.invokeRequestHandler(this.getAuthUrlBuilder(), exports.FIREBASE_AUTH_GET_ACCOUNT_INFO, request); } getAccountInfoByFederatedUid(providerId, rawId) { if (!validator.isNonEmptyString(providerId) || !validator.isNonEmptyString(rawId)) { throw new error_1.FirebaseAuthError(error_1.AuthClientErrorCode.INVALID_PROVIDER_ID); } const request = { federatedUserId: [{ providerId, rawId, }], }; return this.invokeRequestHandler(this.getAuthUrlBuilder(), exports.FIREBASE_AUTH_GET_ACCOUNT_INFO, request); } /** * Looks up multiple users by their identifiers (uid, email, etc). * * @param identifiers - The identifiers indicating the users * to be looked up. Must have <= 100 entries. * @param A - promise that resolves with the set of successfully * looked up users. Possibly empty if no users were looked up. */ getAccountInfoByIdentifiers(identifiers) { if (identifiers.length === 0) { return Promise.resolve({ users: [] }); } else if (identifiers.length > MAX_GET_ACCOUNTS_BATCH_SIZE) { throw new error_1.FirebaseAuthError(error_1.AuthClientErrorCode.MAXIMUM_USER_COUNT_EXCEEDED, '`identifiers` parameter must have <= ' + MAX_GET_ACCOUNTS_BATCH_SIZE + ' entries.'); } let request = {}; for (const id of identifiers) { if ((0, identifier_1.isUidIdentifier)(id)) { request = AbstractAuthRequestHandler.addUidToRequest(id, request); } else if ((0, identifier_1.isEmailIdentifier)(id)) { request = AbstractAuthRequestHandler.addEmailToRequest(id, request); } else if ((0, identifier_1.isPhoneIdentifier)(id)) { request = AbstractAuthRequestHandler.addPhoneToRequest(id, request); } else if ((0, identifier_1.isProviderIdentifier)(id)) { request = AbstractAuthRequestHandler.addProviderToRequest(id, request); } else { throw new error_1.FirebaseAuthError(error_1.AuthClientErrorCode.INVALID_ARGUMENT, 'Unrecognized identifier: ' + id); } } return this.invokeRequestHandler(this.getAuthUrlBuilder(), exports.FIREBASE_AUTH_GET_ACCOUNTS_INFO, request); } /** * Exports the users (single batch only) with a size of maxResults and starting from * the offset as specified by pageToken. * * @param maxResults - The page size, 1000 if undefined. This is also the maximum * allowed limit. * @param pageToken - The next page token. If not specified, returns users starting * without any offset. Users are returned in the order they were created from oldest to * newest, relative to the page token offset. * @returns A promise that resolves with the current batch of downloaded * users and the next page token if available. For the last page, an empty list of users * and no page token are returned. */ downloadAccount(maxResults = MAX_DOWNLOAD_ACCOUNT_PAGE_SIZE, pageToken) { // Construct request. const request = { maxResults, nextPageToken: pageToken, }; // Remove next page token if not provided. if (typeof request.nextPageToken === 'undefined') { delete request.nextPageToken; } return this.invokeRequestHandler(this.getAuthUrlBuilder(), exports.FIREBASE_AUTH_DOWNLOAD_ACCOUNT, request) .then((response) => { // No more users available. if (!response.users) { response.users = []; } return response; }); } /** * Imports the list of users provided to Firebase Auth. This is useful when * migrating from an external authentication system without having to use the Firebase CLI SDK. * At most, 1000 users are allowed to be imported one at a time. * When importing a list of password users, UserImportOptions are required to be specified. * * @param users - The list of user records to import to Firebase Auth. * @param options - The user import options, required when the users provided * include password credentials. * @returns A promise that resolves when the operation completes * with the result of the import. This includes the number of successful imports, the number * of failed uploads and their corresponding errors. */ uploadAccount(users, options) { // This will throw if any error is detected in the hash options. // For errors in the list of users, this will not throw and will report the errors and the // corresponding user index in the user import generated response below. // No need to validate raw request or raw response as this is done in UserImportBuilder. const userImportBuilder = new user_import_builder_1.UserImportBuilder(users, options, (userRequest) => { // Pass true to validate the uploadAccount specific fields. validateCreateEditRequest(userRequest, WriteOperationType.Upload); }); const request = userImportBuilder.buildRequest(); // Fail quickly if more users than allowed are to be imported. if (validator.isArray(users) && users.length > MAX_UPLOAD_ACCOUNT_BATCH_SIZE) { throw new error_1.FirebaseAuthError(error_1.AuthClientErrorCode.MAXIMUM_USER_COUNT_EXCEEDED, `A maximum of ${MAX_UPLOAD_ACCOUNT_BATCH_SIZE} users can be imported at once.`); } // If no remaining user in request after client side processing, there is no need // to send the request to the server. if (!request.users || request.users.length === 0) { return Promise.resolve(userImportBuilder.buildResponse([])); } return this.invokeRequestHandler(this.getAuthUrlBuilder(), exports.FIREBASE_AUTH_UPLOAD_ACCOUNT, request) .then((response) => { // No error object is returned if no error encountered. const failedUploads = (response.error || []); // Rewrite response as UserImportResult and re-insert client previously detected errors. return userImportBuilder.buildResponse(failedUploads); }); } /** * Deletes an account identified by a uid. * * @param uid - The uid of the user to delete. * @returns A promise that resolves when the user is deleted. */ deleteAccount(uid) { if (!validator.isUid(uid)) { return Promise.reject(new error_1.FirebaseAuthError(error_1.AuthClientErrorCode.INVALID_UID)); } const request = { localId: uid, }; return this.invokeRequestHandler(this.getAuthUrlBuilder(), exports.FIREBASE_AUTH_DELETE_ACCOUNT, request); } deleteAccounts(uids, force) { if (uids.length === 0) { return Promise.resolve({}); } else if (uids.length > MAX_DELETE_ACCOUNTS_BATCH_SIZE) { throw new error_1.FirebaseAuthError(error_1.AuthClientErrorCode.MAXIMUM_USER_COUNT_EXCEEDED, '`uids` parameter must have <= ' + MAX_DELETE_ACCOUNTS_BATCH_SIZE + ' entries.'); } const request = { localIds: [], force, }; uids.forEa