UNPKG

@pedwise/next-firebase-auth-edge

Version:

Next.js 13 Firebase Authentication for Edge and server runtimes. Dedicated for Next 13 server components. Compatible with Next.js middleware.

437 lines 19.4 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.AuthRequestHandler = exports.AbstractAuthRequestHandler = exports.FIREBASE_AUTH_SET_ACCOUNT_INFO = exports.RESERVED_CLAIMS = exports.FIREBASE_AUTH_DELETE_ACCOUNT = exports.FIREBASE_AUTH_GET_ACCOUNT_INFO = exports.getSdkVersion = exports.ApiSettings = void 0; const firebase_1 = require("./firebase"); const utils_1 = require("./utils"); const validator_1 = require("./validator"); const error_1 = require("./error"); const credential_1 = require("./credential"); class ApiSettings { constructor(endpoint, httpMethod = "POST") { this.endpoint = endpoint; this.httpMethod = httpMethod; this.setRequestValidator(null).setResponseValidator(null); } getEndpoint() { return this.endpoint; } getHttpMethod() { return this.httpMethod; } setRequestValidator(requestValidator) { const nullFunction = () => undefined; this.requestValidator = requestValidator || nullFunction; return this; } getRequestValidator() { return this.requestValidator; } setResponseValidator(responseValidator) { const nullFunction = () => undefined; this.responseValidator = responseValidator || nullFunction; return this; } getResponseValidator() { return this.responseValidator; } } exports.ApiSettings = ApiSettings; function getSdkVersion() { return "11.2.0"; } exports.getSdkVersion = getSdkVersion; /** Firebase Auth request header. */ const FIREBASE_AUTH_HEADER = { "X-Client-Version": `Node/Admin/${getSdkVersion()}`, Accept: "application/json", "Content-Type": "application/json", }; /** 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}"; class AuthResourceUrlBuilder { constructor(version = "v1", projectId) { this.version = version; this.projectId = projectId; if ((0, firebase_1.useEmulator)()) { this.urlFormat = (0, utils_1.formatString)(FIREBASE_AUTH_EMULATOR_BASE_URL_FORMAT, { host: (0, firebase_1.emulatorHost)(), }); } else { this.urlFormat = FIREBASE_AUTH_BASE_URL_FORMAT; } } async getUrl(api, params) { const baseParams = { version: this.version, projectId: this.projectId, api: api || "", }; const baseUrl = (0, utils_1.formatString)(this.urlFormat, baseParams); return (0, utils_1.formatString)(baseUrl, params || {}); } } exports.FIREBASE_AUTH_GET_ACCOUNT_INFO = new ApiSettings("/accounts:lookup", "POST") .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"); } }) .setResponseValidator((response) => { if (!response.users || !response.users.length) { throw new error_1.FirebaseAuthError(error_1.AuthClientErrorCode.USER_NOT_FOUND); } }); exports.FIREBASE_AUTH_DELETE_ACCOUNT = new ApiSettings("/accounts:delete", "POST").setRequestValidator((request) => { if (!request.localId) { throw new error_1.FirebaseAuthError(error_1.AuthClientErrorCode.INTERNAL_ERROR, "INTERNAL ASSERT FAILED: Server request is missing user identifier"); } }); var WriteOperationType; (function (WriteOperationType) { WriteOperationType["Create"] = "create"; WriteOperationType["Update"] = "update"; WriteOperationType["Upload"] = "upload"; })(WriteOperationType || (WriteOperationType = {})); const MAX_CLAIMS_PAYLOAD_SIZE = 1000; exports.RESERVED_CLAIMS = [ "acr", "amr", "at_hash", "aud", "auth_time", "azp", "cnf", "c_hash", "exp", "iat", "iss", "jti", "nbf", "nonce", "sub", "firebase", ]; function validateProviderUserInfo(request) { const validKeys = { rawId: true, providerId: true, email: true, displayName: true, photoUrl: true, }; for (const key in request) { if (!(key in validKeys)) { delete request[key]; } } if (!(0, validator_1.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 (!(0, validator_1.isNonEmptyString)(request.rawId)) { throw new error_1.FirebaseAuthError(error_1.AuthClientErrorCode.INVALID_UID, `The provider "uid" for "${request.providerId}" must be a valid non-empty string.`); } if (typeof request.email !== "undefined" && !(0, validator_1.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.`); } if (typeof request.photoUrl !== "undefined" && !(0, validator_1.isURL)(request.photoUrl)) { throw new error_1.FirebaseAuthError(error_1.AuthClientErrorCode.INVALID_PHOTO_URL, `The provider "photoURL" for "${request.providerId}" must be a valid URL string.`); } } function validateAuthFactorInfo(request) { const validKeys = { mfaEnrollmentId: true, displayName: true, phoneInfo: true, enrolledAt: true, }; for (const key in request) { if (!(key in validKeys)) { delete request[key]; } } const authFactorInfoIdentifier = request.mfaEnrollmentId || request.phoneInfo || JSON.stringify(request); if (typeof request.mfaEnrollmentId !== "undefined" && !(0, validator_1.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" && !(0, validator_1.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.`); } if (typeof request.enrolledAt !== "undefined" && !(0, validator_1.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."); } if (typeof request.phoneInfo !== "undefined") { if (!(0, validator_1.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 { throw new error_1.FirebaseAuthError(error_1.AuthClientErrorCode.INVALID_ENROLLED_FACTORS, "MFAInfo object provided is invalid."); } } function validateCreateEditRequest(request, writeOperationType) { const uploadAccountRequest = writeOperationType === WriteOperationType.Upload; 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, linkProviderUserInfo: !uploadAccountRequest, tenantId: uploadAccountRequest, passwordHash: uploadAccountRequest, salt: uploadAccountRequest, createdAt: uploadAccountRequest, lastLoginAt: uploadAccountRequest, providerUserInfo: uploadAccountRequest, mfaInfo: uploadAccountRequest, mfa: !uploadAccountRequest, }; for (const key in request) { if (!(key in validKeys)) { delete request[key]; } } if (typeof request.tenantId !== "undefined" && !(0, validator_1.isNonEmptyString)(request.tenantId)) { throw new error_1.FirebaseAuthError(error_1.AuthClientErrorCode.INVALID_TENANT_ID); } if (typeof request.displayName !== "undefined" && !(0, validator_1.isString)(request.displayName)) { throw new error_1.FirebaseAuthError(error_1.AuthClientErrorCode.INVALID_DISPLAY_NAME); } if ((typeof request.localId !== "undefined" || uploadAccountRequest) && !(0, validator_1.isUid)(request.localId)) { throw new error_1.FirebaseAuthError(error_1.AuthClientErrorCode.INVALID_UID); } if (typeof request.email !== "undefined" && !(0, validator_1.isEmail)(request.email)) { throw new error_1.FirebaseAuthError(error_1.AuthClientErrorCode.INVALID_EMAIL); } if (typeof request.phoneNumber !== "undefined" && !(0, validator_1.isPhoneNumber)(request.phoneNumber)) { throw new error_1.FirebaseAuthError(error_1.AuthClientErrorCode.INVALID_PHONE_NUMBER); } if (typeof request.password !== "undefined" && !(0, validator_1.isPassword)(request.password)) { throw new error_1.FirebaseAuthError(error_1.AuthClientErrorCode.INVALID_PASSWORD); } if (typeof request.rawPassword !== "undefined" && !(0, validator_1.isPassword)(request.rawPassword)) { throw new error_1.FirebaseAuthError(error_1.AuthClientErrorCode.INVALID_PASSWORD); } if (typeof request.emailVerified !== "undefined" && typeof request.emailVerified !== "boolean") { throw new error_1.FirebaseAuthError(error_1.AuthClientErrorCode.INVALID_EMAIL_VERIFIED); } if (typeof request.photoUrl !== "undefined" && !(0, validator_1.isURL)(request.photoUrl)) { throw new error_1.FirebaseAuthError(error_1.AuthClientErrorCode.INVALID_PHOTO_URL); } if (typeof request.disabled !== "undefined" && typeof request.disabled !== "boolean") { throw new error_1.FirebaseAuthError(error_1.AuthClientErrorCode.INVALID_DISABLED_FIELD); } if (typeof request.validSince !== "undefined" && !(0, validator_1.isNumber)(request.validSince)) { throw new error_1.FirebaseAuthError(error_1.AuthClientErrorCode.INVALID_TOKENS_VALID_AFTER_TIME); } if (typeof request.createdAt !== "undefined" && !(0, validator_1.isNumber)(request.createdAt)) { throw new error_1.FirebaseAuthError(error_1.AuthClientErrorCode.INVALID_CREATION_TIME); } if (typeof request.lastLoginAt !== "undefined" && !(0, validator_1.isNumber)(request.lastLoginAt)) { throw new error_1.FirebaseAuthError(error_1.AuthClientErrorCode.INVALID_LAST_SIGN_IN_TIME); } if (typeof request.disableUser !== "undefined" && typeof request.disableUser !== "boolean") { throw new error_1.FirebaseAuthError(error_1.AuthClientErrorCode.INVALID_DISABLED_FIELD); } if (typeof request.customAttributes !== "undefined") { let developerClaims; try { developerClaims = JSON.parse(request.customAttributes); } catch (error) { throw new error_1.FirebaseAuthError(error_1.AuthClientErrorCode.INVALID_CLAIMS, error.message); } const invalidClaims = []; exports.RESERVED_CLAIMS.forEach((blacklistedClaim) => { if (Object.prototype.hasOwnProperty.call(developerClaims, blacklistedClaim)) { invalidClaims.push(blacklistedClaim); } }); 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.`); } 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.`); } } if (typeof request.passwordHash !== "undefined" && !(0, validator_1.isString)(request.passwordHash)) { throw new error_1.FirebaseAuthError(error_1.AuthClientErrorCode.INVALID_PASSWORD_HASH); } if (typeof request.salt !== "undefined" && !(0, validator_1.isString)(request.salt)) { throw new error_1.FirebaseAuthError(error_1.AuthClientErrorCode.INVALID_PASSWORD_SALT); } if (typeof request.providerUserInfo !== "undefined" && !(0, validator_1.isArray)(request.providerUserInfo)) { throw new error_1.FirebaseAuthError(error_1.AuthClientErrorCode.INVALID_PROVIDER_DATA); } else if ((0, validator_1.isArray)(request.providerUserInfo)) { request.providerUserInfo.forEach((providerUserInfoEntry) => { validateProviderUserInfo(providerUserInfoEntry); }); } if (typeof request.linkProviderUserInfo !== "undefined") { validateProviderUserInfo(request.linkProviderUserInfo); } let enrollments = null; if (request.mfaInfo) { enrollments = request.mfaInfo; } else if (request.mfa && request.mfa.enrollments) { enrollments = request.mfa.enrollments; } if (enrollments) { if (!(0, validator_1.isArray)(enrollments)) { throw new error_1.FirebaseAuthError(error_1.AuthClientErrorCode.INVALID_ENROLLED_FACTORS); } enrollments.forEach((authFactorInfoEntry) => { validateAuthFactorInfo(authFactorInfoEntry); }); } } exports.FIREBASE_AUTH_SET_ACCOUNT_INFO = new ApiSettings("/accounts:update", "POST") .setRequestValidator((request) => { if (typeof request.localId === "undefined") { throw new error_1.FirebaseAuthError(error_1.AuthClientErrorCode.INTERNAL_ERROR, "INTERNAL ASSERT FAILED: Server request is missing user identifier"); } 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); }) .setResponseValidator((response) => { if (!response.localId) { throw new error_1.FirebaseAuthError(error_1.AuthClientErrorCode.USER_NOT_FOUND); } }); class AbstractAuthRequestHandler { static getErrorCode(response) { return (((0, validator_1.isNonNullObject)(response) && response.error && response.error.message) || null); } constructor(serviceAccount) { this.getToken = (0, credential_1.getFirebaseAdminTokenProvider)(serviceAccount).getToken; } getAccountInfoByUid(uid) { if (!(0, validator_1.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); } deleteAccount(uid) { if (!(0, validator_1.isUid)(uid)) { return Promise.reject(new error_1.FirebaseAuthError(error_1.AuthClientErrorCode.INVALID_UID)); } return this.invokeRequestHandler(this.getAuthUrlBuilder(), exports.FIREBASE_AUTH_DELETE_ACCOUNT, { localId: uid, }); } setCustomUserClaims(uid, customUserClaims) { if (!(0, validator_1.isUid)(uid)) { return Promise.reject(new error_1.FirebaseAuthError(error_1.AuthClientErrorCode.INVALID_UID)); } else if (!(0, validator_1.isObject)(customUserClaims)) { return Promise.reject(new error_1.FirebaseAuthError(error_1.AuthClientErrorCode.INVALID_ARGUMENT, "CustomUserClaims argument must be an object or null.")); } if (customUserClaims === null) { customUserClaims = {}; } const request = { localId: uid, customAttributes: JSON.stringify(customUserClaims), }; return this.invokeRequestHandler(this.getAuthUrlBuilder(), exports.FIREBASE_AUTH_SET_ACCOUNT_INFO, request).then((response) => { return response.localId; }); } async invokeRequestHandler(urlBuilder, apiSettings, requestData, additionalResourceParams) { const url = await urlBuilder.getUrl(apiSettings.getEndpoint(), additionalResourceParams); const token = await this.getToken(); if (requestData) { const requestValidator = apiSettings.getRequestValidator(); requestValidator(requestData); } const res = await fetch(url, { method: apiSettings.getHttpMethod(), headers: Object.assign(Object.assign({}, FIREBASE_AUTH_HEADER), { Authorization: `Bearer ${token.accessToken}` }), body: JSON.stringify(requestData), }); if (!res.ok) { const error = await res.json(); const errorCode = AbstractAuthRequestHandler.getErrorCode(error); if (!errorCode) { throw new error_1.FirebaseAuthError(error_1.AuthClientErrorCode.INTERNAL_ERROR, "Error returned from server: " + error + ". Additionally, an " + "internal error occurred while attempting to extract the " + "errorcode from the error."); } throw error_1.FirebaseAuthError.fromServerError(errorCode, undefined, error); } const data = await res.json(); const responseValidator = apiSettings.getResponseValidator(); responseValidator(data); return data; } getAuthUrlBuilder() { if (!this.authUrlBuilder) { this.authUrlBuilder = this.newAuthUrlBuilder(); } return this.authUrlBuilder; } } exports.AbstractAuthRequestHandler = AbstractAuthRequestHandler; class AuthRequestHandler extends AbstractAuthRequestHandler { constructor(serviceAccount) { super(serviceAccount); this.serviceAccount = serviceAccount; this.authResourceUrlBuilder = new AuthResourceUrlBuilder("v2", serviceAccount.projectId); } newAuthUrlBuilder() { return new AuthResourceUrlBuilder("v1", this.serviceAccount.projectId); } } exports.AuthRequestHandler = AuthRequestHandler; //# sourceMappingURL=auth-request-handler.js.map