UNPKG

embassy

Version:

Simple JSON Web Tokens (JWT) with embedded scopes for services

300 lines (275 loc) 8.41 kB
/* * Embassy * Copyright (c) 2017-2021 Tom Shawver */ export { JsonWebTokenError, NotBeforeError, TokenExpiredError } from 'jsonwebtoken' /** * An array of all supported asymmetric signing algorithms */ export const asymmetricAlgorithms: Readonly<string[]> = [ 'RS256', 'RS384', 'RS512', 'PS256', 'PS384', 'PS512', 'ES256', 'ES384', 'ES512' ] /** * An array of all supported symmetric signing algorithms */ export const symmetricAlgorithms: Readonly<string[]> = [ 'HS256', 'HS384', 'HS512' ] export type Serializable = string | number | boolean | null export type ClaimValue = Serializable | Record<string, Serializable> export interface DomainScopes { [domain: string]: string[] } export interface DomainScopeMap { [domain: string]: { [scope: string]: number } } export type DomainKey = { domain: string key: string } export interface ScopeComponents { /** The scope's index, from the domainScopes map */ idx: number /** The index of the byte inside the blob that contains this scope's bit */ offset: number /** The full array of bytes defining this domain's scope blob */ blob: Uint8Array /** The individual target byte of the blob (same as `blob[offset]`) */ byte: number /** The bit mask targeting the individual scope bit in the provided byte */ mask: number } export type AsymmetricAlgorithm = typeof asymmetricAlgorithms[number] export type SymmetricAlgorithm = typeof symmetricAlgorithms[number] export type SigningAlgorithm = SymmetricAlgorithm | AsymmetricAlgorithm export type PrivateKeyDefinition = { privateKey: string algorithm: SigningAlgorithm } export type KeyDefinition = | { algorithm: SigningAlgorithm privateKey: string publicKey?: string } | { algorithm: AsymmetricAlgorithm privateKey?: string publicKey: string } /** * A function to be called iteratively for multiple domain/scope pairs. * * @param domain - The domain of a scope * @param scope - The scope string * @param breakFn - A function to be called to prevent the loop from continuing */ export type ScopeLoopFunction = ( domain: string, scope: string, breakFn: () => void ) => void | Promise<void> export interface ManualClaims { sub?: string iss?: string aud?: string scope?: string [key: string]: ClaimValue | Record<string, ClaimValue> } export interface Claims extends ManualClaims { iat?: number exp?: number nbf?: number jti?: string } export interface JWTHeader { alg: SigningAlgorithm typ: 'JWT' kid?: string [custom: string]: Serializable } export interface CommonClaimsOptions { /** * The audience string with which to sign and verify tokens by default. */ audience?: string /** * The issuer string with which to sign and verify tokens by default. */ issuer?: string } export interface ExpiringClaimsOptions extends CommonClaimsOptions { /** * The number of seconds after which a newly signed token should expire, by * default. * * @defaultValue 3600 */ expiresInSecs?: number } export interface EmbassyOptions extends ExpiringClaimsOptions { /** * A mapping of domain to maps of scopes to their index under that name. For * example, if the "users" domain has "editSelf" and "editAll" scopes, the * domainScopes might appear as * * ``` * { * users: { * editSelf: 0, * editAll: 1 * } * } * ``` * * The index of each scope within a domain should start at 0 and increment by * 1 with every new scope, never repeating a number. * * @remarks * Once an index has been set, it should never be changed as currently issued * and valid tokens refer to their scopes by index number. This format is used * so that scopes that become inapplicable after time can be deleted without * shifting the indexes of scopes that come after them. */ domainScopes?: DomainScopeMap /** * A mapping of key IDs to KeyDefinitions to check initially before any calls * to find external keys are made. * * @remarks * **IMPORTANT NOTE:** This object will be mutated in order to cache keys for * future token signing and verification. If it's important that the original * object remain unmodified, clone it before passing it in. */ keys?: Record<string, KeyDefinition> /** * A function to update (or retrieve for the first time) the domainScopes map. * When a scope is requested that does not exist in the currently known map, * this function will be called to update the map and look for the scope * before giving up and throwing an Error. Must return, or resolve to, a new * `DomainScopeMap`. */ refreshScopes?: () => DomainScopeMap | Promise<DomainScopeMap> /** * The number of milliseconds that must pass before the scopes can be * refreshed again. If `refreshScopes` is called and a new, unknown scope * is encountered within this amount of time from that call, an Error will be * thrown rather than refreshing the scopes. * * @defaultValue 1000 */ refreshScopesAfterMs?: number /** * A function to be called when attempting to use a currently-unknown key ID * to either: * * - Sign a Token * - Verify a Token that was signed using a shared-secret symmetric algorithm * in the HMAC family * * The function takes a key ID and should return or resolve to a * `PrivateKeyDefinition` with the `algorithm` of the key, and a `privateKey` * property with either the PEM-formatted asymmetric key or shared secret. * * @param kid - The ID of the key to be retrieved * @returns A private key definition for the supplied `kid`, or a promise that * resolves to one. */ getPrivateKey?: ( kid: string ) => PrivateKeyDefinition | Promise<PrivateKeyDefinition> /** * A function to be called when attempting to verify a token that was * signed with a currently-unknown key ID using an asymmetric algorithm. It * takes the key ID as its only argument, and must return a PEM-formatted * public key. * * @remarks * When verifying a token signed with HMAC, `getPrivateKey` is used since * symmetric keys should never be considered "public". This distinction is * made automatically based on the algorithm header of the token being * verified. * * @param kid - The ID of the key to be retrieved * @returns The PEM-encoded public key associated with the supplied `kid`. */ getPublicKey?: (kid: string) => string | Promise<string> } export interface TokenOptions extends EmbassyOptions { /** A mapping of default claims to add to each new JWT, unless overridden. */ claims?: ManualClaims /** A token string to be decoded and parsed to initialize this Token */ token?: string } export interface TokenSigningOptions extends ExpiringClaimsOptions { /** * The ID of the intended user of this token. Optional only if a 'sub' claim * has already been set. */ subject?: string /** * If true, this method will not generate an 'iat' (Issued At) claim. * * @defaultValue false */ noTimestamp?: boolean /** Additional header properties to set. Avoid for most use cases. */ header?: Partial<JWTHeader> } export interface TokenVerificationOptions extends CommonClaimsOptions { /** * List of strings with the names of the allowed algorithms. Allows all * algorithms if omitted. * * @example * ```typescript * ["HS256", "HS384"] * ``` */ algorithms?: SigningAlgorithm[] /** * `true` to allow expired tokens to pass verification checks, `false` * otherwise */ ignoreExpiration?: boolean /** * If specified, will fail verification if the token is older than the * specified number of seconds */ maxAgeSecs?: number /** * The seconds of buffer to allow for differences between machine times when * verifying the token. * * @defaultValue 5 */ clockToleranceSecs?: number /** * A nonce to be verified against the `nonce` claim. Useful for Open ID's ID * tokens. */ nonce?: string /** * The key to use to verify the token's signature. If omitted, Embassy will * look in the `keys` property passed to the constructor, or execute * `getPrivateKey` (for symmetric algorithms) or `getPublicKey` * (for asymmetric) if it's not found in `keys`. */ key?: string }