next-firebase-auth-edge
Version:
Next.js Firebase Authentication for Edge and server runtimes. Compatible with latest Next.js features.
96 lines (95 loc) • 4.4 kB
JavaScript
import { decodeJwt, decodeProtectedHeader, errors } from 'jose';
import { ALGORITHM_RS256 } from '../auth/jwt/verify.js';
import { JWKSSignatureVerifier } from '../auth/signature-verifier';
import { FirebaseAppCheckError } from './api-client.js';
const APP_CHECK_ISSUER = 'https://firebaseappcheck.googleapis.com/';
const JWKS_URL = 'https://firebaseappcheck.googleapis.com/v1/jwks';
export class AppCheckTokenVerifier {
credential;
signatureVerifier;
constructor(credential) {
this.credential = credential;
this.signatureVerifier = new JWKSSignatureVerifier(JWKS_URL);
}
async verifyToken(token, options) {
const projectId = await this.credential.getProjectId();
const decoded = await this.decodeAndVerify(token, projectId, options);
const decodedAppCheckToken = decoded.payload;
decodedAppCheckToken.app_id = decodedAppCheckToken.sub;
return decodedAppCheckToken;
}
async decodeAndVerify(token, projectId, options) {
const header = decodeProtectedHeader(token);
const payload = decodeJwt(token);
this.verifyContent({ header, payload }, projectId);
await this.verifySignature(token, options);
return { header, payload };
}
verifyContent(fullDecodedToken, projectId) {
const header = fullDecodedToken.header;
const payload = fullDecodedToken.payload;
const projectIdMatchMessage = ' Make sure the App Check token comes from the same ' +
'Firebase project as the service account used to authenticate this SDK.';
const scopedProjectId = `projects/${projectId}`;
let errorMessage;
if (header.alg !== ALGORITHM_RS256) {
errorMessage =
'The provided App Check token has incorrect algorithm. Expected "' +
ALGORITHM_RS256 +
'" but got ' +
'"' +
header.alg +
'".';
}
else if (!payload.aud?.includes(scopedProjectId)) {
errorMessage =
'The provided App Check token has incorrect "aud" (audience) claim. Expected "' +
scopedProjectId +
'" but got "' +
payload.aud +
'".' +
projectIdMatchMessage;
}
else if (typeof payload.iss !== 'string' ||
!payload.iss.startsWith(APP_CHECK_ISSUER)) {
errorMessage =
'The provided App Check token has incorrect "iss" (issuer) claim.';
}
else if (typeof payload.sub !== 'string') {
errorMessage =
'The provided App Check token has no "sub" (subject) claim.';
}
else if (payload.sub === '') {
errorMessage =
'The provided App Check token has an empty string "sub" (subject) claim.';
}
if (errorMessage) {
throw new FirebaseAppCheckError('invalid-argument', errorMessage);
}
}
verifySignature(jwtToken, options) {
return this.signatureVerifier
.verify(jwtToken, options)
.catch((error) => {
throw this.mapJwtErrorToAppCheckError(error);
});
}
mapJwtErrorToAppCheckError(error) {
if (error instanceof errors.JWTExpired) {
const errorMessage = 'The provided App Check token has expired. Get a fresh App Check token' +
' from your client app and try again.';
return new FirebaseAppCheckError('app-check-token-expired', errorMessage);
}
else if (error instanceof errors.JWSSignatureVerificationFailed) {
const errorMessage = 'The provided App Check token has invalid signature.';
return new FirebaseAppCheckError('invalid-argument', errorMessage);
}
else if (error instanceof errors.JWKSNoMatchingKey) {
const errorMessage = 'The provided App Check token has "kid" claim which does not ' +
'correspond to a known public key. Most likely the provided App Check token ' +
'is expired, so get a fresh token from your client app and try again.';
return new FirebaseAppCheckError('invalid-argument', errorMessage);
}
return new FirebaseAppCheckError('invalid-argument', error.message);
}
}