# frozen_string_literal: true module Rails module Auth module X509 # Raised when certificate verification is mandatory CertificateVerifyFailed = Class.new(NotAuthorizedError) # Validates X.509 client certificates and adds principal objects for valid # clients to the rack environment as env["rails-auth.principals"]["x509"] class Middleware # Create a new X.509 Middleware object # # @param [Object] app next app in the Rack middleware chain # @param [Hash] cert_filters maps Rack environment names to cert extractors # @param [String] ca_file path to the CA bundle to verify client certs with # @param [OpenSSL::X509::Store] truststore (optional) provide your own truststore (for e.g. CRLs) # @param [Boolean] require_cert causes middleware to raise if certs are unverified # # @return [Rails::Auth::X509::Middleware] new X509 middleware instance def initialize(app, cert_filters: {}, ca_file: nil, truststore: nil, require_cert: false, logger: nil) fail ArgumentError, "no ca_file given" unless ca_file @app = app @logger = logger @truststore = truststore || OpenSSL::X509::Store.new.add_file(ca_file) @require_cert = require_cert @cert_filters = cert_filters @cert_filters.each do |key, filter| next unless filter.is_a?(Symbol) # Shortcut syntax for symbols @cert_filters[key] = Rails::Auth::X509::Filter.const_get(filter.to_s.capitalize).new end end def call(env) principal = extract_principal(env) Rails::Auth.add_principal(env, "x509".freeze, principal.freeze) if principal @app.call(env) end private def extract_principal(env) @cert_filters.each do |key, filter| raw_cert = env[key] next unless raw_cert cert = filter.call(raw_cert) next unless cert if @truststore.verify(cert) log("Verified", cert) return Rails::Auth::X509::Principal.new(cert) else log("Verify FAILED", cert) fail CertificateVerifyFailed, "verify failed: #{subject(cert)}" if @require_cert end end fail CertificateVerifyFailed, "no client certificate in request" if @require_cert nil end def log(message, cert) @logger.debug("rails-auth: #{message} (#{subject(cert)})") if @logger end def subject(cert) cert.subject.to_a.map { |attr, data| "#{attr}=#{data}" }.join(",") end end end end end