@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.
327 lines (285 loc) • 9.04 kB
text/typescript
import { useEmulator } from "./firebase";
import { createIdTokenVerifier, DecodedIdToken } from "./token-verifier";
import { AuthClientErrorCode, ErrorInfo, FirebaseAuthError } from "./error";
import { AuthRequestHandler } from "./auth-request-handler";
import { ServiceAccount, ServiceAccountCredential } from "./credential";
import { UserRecord } from "./user-record";
import { createFirebaseTokenGenerator } from "./token-generator";
import * as runtime from "@edge-runtime/ponyfill";
if (
(typeof crypto === "undefined" || typeof global.crypto === "undefined") &&
Boolean(runtime?.crypto?.subtle)
) {
(global as any).crypto = runtime.crypto;
}
if (
(typeof caches === "undefined" || typeof global.caches === "undefined") &&
Boolean(runtime?.caches?.open)
) {
(global as any).caches = runtime.caches;
}
const getCustomTokenEndpoint = (apiKey: string) => {
if (useEmulator()) {
return `http://${process.env
.FIREBASE_AUTH_EMULATOR_HOST!}/identitytoolkit.googleapis.com/v1/accounts:signInWithCustomToken?key=${apiKey}`;
}
return `https://identitytoolkit.googleapis.com/v1/accounts:signInWithCustomToken?key=${apiKey}`;
};
const getRefreshTokenEndpoint = (apiKey: string) => {
if (useEmulator()) {
return `http://${process.env
.FIREBASE_AUTH_EMULATOR_HOST!}/securetoken.googleapis.com/v1/token?key=${apiKey}`;
}
return `https://securetoken.googleapis.com/v1/token?key=${apiKey}`;
};
export async function customTokenToIdAndRefreshTokens(
customToken: string,
firebaseApiKey: string
): Promise<IdAndRefreshTokens> {
const refreshTokenResponse = await fetch(
getCustomTokenEndpoint(firebaseApiKey),
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
token: customToken,
returnSecureToken: true,
}),
}
);
const refreshTokenJSON =
(await refreshTokenResponse.json()) as DecodedIdToken;
if (!refreshTokenResponse.ok) {
throw new Error(
`Problem getting a refresh token: ${JSON.stringify(refreshTokenJSON)}`
);
}
return {
idToken: refreshTokenJSON.idToken,
refreshToken: refreshTokenJSON.refreshToken,
};
}
interface ErrorResponse {
error: {
code: number;
message: "USER_NOT_FOUND" | "TOKEN_EXPIRED";
status: "INVALID_ARGUMENT";
};
error_description?: string;
}
interface UserNotFoundResponse extends ErrorResponse {
error: {
code: 400;
message: "USER_NOT_FOUND";
status: "INVALID_ARGUMENT";
};
}
const isUserNotFoundResponse = (
data: unknown
): data is UserNotFoundResponse => {
return (
(data as UserNotFoundResponse)?.error?.code === 400 &&
(data as UserNotFoundResponse)?.error?.message === "USER_NOT_FOUND"
);
};
const refreshExpiredIdToken = async (
refreshToken: string,
apiKey: string
): Promise<string> => {
// https://firebase.google.com/docs/reference/rest/auth/#section-refresh-token
const response = await fetch(getRefreshTokenEndpoint(apiKey), {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
body: `grant_type=refresh_token&refresh_token=${refreshToken}`,
cache: "no-store",
});
if (!response.ok) {
const data = await response.json();
const errorMessage = `Error fetching access token: ${JSON.stringify(
data.error
)} ${data.error_description ? `(${data.error_description})` : ""}`;
if (isUserNotFoundResponse(data)) {
throw new FirebaseAuthError(AuthClientErrorCode.USER_NOT_FOUND);
}
throw new FirebaseAuthError(
AuthClientErrorCode.INVALID_CREDENTIAL,
errorMessage
);
}
const data = await response.json();
return data.id_token;
};
export function isUserNotFoundError(
error: unknown
): error is FirebaseAuthError {
return (
(error as FirebaseAuthError)?.code ===
`auth/${AuthClientErrorCode.USER_NOT_FOUND.code}`
);
}
export function isInvalidCredentialError(
error: unknown
): error is FirebaseAuthError {
return (
(error as FirebaseAuthError)?.code ===
`auth/${AuthClientErrorCode.INVALID_CREDENTIAL.code}`
);
}
export async function handleExpiredToken<T>(
verifyIdToken: () => Promise<T>,
onExpired: (e: FirebaseAuthError) => Promise<T>,
onError: (e: unknown) => Promise<T>
): Promise<T> {
try {
return await verifyIdToken();
} catch (e: any) {
// https://firebase.google.com/docs/reference/node/firebase.auth.Error
switch ((e as FirebaseAuthError).code) {
case "auth/invalid-user-token":
case "auth/user-token-expired":
case "auth/user-disabled":
return onError(e);
case "auth/id-token-expired":
case "auth/argument-error":
try {
return await onExpired(e);
} catch (e) {
return onError(e);
}
default:
return onError(e);
}
}
}
export interface IdAndRefreshTokens {
idToken: string;
refreshToken: string;
}
export interface Tokens {
decodedToken: DecodedIdToken;
token: string;
}
export function getFirebaseAuth(
serviceAccount: ServiceAccount,
apiKey: string
) {
const authRequestHandler = new AuthRequestHandler(serviceAccount);
const credential = new ServiceAccountCredential(serviceAccount);
const tokenGenerator = createFirebaseTokenGenerator(credential);
const handleTokenRefresh = async (
refreshToken: string,
firebaseApiKey: string
): Promise<Tokens> => {
const newToken = await refreshExpiredIdToken(refreshToken, firebaseApiKey);
const decodedToken = await verifyIdToken(newToken);
return {
decodedToken: decodedToken,
token: newToken,
};
};
async function getUser(uid: string): Promise<UserRecord> {
return authRequestHandler.getAccountInfoByUid(uid).then((response: any) => {
// Returns the user record populated with server response.
return new UserRecord(response.users[0]);
});
}
async function verifyDecodedJWTNotRevokedOrDisabled(
decodedIdToken: DecodedIdToken,
revocationErrorInfo: ErrorInfo
): Promise<DecodedIdToken> {
// Get tokens valid after time for the corresponding user.
return getUser(decodedIdToken.sub).then((user: UserRecord) => {
if (user.disabled) {
throw new FirebaseAuthError(
AuthClientErrorCode.USER_DISABLED,
"The user record is disabled."
);
}
// If no tokens valid after time available, token is not revoked.
if (user.tokensValidAfterTime) {
// Get the ID token authentication time and convert to milliseconds UTC.
const authTimeUtc = decodedIdToken.auth_time * 1000;
// Get user tokens valid after time in milliseconds UTC.
const validSinceUtc = new Date(user.tokensValidAfterTime).getTime();
// Check if authentication time is older than valid since time.
if (authTimeUtc < validSinceUtc) {
throw new FirebaseAuthError(revocationErrorInfo);
}
}
// All checks above passed. Return the decoded token.
return decodedIdToken;
});
}
async function verifyIdToken(
idToken: string,
checkRevoked = false
): Promise<DecodedIdToken> {
const isEmulator = useEmulator();
const idTokenVerifier = createIdTokenVerifier(serviceAccount.projectId);
const decodedIdToken = await idTokenVerifier.verifyJWT(idToken, isEmulator);
if (checkRevoked) {
return verifyDecodedJWTNotRevokedOrDisabled(
decodedIdToken,
AuthClientErrorCode.ID_TOKEN_REVOKED
);
}
return decodedIdToken;
}
async function verifyAndRefreshExpiredIdToken(
token: string,
refreshToken: string
): Promise<Tokens | null> {
return await handleExpiredToken(
async () => {
const decodedToken = await verifyIdToken(token);
return { token, decodedToken };
},
async () => {
if (refreshToken) {
return handleTokenRefresh(refreshToken, apiKey);
}
return null;
},
async () => {
return null;
}
);
}
function createCustomToken(
uid: string,
developerClaims?: object
): Promise<string> {
return tokenGenerator.createCustomToken(uid, developerClaims);
}
async function getCustomIdAndRefreshTokens(
idToken: string,
firebaseApiKey: string
) {
const tenant = await verifyIdToken(idToken);
const customToken = await createCustomToken(tenant.uid);
return customTokenToIdAndRefreshTokens(customToken, firebaseApiKey);
}
async function deleteUser(uid: string): Promise<void> {
await authRequestHandler.deleteAccount(uid);
}
async function setCustomUserClaims(
uid: string,
customUserClaims: object | null
) {
await authRequestHandler.setCustomUserClaims(uid, customUserClaims);
}
return {
verifyAndRefreshExpiredIdToken,
verifyIdToken,
createCustomToken,
getCustomIdAndRefreshTokens,
handleTokenRefresh,
deleteUser,
setCustomUserClaims,
getUser,
};
}