diff --git a/Gemfile b/Gemfile index 1444b2642b59..944b84aa9024 100644 --- a/Gemfile +++ b/Gemfile @@ -53,7 +53,7 @@ gem 'daemons' gem 'bcrypt', '~> 3.1' gem 'get_process_mem' gem 'rack-cors', '~> 1.0.2', require: 'rack/cors' -gem 'jwt', '~> 2.1.0' +gem 'jwt', '~> 2.2.1' gem 'graphql', '~> 1.8.0' gem 'graphql-batch' diff --git a/app/controllers/concerns/foreman/controller/authentication.rb b/app/controllers/concerns/foreman/controller/authentication.rb index a7a694d1dfc3..88e7e91d6a09 100644 --- a/app/controllers/concerns/foreman/controller/authentication.rb +++ b/app/controllers/concerns/foreman/controller/authentication.rb @@ -71,12 +71,18 @@ def sso_authentication def set_current_user(user) User.current = user - # API access resets the whole session and marks the session as initialized from API # such sessions aren't checked for CSRF # UI access resets only session ID if api_request? - reset_session + # When authenticating using SSO::OpenidConnect, upon successful authentication, we refresh the + # :expires_at and :sso_method values in the session (in OpenidConnect#update_session). + # Hence when we reset_session for SSO::OpenidConnect here, we do not reset the expires_at for session. + if session[:sso_method] == "SSO::OpenidConnect" + backup_session_content([:sso_method, :expires_at]) { reset_session } + else + reset_session + end session[:user] = user.id session[:api_authenticated_session] = true set_activity_time diff --git a/app/controllers/concerns/foreman/controller/session.rb b/app/controllers/concerns/foreman/controller/session.rb index 64348297d031..c631df9a5335 100644 --- a/app/controllers/concerns/foreman/controller/session.rb +++ b/app/controllers/concerns/foreman/controller/session.rb @@ -14,8 +14,8 @@ def session_expiry # Backs up some state from a user's session around a supplied block, which # will usually expire or reset the session in some way - def backup_session_content - save_items = session.to_hash.slice('organization_id', 'location_id', 'original_uri', 'sso_method').symbolize_keys + def backup_session_content(keys = [:organization_id, :location_id, :original_uri, :sso_method]) + save_items = session.to_hash.slice(*keys.map(&:to_s)).symbolize_keys yield if block_given? session.update(save_items) end @@ -25,7 +25,11 @@ def update_activity_time set_activity_time end + # In case of SSO::OpenidConnect Foreman will use :expiry_at from the token. This is + # set when the current user is set (in Authentication#set_current_user method) + # For other SSO types like basic_auth we use expiry at from the Settings def set_activity_time + return if session[:sso_method] == "SSO::OpenidConnect" session[:expires_at] = Setting[:idle_timeout].minutes.from_now.to_i end diff --git a/app/models/setting.rb b/app/models/setting.rb index 5b2519fca209..ab7b3ac2b319 100644 --- a/app/models/setting.rb +++ b/app/models/setting.rb @@ -12,8 +12,8 @@ class Setting < ApplicationRecord TYPES = %w{integer boolean hash array string} FROZEN_ATTRS = %w{name category full_name} NONZERO_ATTRS = %w{puppet_interval idle_timeout entries_per_page max_trend outofsync_interval} - BLANK_ATTRS = %w{ host_owner trusted_hosts login_delegation_logout_url authorize_login_delegation_auth_source_user_autocreate root_pass default_location default_organization websockets_ssl_key websockets_ssl_cert oauth_consumer_key oauth_consumer_secret login_text - smtp_address smtp_domain smtp_user_name smtp_password smtp_openssl_verify_mode smtp_authentication sendmail_arguments sendmail_location http_proxy http_proxy_except_list default_locale default_timezone ssl_certificate ssl_ca_file ssl_priv_key default_pxe_item_global default_pxe_item_local } + BLANK_ATTRS = %w{ host_owner trusted_hosts login_delegation_logout_url authorize_login_delegation_auth_source_user_autocreate root_pass default_location default_organization websockets_ssl_key websockets_ssl_cert oauth_consumer_key oauth_consumer_secret login_text oidc_audience oidc_issuer oidc_algorithm + smtp_address smtp_domain smtp_user_name smtp_password smtp_openssl_verify_mode smtp_authentication sendmail_arguments sendmail_location http_proxy http_proxy_except_list default_locale default_timezone ssl_certificate ssl_ca_file ssl_priv_key default_pxe_item_global default_pxe_item_local oidc_jwks_url } ARRAY_HOSTNAMES = %w{trusted_hosts} URI_ATTRS = %w{foreman_url unattended_url} URI_BLANK_ATTRS = %w{login_delegation_logout_url} diff --git a/app/models/setting/auth.rb b/app/models/setting/auth.rb index b451b4db3539..0de0e1b123d1 100644 --- a/app/models/setting/auth.rb +++ b/app/models/setting/auth.rb @@ -26,6 +26,10 @@ def self.default_settings self.set('idle_timeout', N_("Log out idle users after a certain number of minutes"), 60, N_('Idle timeout')), self.set('bcrypt_cost', N_("Cost value of bcrypt password hash function for internal auth-sources (4-30). Higher value is safer but verification is slower particularly for stateless API calls and UI logins. Password change needed to take effect."), 4, N_('BCrypt password cost')), self.set('bmc_credentials_accessible', N_("Permits access to BMC interface passwords through ENC YAML output and in templates"), true, N_('BMC credentials access')), + self.set('oidc_jwks_url', N_("OpenID Connect JSON Web Key Set(JWKS) URL. For example if you are using Keycloak this url(https:///auth/realms//protocol/openid-connect/certs) would be found as `jwk_uri` at https:///auth/realms//.well-known/openid-configuration."), nil, N_('OIDC JWKs URL')), + self.set('oidc_audience', N_("Name of the OpenID Connect Audience that is being used for Authentication. For example in case of Keycloak this is the Client ID."), nil, N_('OIDC Audience')), + self.set('oidc_issuer', N_("The iss(issuer) claim identifies the principal that issued the JWT, which exists at a `/.well-known/openid-configuration` in case of most of the IDP's."), nil, N_('OIDC Issuer')), + self.set('oidc_algorithm', N_("The algorithm with which JWT was encoded in the IDP."), nil, N_('OIDC Algorithm')), ] end diff --git a/app/models/user.rb b/app/models/user.rb index 01e192b9cca4..099ffde1e519 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -299,10 +299,10 @@ def self.find_or_create_external_user(attrs, auth_source_name) user.usergroups = new_usergroups.uniq end - return true + return user # not existing user and creating is disabled by settings elsif auth_source_name.nil? - return false + return nil # not existing user and auth source is set, we'll create the user and auth source if needed else User.as_anonymous_admin do @@ -315,7 +315,7 @@ def self.find_or_create_external_user(attrs, auth_source_name) end user.post_successful_login end - return true + return user end end diff --git a/app/services/jwt_token.rb b/app/services/jwt_token.rb index 6baf78e185eb..cf31cf62c1f0 100644 --- a/app/services/jwt_token.rb +++ b/app/services/jwt_token.rb @@ -33,6 +33,11 @@ def to_s token end + # This method does not verify if the token signature is valid + def decoded_payload + @decoded_payload ||= JWT.decode(token, nil, false).first + end + private def secret @@ -43,9 +48,4 @@ def secret def user_id @user_id ||= decoded_payload['user_id'] end - - # This method does not verify if the token signature is valid - def decoded_payload - @decoded_payload ||= JWT.decode(token, nil, false).first - end end diff --git a/app/services/oidc_jwt.rb b/app/services/oidc_jwt.rb new file mode 100644 index 000000000000..ba65627ff711 --- /dev/null +++ b/app/services/oidc_jwt.rb @@ -0,0 +1,6 @@ +class OidcJwt < JwtToken + def decode + return if token.blank? + OidcJwtValidate.new(token).decoded_payload + end +end diff --git a/app/services/oidc_jwt_validate.rb b/app/services/oidc_jwt_validate.rb new file mode 100644 index 000000000000..8612cf737733 --- /dev/null +++ b/app/services/oidc_jwt_validate.rb @@ -0,0 +1,50 @@ +class OidcJwtValidate + attr_reader :decoded_token + delegate :logger, to: :Rails + + def initialize(jwt_token) + @jwt_token = jwt_token + end + + def decoded_payload + # OpenSSL#set_key method does not support ruby version < 2.4.0, apparently the JWT gem uses + # OpenSSL#set_key method for all ruby version. We must remove this condition once new version + # of the JWT(2.2.2) is released. + unless OpenSSL::PKey::RSA.new.respond_to?(:set_key) + Foreman::Logging.logger('app').error "SSO feature is not available for Ruby < 2.4.0" + return nil + end + JWT.decode(@jwt_token, nil, true, + { aud: Setting['oidc_audience'], + verify_aud: true, + iss: Setting['oidc_issuer'], + verify_iss: true, + algorithms: [Setting['oidc_algorithm']], + jwks: jwks_loader } + ).first + rescue JWT::DecodeError => e + Foreman::Logging.exception('Failed to decode JWT', e) + nil + end + + private + + def jwks_loader(options = {}) + response = RestClient::Request.execute( + :url => Setting['oidc_jwks_url'], + :method => :get, + :verify_ssl => true + ) + json_response = JSON.parse(response) + if json_response.is_a?(Hash) + jwks_keys = json_response['keys'] + @cached_keys = nil if options[:invalidate] # need to reload the keys + @cached_keys ||= { keys: jwks_keys.map(&:symbolize_keys) } + else + raise JSON::ParserError.new('Invalid JWKS response') + end + rescue RestClient::Exception, SocketError, JSON::ParserError => e + Foreman::Logging.exception('Failed to load the JWKS', e) + {} + end +end diff --git a/app/services/sso.rb b/app/services/sso.rb index 63577233be7c..d4de3945cc58 100644 --- a/app/services/sso.rb +++ b/app/services/sso.rb @@ -1,5 +1,5 @@ module SSO - METHODS = [Apache, Basic, Jwt, Oauth] + METHODS = [Apache, Basic, Jwt, Oauth, OpenidConnect] def self.get_available(controller) all_methods = all.map { |method| method.new(controller) } diff --git a/app/services/sso/jwt.rb b/app/services/sso/jwt.rb index c06cc6ad9597..338af121b05c 100644 --- a/app/services/sso/jwt.rb +++ b/app/services/sso/jwt.rb @@ -3,7 +3,7 @@ class Jwt < Base attr_reader :current_user def available? - controller.api_request? && bearer_token_set? + controller.api_request? && bearer_token_set? && no_issuer? end def authenticate! @@ -35,5 +35,9 @@ def jwt_token_from_request def bearer_token_set? request.authorization.present? && request.authorization.start_with?('Bearer') end + + def no_issuer? + !jwt_token.decoded_payload.key?('iss') + end end end diff --git a/app/services/sso/openid_connect.rb b/app/services/sso/openid_connect.rb new file mode 100644 index 000000000000..123de34eeb70 --- /dev/null +++ b/app/services/sso/openid_connect.rb @@ -0,0 +1,58 @@ +module SSO + class OpenidConnect < Base + delegate :session, :to => :controller + attr_reader :current_user + + def available? + controller.api_request? && bearer_token_set? && valid_issuer? + end + + def authenticate! + payload = jwt_token.decode + return nil if payload.nil? + user = find_or_create_user_from_jwt(payload) + @current_user = user + update_session(payload) + user&.login + end + + def authenticated? + self.user = User.current.presence || authenticate! + end + + private + + def jwt_token + @jwt_token ||= jwt_token_from_request + end + + def jwt_token_from_request + token = request.authorization.split(' ')[1] + OidcJwt.new(token) + end + + def bearer_token_set? + request.authorization.present? && request.authorization.start_with?('Bearer') + end + + def valid_issuer? + payload = jwt_token.decoded_payload + payload.key?('iss') && (payload['iss'] == Setting['oidc_issuer']) + end + + def update_session(payload) + session[:sso_method] = self.class.to_s + session[:expires_at] = payload['exp'] + end + + def find_or_create_user_from_jwt(payload) + User.find_or_create_external_user( + { login: payload['preferred_username'], + mail: payload['email'], + firstname: payload['given_name'], + lastname: payload['family_name']}, + Setting['authorize_login_delegation_auth_source_user_autocreate'] + ) + end + end +end diff --git a/test/fixtures/settings.yml b/test/fixtures/settings.yml index 2ca51ec0946f..3cafbda0bf7b 100644 --- a/test/fixtures/settings.yml +++ b/test/fixtures/settings.yml @@ -404,3 +404,28 @@ attribute87: category: Setting::General default: '2abbbe02-4ace-4269-9e20-2753f3206cc2' description: 'Foreman UUID' +attribute88: + name: oidc_jwks_url + category: Setting::Auth + default: 127.0.0.1 + description: 'OpenID Connect JSON Web Key Set(JWKS) URL. For example if you are using Keycloak this url(https:///auth/realms//protocol/openid-connect/certs) would be found as `jwk_uri` at https:///auth/realms//.well-known/openid-configuration.' +attribute89: + name: oidc_audience + category: Setting::Auth + default: 'rest-client' + description: 'Name of the OpenID Connect Audience that is being used for Authentication. For exmaple in case of Keycloak this is the Client ID.' +attribute90: + name: oidc_issuer + category: Setting::Auth + default: 127.0.0.1 + description: 'The iss (issuer) claim identifies the principal that issued the JWT, which exists at a `/.well-known/openid-configuration` in case of most of the IDPs.' +attribute91: + name: oidc_algorithm + category: Setting::Auth + default: 'RS512' + description: 'The alogirithm with which JWT was encoded in the IDP.' +attribute92: + name: authorize_login_delegation_auth_source_user_autocreate + category: Setting::Auth + default: 'External' + description: 'Name of the external auth source where unknown externally authentication users (see authorize_login_delegation) should be created (keep unset to prevent the autocreation)' diff --git a/test/models/user_test.rb b/test/models/user_test.rb index a43b85fe2845..8354ddf75e19 100644 --- a/test/models/user_test.rb +++ b/test/models/user_test.rb @@ -687,29 +687,36 @@ def setup_user(operation, type = 'users', search = nil) end context "find_or_create_external_user" do + not_existing_user_login = 'not_existing_user' + not_existing_auth_source = 'new_external_source' + context "internal or not existing AuthSource" do test 'existing user' do assert_difference('User.count', 0) do - assert User.find_or_create_external_user({:login => users(:one).login}, nil) + login = users(:one).login + assert_equal User.find_or_create_external_user({:login => login}, nil), + User.find_by_login(login) end end test 'not existing user without auth source specified' do assert_difference('User.count', 0) do - refute User.find_or_create_external_user({:login => 'not_existing_user'}, nil) + user = User.find_or_create_external_user({:login => not_existing_user_login}, nil) + assert user.nil? end end test 'not existing user with non existing auth source' do assert_difference('User.count', 1) do assert_difference('AuthSource.count', 1) do - assert User.find_or_create_external_user({:login => 'not_existing_user'}, - 'new_external_source') + user = User.find_or_create_external_user({:login => not_existing_user_login}, + not_existing_auth_source) + assert_equal user, User.find_by_login(not_existing_user_login) + + new_source = AuthSourceExternal.find_by_name(not_existing_auth_source) + assert_equal new_source.name, user.auth_source.name end end - created_user = User.find_by_login('not_existing_user') - new_source = AuthSourceExternal.find_by_name('new_external_source') - assert_equal new_source.name, created_user.auth_source.name end end @@ -721,23 +728,24 @@ def setup_user(operation, type = 'users', search = nil) test "not existing" do assert_difference('User.count', 1) do assert_difference('AuthSource.count', 0) do - assert User.find_or_create_external_user({:login => 'not_existing_user'}, - @apache_source.name) + assert_equal User.find_or_create_external_user( + {:login => not_existing_user_login}, @apache_source.name), + User.find_by_login(not_existing_user_login) end end end test "not existing with attributes" do - assert User.find_or_create_external_user({:login => 'not_existing_user', - :mail => 'foobar@example.com', - :firstname => 'Foo', - :lastname => 'Bar'}, - @apache_source.name) - created_user = User.find_by_login('not_existing_user') - assert_equal @apache_source.name, created_user.auth_source.name + created_user = User.find_or_create_external_user( + {:login => not_existing_user_login, + :mail => 'foobar@example.com', + :firstname => 'Foo', + :lastname => 'Bar'}, @apache_source.name) + assert_equal not_existing_user_login, created_user.login + assert_equal @apache_source.name, created_user.auth_source.name assert_equal 'foobar@example.com', created_user.mail - assert_equal 'Foo', created_user.firstname - assert_equal 'Bar', created_user.lastname + assert_equal 'Foo', created_user.firstname + assert_equal 'Bar', created_user.lastname end context 'with external user groups' do @@ -749,11 +757,9 @@ def setup_user(operation, type = 'users', search = nil) test "existing user groups that are assigned" do @external.update(:usergroup => @usergroup, :name => @usergroup.name) - assert User.find_or_create_external_user({:login => "not_existing_user", - :groups => [@external.name, - "notexistentexternal"]}, - @apache_source.name) - created_user = User.find_by_login("not_existing_user") + created_user = User.find_or_create_external_user( + {:login => not_existing_user_login, :groups => [@external.name, 'notexistentexternal']}, + @apache_source.name) assert_equal [@usergroup], created_user.usergroups end end diff --git a/test/unit/oidc_jwt_validate_test.rb b/test/unit/oidc_jwt_validate_test.rb new file mode 100644 index 000000000000..097b090683b1 --- /dev/null +++ b/test/unit/oidc_jwt_validate_test.rb @@ -0,0 +1,72 @@ +require 'test_helper' +require 'ostruct' +require 'openssl' +require 'jwt' + +class OidcJwtValidateTest < ActiveSupport::TestCase + context '#decoded_payload?' do + def setup + skip "SSO feature is not available for Ruby < 2.4.0" unless RUBY_VERSION >= '2.4' + @jwk = JWT::JWK.new(OpenSSL::PKey::RSA.new(2048)) + exp = Time.now.to_i + 4 * 3600 + payload, headers = { "name": "jwt token", "iat": 1557224758, "exp": exp, "typ": "Bearer", "aud": "rest-client", "iss": "127.0.0.1"}, { kid: @jwk.kid } + + @token = JWT.encode(payload, @jwk.keypair, 'RS512', headers) + @decoded_payload = payload.with_indifferent_access + end + + test 'if valid jwk json is passed' do + stub_request(:get, Setting['oidc_jwks_url']) + .to_return(body: {"keys": [@jwk.export]}.to_json) + actual = OidcJwtValidate.new(@token).decoded_payload + expected = @decoded_payload + assert_equal expected, actual + end + + test 'must decode with valid signature and claims' do + stub_request(:get, Setting['oidc_jwks_url']) + .to_return(body: {"keys": [@jwk.export]}.to_json) + actual = OidcJwtValidate.new(@token).decoded_payload + expected = @decoded_payload + assert_equal expected, actual + end + + test 'if signature is not valid' do + other_jwk = JWT::JWK.new(OpenSSL::PKey::RSA.new(2048)) + stub_request(:get, Setting['oidc_jwks_url']) + .to_return(body: {"keys": [other_jwk.export]}.to_json) + actual = OidcJwtValidate.new(@token).decoded_payload + expected = nil + assert_nil expected, actual + end + + test 'if audiance is not valid' do + Setting['oidc_audience'] = "no-client" + stub_request(:get, Setting['oidc_jwks_url']) + .to_return(body: {"keys": [@jwk.export]}.to_json) + actual = OidcJwtValidate.new(@token).decoded_payload + expected = nil + assert_nil expected, actual + Setting['oidc_audience'] = "rest-client" + end + + test 'if token has expired' do + stub_request(:get, Setting['oidc_jwks_url']) + .to_return(body: {"keys": [@jwk.export]}.to_json) + actual = travel 1.day do + OidcJwtValidate.new(@token).decoded_payload + end + expected = nil + assert_nil expected, actual + end + + test 'with invalid token' do + other_token = 'eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxMjMsImlhdCI6MTUxNDgwNDQwMCwianRpIjoiM2U4Mjg2OTQwZWIxNjJlYzczNWY4YTVhYWM1OTI2MDM3ZmRjN2YwNWU1MjFhNzQyMzFhNjVmMjU1N2E4YWY5NCJ9.TSM2xMnMuMEWwAVoIidyLIwJElJQZVQvcfaxyVA7cDI' + stub_request(:get, Setting['oidc_jwks_url']) + .to_return(body: {"keys": [@jwk.export]}.to_json) + actual = OidcJwtValidate.new(other_token).decoded_payload + expected = nil + assert_nil expected, actual + end + end +end diff --git a/test/unit/sso/apache_test.rb b/test/unit/sso/apache_test.rb index 3885a40d2416..ca9e9298c2ea 100644 --- a/test/unit/sso/apache_test.rb +++ b/test/unit/sso/apache_test.rb @@ -53,7 +53,7 @@ def test_authenticated_passes_attributes apache.controller.request.env['REMOTE_USER_LASTNAME'] = 'Bar' User.expects(:find_or_create_external_user). with({:login => 'ares', :mail => 'foobar@example.com', :firstname => 'Foo', :lastname => 'Bar'}, 'apache'). - returns(true) + returns(User.new) assert apache.authenticated? end @@ -68,7 +68,7 @@ def test_authenticated_parses_user_groups apache.controller.request.env['REMOTE_USER_GROUP_2'] = 'does-not-exist-for-sure' User.expects(:find_or_create_external_user). with({:login => 'ares', :groups => [existing.name, 'does-not-exist-for-sure']}, 'apache'). - returns(true) + returns(User.new) assert apache.authenticated? end diff --git a/test/unit/sso/jwt_test.rb b/test/unit/sso/jwt_test.rb index c0973f0f8de8..9b2f276c5eb8 100644 --- a/test/unit/sso/jwt_test.rb +++ b/test/unit/sso/jwt_test.rb @@ -6,7 +6,7 @@ class JwtTest < ActiveSupport::TestCase end context 'api request' do - let(:token) { 'invalid' } + let(:token) { users(:one).jwt_token! } let(:controller) { get_controller(true, token) } let(:sso) { SSO::Jwt.new(controller) } @@ -22,8 +22,6 @@ class JwtTest < ActiveSupport::TestCase end context 'with valid token' do - let(:token) { users(:one).jwt_token! } - test '#authenticate! authenticates a user' do assert_equal users(:one).login, sso.authenticated? assert_equal users(:one).login, sso.user @@ -32,6 +30,7 @@ class JwtTest < ActiveSupport::TestCase end context 'with invalid token' do + let(:token) { 'invalid' } test '#authenticate! does not set user' do assert_nil sso.authenticated? assert_nil sso.user @@ -64,6 +63,7 @@ class JwtTest < ActiveSupport::TestCase def get_controller(api_request, jwt_token = 'invalid', headers = {}) controller = Struct.new(:request).new(Struct.new(:authorization, :headers).new("Bearer #{jwt_token}", headers)) controller.stubs(:api_request?).returns(api_request) + controller.stubs(:session).returns(ActionController::TestSession.new) controller end end diff --git a/test/unit/sso/openid_connect_test.rb b/test/unit/sso/openid_connect_test.rb new file mode 100644 index 000000000000..a38a5d0accde --- /dev/null +++ b/test/unit/sso/openid_connect_test.rb @@ -0,0 +1,101 @@ +require 'test_helper' +require 'securerandom' + +class OpenidConnectTest < ActiveSupport::TestCase + # Generates token: "eyJhbGciOiJub25lIn0.eyJuYW1lIjoiand0IHRva2VuIiwiaWF0IjoxNTU3MjI0NzU4LCJleHAiOjE1NjgyMzc4NzcsInR5cCI6IkJlYXJlciIsImF1ZCI6InJlc3QtY2xpZW50IiwiaXNzIjoiMTI3LjAuMC4xIn0." + let(:payload) do + { "name": "jwt token", + "iat": 1557224758, + "exp": Time.now.to_i + 4 * 3600, + "typ": "Bearer", + "aud": "rest-client", + "iss": "127.0.0.1", + "preferred_username": "jwt", + "email": "jwt@test.com", + "given_name": "jwt", + "family_name": "family" } + end + + let(:decoded_payload) do + payload.with_indifferent_access + end + + describe '#available?' do + test 'returns false when not a api_request' do + subject = SSO::OpenidConnect.new(non_api_controller) + assert_equal subject.available?, false + end + + test "returns true when api request contain valid JWT token" do + token = JWT.encode(payload, nil, 'none') + controller = api_controller({:authorization => "Bearer #{token}"}) + subject = SSO::OpenidConnect.new(controller) + assert_equal subject.available?, true + end + + test "returns true when api request contain invalid JWT token" do + invalid_payload = payload + invalid_payload['iss'] = "random_value" + token = JWT.encode(invalid_payload, nil, 'none') + controller = api_controller({:authorization => "Bearer #{token}"}) + subject = SSO::OpenidConnect.new(controller) + assert_equal subject.available?, false + end + + test "returns false if api request does not contain JWT token" do + controller = api_controller() + subject = SSO::OpenidConnect.new(controller) + assert_equal subject.available?, false + end + end + + describe "#authenticated?" do + let(:subject) do + token = JWT.encode(payload, nil, 'none') + SSO::OpenidConnect.new api_controller({:authorization => "Bearer #{token}"}) + end + + test "it returns nil for Ruby < 2.4.0" do + User.current = nil + skip if RUBY_VERSION >= '2.4' + assert_equal subject.authenticated?, nil + end + + test "it authenticates and sets user when currect user does not exists" do + skip "SSO feature is not available for Ruby < 2.4.0" unless RUBY_VERSION >= '2.4' + User.current = nil + OidcJwt.any_instance.stubs(:decode).returns(decoded_payload) + assert_equal subject.authenticated?, decoded_payload['preferred_username'] + end + + test "it sets user to current_user when currrent_user exists" do + as_user(:one) do + subject.expects(:authenticate!).never + assert_equal users(:one), subject.authenticated? + end + end + end + + private + + def non_api_controller + controller = Struct.new(:request) do + def api_request? + false + end + end + controller.new(ActionDispatch::TestRequest.new({})) + end + + def api_controller(headers = {}) + controller = Struct.new(:request, :session) do + def api_request? + true + end + end + request = ActionDispatch::TestRequest.new({}) + request.headers.merge! headers + session = {} + controller.new(request, session) + end +end