Commit 203be787 authored by Douwe Maan's avatar Douwe Maan

Merge branch 'feature/honor-saml-assurance-level' into 'master'

Honor saml assurance level to allow 2FA bypassing

See merge request gitlab-org/gitlab-ce!19651
parents 77fe4166 2efe27ba
...@@ -119,7 +119,7 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController ...@@ -119,7 +119,7 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
set_remember_me(user) set_remember_me(user)
if user.two_factor_enabled? if user.two_factor_enabled? && !auth_user.bypass_two_factor?
prompt_for_two_factor(user) prompt_for_two_factor(user)
else else
sign_in_and_redirect(user) sign_in_and_redirect(user)
......
...@@ -179,6 +179,81 @@ tell GitLab which groups are external via the `external_groups:` element: ...@@ -179,6 +179,81 @@ tell GitLab which groups are external via the `external_groups:` element:
} } } }
``` ```
## Bypass two factor authentication
If you want some SAML authentication methods to count as 2FA on a per session basis, you can register them in the
`upstream_two_factor_authn_contexts` list:
**For Omnibus installations:**
1. Edit `/etc/gitlab/gitlab.rb`:
```ruby
gitlab_rails['omniauth_providers'] = [
{
name: 'saml',
args: {
assertion_consumer_service_url: 'https://gitlab.example.com/users/auth/saml/callback',
idp_cert_fingerprint: '43:51:43:a1:b5:fc:8b:b7:0a:3a:a9:b1:0f:66:73:a8',
idp_sso_target_url: 'https://login.example.com/idp',
issuer: 'https://gitlab.example.com',
name_identifier_format: 'urn:oasis:names:tc:SAML:2.0:nameid-format:persistent',
upstream_two_factor_authn_contexts:
%w(
urn:oasis:names:tc:SAML:2.0:ac:classes:CertificateProtectedTransport
urn:oasis:names:tc:SAML:2.0:ac:classes:SecondFactorOTPSMS
urn:oasis:names:tc:SAML:2.0:ac:classes:SecondFactorIGTOKEN
)
},
label: 'Company Login' # optional label for SAML login button, defaults to "Saml"
}
]
```
1. Save the file and [reconfigure][] GitLab for the changes to take effect.
---
**For installations from source:**
1. Edit `config/gitlab.yml`:
```yaml
- {
name: 'saml',
args: {
assertion_consumer_service_url: 'https://gitlab.example.com/users/auth/saml/callback',
idp_cert_fingerprint: '43:51:43:a1:b5:fc:8b:b7:0a:3a:a9:b1:0f:66:73:a8',
idp_sso_target_url: 'https://login.example.com/idp',
issuer: 'https://gitlab.example.com',
name_identifier_format: 'urn:oasis:names:tc:SAML:2.0:nameid-format:persistent',
upstream_two_factor_authn_contexts:
[
'urn:oasis:names:tc:SAML:2.0:ac:classes:CertificateProtectedTransport',
'urn:oasis:names:tc:SAML:2.0:ac:classes:SecondFactorOTPSMS',
'urn:oasis:names:tc:SAML:2.0:ac:classes:SecondFactorIGTOKEN'
]
},
label: 'Company Login' # optional label for SAML login button, defaults to "Saml"
}
```
1. Save the file and [restart GitLab][] for the changes ot take effect
In addition to the changes in GitLab, make sure that your Idp is returning the
`AuthnContext`. For example:
```xml
<saml:AuthnStatement>
<saml:AuthnContext>
<saml:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:MediumStrongCertificateProtectedTransport</saml:AuthnContextClassRef>
</saml:AuthnContext>
</saml:AuthnStatement>
```
## Customization ## Customization
### `auto_sign_in_with_provider` ### `auto_sign_in_with_provider`
......
...@@ -74,6 +74,10 @@ module Gitlab ...@@ -74,6 +74,10 @@ module Gitlab
gl_user gl_user
end end
def bypass_two_factor?
false
end
protected protected
def should_save? def should_save?
......
...@@ -6,6 +6,17 @@ module Gitlab ...@@ -6,6 +6,17 @@ module Gitlab
Array.wrap(get_raw(Gitlab::Auth::Saml::Config.groups)) Array.wrap(get_raw(Gitlab::Auth::Saml::Config.groups))
end end
def authn_context
response_object = auth_hash.extra[:response_object]
return nil if response_object.blank?
document = response_object.decrypted_document
document ||= response_object.document
return nil if document.blank?
extract_authn_context(document)
end
private private
def get_raw(key) def get_raw(key)
...@@ -13,6 +24,10 @@ module Gitlab ...@@ -13,6 +24,10 @@ module Gitlab
# otherwise just the first value is returned # otherwise just the first value is returned
auth_hash.extra[:raw_info].all[key] auth_hash.extra[:raw_info].all[key]
end end
def extract_authn_context(document)
REXML::XPath.first(document, "//saml:AuthnStatement/saml:AuthnContext/saml:AuthnContextClassRef/text()").to_s
end
end end
end end
end end
......
...@@ -7,6 +7,10 @@ module Gitlab ...@@ -7,6 +7,10 @@ module Gitlab
Gitlab::Auth::OAuth::Provider.config_for('saml') Gitlab::Auth::OAuth::Provider.config_for('saml')
end end
def upstream_two_factor_authn_contexts
options.args[:upstream_two_factor_authn_contexts]
end
def groups def groups
options[:groups_attribute] options[:groups_attribute]
end end
......
...@@ -34,6 +34,10 @@ module Gitlab ...@@ -34,6 +34,10 @@ module Gitlab
gl_user.changed? || gl_user.identities.any?(&:changed?) gl_user.changed? || gl_user.identities.any?(&:changed?)
end end
def bypass_two_factor?
saml_config.upstream_two_factor_authn_contexts&.include?(auth_hash.authn_context)
end
protected protected
def saml_config def saml_config
......
require 'spec_helper' require 'spec_helper'
describe OmniauthCallbacksController do describe OmniauthCallbacksController, type: :controller do
include LoginHelpers include LoginHelpers
let(:user) { create(:omniauth_user, extern_uid: extern_uid, provider: provider) } describe 'omniauth' do
let(:user) { create(:omniauth_user, extern_uid: extern_uid, provider: provider) }
before do
mock_auth_hash(provider.to_s, extern_uid, user.email)
stub_omniauth_provider(provider, context: request)
end
context 'when the user is on the last sign in attempt' do
let(:extern_uid) { 'my-uid' }
before do before do
user.update(failed_attempts: User.maximum_attempts.pred) mock_auth_hash(provider.to_s, extern_uid, user.email)
subject.response = ActionDispatch::Response.new stub_omniauth_provider(provider, context: request)
end end
context 'when using a form based provider' do context 'when the user is on the last sign in attempt' do
let(:provider) { :ldap } let(:extern_uid) { 'my-uid' }
it 'locks the user when sign in fails' do
allow(subject).to receive(:params).and_return(ActionController::Parameters.new(username: user.username))
request.env['omniauth.error.strategy'] = OmniAuth::Strategies::LDAP.new(nil)
subject.send(:failure)
expect(user.reload).to be_access_locked before do
user.update(failed_attempts: User.maximum_attempts.pred)
subject.response = ActionDispatch::Response.new
end end
end
context 'when using a button based provider' do context 'when using a form based provider' do
let(:provider) { :github } let(:provider) { :ldap }
it 'does not lock the user when sign in fails' do it 'locks the user when sign in fails' do
request.env['omniauth.error.strategy'] = OmniAuth::Strategies::GitHub.new(nil) allow(subject).to receive(:params).and_return(ActionController::Parameters.new(username: user.username))
request.env['omniauth.error.strategy'] = OmniAuth::Strategies::LDAP.new(nil)
subject.send(:failure) subject.send(:failure)
expect(user.reload).not_to be_access_locked expect(user.reload).to be_access_locked
end
end end
end
end
context 'strategies' do context 'when using a button based provider' do
context 'github' do let(:provider) { :github }
let(:extern_uid) { 'my-uid' }
let(:provider) { :github }
it 'allows sign in' do it 'does not lock the user when sign in fails' do
post provider request.env['omniauth.error.strategy'] = OmniAuth::Strategies::GitHub.new(nil)
expect(request.env['warden']).to be_authenticated subject.send(:failure)
end
shared_context 'sign_up' do
let(:user) { double(email: 'new@example.com') }
before do expect(user.reload).not_to be_access_locked
stub_omniauth_setting(block_auto_created_users: false)
end end
end end
end
context 'sign up' do context 'strategies' do
include_context 'sign_up' context 'github' do
let(:extern_uid) { 'my-uid' }
let(:provider) { :github }
it 'is allowed' do it 'allows sign in' do
post provider post provider
expect(request.env['warden']).to be_authenticated expect(request.env['warden']).to be_authenticated
end end
end
context 'when OAuth is disabled' do
before do
stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false')
settings = Gitlab::CurrentSettings.current_application_settings
settings.update(disabled_oauth_sign_in_sources: [provider.to_s])
end
it 'prevents login via POST' do shared_context 'sign_up' do
post provider let(:user) { double(email: 'new@example.com') }
expect(request.env['warden']).not_to be_authenticated before do
stub_omniauth_setting(block_auto_created_users: false)
end
end end
it 'shows warning when attempting login' do context 'sign up' do
post provider include_context 'sign_up'
expect(response).to redirect_to new_user_session_path
expect(flash[:alert]).to eq('Signing in using GitHub has been disabled')
end
it 'allows linking the disabled provider' do it 'is allowed' do
user.identities.destroy_all post provider
sign_in(user)
expect { post provider }.to change { user.reload.identities.count }.by(1) expect(request.env['warden']).to be_authenticated
end
end end
context 'sign up' do context 'when OAuth is disabled' do
include_context 'sign_up' before do
stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false')
settings = Gitlab::CurrentSettings.current_application_settings
settings.update(disabled_oauth_sign_in_sources: [provider.to_s])
end
it 'is prevented' do it 'prevents login via POST' do
post provider post provider
expect(request.env['warden']).not_to be_authenticated expect(request.env['warden']).not_to be_authenticated
end end
it 'shows warning when attempting login' do
post provider
expect(response).to redirect_to new_user_session_path
expect(flash[:alert]).to eq('Signing in using GitHub has been disabled')
end
it 'allows linking the disabled provider' do
user.identities.destroy_all
sign_in(user)
expect { post provider }.to change { user.reload.identities.count }.by(1)
end
context 'sign up' do
include_context 'sign_up'
it 'is prevented' do
post provider
expect(request.env['warden']).not_to be_authenticated
end
end
end
end
context 'auth0' do
let(:extern_uid) { '' }
let(:provider) { :auth0 }
it 'does not allow sign in without extern_uid' do
post 'auth0'
expect(request.env['warden']).not_to be_authenticated
expect(response.status).to eq(302)
expect(controller).to set_flash[:alert].to('Wrong extern UID provided. Make sure Auth0 is configured correctly.')
end end
end end
end end
end
describe '#saml' do
let(:user) { create(:omniauth_user, :two_factor, extern_uid: 'my-uid', provider: 'saml') }
let(:mock_saml_response) { File.read('spec/fixtures/authentication/saml_response.xml') }
let(:saml_config) { mock_saml_config_with_upstream_two_factor_authn_contexts }
before do
stub_omniauth_saml_config({ enabled: true, auto_link_saml_user: true, allow_single_sign_on: ['saml'],
providers: [saml_config] })
mock_auth_hash('saml', 'my-uid', user.email, mock_saml_response)
request.env["devise.mapping"] = Devise.mappings[:user]
request.env['omniauth.auth'] = Rails.application.env_config['omniauth.auth']
post :saml, params: { SAMLResponse: mock_saml_response }
end
context 'auth0' do context 'when worth two factors' do
let(:extern_uid) { '' } let(:mock_saml_response) do
let(:provider) { :auth0 } File.read('spec/fixtures/authentication/saml_response.xml')
.gsub('urn:oasis:names:tc:SAML:2.0:ac:classes:Password', 'urn:oasis:names:tc:SAML:2.0:ac:classes:SecondFactorIGTOKEN')
end
it 'does not allow sign in without extern_uid' do it 'expects user to be signed_in' do
post 'auth0' expect(request.env['warden']).to be_authenticated
end
end
context 'when not worth two factors' do
it 'expects user to provide second factor' do
expect(response).to render_template('devise/sessions/two_factor')
expect(request.env['warden']).not_to be_authenticated expect(request.env['warden']).not_to be_authenticated
expect(response.status).to eq(302)
expect(controller).to set_flash[:alert].to('Wrong extern UID provided. Make sure Auth0 is configured correctly.')
end end
end end
end end
......
require 'spec_helper'
require 'omniauth/strategies/saml'
describe 'processing of SAMLResponse in dependencies' do
let(:mock_saml_response) { File.read('spec/fixtures/authentication/saml_response.xml') }
let(:saml_strategy) { OmniAuth::Strategies::SAML.new({}) }
let(:session_mock) { {} }
let(:settings) { OpenStruct.new({ soft: false, idp_cert_fingerprint: 'something' }) }
let(:auth_hash) { Gitlab::Auth::Saml::AuthHash.new(saml_strategy) }
subject { auth_hash.authn_context }
before do
allow(saml_strategy).to receive(:session).and_return(session_mock)
allow_any_instance_of(OneLogin::RubySaml::Response).to receive(:is_valid?).and_return(true)
saml_strategy.send(:handle_response, mock_saml_response, {}, settings ) { }
end
it 'can extract AuthnContextClassRef from SAMLResponse param' do
is_expected.to eq 'urn:oasis:names:tc:SAML:2.0:ac:classes:Password'
end
end
...@@ -177,14 +177,35 @@ feature 'Login' do ...@@ -177,14 +177,35 @@ feature 'Login' do
end end
context 'logging in via OAuth' do context 'logging in via OAuth' do
it 'shows 2FA prompt after OAuth login' do let(:user) { create(:omniauth_user, :two_factor, extern_uid: 'my-uid', provider: 'saml')}
stub_omniauth_saml_config(enabled: true, auto_link_saml_user: true, allow_single_sign_on: ['saml'], providers: [mock_saml_config]) let(:mock_saml_response) do
user = create(:omniauth_user, :two_factor, extern_uid: 'my-uid', provider: 'saml') File.read('spec/fixtures/authentication/saml_response.xml')
gitlab_sign_in_via('saml', user, 'my-uid') end
expect(page).to have_content('Two-Factor Authentication') before do
enter_code(user.current_otp) stub_omniauth_saml_config(enabled: true, auto_link_saml_user: true, allow_single_sign_on: ['saml'],
expect(current_path).to eq root_path providers: [mock_saml_config_with_upstream_two_factor_authn_contexts])
gitlab_sign_in_via('saml', user, 'my-uid', mock_saml_response)
end
context 'when authn_context is worth two factors' do
let(:mock_saml_response) do
File.read('spec/fixtures/authentication/saml_response.xml')
.gsub('urn:oasis:names:tc:SAML:2.0:ac:classes:Password', 'urn:oasis:names:tc:SAML:2.0:ac:classes:SecondFactorOTPSMS')
end
it 'signs user in without prompting for second factor' do
expect(page).not_to have_content('Two-Factor Authentication')
expect(current_path).to eq root_path
end
end
context 'when authn_context is not worth two factors' do
it 'shows 2FA prompt after OAuth login' do
expect(page).to have_content('Two-Factor Authentication')
enter_code(user.current_otp)
expect(current_path).to eq root_path
end
end end
end end
end end
......
<?xml version='1.0'?>
<samlp:Response xmlns:samlp='urn:oasis:names:tc:SAML:2.0:protocol' xmlns:saml='urn:oasis:names:tc:SAML:2.0:assertion' ID='pfxb9b71715-2202-9a51-8ae5-689d5b9dd25a' Version='2.0' IssueInstant='2014-07-17T01:01:48Z' Destination='http://sp.example.com/demo1/index.php?acs' InResponseTo='ONELOGIN_4fee3b046395c4e751011e97f8900b5273d56685'>
<saml:Issuer>http://idp.example.com/metadata.php</saml:Issuer><ds:Signature xmlns:ds='http://www.w3.org/2000/09/xmldsig#'>
<ds:SignedInfo><ds:CanonicalizationMethod Algorithm='http://www.w3.org/2001/10/xml-exc-c14n#'/>
<ds:SignatureMethod Algorithm='http://www.w3.org/2000/09/xmldsig#rsa-sha1'/>
<ds:Reference URI='#pfxb9b71715-2202-9a51-8ae5-689d5b9dd25a'><ds:Transforms><ds:Transform Algorithm='http://www.w3.org/2000/09/xmldsig#enveloped-signature'/><ds:Transform Algorithm='http://www.w3.org/2001/10/xml-exc-c14n#'/></ds:Transforms><ds:DigestMethod Algorithm='http://www.w3.org/2000/09/xmldsig#sha1'/><ds:DigestValue>z0Y25hsUHVJJnYhgB5LzPVjqbgM=</ds:DigestValue></ds:Reference></ds:SignedInfo><ds:SignatureValue>NSdsZopzNX4kJETipLNbU+7dG4GPTj5e40iSBaUeUMc1UUSX4UCe9Qx6R9ADEkEQgNekgYaCFOuY90kLNh9Ky0Czq8gd4w7ykQJEVJ7VF7LakmG8dPedHAKyAMAuZ8y3mNGye31vtR9frYaznCVoxB3eAi9rbVOXkQtdOTRMHec=</ds:SignatureValue>
<ds:KeyInfo><ds:X509Data><ds:X509Certificate>MIICajCCAdOgAwIBAgIBADANBgkqhkiG9w0BAQ0FADBSMQswCQYDVQQGEwJ1czETMBEGA1UECAwKQ2FsaWZvcm5pYTEVMBMGA1UECgwMT25lbG9naW4gSW5jMRcwFQYDVQQDDA5zcC5leGFtcGxlLmNvbTAeFw0xNDA3MTcxNDEyNTZaFw0xNTA3MTcxNDEyNTZaMFIxCzAJBgNVBAYTAnVzMRMwEQYDVQQIDApDYWxpZm9ybmlhMRUwEwYDVQQKDAxPbmVsb2dpbiBJbmMxFzAVBgNVBAMMDnNwLmV4YW1wbGUuY29tMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDZx+ON4IUoIWxgukTb1tOiX3bMYzYQiwWPUNMp+Fq82xoNogso2bykZG0yiJm5o8zv/sd6pGouayMgkx/2FSOdc36T0jGbCHuRSbtia0PEzNIRtmViMrt3AeoWBidRXmZsxCNLwgIV6dn2WpuE5Az0bHgpZnQxTKFek0BMKU/d8wIDAQABo1AwTjAdBgNVHQ4EFgQUGHxYqZYyX7cTxKVODVgZwSTdCnwwHwYDVR0jBBgwFoAUGHxYqZYyX7cTxKVODVgZwSTdCnwwDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQ0FAAOBgQByFOl+hMFICbd3DJfnp2Rgd/dqttsZG/tyhILWvErbio/DEe98mXpowhTkC04ENprOyXi7ZbUqiicF89uAGyt1oqgTUCD1VsLahqIcmrzgumNyTwLGWo17WDAa1/usDhetWAMhgzF/Cnf5ek0nK00m0YZGyc4LzgD0CROMASTWNg==</ds:X509Certificate></ds:X509Data></ds:KeyInfo></ds:Signature>
<samlp:Status>
<samlp:StatusCode Value='urn:oasis:names:tc:SAML:2.0:status:Success'/>
</samlp:Status>
<saml:Assertion xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance' xmlns:xs='http://www.w3.org/2001/XMLSchema' ID='_d71a3a8e9fcc45c9e9d248ef7049393fc8f04e5f75' Version='2.0' IssueInstant='2014-07-17T01:01:48Z'>
<saml:Issuer>http://idp.example.com/metadata.php</saml:Issuer>
<saml:Subject>
<saml:NameID SPNameQualifier='http://sp.example.com/demo1/metadata.php' Format='urn:oasis:names:tc:SAML:2.0:nameid-format:transient'>_ce3d2948b4cf20146dee0a0b3dd6f69b6cf86f62d7</saml:NameID>
<saml:SubjectConfirmation Method='urn:oasis:names:tc:SAML:2.0:cm:bearer'>
<saml:SubjectConfirmationData NotOnOrAfter='2024-01-18T06:21:48Z' Recipient='http://sp.example.com/demo1/index.php?acs' InResponseTo='ONELOGIN_4fee3b046395c4e751011e97f8900b5273d56685'/>
</saml:SubjectConfirmation>
</saml:Subject>
<saml:Conditions NotBefore='2014-07-17T01:01:18Z' NotOnOrAfter='2024-01-18T06:21:48Z'>
<saml:AudienceRestriction>
<saml:Audience>http://sp.example.com/demo1/metadata.php</saml:Audience>
</saml:AudienceRestriction>
</saml:Conditions>
<saml:AuthnStatement AuthnInstant='2014-07-17T01:01:48Z' SessionNotOnOrAfter='2024-07-17T09:01:48Z' SessionIndex='_be9967abd904ddcae3c0eb4189adbe3f71e327cf93'>
<saml:AuthnContext>
<saml:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:Password</saml:AuthnContextClassRef>
</saml:AuthnContext>
</saml:AuthnStatement>
<saml:AttributeStatement>
<saml:Attribute Name='uid' NameFormat='urn:oasis:names:tc:SAML:2.0:attrname-format:basic'>
<saml:AttributeValue xsi:type='xs:string'>test</saml:AttributeValue>
</saml:Attribute>
<saml:Attribute Name='mail' NameFormat='urn:oasis:names:tc:SAML:2.0:attrname-format:basic'>
<saml:AttributeValue xsi:type='xs:string'>test@example.com</saml:AttributeValue>
</saml:Attribute>
<saml:Attribute Name='eduPersonAffiliation' NameFormat='urn:oasis:names:tc:SAML:2.0:attrname-format:basic'>
<saml:AttributeValue xsi:type='xs:string'>users</saml:AttributeValue>
<saml:AttributeValue xsi:type='xs:string'>examplerole1</saml:AttributeValue>
</saml:Attribute>
</saml:AttributeStatement>
</saml:Assertion>
</samlp:Response>
...@@ -779,4 +779,12 @@ describe Gitlab::Auth::OAuth::User do ...@@ -779,4 +779,12 @@ describe Gitlab::Auth::OAuth::User do
end end
end end
end end
describe '#bypass_two_factor?' do
subject { oauth_user.bypass_two_factor? }
it 'returns always false' do
is_expected.to be_falsey
end
end
end end
...@@ -37,4 +37,55 @@ describe Gitlab::Auth::Saml::AuthHash do ...@@ -37,4 +37,55 @@ describe Gitlab::Auth::Saml::AuthHash do
end end
end end
end end
describe '#authn_context' do
let(:auth_hash_data) do
{
provider: 'saml',
uid: 'some_uid',
info:
{
name: 'mockuser',
email: 'mock@email.ch',
image: 'mock_user_thumbnail_url'
},
credentials:
{
token: 'mock_token',
secret: 'mock_secret'
},
extra:
{
raw_info:
{
info:
{
name: 'mockuser',
email: 'mock@email.ch',
image: 'mock_user_thumbnail_url'
}
}
}
}
end
subject(:saml_auth_hash) { described_class.new(OmniAuth::AuthHash.new(auth_hash_data)) }
context 'with response_object' do
before do
auth_hash_data[:extra][:response_object] = { document:
saml_xml(File.read('spec/fixtures/authentication/saml_response.xml')) }
end
it 'can extract authn_context' do
expect(saml_auth_hash.authn_context).to eq 'urn:oasis:names:tc:SAML:2.0:ac:classes:Password'
end
end
context 'without response_object' do
it 'returns an empty string' do
expect(saml_auth_hash.authn_context).to be_nil
end
end
end
end end
...@@ -400,4 +400,45 @@ describe Gitlab::Auth::Saml::User do ...@@ -400,4 +400,45 @@ describe Gitlab::Auth::Saml::User do
end end
end end
end end
describe '#bypass_two_factor?' do
let(:saml_config) { mock_saml_config_with_upstream_two_factor_authn_contexts }
subject { saml_user.bypass_two_factor? }
context 'with authn_contexts_worth_two_factors configured' do
before do
stub_omniauth_saml_config(enabled: true, auto_link_saml_user: true, allow_single_sign_on: ['saml'], providers: [saml_config])
end
it 'returns true when authn_context is worth two factors' do
allow(saml_user.auth_hash).to receive(:authn_context).and_return('urn:oasis:names:tc:SAML:2.0:ac:classes:SecondFactorOTPSMS')
is_expected.to be_truthy
end
it 'returns false when authn_context is not worth two factors' do
allow(saml_user.auth_hash).to receive(:authn_context).and_return('urn:oasis:names:tc:SAML:2.0:ac:classes:Password')
is_expected.to be_falsey
end
it 'returns false when authn_context is blank' do
is_expected.to be_falsey
end
end
context 'without auth_contexts_worth_two_factors_configured' do
before do
stub_omniauth_saml_config(enabled: true, auto_link_saml_user: true, allow_single_sign_on: ['saml'], providers: [mock_saml_config])
end
it 'returns false when authn_context is present' do
allow(saml_user.auth_hash).to receive(:authn_context).and_return('urn:oasis:names:tc:SAML:2.0:ac:classes:SecondFactorOTPSMS')
is_expected.to be_falsey
end
it 'returns false when authn_context is blank' do
is_expected.to be_falsey
end
end
end
end end
...@@ -46,8 +46,8 @@ module LoginHelpers ...@@ -46,8 +46,8 @@ module LoginHelpers
@current_user = user @current_user = user
end end
def gitlab_sign_in_via(provider, user, uid) def gitlab_sign_in_via(provider, user, uid, saml_response = nil)
mock_auth_hash(provider, uid, user.email) mock_auth_hash(provider, uid, user.email, saml_response)
visit new_user_session_path visit new_user_session_path
click_link provider click_link provider
end end
...@@ -87,7 +87,7 @@ module LoginHelpers ...@@ -87,7 +87,7 @@ module LoginHelpers
click_link "oauth-login-#{provider}" click_link "oauth-login-#{provider}"
end end
def mock_auth_hash(provider, uid, email) def mock_auth_hash(provider, uid, email, saml_response = nil)
# The mock_auth configuration allows you to set per-provider (or default) # The mock_auth configuration allows you to set per-provider (or default)
# authentication hashes to return during integration testing. # authentication hashes to return during integration testing.
OmniAuth.config.mock_auth[provider.to_sym] = OmniAuth::AuthHash.new({ OmniAuth.config.mock_auth[provider.to_sym] = OmniAuth::AuthHash.new({
...@@ -109,12 +109,21 @@ module LoginHelpers ...@@ -109,12 +109,21 @@ module LoginHelpers
email: email, email: email,
image: 'mock_user_thumbnail_url' image: 'mock_user_thumbnail_url'
} }
},
response_object: {
document: saml_xml(saml_response)
} }
} }
}) })
Rails.application.env_config['omniauth.auth'] = OmniAuth.config.mock_auth[provider.to_sym] Rails.application.env_config['omniauth.auth'] = OmniAuth.config.mock_auth[provider.to_sym]
end end
def saml_xml(raw_saml_response)
return '' if raw_saml_response.blank?
XMLSecurity::SignedDocument.new(raw_saml_response, [])
end
def mock_saml_config def mock_saml_config
OpenStruct.new(name: 'saml', label: 'saml', args: { OpenStruct.new(name: 'saml', label: 'saml', args: {
assertion_consumer_service_url: 'https://localhost:3443/users/auth/saml/callback', assertion_consumer_service_url: 'https://localhost:3443/users/auth/saml/callback',
...@@ -125,6 +134,14 @@ module LoginHelpers ...@@ -125,6 +134,14 @@ module LoginHelpers
}) })
end end
def mock_saml_config_with_upstream_two_factor_authn_contexts
config = mock_saml_config
config.args[:upstream_two_factor_authn_contexts] = %w(urn:oasis:names:tc:SAML:2.0:ac:classes:CertificateProtectedTransport
urn:oasis:names:tc:SAML:2.0:ac:classes:SecondFactorOTPSMS
urn:oasis:names:tc:SAML:2.0:ac:classes:SecondFactorIGTOKEN)
config
end
def stub_omniauth_provider(provider, context: Rails.application) def stub_omniauth_provider(provider, context: Rails.application)
env = env_from_context(context) env = env_from_context(context)
...@@ -140,13 +157,16 @@ module LoginHelpers ...@@ -140,13 +157,16 @@ module LoginHelpers
env['omniauth.error.strategy'] = strategy env['omniauth.error.strategy'] = strategy
end end
def stub_omniauth_saml_config(messages) def stub_omniauth_saml_config(messages, context: Rails.application)
set_devise_mapping(context: Rails.application) set_devise_mapping(context: context)
Rails.application.routes.disable_clear_and_finalize = true routes = Rails.application.routes
Rails.application.routes.draw do routes.disable_clear_and_finalize = true
routes.formatter.clear
routes.draw do
post '/users/auth/saml' => 'omniauth_callbacks#saml' post '/users/auth/saml' => 'omniauth_callbacks#saml'
end end
allow(Gitlab::Auth::OAuth::Provider).to receive_messages(providers: [:saml], config_for: mock_saml_config) saml_config = messages.key?(:providers) ? messages[:providers].first : mock_saml_config
allow(Gitlab::Auth::OAuth::Provider).to receive_messages(providers: [:saml], config_for: saml_config)
stub_omniauth_setting(messages) stub_omniauth_setting(messages)
stub_saml_authorize_path_helpers stub_saml_authorize_path_helpers
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