NaturalCycles/nodejs-lib

View on GitHub
src/jwt/jwt.service.ts

Summary

Maintainability
A
0 mins
Test Coverage
A
95%
import { _assert, _errorDataAppend, AnyObject, ErrorData, JWTString } from '@naturalcycles/js-lib'
import { AnySchema } from 'joi'
import type { Algorithm, VerifyOptions, JwtHeader, SignOptions } from 'jsonwebtoken'
import jsonwebtoken from 'jsonwebtoken'
import { anyObjectSchema } from '../validation/joi/joi.shared.schemas'
import { validate } from '../validation/joi/joi.validation.util'
export { jsonwebtoken }
export type { Algorithm, VerifyOptions, SignOptions, JwtHeader }

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<T extends AnyObject>(payload: T, schema?: AnySchema<T>, opt: SignOptions = {}): JWTString {
    _assert(
      this.cfg.privateKey,
      'JWTService: privateKey is required to be able to verify, but not provided',
    )

    if (schema) {
      validate(payload, schema)
    }

    return jsonwebtoken.sign(payload, this.cfg.privateKey, {
      algorithm: this.cfg.algorithm,
      noTimestamp: true,
      ...this.cfg.signOptions,
      ...opt,
    })
  }

  verify<T extends AnyObject>(
    token: JWTString,
    schema?: AnySchema<T>,
    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

      if (schema) {
        validate(data, schema)
      }

      return data
    } catch (err) {
      if (this.cfg.errorData) {
        _errorDataAppend(err, {
          ...this.cfg.errorData,
        })
      }
      throw err
    }
  }

  decode<T extends AnyObject>(
    token: JWTString,
    schema?: AnySchema<T>,
  ): {
    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,
    })

    validate(data.payload, schema || anyObjectSchema)

    return data
  }
}