UNPKG

firebase-auth-cloudflare-workers

Version:

Zero-dependencies firebase auth library for Cloudflare Workers.

89 lines (88 loc) 3.57 kB
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; } };