From d8df043dc12c76233807101985989db9356764aa Mon Sep 17 00:00:00 2001
From: Imre Farkas <ifarkas@gitlab.com>
Date: Tue, 3 Mar 2020 21:16:33 +0100
Subject: [PATCH] Make hostname configurable for smartcard authentication

Hostname is configurable for the endpoint requiring client side
certificate.
---
 ...6-smartcard_support_different_hostname.yml |   5 +
 config/gitlab.yml.example                     |   4 +-
 config/initializers/1_settings.rb             |   1 +
 ee/app/controllers/smartcard_controller.rb    |  97 +++-
 .../devise/sessions/_new_smartcard.html.haml  |   2 +-
 .../sessions/_new_smartcard_ldap.html.haml    |   3 +-
 ee/config/routes/smartcard.rb                 |   9 +-
 ee/spec/requests/smartcard_controller_spec.rb | 434 ++++++++++++------
 8 files changed, 380 insertions(+), 175 deletions(-)
 create mode 100644 changelogs/unreleased/10526-smartcard_support_different_hostname.yml

diff --git a/changelogs/unreleased/10526-smartcard_support_different_hostname.yml b/changelogs/unreleased/10526-smartcard_support_different_hostname.yml
new file mode 100644
index 0000000000000..6990449d171e2
--- /dev/null
+++ b/changelogs/unreleased/10526-smartcard_support_different_hostname.yml
@@ -0,0 +1,5 @@
+---
+title: Make hostname configurable for smartcard authentication
+merge_request: 26411
+author:
+type: added
diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example
index 760688d808805..81085d4641e5b 100644
--- a/config/gitlab.yml.example
+++ b/config/gitlab.yml.example
@@ -752,7 +752,9 @@ production: &base
     # Path to a file containing a CA certificate
     ca_file: '/etc/ssl/certs/CA.pem'
 
-    # Port where the client side certificate is requested by the webserver (NGINX/Apache)
+    # Host and port where the client side certificate is requested by the
+    # webserver (NGINX/Apache)
+    # client_certificate_required_host: smartcard.gitlab.example.com
     # client_certificate_required_port: 3444
 
     # Browser session with smartcard sign-in is required for Git access
diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb
index f22ddc7f08179..1b368346cfaf5 100644
--- a/config/initializers/1_settings.rb
+++ b/config/initializers/1_settings.rb
@@ -77,6 +77,7 @@
 Gitlab.ee do
   Settings['smartcard'] ||= Settingslogic.new({})
   Settings.smartcard['enabled'] = false if Settings.smartcard['enabled'].nil?
+  Settings.smartcard['client_certificate_required_host'] = Settings.gitlab['host'] if Settings.smartcard['client_certificate_required_host'].nil?
   Settings.smartcard['client_certificate_required_port'] = 3444 if Settings.smartcard['client_certificate_required_port'].nil?
   Settings.smartcard['required_for_git_access'] = false if Settings.smartcard['required_for_git_access'].nil?
   Settings.smartcard['san_extensions'] = false if Settings.smartcard['san_extensions'].nil?
diff --git a/ee/app/controllers/smartcard_controller.rb b/ee/app/controllers/smartcard_controller.rb
index 8a7818795b307..ab0d086c95a4a 100644
--- a/ee/app/controllers/smartcard_controller.rb
+++ b/ee/app/controllers/smartcard_controller.rb
@@ -5,25 +5,58 @@ class SmartcardController < ApplicationController
   skip_before_action :verify_authenticity_token
 
   before_action :check_feature_availability
-  before_action :check_certificate_headers
+  before_action :check_certificate_required_host_and_port, only: :extract_certificate
+  before_action :check_ngingx_certificate_header, only: :extract_certificate
+  before_action :check_certificate_param, only: :verify_certificate
 
   def auth
-    certificate = Gitlab::Auth::Smartcard::Certificate.new(certificate_header)
-    sign_in_with(certificate)
+    redirect_to extract_certificate_smartcard_url(extract_certificate_url_options)
   end
 
-  def ldap_auth
-    certificate = Gitlab::Auth::Smartcard::LdapCertificate.new(params[:provider], certificate_header)
-    sign_in_with(certificate)
+  def extract_certificate
+    redirect_to verify_certificate_smartcard_url(verify_certificate_url_options)
+  end
+
+  def verify_certificate
+    sign_in_with(client_certificate)
   end
 
   private
 
