import type { ErrorData } from '@naturalcycles/js-lib/error' import { _assert } from '@naturalcycles/js-lib/error/assert.js' import { _errorDataAppend } from '@naturalcycles/js-lib/error/error.util.js' import type { AnyObject, JWTString } from '@naturalcycles/js-lib/types' import type { Algorithm, JwtHeader, SignOptions, VerifyOptions } from 'jsonwebtoken' import jsonwebtoken from 'jsonwebtoken' import type { AjvSchema, JSchema } from '../validation/ajv/jSchema.js' export { jsonwebtoken } export type { Algorithm, JwtHeader, SignOptions, VerifyOptions } export interface JWTServiceCfg { /** * Public key is required to Verify incoming tokens. * Optional if you only want to Decode or Sign. */ publicKey?: string | Buffer /** * Private key is required to Sign (create) outgoing tokens. * Optional if you only want to Decode or Verify. */ privateKey?: string | Buffer /** * Recommended: ES256 * Keys (private/public) should be generated using proper settings * that fit the used Algorithm. */ algorithm: Algorithm /** * If provided - will be applied to every Sign operation. */ signOptions?: SignOptions /** * If provided - will be applied to every Sign operation. */ verifyOptions?: VerifyOptions /** * If set - errors thrown from this service will be extended * with this errorData (in err.data) */ errorData?: ErrorData } // todo: define JWTError and list possible options // jwt expired (TokenExpiredError) // jwt invalid // jwt token is empty /** * Wraps popular `jsonwebtoken` library. * You should create one instance of JWTService for each pair of private/public key. * * Generate key pair like this. * Please note that parameters should be different for different algorithms. * For ES256 (default algo in JWTService) key should have `prime256v1` parameter: * * openssl ecparam -name prime256v1 -genkey -noout -out key.pem * openssl ec -in key.pem -pubout > key.pub.pem */ export class JWTService { constructor(public cfg: JWTServiceCfg) {} sign( payload: T, schema?: JSchema | AjvSchema, opt: SignOptions = {}, ): JWTString { _assert( this.cfg.privateKey, 'JWTService: privateKey is required to be able to verify, but not provided', ) schema?.validate(payload) return jsonwebtoken.sign(payload, this.cfg.privateKey, { algorithm: this.cfg.algorithm, noTimestamp: true, ...this.cfg.signOptions, ...opt, }) } verify( token: JWTString, schema?: JSchema | AjvSchema, opt: VerifyOptions = {}, publicKey?: string, // allows to override public key ): T { _assert( this.cfg.publicKey, 'JWTService: publicKey is required to be able to verify, but not provided', ) try { const data = jsonwebtoken.verify(token, publicKey || this.cfg.publicKey, { algorithms: [this.cfg.algorithm], ...this.cfg.verifyOptions, ...opt, }) as T schema?.validate(data) return data } catch (err) { if (this.cfg.errorData) { _errorDataAppend(err, { ...this.cfg.errorData, }) } throw err } } decode( token: JWTString, schema?: JSchema | AjvSchema, ): { header: JwtHeader payload: T signature: string } { const data = jsonwebtoken.decode(token, { complete: true, }) as { header: JwtHeader payload: T signature: string } | null _assert(data?.payload, 'invalid token, decoded value is empty', { ...this.cfg.errorData, }) schema?.validate(data.payload) return data } }