@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
JavaScript
;
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