+  def extract_certificate_url_options
+    {
+      host: ::Gitlab.config.smartcard.client_certificate_required_host,
+      port: ::Gitlab.config.smartcard.client_certificate_required_port,
+      provider: params[:provider]
+    }.compact
+  end
+
+  def verify_certificate_url_options
+    {
+      host: ::Gitlab.config.gitlab.host,
+      port: ::Gitlab.config.gitlab.port,
+      provider: params[:provider],
+      client_certificate: request.headers['HTTP_X_SSL_CLIENT_CERTIFICATE']
+    }.compact
+  end
+
+  def client_certificate
+    if ldap_provider?
+      Gitlab::Auth::Smartcard::LdapCertificate.new(params[:provider], certificate_param)
+    else
+      Gitlab::Auth::Smartcard::Certificate.new(certificate_param)
+    end
+  end
+
+  def ldap_provider?
+    params[:provider].present?
+  end
+
   def sign_in_with(certificate)
     user = certificate.find_or_create_user
     unless user&.persisted?
       flash[:alert] = _('Failed to signing using smartcard authentication')
-      redirect_to new_user_session_path(port: Gitlab.config.gitlab.port)
+      redirect_to new_user_session_path
 
       return
     end
@@ -33,13 +66,43 @@ def sign_in_with(certificate)
     sign_in_and_redirect(user)
   end
 
+  def nginx_certificate_header
+    request.headers['HTTP_X_SSL_CLIENT_CERTIFICATE']
+  end
+
+  def certificate_param
+    param = params[:client_certificate]
+    return unless param
+
+    unescaped_param = CGI.unescape(param)
+    if unescaped_param.include?("\n")
+      # NGINX forwarding the $ssl_client_escaped_cert variable
+      unescaped_param
+    else
+      # older version of NGINX forwarding the now deprecated $ssl_client_cert variable
+      param.gsub(/ (?!CERTIFICATE)/, "\n")
+    end
+  end
+
   def check_feature_availability
     render_404 unless ::Gitlab::Auth::Smartcard.enabled?
   end
 
-  def check_certificate_headers
-    # Failing on requests coming from the port not requiring client side certificate
-    unless certificate_header.present?
+  def check_certificate_required_host_and_port
+    unless request.host == ::Gitlab.config.smartcard.client_certificate_required_host &&
+      request.port == ::Gitlab.config.smartcard.client_certificate_required_port
+      render_404
+    end
+  end
+
+  def check_ngingx_certificate_header
+    unless nginx_certificate_header.present?
+      access_denied!(_('Smartcard authentication failed: client certificate header is missing.'), 401)
+    end
+  end
+
+  def check_certificate_param
+    unless certificate_param.present?
       access_denied!(_('Smartcard authentication failed: client certificate header is missing.'), 401)
     end
   end
@@ -52,20 +115,6 @@ def log_audit_event(user, options = {})
     AuditEventService.new(user, user, options).for_authentication.security_event
   end
 
-  def certificate_header
-    header = request.headers['HTTP_X_SSL_CLIENT_CERTIFICATE']
-    return unless header
-
-    unescaped_header = CGI.unescape(header)
-    if unescaped_header.include?("\n")
-      # NGINX forwarding the $ssl_client_escaped_cert variable
-      unescaped_header
-    else
-      # older version of NGINX forwarding the now deprecated $ssl_client_cert variable
-      header.gsub(/ (?!CERTIFICATE)/, "\n")
-    end
-  end
-
   def after_sign_in_path_for(resource)
     stored_location_for(:redirect) || stored_location_for(resource) || root_url(port: Gitlab.config.gitlab.port)
   end
diff --git a/ee/app/views/devise/sessions/_new_smartcard.html.haml b/ee/app/views/devise/sessions/_new_smartcard.html.haml
index ae19cd257940c..a836dc82305ba 100644
--- a/ee/app/views/devise/sessions/_new_smartcard.html.haml
+++ b/ee/app/views/devise/sessions/_new_smartcard.html.haml
@@ -1,6 +1,6 @@
 - if smartcard_enabled?
   .login-box.tab-pane{ id: 'smartcard', role: 'tabpanel', class: active_when(form_based_auth_provider_has_active_class?(:smartcard)) }
     .login-body
