All files / src IDToken.js

100% Statements 58/58
100% Branches 24/24
100% Functions 10/10
100% Lines 56/56
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167      1x 1x 1x 1x   1x 1x                     12x                                                       7x   7x   7x 7x 7x 7x   7x   7x 7x 7x   7x 7x   7x 7x   7x   7x             4x   4x 4x 4x       4x 3x 3x 3x 3x       1x 1x 1x 1x     4x     4x             4x   4x   4x       4x       4x       4x                             8x 4x 4x   4x 4x 4x 4x           7x 7x       1x 1x         1x  
/**
 * Local dependencies
 */
const crypto = require('@trust/webcrypto')
const base64url = require('base64url')
const {JWT} = require('@trust/jose')
const IDTokenSchema = require('./schemas/IDTokenSchema')
 
const DEFAULT_MAX_AGE = 3600  // Default ID token expiration, in seconds
const DEFAULT_SIG_ALGORITHM = 'RS256'
 
/**
 * IDToken
 */
class IDToken extends JWT {
 
  /**
   * Schema
   */
  static get schema () {
    return IDTokenSchema
  }
 
  /**
   * issue
   *
   * @param provider {Provider} OIDC Identity Provider issuing the token
   * @param provider.issuer {string} Provider URI
   * @param provider.keys {KeyChain}
   *
   * @param options {Object}
   * @param options.aud {string|Array<string>} Audience for the token
   *   (such as the Relying Party client_id)
   * @param options.sub {string} Subject id for the token (opaque, unique to
   *   the issuer)
   * @param options.nonce {string} Nonce generated by Relying Party
   *
   * Optional:
   * @param [options.alg] {string} Algorithm for signing the id token
   * @param [options.jti] {string} Unique JWT id (to prevent reuse)
   * @param [options.iat] {number} Issued at timestamp (in seconds)
   * @param [options.max] {number} Max token lifetime in seconds
   * @param [options.at_hash] {string} Access Token Hash
   * @param [options.c_hash] {string} Code hash
   *
   * @returns {IDToken} ID Token (JWT instance)
   */
  static issue (provider, options) {
    let { issuer, keys } = provider
 
    let { aud, sub, nonce, at_hash, c_hash } = options
 
    let alg = options.alg || DEFAULT_SIG_ALGORITHM
    let jti = options.jti || IDToken.random(8)
    let iat = options.iat || Math.floor(Date.now() / 1000)
    let max = options.max || DEFAULT_MAX_AGE
 
    let exp = iat + max  // token expiration
 
    let iss = issuer
    let key = keys['id_token'].signing[alg].privateKey
    let kid = keys['id_token'].signing[alg].publicJwk.kid
 
    let header = { alg, kid }
    let payload = { iss, aud, sub, exp, iat, jti, nonce }
 
    if (at_hash) { payload.at_hash = at_hash }
    if (c_hash) { payload.c_hash = c_hash }
 
    let jwt = new IDToken({ header, payload, key })
 
    return jwt
  }
 
  /**
   * issueForRequest
   */
  static issueForRequest (request, response) {
    let {params, code, provider, client, subject} = request
 
    let alg = client['id_token_signed_response_alg'] || DEFAULT_SIG_ALGORITHM
    let jti = IDToken.random(8)
    let iat = Math.floor(Date.now() / 1000)
    let aud, sub, max, nonce
 
    // authentication request
    if (!code) {
      aud = client['client_id']
      sub = subject['_id']
      max = parseInt(params['max_age']) || client['default_max_age'] || DEFAULT_MAX_AGE
      nonce = params.nonce
 
    // token request
    } else {
      aud = code.aud
      sub = code.sub
      max = parseInt(code['max']) || client['default_max_age'] || DEFAULT_MAX_AGE
      nonce = code.nonce
    }
 
    let len = alg.match(/(256|384|512)$/)[0]
 
    // generate hashes
    return Promise.all([
      IDToken.hashClaim(response['access_token'], len),
      IDToken.hashClaim(response['code'], len)
    ])
 
      // build the id_token
      .then(hashes => {
        let [at_hash, c_hash] = hashes
 
        let options = { alg, aud, sub, iat, jti, nonce, at_hash, c_hash }
 
        return IDToken.issue(provider, options)
      })
 
      // sign id token
      .then(jwt => jwt.encode())
 
      // add to response
      .then(compact => {
        response['id_token'] = compact
      })
 
      // resolve the response
      .then(() => response)
  }
 
  /**
   * hashClaim
   *
   * @description
   * Create a hash for at_hash or c_hash claim
   *
   * @param {string} token
   * @param {string} hashLength
   *
   * @returns {Promise<string>}
   */
  static hashClaim (value, hashLength) {
    if (value) {
      let alg = { name: `SHA-${hashLength}`}
      let octets = new Buffer(value, 'ascii')
 
      return crypto.subtle.digest(alg, new Uint8Array(octets)).then(digest => {
        let hash = Buffer.from(digest)
        let half = hash.slice(0, hash.byteLength / 2)
        return base64url(half)
      })
    }
  }
 
  static random (byteLen) {
    let value = crypto.getRandomValues(new Uint8Array(byteLen))
    return Buffer.from(value).toString('hex')
  }
}
 
IDToken.DEFAULT_MAX_AGE = DEFAULT_MAX_AGE
IDToken.DEFAULT_SIG_ALGORITHM = DEFAULT_SIG_ALGORITHM
 
/**
 * Export
 */
module.exports = IDToken