@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.
221 lines (200 loc) • 6.03 kB
text/typescript
import {
CryptoSigner,
CryptoSignerError,
CryptoSignerErrorCode,
ServiceAccountSigner,
} from "./jwt/crypto-signer";
import { objectToBase64, stringToBase64 } from "./jwt/utils";
import { isNonEmptyString, isNonNullObject } from "./validator";
import { AuthClientErrorCode, ErrorInfo, FirebaseAuthError } from "./error";
import { useEmulator } from "./firebase";
import { ServiceAccountCredential } from "./credential";
const ALGORITHM_NONE = "none" as const;
const ONE_HOUR_IN_SECONDS = 60 * 60;
export const BLACKLISTED_CLAIMS = [
"acr",
"amr",
"at_hash",
"aud",
"auth_time",
"azp",
"cnf",
"c_hash",
"exp",
"iat",
"iss",
"jti",
"nbf",
"nonce",
];
const FIREBASE_AUDIENCE =
"https://identitytoolkit.googleapis.com/google.identity.identitytoolkit.v1.IdentityToolkit";
interface JWTHeader {
alg: string;
typ: string;
}
interface JWTBody {
claims?: object;
uid: string;
aud: string;
iat: number;
exp: number;
iss: string;
sub: string;
tenant_id?: string;
}
export class EmulatedSigner implements CryptoSigner {
algorithm = ALGORITHM_NONE;
public async sign(token: string): Promise<string> {
return stringToBase64(token);
}
public getAccountId(): Promise<string> {
return Promise.resolve("firebase-auth-emulator@example.com");
}
}
export class FirebaseTokenGenerator {
private readonly signer: CryptoSigner;
constructor(signer: CryptoSigner, public readonly tenantId?: string) {
if (!isNonNullObject(signer)) {
throw new FirebaseAuthError(
AuthClientErrorCode.INVALID_CREDENTIAL,
"INTERNAL ASSERT: Must provide a CryptoSigner to use FirebaseTokenGenerator."
);
}
if (
typeof this.tenantId !== "undefined" &&
!isNonEmptyString(this.tenantId)
) {
throw new FirebaseAuthError(
AuthClientErrorCode.INVALID_ARGUMENT,
"`tenantId` argument must be a non-empty string."
);
}
this.signer = signer;
}
public createCustomToken(
uid: string,
developerClaims?: { [key: string]: any }
): Promise<string> {
let errorMessage: string | undefined;
if (!isNonEmptyString(uid)) {
errorMessage = "`uid` argument must be a non-empty string uid.";
} else if (uid.length > 128) {
errorMessage =
"`uid` argument must a uid with less than or equal to 128 characters.";
} else if (
!FirebaseTokenGenerator.isDeveloperClaimsValid_(developerClaims)
) {
errorMessage =
"`developerClaims` argument must be a valid, non-null object containing the developer claims.";
}
if (errorMessage) {
throw new FirebaseAuthError(
AuthClientErrorCode.INVALID_ARGUMENT,
errorMessage
);
}
const claims: { [key: string]: any } = {};
if (typeof developerClaims !== "undefined") {
for (const key in developerClaims) {
if (Object.prototype.hasOwnProperty.call(developerClaims, key)) {
if (BLACKLISTED_CLAIMS.indexOf(key) !== -1) {
throw new FirebaseAuthError(
AuthClientErrorCode.INVALID_ARGUMENT,
`Developer claim "${key}" is reserved and cannot be specified.`
);
}
claims[key] = developerClaims[key];
}
}
}
return this.signer
.getAccountId()
.then(async (account) => {
const header: JWTHeader = {
alg: this.signer.algorithm,
typ: "JWT",
};
const iat = Math.floor(Date.now() / 1000);
const body: JWTBody = {
aud: FIREBASE_AUDIENCE,
iat,
exp: iat + ONE_HOUR_IN_SECONDS,
iss: account,
sub: account,
uid,
};
if (this.tenantId) {
body.tenant_id = this.tenantId;
}
if (Object.keys(claims).length > 0) {
body.claims = claims;
}
const token = `${FirebaseTokenGenerator.encodeSegment(
header
)}.${FirebaseTokenGenerator.encodeSegment(body)}`;
const signPromise = await this.signer.sign(token);
return Promise.all([token, signPromise]);
})
.then(([token, signature]) => {
return `${token}.${signature}`;
})
.catch((err) => {
throw handleCryptoSignerError(err);
});
}
private static encodeSegment(segment: object | string): string {
if (typeof segment === "object") {
return objectToBase64(segment);
}
return stringToBase64(segment);
}
private static isDeveloperClaimsValid_(developerClaims?: object): boolean {
if (typeof developerClaims === "undefined") {
return true;
}
return isNonNullObject(developerClaims);
}
}
export function handleCryptoSignerError(err: Error): Error {
if (!(err instanceof CryptoSignerError)) {
return err;
}
if (
err.code === CryptoSignerErrorCode.SERVER_ERROR &&
isNonNullObject(err.cause)
) {
return new FirebaseAuthError(
AuthClientErrorCode.INTERNAL_ERROR,
"Error returned from server: " +
err.cause?.message +
". Additionally, an " +
"internal error occurred while attempting to extract the " +
"errorcode from the error."
);
}
return new FirebaseAuthError(mapToAuthClientErrorCode(err.code), err.message);
}
function mapToAuthClientErrorCode(code: string): ErrorInfo {
switch (code) {
case CryptoSignerErrorCode.INVALID_CREDENTIAL:
return AuthClientErrorCode.INVALID_CREDENTIAL;
case CryptoSignerErrorCode.INVALID_ARGUMENT:
return AuthClientErrorCode.INVALID_ARGUMENT;
default:
return AuthClientErrorCode.INTERNAL_ERROR;
}
}
export function createFirebaseTokenGenerator(
credential: ServiceAccountCredential,
tenantId?: string
): FirebaseTokenGenerator {
try {
const signer = useEmulator()
? new EmulatedSigner()
: new ServiceAccountSigner(credential);
return new FirebaseTokenGenerator(signer, tenantId);
} catch (err: any) {
throw handleCryptoSignerError(err);
}
}