All files / src/middleware tlsAuthentication.coffee

29.58% Statements 21/71
0% Branches 0/8
0% Functions 0/12
30% Lines 21/70
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 1591x 1x 1x 1x 1x 1x 1x 1x   1x 1x 1x 1x 1x 1x   1x 1x         1x                             1x                                                                         1x                                                                                               1x         1x                                                              
fs = require "fs"
Q = require "q"
Client = require("../model/clients").Client
Keystore = require("../model/keystore").Keystore
logger = require "winston"
utils = require '../utils'
pem = require 'pem'
rootCAs = require('ssl-root-cas/latest').rootCas
 
config = require '../config/config'
config.tlsClientLookup = config.get('tlsClientLookup')
statsdServer = config.get 'statsd'
application = config.get 'application'
SDC = require 'statsd-client'
os = require 'os'
 
domain = "#{os.hostname()}.#{application.name}.appMetrics"
sdc = new SDC statsdServer
 
###
# Fetches the trusted certificates, callsback with an array of certs.
###
exports.getTrustedClientCerts = (done) ->
  Keystore.findOne (err, keystore) ->
    done err, null if err
    certs = rootCAs
    if keystore.ca?
      for cert in keystore.ca
        certs.push cert.data
 
    return done null, certs
 
###
# Gets server options object for use with a HTTPS node server
#
# mutualTLS is a boolean, when true mutual TLS authentication is enabled
###
exports.getServerOptions = (mutualTLS, done) ->
  Keystore.findOne (err, keystore) ->
    if err
      logger.error "Could not fetch keystore: #{err}"
      return done err
 
    if keystore?
      options =
        key:  keystore.key
        cert: keystore.cert.data
 
      #if key has password add it to the options
      if keystore.passphrase
        options.passphrase = keystore.passphrase
 
    else
      return done(new Error 'Keystore does not exist')
 
    if mutualTLS
      exports.getTrustedClientCerts (err, certs) ->
        if err
          logger.error "Could not fetch trusted certificates: #{err}"
          return done err, null
 
        options.ca = certs
        options.requestCert = true
        options.rejectUnauthorized = false  # we test authority ourselves
        done null, options
    else
      done null, options
 
###
# A promise returning function that lookups up a client via the given cert fingerprint,
# if not found and config.tlsClientLookup.type is 'in-chain' then the function will
# recursively walk up the certificate chain and look for clients with certificates
# higher in the chain.
###
clientLookup = (fingerprint, subjectCN, issuerCN) ->
  logger.debug "Looking up client linked to cert with fingerprint #{fingerprint} with subject #{subjectCN} and issuer #{issuerCN}"
  deferred = Q.defer()
 
  Client.findOne certFingerprint: fingerprint, (err, result) ->
    deferred.reject err if err
 
    if result?
      # found a match
      return deferred.resolve result
 
    if subjectCN is issuerCN
      # top certificate reached
      return deferred.resolve null
 
    if config.tlsClientLookup.type is 'in-chain'
      # walk further up and cert chain and check
      utils.getKeystore (err, keystore) ->
        deferred.reject err if err
        missedMatches = 0
        # find the isser cert
        if not keystore.ca? || keystore.ca.length < 1
          logger.info "Issuer cn=#{issuerCN} for cn=#{subjectCN} not found in keystore."
          deferred.resolve null
        else
          for cert in keystore.ca
            do (cert) ->
              pem.readCertificateInfo cert.data, (err, info) ->
                if err
                  return deferred.reject err
 
                if info.commonName is issuerCN
                  promise = clientLookup cert.fingerprint, info.commonName, info.issuer.commonName
                  promise.then (result) -> deferred.resolve result
                else
                  missedMatches++
 
                if missedMatches is keystore.ca.length
                  logger.info "Issuer cn=#{issuerCN} for cn=#{subjectCN} not found in keystore."
                  deferred.resolve null
    else
      if config.tlsClientLookup.type isnt 'strict'
        logger.warn "tlsClientLookup.type config option does not contain a known value, defaulting to 'strict'. Available options are 'strict' and 'in-chain'."
      deferred.resolve null
 
  return deferred.promise
 
if process.env.NODE_ENV == "test"
  exports.clientLookup = clientLookup
 
###
# Koa middleware for mutual TLS authentication
###
exports.koaMiddleware = (next) ->
  startTime = new Date() if statsdServer.enabled
  if this.authenticated?
    yield next
  else
    if this.req.client.authorized is true
      cert = this.req.connection.getPeerCertificate true
      logger.info "#{cert.subject.CN} is authenticated via TLS."
 
      # lookup client by cert fingerprint and set them as the authenticated user
      try
        this.authenticated = yield clientLookup cert.fingerprint, cert.subject.CN, cert.issuer.CN
      catch err
        logger.error "Failed to lookup client: #{err}"
 
      if this.authenticated?
        if this.authenticated.clientID?
          this.header['X-OpenHIM-ClientID'] = this.authenticated.clientID
        sdc.timing "#{domain}.tlsAuthenticationMiddleware", startTime if statsdServer.enabled
        this.authenticationType = 'tls'
        yield next
      else
        this.authenticated = null
        logger.info "Certificate Authentication Failed: the certificate's fingerprint #{cert.fingerprint} did not match any client's certFingerprint attribute, trying next auth mechanism if any..."
        sdc.timing "#{domain}.tlsAuthenticationMiddleware", startTime if statsdServer.enabled
        yield next
    else
      this.authenticated = null
      logger.info "Could NOT authenticate via TLS: #{this.req.client.authorizationError}, trying next auth mechanism if any..."
      sdc.timing "#{domain}.tlsAuthenticationMiddleware", startTime if statsdServer.enabled
      yield next