firebase-auth-cloudflare-workers
Version:
Zero-dependencies firebase auth library for Cloudflare Workers.
89 lines (88 loc) • 3.57 kB
JavaScript
import { decodeBase64Url } from './base64';
import { JwtError, JwtErrorCode } from './errors';
import { utf8Decoder, utf8Encoder } from './utf8';
import { isNonEmptyString, isNumber, isString } from './validator';
export class RS256Token {
rawToken;
decodedToken;
constructor(rawToken, decodedToken) {
this.rawToken = rawToken;
this.decodedToken = decodedToken;
}
/**
*
* @param token - The JWT to verify.
* @param currentTimestamp - Current timestamp in seconds since the Unix epoch.
* @param skipVerifyHeader - skip verification header content if true.
* @throw Error if the token is invalid.
* @returns
*/
static decode(token, currentTimestamp, skipVerifyHeader = false) {
const tokenParts = token.split('.');
if (tokenParts.length !== 3) {
throw new JwtError(JwtErrorCode.INVALID_ARGUMENT, 'token must consist of 3 parts');
}
const header = decodeHeader(tokenParts[0], skipVerifyHeader);
const payload = decodePayload(tokenParts[1], currentTimestamp);
return new RS256Token(token, {
header,
payload,
signature: decodeBase64Url(tokenParts[2]),
});
}
getHeaderPayloadBytes() {
const rawToken = this.rawToken;
// `${token.header}.${token.payload}`
const trimmedSignature = rawToken.substring(0, rawToken.lastIndexOf('.'));
return utf8Encoder.encode(trimmedSignature);
}
}
const decodeHeader = (headerPart, skipVerifyHeader) => {
const header = decodeBase64JSON(headerPart);
if (skipVerifyHeader) {
return header;
}
const kid = header.kid;
if (!isString(kid)) {
throw new JwtError(JwtErrorCode.NO_KID_IN_HEADER, `kid must be a string but got ${kid}`);
}
const alg = header.alg;
if (isString(alg) && alg !== 'RS256') {
throw new JwtError(JwtErrorCode.INVALID_ARGUMENT, `algorithm must be RS256 but got ${alg}`);
}
return header;
};
const decodePayload = (payloadPart, currentTimestamp) => {
const payload = decodeBase64JSON(payloadPart);
if (!isNonEmptyString(payload.aud)) {
throw new JwtError(JwtErrorCode.INVALID_ARGUMENT, `"aud" claim must be a string but got "${payload.aud}"`);
}
if (!isNonEmptyString(payload.sub)) {
throw new JwtError(JwtErrorCode.INVALID_ARGUMENT, `"sub" claim must be a string but got "${payload.sub}"`);
}
if (!isNonEmptyString(payload.iss)) {
throw new JwtError(JwtErrorCode.INVALID_ARGUMENT, `"iss" claim must be a string but got "${payload.iss}"`);
}
if (!isNumber(payload.iat)) {
throw new JwtError(JwtErrorCode.INVALID_ARGUMENT, `"iat" claim must be a number but got "${payload.iat}"`);
}
if (currentTimestamp < payload.iat) {
throw new JwtError(JwtErrorCode.INVALID_ARGUMENT, `Incorrect "iat" claim must be a older than "${currentTimestamp}" (iat: "${payload.iat}")`);
}
if (!isNumber(payload.exp)) {
throw new JwtError(JwtErrorCode.INVALID_ARGUMENT, `"exp" claim must be a number but got "${payload.exp}"`);
}
if (currentTimestamp > payload.exp) {
throw new JwtError(JwtErrorCode.TOKEN_EXPIRED, `Incorrect "exp" (expiration time) claim must be a newer than "${currentTimestamp}" (exp: "${payload.exp}")`);
}
return payload;
};
const decodeBase64JSON = (b64Url) => {
const decoded = decodeBase64Url(b64Url);
try {
return JSON.parse(utf8Decoder.decode(decoded));
}
catch {
return null;
}
};