Commit 277830e9 authored by Nick Thomas's avatar Nick Thomas

Merge branch 'if-10526-smartcard_support_different_hostname' into 'master'

Make hostname configurable for smartcard authentication

See merge request gitlab-org/gitlab!26411
parents 7184b409 d8df043d
---
title: Make hostname configurable for smartcard authentication
merge_request: 26411
author:
type: added
...@@ -752,7 +752,9 @@ production: &base ...@@ -752,7 +752,9 @@ production: &base
# Path to a file containing a CA certificate # Path to a file containing a CA certificate
ca_file: '/etc/ssl/certs/CA.pem' 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 # client_certificate_required_port: 3444
# Browser session with smartcard sign-in is required for Git access # Browser session with smartcard sign-in is required for Git access
......
...@@ -77,6 +77,7 @@ end ...@@ -77,6 +77,7 @@ end
Gitlab.ee do Gitlab.ee do
Settings['smartcard'] ||= Settingslogic.new({}) Settings['smartcard'] ||= Settingslogic.new({})
Settings.smartcard['enabled'] = false if Settings.smartcard['enabled'].nil? 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['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['required_for_git_access'] = false if Settings.smartcard['required_for_git_access'].nil?
Settings.smartcard['san_extensions'] = false if Settings.smartcard['san_extensions'].nil? Settings.smartcard['san_extensions'] = false if Settings.smartcard['san_extensions'].nil?
......
...@@ -5,25 +5,58 @@ class SmartcardController < ApplicationController ...@@ -5,25 +5,58 @@ class SmartcardController < ApplicationController
skip_before_action :verify_authenticity_token skip_before_action :verify_authenticity_token
before_action :check_feature_availability 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 def auth
certificate = Gitlab::Auth::Smartcard::Certificate.new(certificate_header) redirect_to extract_certificate_smartcard_url(extract_certificate_url_options)
sign_in_with(certificate)
end end
def ldap_auth def extract_certificate
certificate = Gitlab::Auth::Smartcard::LdapCertificate.new(params[:provider], certificate_header) redirect_to verify_certificate_smartcard_url(verify_certificate_url_options)
sign_in_with(certificate) end
def verify_certificate
sign_in_with(client_certificate)
end end
private 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) def sign_in_with(certificate)
user = certificate.find_or_create_user user = certificate.find_or_create_user
unless user&.persisted? unless user&.persisted?
flash[:alert] = _('Failed to signing using smartcard authentication') 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 return
end end
...@@ -33,13 +66,43 @@ class SmartcardController < ApplicationController ...@@ -33,13 +66,43 @@ class SmartcardController < ApplicationController
sign_in_and_redirect(user) sign_in_and_redirect(user)
end 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 def check_feature_availability
render_404 unless ::Gitlab::Auth::Smartcard.enabled? render_404 unless ::Gitlab::Auth::Smartcard.enabled?
end end
def check_certificate_headers def check_certificate_required_host_and_port
# Failing on requests coming from the port not requiring client side certificate unless request.host == ::Gitlab.config.smartcard.client_certificate_required_host &&
unless certificate_header.present? 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) access_denied!(_('Smartcard authentication failed: client certificate header is missing.'), 401)
end end
end end
...@@ -52,20 +115,6 @@ class SmartcardController < ApplicationController ...@@ -52,20 +115,6 @@ class SmartcardController < ApplicationController
AuditEventService.new(user, user, options).for_authentication.security_event AuditEventService.new(user, user, options).for_authentication.security_event
end 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) def after_sign_in_path_for(resource)
stored_location_for(:redirect) || stored_location_for(resource) || root_url(port: Gitlab.config.gitlab.port) stored_location_for(:redirect) || stored_location_for(resource) || root_url(port: Gitlab.config.gitlab.port)
end end
......
- if smartcard_enabled? - if smartcard_enabled?
.login-box.tab-pane{ id: 'smartcard', role: 'tabpanel', class: active_when(form_based_auth_provider_has_active_class?(:smartcard)) } .login-box.tab-pane{ id: 'smartcard', role: 'tabpanel', class: active_when(form_based_auth_provider_has_active_class?(:smartcard)) }
.login-body .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-container
= submit_tag _('Login with smartcard'), class: 'btn btn-success' = submit_tag _('Login with smartcard'), class: 'btn btn-success'
...@@ -9,8 +9,7 @@ ...@@ -9,8 +9,7 @@
= _('Use your smart card to authenticate with the LDAP server.') = _('Use your smart card to authenticate with the LDAP server.')
.login-body .login-body
= form_tag(smartcard_ldap_auth_url(provider: server['provider_name'], = form_tag(auth_smartcard_url(provider: server['provider_name']),
port: smartcard_config_port),
html: { 'aria-live' => 'assertive'}) do html: { 'aria-live' => 'assertive'}) do
.submit-container .submit-container
= submit_tag(_('Sign in with smart card'), = submit_tag(_('Sign in with smart card'),
......
# frozen_string_literal: true # frozen_string_literal: true
post 'smartcard/auth' => 'smartcard#auth' resource :smartcard, only: [], controller: :smartcard do
post 'smartcard/ldap_auth' => 'smartcard#ldap_auth' collection do
post :auth
get :extract_certificate
get :verify_certificate
end
end
...@@ -5,11 +5,150 @@ require 'spec_helper' ...@@ -5,11 +5,150 @@ require 'spec_helper'
describe SmartcardController, type: :request do describe SmartcardController, type: :request do
include LdapHelpers include LdapHelpers
let(:certificate_headers) { { 'X-SSL-CLIENT-CERTIFICATE': 'certificate' } } let(:smartcard_host) { 'smartcard.example.com' }
let(:openssl_certificate_store) { instance_double(OpenSSL::X509::Store) } let(:smartcard_port) { 3443 }
let(:audit_event_service) { instance_double(AuditEventService) }
let(:session_enforcer) { instance_double(Gitlab::Auth::Smartcard::SessionEnforcer) }
describe '#auth' do
let(:params) { {} }
subject { post auth_smartcard_path, params: params }
before do
stub_smartcard_config(
client_certificate_required_host: smartcard_host,
client_certificate_required_port: smartcard_port
)
end
context 'with smartcard_auth enabled' do
before do
enable_smartcard_authentication
end
it 'redirects to extract certificate' do
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 'with provider param' do
let(:provider) { 'ldap-provider' }
let(:params) { { provider: provider } }
it 'forwards the provider param' do
subject
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
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
describe '#extract_certificate' do
let(:certificate) { 'certificate' }
let(:certificate_headers) { { 'X-SSL-CLIENT-CERTIFICATE': certificate } }
let(:params) { {} }
subject do
get(extract_certificate_smartcard_path, headers: certificate_headers,
params: params)
end
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 'with smartcard_auth enabled' do
before do
enable_smartcard_authentication
end
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
context 'with provider param' do
let(:provider) { 'ldap-provider' }
let(:params) { { provider: provider } }
it 'forwards the provider param' 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,
provider: provider)))
end
end
context 'missing NGINX client certificate header' do
let(:certificate_headers) { {} }
it 'renders unauthorized' do
subject
expect(response).to have_gitlab_http_status(:unauthorized)
end
end
context 'request from different host / port' do
it 'renders 404' do
host! 'another.host:42'
subject
expect(response).to have_gitlab_http_status(:not_found)
end
end
end
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
describe '#verify_certificate' do
shared_examples 'a client certificate authentication' do |auth_method| shared_examples 'a client certificate authentication' do |auth_method|
context 'with smartcard_auth enabled' do context 'with smartcard_auth enabled' do
it 'allows sign in' do it 'allows sign in' do
...@@ -25,6 +164,8 @@ describe SmartcardController, type: :request do ...@@ -25,6 +164,8 @@ describe SmartcardController, type: :request do
end end
it 'logs audit event' do it 'logs audit event' do
audit_event_service = instance_double(AuditEventService)
expect(AuditEventService).to( expect(AuditEventService).to(
receive(:new) receive(:new)
.with(instance_of(User), instance_of(User), with: auth_method) .with(instance_of(User), instance_of(User), with: auth_method)
...@@ -35,6 +176,8 @@ describe SmartcardController, type: :request do ...@@ -35,6 +176,8 @@ describe SmartcardController, type: :request do
end end
it 'stores active session' do it 'stores active session' do
session_enforcer = instance_double(Gitlab::Auth::Smartcard::SessionEnforcer)
expect(::Gitlab::Auth::Smartcard::SessionEnforcer).to( expect(::Gitlab::Auth::Smartcard::SessionEnforcer).to(
receive(:new).and_return(session_enforcer)) receive(:new).and_return(session_enforcer))
expect(session_enforcer).to receive(:update_session) expect(session_enforcer).to receive(:update_session)
...@@ -62,6 +205,17 @@ describe SmartcardController, type: :request do ...@@ -62,6 +205,17 @@ describe SmartcardController, type: :request do
end end
end end
end end
context 'missing client certificate param' do
let(:params) { {} }
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
context 'with smartcard_auth disabled' do context 'with smartcard_auth disabled' do
...@@ -77,22 +231,20 @@ describe SmartcardController, type: :request do ...@@ -77,22 +231,20 @@ describe SmartcardController, type: :request do
end end
end end
describe '#auth' do 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(: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(:issuer_dn) { 'CN=Random Corp,O=Random Corp Ltd,C=US' }
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) }
before do before do
allow(Gitlab::Auth::Smartcard).to receive(:enabled?).and_return(true) enable_smartcard_authentication
allow(Gitlab::Auth::Smartcard::Certificate).to receive(:store).and_return(openssl_certificate_store) stub_certificate_store
allow(openssl_certificate_store).to receive(:verify).and_return(true) stub_certificate
allow(OpenSSL::X509::Certificate).to receive(:new).and_return(openssl_certificate)
end end
subject { post '/-/smartcard/auth', params: {}, headers: certificate_headers } context 'Smartcard::Certificate' do
subject { get verify_certificate_smartcard_path, params: params }
it_behaves_like 'a client certificate authentication', 'smartcard' it_behaves_like 'a client certificate authentication', 'smartcard'
...@@ -122,41 +274,22 @@ describe SmartcardController, type: :request do ...@@ -122,41 +274,22 @@ describe SmartcardController, type: :request do
let(:expected_certificate) { "-----BEGIN CERTIFICATE-----\nrow\nrow\n-----END CERTIFICATE-----" } let(:expected_certificate) { "-----BEGIN CERTIFICATE-----\nrow\nrow\n-----END CERTIFICATE-----" }
context 'escaped format' do context 'escaped format' do
let(:certificate_headers) { { 'X-SSL-CLIENT-CERTIFICATE': '-----BEGIN%20CERTIFICATE-----%0Arow%0Arow%0A-----END%20CERTIFICATE-----' } } let(:client_certificate) { '-----BEGIN%20CERTIFICATE-----%0Arow%0Arow%0A-----END%20CERTIFICATE-----' }
it_behaves_like 'valid certificate header' it_behaves_like 'valid certificate header'
end end
context 'deprecated format' do context 'deprecated format' do
let(:certificate_headers) { { 'X-SSL-CLIENT-CERTIFICATE': '-----BEGIN CERTIFICATE----- row row -----END CERTIFICATE-----' } } let(:client_certificate) { '-----BEGIN CERTIFICATE----- row row -----END CERTIFICATE-----' }
it_behaves_like 'valid certificate header' it_behaves_like 'valid certificate header'
end end
end end
context 'missing certificate headers' do
let(:certificate_headers) { nil }
it 'renders 401' do
subject
expect(response).to have_gitlab_http_status(:unauthorized)
expect(request.env['warden']).not_to be_authenticated
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 end
context 'Smartcard::LdapCertificate' do
let(:ldap_connection) { instance_double(::Net::LDAP) } 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_email) { 'john.doe@example.com' }
let(:ldap_entry) do let(:ldap_entry) do
Net::LDAP::Entry.new.tap do |entry| Net::LDAP::Entry.new.tap do |entry|
...@@ -176,21 +309,11 @@ describe SmartcardController, type: :request do ...@@ -176,21 +309,11 @@ describe SmartcardController, type: :request do
end end
subject do subject do
post('/-/smartcard/ldap_auth', get(verify_certificate_smartcard_path,
{ params: { provider: 'ldapmain' }, { params: params.merge({ provider: 'ldapmain' }) })
headers: certificate_headers } )
end end
before do before do
allow(Gitlab::Auth::Smartcard).to receive(:enabled?).and_return(true)
allow(Gitlab::Auth::Smartcard::LdapCertificate).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))
allow(Net::LDAP).to receive(:new).and_return(ldap_connection) allow(Net::LDAP).to receive(:new).and_return(ldap_connection)
allow(ldap_connection).to( allow(ldap_connection).to(
receive(:search).with(ldap_search_params).and_return([ldap_entry])) receive(:search).with(ldap_search_params).and_return([ldap_entry]))
...@@ -218,7 +341,7 @@ describe SmartcardController, type: :request do ...@@ -218,7 +341,7 @@ describe SmartcardController, type: :request do
expect(request.env['warden']).to be_authenticated expect(request.env['warden']).to be_authenticated
end end
context "user has a different identity" do context 'user has a different identity' do
let(:ldap_email) { user.email } let(:ldap_email) { user.email }
before do before do
...@@ -239,4 +362,25 @@ describe SmartcardController, type: :request do ...@@ -239,4 +362,25 @@ describe SmartcardController, type: :request do
end end
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 end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment