@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.
325 lines (290 loc) • 9.65 kB
text/typescript
import { AuthClientErrorCode, FirebaseAuthError } from "./error";
import { addReadonlyGetter, deepCopy } from "./utils";
import { isNonNullObject } from "./validator";
import { stringToBase64 } from "./jwt/utils";
const B64_REDACTED = stringToBase64("REDACTED");
function parseDate(time: any): string | null {
try {
const date = new Date(parseInt(time, 10));
if (!isNaN(date.getTime())) {
return date.toUTCString();
}
} catch (e) {}
return null;
}
export interface MultiFactorInfoResponse {
mfaEnrollmentId: string;
displayName?: string;
phoneInfo?: string;
enrolledAt?: string;
[key: string]: any;
}
export interface ProviderUserInfoResponse {
rawId: string;
displayName?: string;
email?: string;
photoUrl?: string;
phoneNumber?: string;
providerId: string;
federatedId?: string;
}
export interface GetAccountInfoUserResponse {
localId: string;
email?: string;
emailVerified?: boolean;
phoneNumber?: string;
displayName?: string;
photoUrl?: string;
disabled?: boolean;
passwordHash?: string;
salt?: string;
customAttributes?: string;
validSince?: string;
tenantId?: string;
providerUserInfo?: ProviderUserInfoResponse[];
mfaInfo?: MultiFactorInfoResponse[];
createdAt?: string;
lastLoginAt?: string;
lastRefreshAt?: string;
[key: string]: any;
}
enum MultiFactorId {
Phone = "phone",
}
export abstract class MultiFactorInfo {
public readonly uid!: string;
public readonly displayName?: string;
public readonly factorId!: string;
public readonly enrollmentTime?: string;
public static initMultiFactorInfo(
response: MultiFactorInfoResponse
): MultiFactorInfo | null {
let multiFactorInfo: MultiFactorInfo | null = null;
try {
multiFactorInfo = new PhoneMultiFactorInfo(response);
} catch (e) {}
return multiFactorInfo;
}
constructor(response: MultiFactorInfoResponse) {
this.initFromServerResponse(response);
}
public toJSON(): object {
return {
uid: this.uid,
displayName: this.displayName,
factorId: this.factorId,
enrollmentTime: this.enrollmentTime,
};
}
protected abstract getFactorId(
response: MultiFactorInfoResponse
): string | null;
private initFromServerResponse(response: MultiFactorInfoResponse): void {
const factorId = response && this.getFactorId(response);
if (!factorId || !response || !response.mfaEnrollmentId) {
throw new FirebaseAuthError(
AuthClientErrorCode.INTERNAL_ERROR,
"INTERNAL ASSERT FAILED: Invalid multi-factor info response"
);
}
addReadonlyGetter(this, "uid", response.mfaEnrollmentId);
addReadonlyGetter(this, "factorId", factorId);
addReadonlyGetter(this, "displayName", response.displayName);
if (response.enrolledAt) {
addReadonlyGetter(
this,
"enrollmentTime",
new Date(response.enrolledAt).toUTCString()
);
} else {
addReadonlyGetter(this, "enrollmentTime", null);
}
}
}
export class PhoneMultiFactorInfo extends MultiFactorInfo {
public readonly phoneNumber!: string;
constructor(response: MultiFactorInfoResponse) {
super(response);
addReadonlyGetter(this, "phoneNumber", response.phoneInfo);
}
public toJSON(): object {
return Object.assign(super.toJSON(), {
phoneNumber: this.phoneNumber,
});
}
protected getFactorId(response: MultiFactorInfoResponse): string | null {
return response && response.phoneInfo ? MultiFactorId.Phone : null;
}
}
export class MultiFactorSettings {
public enrolledFactors!: MultiFactorInfo[];
constructor(response: GetAccountInfoUserResponse) {
const parsedEnrolledFactors: MultiFactorInfo[] = [];
if (!isNonNullObject(response)) {
throw new FirebaseAuthError(
AuthClientErrorCode.INTERNAL_ERROR,
"INTERNAL ASSERT FAILED: Invalid multi-factor response"
);
} else if (response.mfaInfo) {
response.mfaInfo.forEach((factorResponse) => {
const multiFactorInfo =
MultiFactorInfo.initMultiFactorInfo(factorResponse);
if (multiFactorInfo) {
parsedEnrolledFactors.push(multiFactorInfo);
}
});
}
addReadonlyGetter(
this,
"enrolledFactors",
Object.freeze(parsedEnrolledFactors)
);
}
public toJSON(): object {
return {
enrolledFactors: this.enrolledFactors.map((info) => info.toJSON()),
};
}
}
export class UserMetadata {
public readonly creationTime!: string;
public readonly lastSignInTime!: string;
public readonly lastRefreshTime?: string | null;
constructor(response: GetAccountInfoUserResponse) {
addReadonlyGetter(this, "creationTime", parseDate(response.createdAt));
addReadonlyGetter(this, "lastSignInTime", parseDate(response.lastLoginAt));
const lastRefreshAt = response.lastRefreshAt
? new Date(response.lastRefreshAt).toUTCString()
: null;
addReadonlyGetter(this, "lastRefreshTime", lastRefreshAt);
}
public toJSON(): object {
return {
lastSignInTime: this.lastSignInTime,
creationTime: this.creationTime,
lastRefreshTime: this.lastRefreshTime,
};
}
}
export class UserInfo {
public readonly uid!: string;
public readonly displayName!: string;
public readonly email!: string;
public readonly photoURL!: string;
public readonly providerId!: string;
public readonly phoneNumber!: string;
constructor(response: ProviderUserInfoResponse) {
if (!response.rawId || !response.providerId) {
throw new FirebaseAuthError(
AuthClientErrorCode.INTERNAL_ERROR,
"INTERNAL ASSERT FAILED: Invalid user info response"
);
}
addReadonlyGetter(this, "uid", response.rawId);
addReadonlyGetter(this, "displayName", response.displayName);
addReadonlyGetter(this, "email", response.email);
addReadonlyGetter(this, "photoURL", response.photoUrl);
addReadonlyGetter(this, "providerId", response.providerId);
addReadonlyGetter(this, "phoneNumber", response.phoneNumber);
}
public toJSON(): object {
return {
uid: this.uid,
displayName: this.displayName,
email: this.email,
photoURL: this.photoURL,
providerId: this.providerId,
phoneNumber: this.phoneNumber,
};
}
}
export class UserRecord {
public readonly uid!: string;
public readonly email?: string;
public readonly emailVerified!: boolean;
public readonly displayName?: string;
public readonly photoURL?: string;
public readonly phoneNumber?: string;
public readonly disabled!: boolean;
public readonly metadata!: UserMetadata;
public readonly providerData!: UserInfo[];
public readonly passwordHash?: string;
public readonly passwordSalt?: string;
public readonly customClaims?: { [key: string]: any };
public readonly tenantId?: string | null;
public readonly tokensValidAfterTime?: string;
public readonly multiFactor?: MultiFactorSettings;
constructor(response: GetAccountInfoUserResponse) {
if (!response.localId) {
throw new FirebaseAuthError(
AuthClientErrorCode.INTERNAL_ERROR,
"INTERNAL ASSERT FAILED: Invalid user response"
);
}
addReadonlyGetter(this, "uid", response.localId);
addReadonlyGetter(this, "email", response.email);
addReadonlyGetter(this, "emailVerified", !!response.emailVerified);
addReadonlyGetter(this, "displayName", response.displayName);
addReadonlyGetter(this, "photoURL", response.photoUrl);
addReadonlyGetter(this, "phoneNumber", response.phoneNumber);
addReadonlyGetter(this, "disabled", response.disabled || false);
addReadonlyGetter(this, "metadata", new UserMetadata(response));
const providerData: UserInfo[] = [];
for (const entry of response.providerUserInfo || []) {
providerData.push(new UserInfo(entry));
}
addReadonlyGetter(this, "providerData", providerData);
if (response.passwordHash === B64_REDACTED) {
addReadonlyGetter(this, "passwordHash", undefined);
} else {
addReadonlyGetter(this, "passwordHash", response.passwordHash);
}
addReadonlyGetter(this, "passwordSalt", response.salt);
if (response.customAttributes) {
addReadonlyGetter(
this,
"customClaims",
JSON.parse(response.customAttributes)
);
}
let validAfterTime: string | null = null;
if (typeof response.validSince !== "undefined") {
validAfterTime = parseDate(parseInt(response.validSince, 10) * 1000);
}
addReadonlyGetter(
this,
"tokensValidAfterTime",
validAfterTime || undefined
);
addReadonlyGetter(this, "tenantId", response.tenantId);
const multiFactor = new MultiFactorSettings(response);
if (multiFactor.enrolledFactors.length > 0) {
addReadonlyGetter(this, "multiFactor", multiFactor);
}
}
public toJSON(): object {
const json: any = {
uid: this.uid,
email: this.email,
emailVerified: this.emailVerified,
displayName: this.displayName,
photoURL: this.photoURL,
phoneNumber: this.phoneNumber,
disabled: this.disabled,
metadata: this.metadata.toJSON(),
passwordHash: this.passwordHash,
passwordSalt: this.passwordSalt,
customClaims: deepCopy(this.customClaims),
tokensValidAfterTime: this.tokensValidAfterTime,
tenantId: this.tenantId,
};
if (this.multiFactor) {
json.multiFactor = this.multiFactor.toJSON();
}
json.providerData = [];
for (const entry of this.providerData) {
json.providerData.push(entry.toJSON());
}
return json;
}
}