All files / src AccessToken.js

100% Statements 55/55
100% Branches 26/26
100% Functions 10/10
100% Lines 55/55
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 168 169 170 171 172      1x 1x 1x   1x 1x                     12x                                                                                         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 4x         4x         4x     4x 1x     4x 1x 1x         4x       9x 9x       1x 1x         1x  
/**
 * Local dependencies
 */
const {JWT} = require('@trust/jose')
const crypto = require('@trust/webcrypto')
const AccessTokenSchema = require('./schemas/AccessTokenSchema')
 
const DEFAULT_MAX_AGE = 3600  // Default Access token expiration, in seconds
const DEFAULT_SIG_ALGORITHM = 'RS256'
 
/**
 * AccessToken
 */
class AccessToken extends JWT {
 
  /**
   * Schema
   */
  static get schema () {
    return AccessTokenSchema
  }
 
  /**
   * issue
   *
   * Creates an access token issued by a given provider. Useful for integration
   * testing of client app code, to generate access tokens to pass along to
   * `supertest` http requests. Usage (in mocha `beforeEach()`, for example):
   *
   *   ```
   *   // This assumes you have a Provider instance, an RP Client id,
   *   // and a particular user in mind (for the `sub`ject claim)
   *
   *   let options = { aud: rpClientId, sub: userId, scope: 'openid profile' }
   *   let token = AccessToken.issue(provider, options)
 
   *   return token.encode()
   *     .then(compactToken => {
   *       headers['Authorization'] = 'Bearer ' + compactToken
   *     })
   *   ```
   *
   * @param options {Object}
   *
   * Required:
   * @param provider {Provider} OIDC Identity Provider issuing the token
   * @param provider.issuer {string} Provider URI
   * @param provider.keys
   *
   * @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.scope {string} OAuth2 scope
   *
   * Optional:
   * @param [options.alg] {string} Algorithm for signing the access 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
   *
   * @returns {JWT} Access token (JWT instance)
   */
  static issue (provider, options) {
    let { issuer, keys } = provider
 
    let { aud, sub, scope } = options
 
    let alg = options.alg || DEFAULT_SIG_ALGORITHM
    let jti = options.jti || AccessToken.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.token.signing[alg].privateKey
    let kid = keys.token.signing[alg].publicJwk.kid
 
    let header = { alg, kid }
    let payload = { iss, aud, sub, exp, iat, jti, scope }
 
    let jwt = new AccessToken({ header, payload, key })
 
    return jwt
  }
 
  /**
   * issue
   */
  static issueForRequest (request, response) {
    let { params, code, provider, client, subject } = request
 
    let alg = client['access_token_signed_response_alg'] || DEFAULT_SIG_ALGORITHM
    let jti = AccessToken.random(8)
    let iat = Math.floor(Date.now() / 1000)
    let aud, sub, max, scope
 
    // authentication request
    if (!code) {
      aud = client['client_id']
      sub = subject['_id']
      max = parseInt(params['max_age']) || client['default_max_age'] || DEFAULT_MAX_AGE
      scope = request.scope
 
    // token request
    } else {
      aud = code.aud
      sub = code.sub
      max = parseInt(code['max']) || client['default_max_age'] || DEFAULT_MAX_AGE
      scope = code.scope
    }
 
    let options = { aud, sub, scope, alg, jti, iat, max }
 
    let header, payload
 
    return Promise.resolve()
      .then(() => AccessToken.issue(provider, options))
 
      .then(jwt => {
        header = jwt.header
        payload = jwt.payload
 
        return jwt.encode()
      })
 
      // set the response properties
      .then(compact => {
        response['access_token'] = compact
        response['token_type'] = 'Bearer'
        response['expires_in'] = max
      })
 
      // store access token by "jti" claim
      .then(() => {
        return provider.backend.put('tokens', `${jti}`, { header, payload })
      })
 
      // store access token by "refresh_token", if applicable
      .then(() => {
        let responseTypes = request.responseTypes || []
        let refresh
 
        if (code || responseTypes.includes('code')) {
          refresh = AccessToken.random(16)
        }
 
        if (refresh) {
          response['refresh_token'] = refresh
          return provider.backend.put('refresh', `${refresh}`, { header, payload })
        }
      })
 
      // resolve the response
      .then(() => response)
  }
 
  static random (byteLen) {
    let value = crypto.getRandomValues(new Uint8Array(byteLen))
    return Buffer.from(value).toString('hex')
  }
}
 
AccessToken.DEFAULT_MAX_AGE = DEFAULT_MAX_AGE
AccessToken.DEFAULT_SIG_ALGORITHM = DEFAULT_SIG_ALGORITHM
 
/**
 * Export
 */
module.exports = AccessToken