-      = form_tag(smartcard_auth_url(port: smartcard_config_port), html: { 'aria-live' => 'assertive'}) do
+      = form_tag(auth_smartcard_url, html: { 'aria-live' => 'assertive'}) do
         .submit-container
           = submit_tag _('Login with smartcard'), class: 'btn btn-success'
diff --git a/ee/app/views/devise/sessions/_new_smartcard_ldap.html.haml b/ee/app/views/devise/sessions/_new_smartcard_ldap.html.haml
index 46125b1ebe139..294d9e077b7cb 100644
--- a/ee/app/views/devise/sessions/_new_smartcard_ldap.html.haml
+++ b/ee/app/views/devise/sessions/_new_smartcard_ldap.html.haml
@@ -9,8 +9,7 @@
       = _('Use your smart card to authenticate with the LDAP server.')
 
     .login-body
-      = form_tag(smartcard_ldap_auth_url(provider: server['provider_name'],
-                                         port: smartcard_config_port),
+      = form_tag(auth_smartcard_url(provider: server['provider_name']),
                  html: { 'aria-live' => 'assertive'}) do
         .submit-container
           = submit_tag(_('Sign in with smart card'),
diff --git a/ee/config/routes/smartcard.rb b/ee/config/routes/smartcard.rb
index 4fdaeaabc2536..569afb57a7e53 100644
--- a/ee/config/routes/smartcard.rb
+++ b/ee/config/routes/smartcard.rb
@@ -1,4 +1,9 @@
 # frozen_string_literal: true
 
-post 'smartcard/auth' => 'smartcard#auth'
-post 'smartcard/ldap_auth' => 'smartcard#ldap_auth'
+resource :smartcard, only: [], controller: :smartcard do
+  collection do
+    post :auth
+    get :extract_certificate
+    get :verify_certificate
+  end
+end
diff --git a/ee/spec/requests/smartcard_controller_spec.rb b/ee/spec/requests/smartcard_controller_spec.rb
index 99f842cd780fc..d5cdad69dcbe9 100644
--- a/ee/spec/requests/smartcard_controller_spec.rb
+++ b/ee/spec/requests/smartcard_controller_spec.rb
@@ -5,61 +5,47 @@
 describe SmartcardController, type: :request do
   include LdapHelpers
 
-  let(:certificate_headers) { { 'X-SSL-CLIENT-CERTIFICATE': 'certificate' } }
-  let(:openssl_certificate_store) { instance_double(OpenSSL::X509::Store) }
-  let(:audit_event_service) { instance_double(AuditEventService) }
-  let(:session_enforcer) { instance_double(Gitlab::Auth::Smartcard::SessionEnforcer) }
+  let(:smartcard_host) { 'smartcard.example.com' }
+  let(:smartcard_port) { 3443 }
 
-  shared_examples 'a client certificate authentication' do |auth_method|
-    context 'with smartcard_auth enabled' do
-      it 'allows sign in' do
-        subject
+  describe '#auth' do
+    let(:params) { {} }
 
-        expect(request.env['warden']).to be_authenticated
-      end
+    subject { post auth_smartcard_path, params: params }
 
-      it 'redirects to root' do
-        subject
+    before do
+      stub_smartcard_config(
+        client_certificate_required_host: smartcard_host,
+        client_certificate_required_port: smartcard_port
+      )
+    end
 
-        expect(response).to redirect_to(root_url)
+    context 'with smartcard_auth enabled' do
+      before do
+        enable_smartcard_authentication
       end
 
-      it 'logs audit event' do
-        expect(AuditEventService).to(
-          receive(:new)
-            .with(instance_of(User), instance_of(User), with: auth_method)
-            .and_return(audit_event_service))
-        expect(audit_event_service).to receive_message_chain(:for_authentication, :security_event)
-
+      it 'redirects to extract certificate' do
         subject
-      end
-
-      it 'stores active session' do
-        expect(::Gitlab::Auth::Smartcard::SessionEnforcer).to(
-          receive(:new).and_return(session_enforcer))
-        expect(session_enforcer).to receive(:update_session)
 
-        subject
+        expect(response).to have_gitlab_http_status(:redirect)
+        expect(response.location).to(
+          eq(extract_certificate_smartcard_url(host: smartcard_host,
+                                               port: smartcard_port)))
       end
 
-      context 'user does not exist' do
-        context 'signup allowed' do
-          it 'creates user' do
-            expect { subject }.to change { User.count }.from(0).to(1)
-          end
-        end
-
-        context 'signup disabled' do
-          it 'renders 401' do
-            allow(Gitlab::CurrentSettings.current_application_settings).to(
-              receive(:allow_signup?).and_return(false))
+      context 'with provider param' do
+        let(:provider) { 'ldap-provider' }
+        let(:params) { { provider: provider } }
 
-            subject
+        it 'forwards the provider param' do
+          subject
 
-            expect(flash[:alert]).to eql('Failed to signing using smartcard authentication')
-            expect(response).to redirect_to(new_user_session_path)
-            expect(request.env['warden']).not_to be_authenticated
-          end
+          expect(response).to have_gitlab_http_status(:redirect)
+          expect(response.location).to(
+            eq(extract_certificate_smartcard_url(host: smartcard_host,
+                                                 port: smartcard_port,
+                                                 provider: provider)))
         end
       end
     end
@@ -77,166 +63,324 @@
     end
   end
 
-  describe '#auth' do
-    let(:subject_dn) { '/O=Random Corp Ltd/CN=gitlab-user/emailAddress=gitlab-user@random-corp.org' }
-    let(:issuer_dn) { '/O=Random Corp Ltd/CN=Random Corp' }
-    let(:certificate_headers) { { 'X-SSL-CLIENT-CERTIFICATE': 'certificate' } }
-    let(:openssl_certificate_store) { instance_double(OpenSSL::X509::Store) }
-    let(:openssl_certificate) { instance_double(OpenSSL::X509::Certificate, subject: subject_dn, issuer: issuer_dn) }
-    let(:audit_event_service) { instance_double(AuditEventService) }
+  describe '#extract_certificate' do
+    let(:certificate) { 'certificate' }
+    let(:certificate_headers) { { 'X-SSL-CLIENT-CERTIFICATE': certificate } }
+    let(:params) { {} }
 
-    before do
-      allow(Gitlab::Auth::Smartcard).to receive(:enabled?).and_return(true)
-      allow(Gitlab::Auth::Smartcard::Certificate).to receive(:store).and_return(openssl_certificate_store)
-      allow(openssl_certificate_store).to receive(:verify).and_return(true)
-      allow(OpenSSL::X509::Certificate).to receive(:new).and_return(openssl_certificate)
+    subject do
+      get(extract_certificate_smartcard_path, headers: certificate_headers,
+                                              params: params)
     end
 
-    subject { post '/-/smartcard/auth', params: {}, headers: certificate_headers }
-
-    it_behaves_like 'a client certificate authentication', 'smartcard'
+    before do
+      stub_config_setting(host: 'example.com',
+                          port: 443)
+      stub_smartcard_config(
+        client_certificate_required_host: smartcard_host,
+        client_certificate_required_port: smartcard_port
+      )
+      host! "#{smartcard_host}:#{smartcard_port}"
+    end
 
-    context 'user already exists' do
+    context 'with smartcard_auth enabled' do
       before do
-        user = create(:user)
-        create(:smartcard_identity, subject: subject_dn, issuer: issuer_dn, user: user)
+        enable_smartcard_authentication
       end
 
-      it 'finds existing user' do
-        expect { subject }.not_to change { User.count }
-        expect(request.env['warden']).to be_authenticated
+      it 'redirects to verify certificate' do
+        subject
+
+        expect(response).to have_gitlab_http_status(:redirect)
+        expect(response.location).to(
+          eq(verify_certificate_smartcard_url(host: ::Gitlab.config.gitlab.host,
+                                              port: ::Gitlab.config.gitlab.port,
+                                              client_certificate: certificate)))
       end
-    end
 
-    context 'certificate header formats from NGINX' do
-      shared_examples 'valid certificate header' do
-        it 'authenticates user' do
-          expect(Gitlab::Auth::Smartcard::Certificate).to receive(:new).with(expected_certificate).and_call_original
+      context 'with provider param' do
+        let(:provider) { 'ldap-provider' }
+        let(:params) { { provider: provider } }
 
+        it 'forwards the provider param' do
           subject
 
-          expect(request.env['warden']).to be_authenticated
+          expect(response).to have_gitlab_http_status(:redirect)
+          expect(response.location).to(
+            eq(verify_certificate_smartcard_url(host: ::Gitlab.config.gitlab.host,
+                                                port: ::Gitlab.config.gitlab.port,
+                                                client_certificate: certificate,
+                                                provider: provider)))
         end
       end
 
-      let(:expected_certificate) { "-----BEGIN CERTIFICATE-----\nrow\nrow\n-----END CERTIFICATE-----" }
+      context 'missing NGINX client certificate header' do
+        let(:certificate_headers) { {} }
 
-      context 'escaped format' do
-        let(:certificate_headers) { { 'X-SSL-CLIENT-CERTIFICATE': '-----BEGIN%20CERTIFICATE-----%0Arow%0Arow%0A-----END%20CERTIFICATE-----' } }
+        it 'renders unauthorized' do
+          subject
 
-        it_behaves_like 'valid certificate header'
+          expect(response).to have_gitlab_http_status(:unauthorized)
+        end
       end
 
-      context 'deprecated format' do
-        let(:certificate_headers) { { 'X-SSL-CLIENT-CERTIFICATE': '-----BEGIN CERTIFICATE----- row row -----END CERTIFICATE-----' } }
+      context 'request from different host / port' do
+        it 'renders 404' do
+          host! 'another.host:42'
+
+          subject
 
-        it_behaves_like 'valid certificate header'
+          expect(response).to have_gitlab_http_status(:not_found)
+        end
       end
     end
 
-    context 'missing certificate headers' do
-      let(:certificate_headers) { nil }
+    context 'with smartcard_auth disabled' do
+      before do
+        allow(Gitlab::Auth::Smartcard).to receive(:enabled?).and_return(false)
+      end
 
-      it 'renders 401' do
+      it 'renders 404' do
         subject
 
-        expect(response).to have_gitlab_http_status(:unauthorized)
-        expect(request.env['warden']).not_to be_authenticated
+        expect(response).to have_gitlab_http_status(:not_found)
       end
     end
   end
 
-  describe '#ldap_auth ' do
-    let(:subject_ldap_dn) { 'uid=john doe,ou=people,dc=example,dc=com' }
-    let(:issuer_dn) { 'CN=Random Corp,O=Random Corp Ltd,C=US' }
-    let(:issuer) { instance_double(OpenSSL::X509::Name, to_s: issuer_dn) }
-    let(:serial) { '42' }
-    let(:openssl_certificate) do
-      instance_double(OpenSSL::X509::Certificate,
-                      issuer: issuer, serial: serial)
-    end
+  describe '#verify_certificate' do
+    shared_examples 'a client certificate authentication' do |auth_method|
+      context 'with smartcard_auth enabled' do
+        it 'allows sign in' do
+          subject
+
+          expect(request.env['warden']).to be_authenticated
+        end
+
+        it 'redirects to root' do
+          subject
+
+          expect(response).to redirect_to(root_url)
+        end
+
+        it 'logs audit event' do
+          audit_event_service = instance_double(AuditEventService)
+
+          expect(AuditEventService).to(
+            receive(:new)
+              .with(instance_of(User), instance_of(User), with: auth_method)
+              .and_return(audit_event_service))
+          expect(audit_event_service).to receive_message_chain(:for_authentication, :security_event)
+
+          subject
+        end
+
+        it 'stores active session' do
+          session_enforcer = instance_double(Gitlab::Auth::Smartcard::SessionEnforcer)
+
+          expect(::Gitlab::Auth::Smartcard::SessionEnforcer).to(
+            receive(:new).and_return(session_enforcer))
+          expect(session_enforcer).to receive(:update_session)
+
+          subject
+        end
+
+        context 'user does not exist' do
+          context 'signup allowed' do
+            it 'creates user' do
+              expect { subject }.to change { User.count }.from(0).to(1)
+            end
+          end
+
+          context 'signup disabled' do
+            it 'renders 401' do
+              allow(Gitlab::CurrentSettings.current_application_settings).to(
+                receive(:allow_signup?).and_return(false))
+
+              subject
+
+              expect(flash[:alert]).to eql('Failed to signing using smartcard authentication')
+              expect(response).to redirect_to(new_user_session_path)
+              expect(request.env['warden']).not_to be_authenticated
+            end
+          end
+        end
+
+        context 'missing client certificate param' do
+          let(:params) { {} }
 
-    let(:ldap_connection) { instance_double(::Net::LDAP) }
-    let(:ldap_email) { 'john.doe@example.com' }
-    let(:ldap_entry) do
-      Net::LDAP::Entry.new.tap do |entry|
-        entry['dn'] = subject_ldap_dn
-        entry['uid'] = 'john doe'
-        entry['cn'] = 'John Doe'
-        entry['mail'] = ldap_email
+          it 'renders unauthorized' do
+            subject
+
+            expect(response).to have_gitlab_http_status(:unauthorized)
+            expect(request.env['warden']).not_to be_authenticated
+          end
+        end
       end
-    end
-    let(:ldap_user_search_scope) { 'dc=example,dc=com' }
-    let(:ldap_search_params) do
-      { attributes: array_including('dn', 'cn', 'mail', 'uid', 'userid'),
-        base: ldap_user_search_scope,
-        filter: Net::LDAP::Filter.ex(
-          'userCertificate:certificateExactMatch',
-          "{ serialNumber #{serial}, issuer \"#{issuer_dn}\" }") }
-    end
 
-    subject do
-      post('/-/smartcard/ldap_auth',
-           { params: { provider: 'ldapmain' },
-             headers: certificate_headers } )
+      context 'with smartcard_auth disabled' do
+        before do
+          allow(Gitlab::Auth::Smartcard).to receive(:enabled?).and_return(false)
+        end
+
+        it 'renders 404' do
+          subject
+
+          expect(response).to have_gitlab_http_status(:not_found)
+        end
+      end
     end
 
+    let(:client_certificate) { 'certificate' }
+    let(:params) { { client_certificate: client_certificate } }
+    let(:serial) { '42' }
+    let(:subject_dn) { '/O=Random Corp Ltd/CN=gitlab-user/emailAddress=gitlab-user@random-corp.org' }
+    let(:issuer_dn) { 'CN=Random Corp,O=Random Corp Ltd,C=US' }
+
     before do
-      allow(Gitlab::Auth::Smartcard).to receive(:enabled?).and_return(true)
+      enable_smartcard_authentication
+      stub_certificate_store
+      stub_certificate
+    end
 
-      allow(Gitlab::Auth::Smartcard::LdapCertificate).to(
-        receive(:store).and_return(openssl_certificate_store))
-      allow(openssl_certificate_store).to receive(:verify).and_return(true)
+    context 'Smartcard::Certificate' do
+      subject { get verify_certificate_smartcard_path, params: params }
 
-      allow(OpenSSL::X509::Certificate).to(
-        receive(:new).and_return(openssl_certificate))
+      it_behaves_like 'a client certificate authentication', 'smartcard'
 
-      allow(Net::LDAP).to receive(:new).and_return(ldap_connection)
-      allow(ldap_connection).to(
-        receive(:search).with(ldap_search_params).and_return([ldap_entry]))
-    end
+      context 'user already exists' do
+        before do
+          user = create(:user)
+          create(:smartcard_identity, subject: subject_dn, issuer: issuer_dn, user: user)
+        end
+
+        it 'finds existing user' do
+          expect { subject }.not_to change { User.count }
+          expect(request.env['warden']).to be_authenticated
+        end
+      end
+
+      context 'certificate header formats from NGINX' do
+        shared_examples 'valid certificate header' do
+          it 'authenticates user' do
+            expect(Gitlab::Auth::Smartcard::Certificate).to receive(:new).with(expected_certificate).and_call_original
 
-    it_behaves_like 'a client certificate authentication', 'smartcard_ldap'
+            subject
 
-    it 'sets correct parameters for LDAP search' do
-      expect(ldap_connection).to(
-        receive(:search).with(ldap_search_params).and_return([ldap_entry]))
+            expect(request.env['warden']).to be_authenticated
+          end
+        end
+
+        let(:expected_certificate) { "-----BEGIN CERTIFICATE-----\nrow\nrow\n-----END CERTIFICATE-----" }
+
+        context 'escaped format' do
+          let(:client_certificate) { '-----BEGIN%20CERTIFICATE-----%0Arow%0Arow%0A-----END%20CERTIFICATE-----' }
+
+          it_behaves_like 'valid certificate header'
+        end
 
-      subject
+        context 'deprecated format' do
+          let(:client_certificate) { '-----BEGIN CERTIFICATE----- row row -----END CERTIFICATE-----' }
+
+          it_behaves_like 'valid certificate header'
+        end
+      end
     end
 
-    context 'user already exists' do
-      let_it_be(:user) { create(:user) }
+    context 'Smartcard::LdapCertificate' do
+      let(:ldap_connection) { instance_double(::Net::LDAP) }
+      let(:subject_ldap_dn) { 'uid=john doe,ou=people,dc=example,dc=com' }
+      let(:ldap_email) { 'john.doe@example.com' }
+      let(:ldap_entry) do
+        Net::LDAP::Entry.new.tap do |entry|
+          entry['dn'] = subject_ldap_dn
+          entry['uid'] = 'john doe'
+          entry['cn'] = 'John Doe'
+          entry['mail'] = ldap_email
+        end
+      end
+      let(:ldap_user_search_scope) { 'dc=example,dc=com' }
+      let(:ldap_search_params) do
+        { attributes: array_including('dn', 'cn', 'mail', 'uid', 'userid'),
+          base: ldap_user_search_scope,
+          filter: Net::LDAP::Filter.ex(
+            'userCertificate:certificateExactMatch',
+            "{ serialNumber #{serial}, issuer \"#{issuer_dn}\" }") }
+      end
+
+      subject do
+        get(verify_certificate_smartcard_path,
+            { params: params.merge({ provider: 'ldapmain' }) })
+      end
+
+      before do
+        allow(Net::LDAP).to receive(:new).and_return(ldap_connection)
+        allow(ldap_connection).to(
+          receive(:search).with(ldap_search_params).and_return([ldap_entry]))
+      end
 
-      it 'finds existing user' do
-        create(:identity, { provider: 'ldapmain',
-                            extern_uid: subject_ldap_dn,
-                            user: user })
+      it_behaves_like 'a client certificate authentication', 'smartcard_ldap'
 
-        expect { subject }.not_to change { User.count }
+      it 'sets correct parameters for LDAP search' do
+        expect(ldap_connection).to(
+          receive(:search).with(ldap_search_params).and_return([ldap_entry]))
 
-        expect(request.env['warden']).to be_authenticated
+        subject
       end
 
-      context "user has a different identity" do
-        let(:ldap_email) { user.email }
+      context 'user already exists' do
+        let_it_be(:user) { create(:user) }
 
-        before do
+        it 'finds existing user' do
           create(:identity, { provider: 'ldapmain',
-                              extern_uid: 'different_identity_dn',
+                              extern_uid: subject_ldap_dn,
                               user: user })
-        end
 
-        it "doesn't login a user" do
-          subject
+          expect { subject }.not_to change { User.count }
 
-          expect(request.env['warden']).not_to be_authenticated
+          expect(request.env['warden']).to be_authenticated
         end
 
-        it "doesn't create a new user entry either" do
-          expect { subject }.not_to change { User.count }
+        context 'user has a different identity' do
+          let(:ldap_email) { user.email }
+
+          before do
+            create(:identity, { provider: 'ldapmain',
+                                extern_uid: 'different_identity_dn',
+                                user: user })
+          end
+
+          it "doesn't login a user" do
+            subject
+
+            expect(request.env['warden']).not_to be_authenticated
+          end
+
+          it "doesn't create a new user entry either" do
+            expect { subject }.not_to change { User.count }
+          end
         end
       end
     end
   end
+
+  def enable_smartcard_authentication
+    allow(Gitlab::Auth::Smartcard).to receive(:enabled?).and_return(true)
+  end
+
+  def stub_smartcard_config(smartcard_settings)
+    allow(::Gitlab.config.smartcard).to(receive_messages(smartcard_settings))
+  end
+
+  def stub_certificate_store
+    openssl_certificate_store = instance_double(OpenSSL::X509::Store)
+    allow(Gitlab::Auth::Smartcard::Base).to receive(:store).and_return(openssl_certificate_store)
+    allow(openssl_certificate_store).to receive(:verify).and_return(true)
+  end
+
+  def stub_certificate
+    issuer = instance_double(OpenSSL::X509::Name, to_s: issuer_dn)
+    openssl_certificate = instance_double(OpenSSL::X509::Certificate, subject: subject_dn, issuer: issuer, serial: serial)
+    allow(OpenSSL::X509::Certificate).to receive(:new).and_return(openssl_certificate)
+  end
 end
-- 
GitLab