From 7cf23715f1ce17a2747d1dae31c09864ef4f7c41 Mon Sep 17 00:00:00 2001 From: Bryan Rehbein Date: Fri, 9 Dec 2016 19:04:38 -0800 Subject: [PATCH 1/5] use latest openid_connect library --- omniauth-openid-connect.gemspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/omniauth-openid-connect.gemspec b/omniauth-openid-connect.gemspec index 5fad920c..a808324b 100644 --- a/omniauth-openid-connect.gemspec +++ b/omniauth-openid-connect.gemspec @@ -19,7 +19,7 @@ Gem::Specification.new do |spec| spec.require_paths = ["lib"] spec.add_dependency 'omniauth', '~> 1.1' - spec.add_dependency 'openid_connect', '~> 0.9.2' + spec.add_dependency 'openid_connect', '~> 0.12.0' spec.add_dependency 'addressable', '~> 2.3' spec.add_development_dependency "bundler", "~> 1.5" spec.add_development_dependency "minitest" From 77188faf59a48ae239844b2bcbbd918f95e2c8ae Mon Sep 17 00:00:00 2001 From: Bryan Rehbein Date: Fri, 9 Dec 2016 19:05:25 -0800 Subject: [PATCH 2/5] add default name and minor fix with signing alg --- lib/omniauth/strategies/openid_connect.rb | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/omniauth/strategies/openid_connect.rb b/lib/omniauth/strategies/openid_connect.rb index e4705c90..dcc312b5 100644 --- a/lib/omniauth/strategies/openid_connect.rb +++ b/lib/omniauth/strategies/openid_connect.rb @@ -10,6 +10,9 @@ module Strategies class OpenIDConnect include OmniAuth::Strategy + # Default name (to support Devise better) + option :name, 'openid_connect' + option :client_options, { identifier: nil, secret: nil, @@ -201,7 +204,7 @@ def session end def key_or_secret - case options.client_signing_alg + case options.client_signing_alg.to_sym when :HS256, :HS384, :HS512 return client_options.secret when :RS256, :RS384, :RS512 From b7431b4c3ffa76aff59fcefdfcfe6ada10ffbcf5 Mon Sep 17 00:00:00 2001 From: Bryan Rehbein Date: Fri, 9 Dec 2016 19:49:25 -0800 Subject: [PATCH 3/5] added functionality to validate gsuite domain / hd --- lib/omniauth/strategies/openid_connect.rb | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/lib/omniauth/strategies/openid_connect.rb b/lib/omniauth/strategies/openid_connect.rb index dcc312b5..272f1073 100644 --- a/lib/omniauth/strategies/openid_connect.rb +++ b/lib/omniauth/strategies/openid_connect.rb @@ -45,6 +45,7 @@ class OpenIDConnect option :send_nonce, true option :send_scope_to_token_endpoint, true option :client_auth_method + option :allowed_gsuite_domains, [] uid { user_info.sub } @@ -128,6 +129,12 @@ def authorize_uri nonce: (new_nonce if options.send_nonce), hd: options.hd, } + + # Add the hd parameter for GSuite if we haven't already defined it + opts[:hd] ||= options.allowed_gsuite_domains.first if options.allowed_gsuite_domains.length == 1 + opts[:hd] ||= "*" if options.allowed_gsuite_domains.length > 1 + + puts client.authorization_uri(opts.reject{|k,v| v.nil?}) client.authorization_uri(opts.reject{|k,v| v.nil?}) end @@ -169,6 +176,9 @@ def access_token client_id: client_options.identifier, nonce: stored_nonce ) + unless options.allowed_gsuite_domains.empty? + raise "Invalid GSuite Domain" unless options.allowed_gsuite_domains.include?(_id_token.raw_attributes['hd']) + end _access_token }.call() end From 09e41fe2d186ba92b9cc4e00f49fa17a4d62e7df Mon Sep 17 00:00:00 2001 From: Bryan Rehbein Date: Mon, 20 Mar 2017 09:06:54 -0700 Subject: [PATCH 4/5] added prompt parameter support --- lib/omniauth/openid_connect/version.rb | 2 +- lib/omniauth/strategies/openid_connect.rb | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/omniauth/openid_connect/version.rb b/lib/omniauth/openid_connect/version.rb index 69a31055..b537d2f3 100644 --- a/lib/omniauth/openid_connect/version.rb +++ b/lib/omniauth/openid_connect/version.rb @@ -1,5 +1,5 @@ module OmniAuth module OpenIDConnect - VERSION = "0.2.2" + VERSION = "0.2.2.vn2" end end diff --git a/lib/omniauth/strategies/openid_connect.rb b/lib/omniauth/strategies/openid_connect.rb index 272f1073..7eacf37d 100644 --- a/lib/omniauth/strategies/openid_connect.rb +++ b/lib/omniauth/strategies/openid_connect.rb @@ -127,6 +127,7 @@ def authorize_uri scope: options.scope, state: new_state, nonce: (new_nonce if options.send_nonce), + prompt: options.prompt, hd: options.hd, } From a014d1a22a30409c8a96a1642ed9300e3cee92dc Mon Sep 17 00:00:00 2001 From: Bryan Rehbein Date: Tue, 18 Aug 2020 19:31:35 -0500 Subject: [PATCH 5/5] initial import, wip --- CHANGELOG.md | 47 + Gemfile | 2 + Guardfile | 8 +- Rakefile | 2 +- lib/omniauth-openid-connect.rb | 1 - lib/omniauth/openid_connect.rb | 8 +- lib/omniauth/openid_connect/errors.rb | 3 + lib/omniauth/openid_connect/version.rb | 4 +- lib/omniauth_openid_connect.rb | 3 + omniauth-openid-connect.gemspec | 48 +- .../omniauth/openid_connect/version_test.rb | 7 - .../strategies/openid_connect_test.rb | 956 +++++++++++------- test/strategy_test_case.rb | 51 + test/test_helper.rb | 68 +- 14 files changed, 769 insertions(+), 439 deletions(-) create mode 100644 CHANGELOG.md delete mode 100644 lib/omniauth-openid-connect.rb create mode 100644 lib/omniauth_openid_connect.rb delete mode 100644 test/lib/omniauth/openid_connect/version_test.rb create mode 100644 test/strategy_test_case.rb diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..8f4d5d75 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,47 @@ +# v0.3.5 (07.06.2020) + +- bugfix: Info from decoded id_token is not exposed into `request.env['omniauth.auth']` [#61](https://github.com/m0n9oose/omniauth_openid_connect/pull/61) +- bugfix: NoMethodError (`undefined method 'count' for #`) [#60](https://github.com/m0n9oose/omniauth_openid_connect/pull/60) + +# v0.3.4 (21.05.2020) + +- Try to verify id_token when response_type is code [#44](https://github.com/m0n9oose/omniauth_openid_connect/pull/44) +- Provide more information on error [#49](https://github.com/m0n9oose/omniauth_openid_connect/pull/49) +- Update configuration documentation [#53](https://github.com/m0n9oose/omniauth_openid_connect/pull/53) +- Add documentation about the send_scope_to_token_endpoint config property [#52](https://github.com/m0n9oose/omniauth_openid_connect/pull/52) +- refactor: take uid_field from raw_attributes [#54](https://github.com/m0n9oose/omniauth_openid_connect/pull/54) +- chore(ci): add 2.7, ruby-head and jruby-head [#55](https://github.com/m0n9oose/omniauth_openid_connect/pull/55) + +# v0.3.3 (09.11.2019) + +- Pass `acr_values` to authorize url [#43](https://github.com/m0n9oose/omniauth_openid_connect/pull/43) +- Add raw info for id token [#42](https://github.com/m0n9oose/omniauth_openid_connect/pull/42) +- Fixed `id_token` verification when `id_token` is not used [#41](https://github.com/m0n9oose/omniauth_openid_connect/pull/41) +- Cast `response_type` to string when checking if it is set in params [#36](https://github.com/m0n9oose/omniauth_openid_connect/pull/36) +- Support both symbol and string version of `response_type` option [#35](https://github.com/m0n9oose/omniauth_openid_connect/pull/35) +- Fix gemspec homepage [#33](https://github.com/m0n9oose/omniauth_openid_connect/pull/33) +- Add support for `response_type` `id_token` [#32](https://github.com/m0n9oose/omniauth_openid_connect/pull/32) + +# v0.3.2 (03.08.2019) + +- Use response_mode in `authorize_uri` if the option is defined [#30](https://github.com/m0n9oose/omniauth_openid_connect/pull/30) +- Move verification of `id_token` to before accessing tokens [#28](https://github.com/m0n9oose/omniauth_openid_connect/pull/28) +- Update omniauth dependency [#26](https://github.com/m0n9oose/omniauth_openid_connect/pull/26) + +# v0.3.1 (08.06.2019) + +- Set default OmniAuth name to openid_connect [#23](https://github.com/m0n9oose/omniauth_openid_connect/pull/23) + +# v0.3.0 (27.04.2019) + +- RP-Initiated Logout phase [#5](https://github.com/m0n9oose/omniauth_openid_connect/pull/5) +- Allows `ui_locales`, `claims_locales` and `login_hint` as request params [#6](https://github.com/m0n9oose/omniauth_openid_connect/pull/6) +- Make uid label configurable [#11](https://github.com/m0n9oose/omniauth_openid_connect/pull/11) +- Allow rails applications to handle state mismatch [#14](https://github.com/m0n9oose/omniauth_openid_connect/pull/14) +- Handle errors when fetching access_token at callback_phase [#17](https://github.com/m0n9oose/omniauth_openid_connect/pull/17) +- Allow state method to receive env [#19](https://github.com/m0n9oose/omniauth_openid_connect/pull/19) + +# v0.2.4 (06.01.2019) + +- Prompt and login hint [#4](https://github.com/m0n9oose/omniauth_openid_connect/pull/4) +- Bump openid_connect dependency [#9](https://github.com/m0n9oose/omniauth_openid_connect/pull/9) diff --git a/Gemfile b/Gemfile index 851fabc2..5f10ba8c 100644 --- a/Gemfile +++ b/Gemfile @@ -1,2 +1,4 @@ +# frozen_string_literal: true + source 'https://rubygems.org' gemspec diff --git a/Guardfile b/Guardfile index c1fea193..6f684bfd 100644 --- a/Guardfile +++ b/Guardfile @@ -1,11 +1,13 @@ +# frozen_string_literal: true + # A sample Guardfile # More info at https://github.com/guard/guard#readme guard 'minitest' do # with Minitest::Unit - watch(%r|^test/(.*)\/(.*)_test\.rb|) - watch(%r|^lib/(.*)\.rb|) { |m| "test/lib/#{m[1]}_test.rb" } - watch(%r|^test/test_helper\.rb|) { "test" } + watch(%r{^test/(.*)\/(.*)_test\.rb}) + watch(%r{^lib/(.*)\.rb}) { |m| "test/lib/#{m[1]}_test.rb" } + watch(%r{^test/test_helper\.rb}) { 'test' } end guard :bundler do diff --git a/Rakefile b/Rakefile index de8d8a26..175d7b4e 100644 --- a/Rakefile +++ b/Rakefile @@ -2,7 +2,7 @@ require 'bundler/gem_tasks' require 'rake/testtask' Rake::TestTask.new do |t| - t.libs << 'lib/omniauth-openid-connect' + t.libs << 'test' t.test_files = FileList['test/lib/omniauth/**/*_test.rb'] t.verbose = true end diff --git a/lib/omniauth-openid-connect.rb b/lib/omniauth-openid-connect.rb deleted file mode 100644 index e7a33938..00000000 --- a/lib/omniauth-openid-connect.rb +++ /dev/null @@ -1 +0,0 @@ -require "omniauth/openid_connect" diff --git a/lib/omniauth/openid_connect.rb b/lib/omniauth/openid_connect.rb index 060111b0..6158e0f0 100644 --- a/lib/omniauth/openid_connect.rb +++ b/lib/omniauth/openid_connect.rb @@ -1,3 +1,5 @@ -require "omniauth/openid_connect/errors" -require "omniauth/openid_connect/version" -require "omniauth/strategies/openid_connect" +# frozen_string_literal: true + +require 'omniauth/openid_connect/errors' +require 'omniauth/openid_connect/version' +require 'omniauth/strategies/openid_connect' diff --git a/lib/omniauth/openid_connect/errors.rb b/lib/omniauth/openid_connect/errors.rb index 7c5c637a..c17f8a66 100644 --- a/lib/omniauth/openid_connect/errors.rb +++ b/lib/omniauth/openid_connect/errors.rb @@ -1,6 +1,9 @@ +# frozen_string_literal: true + module OmniAuth module OpenIDConnect class Error < RuntimeError; end class MissingCodeError < Error; end + class MissingIdTokenError < Error; end end end diff --git a/lib/omniauth/openid_connect/version.rb b/lib/omniauth/openid_connect/version.rb index b537d2f3..ed60aed0 100644 --- a/lib/omniauth/openid_connect/version.rb +++ b/lib/omniauth/openid_connect/version.rb @@ -1,5 +1,7 @@ +# frozen_string_literal: true + module OmniAuth module OpenIDConnect - VERSION = "0.2.2.vn2" + VERSION = '0.3.5.vn1' end end diff --git a/lib/omniauth_openid_connect.rb b/lib/omniauth_openid_connect.rb new file mode 100644 index 00000000..e04c2d37 --- /dev/null +++ b/lib/omniauth_openid_connect.rb @@ -0,0 +1,3 @@ +# frozen_string_literal: true + +require 'omniauth/openid_connect' diff --git a/omniauth-openid-connect.gemspec b/omniauth-openid-connect.gemspec index a808324b..83f40b34 100644 --- a/omniauth-openid-connect.gemspec +++ b/omniauth-openid-connect.gemspec @@ -1,35 +1,35 @@ -# coding: utf-8 -lib = File.expand_path('../lib', __FILE__) +# frozen_string_literal: true + +lib = File.expand_path('lib', __dir__) $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) require 'omniauth/openid_connect/version' Gem::Specification.new do |spec| - spec.name = "omniauth-openid-connect" + spec.name = 'omniauth-openid-connect' spec.version = OmniAuth::OpenIDConnect::VERSION - spec.authors = ["John Bohn"] - spec.email = ["jjbohn@gmail.com"] - spec.summary = %q{OpenID Connect Strategy for OmniAuth} - spec.description = %q{OpenID Connect Strategy for OmniAuth} - spec.homepage = "https://github.com/jjbohn/omniauth-openid-connect" - spec.license = "MIT" + spec.authors = ['John Bohn', 'Ilya Shcherbinin', 'Bryan Rehbein'] + spec.email = ['jjbohn@gmail.com', 'm0n9oose@gmail.com', 'bryan.rehbein@venuenext.com'] + spec.summary = 'OpenID Connect Strategy for OmniAuth' + spec.description = 'OpenID Connect Strategy for OmniAuth.' + spec.homepage = 'https://github.com/m0n9oose/omniauth_openid_connect' + spec.license = 'MIT' spec.files = `git ls-files -z`.split("\x0") spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } spec.test_files = spec.files.grep(%r{^(test|spec|features)/}) - spec.require_paths = ["lib"] + spec.require_paths = ['lib'] - spec.add_dependency 'omniauth', '~> 1.1' - spec.add_dependency 'openid_connect', '~> 0.12.0' - spec.add_dependency 'addressable', '~> 2.3' - spec.add_development_dependency "bundler", "~> 1.5" - spec.add_development_dependency "minitest" - spec.add_development_dependency "mocha" - spec.add_development_dependency "guard" - spec.add_development_dependency "guard-minitest" - spec.add_development_dependency "guard-bundler" - spec.add_development_dependency "rake" - spec.add_development_dependency "simplecov" - spec.add_development_dependency "pry" - spec.add_development_dependency "coveralls" - spec.add_development_dependency "faker" + spec.add_dependency 'addressable', '~> 2.5' + spec.add_dependency 'omniauth', '~> 1.9' + spec.add_dependency 'openid_connect', '~> 1.1' + spec.add_development_dependency 'coveralls', '~> 0.8' + spec.add_development_dependency 'faker', '~> 1.6' + spec.add_development_dependency 'guard', '~> 2.14' + spec.add_development_dependency 'guard-bundler', '~> 2.2' + spec.add_development_dependency 'guard-minitest', '~> 2.4' + spec.add_development_dependency 'minitest', '~> 5.1' + spec.add_development_dependency 'mocha', '~> 1.7' + spec.add_development_dependency 'rake', '~> 12.0' + spec.add_development_dependency 'rubocop', '~> 0.63' + spec.add_development_dependency 'simplecov', '~> 0.12' end diff --git a/test/lib/omniauth/openid_connect/version_test.rb b/test/lib/omniauth/openid_connect/version_test.rb deleted file mode 100644 index acc4c4d6..00000000 --- a/test/lib/omniauth/openid_connect/version_test.rb +++ /dev/null @@ -1,7 +0,0 @@ -require_relative '../../../test_helper' - -class OmniAuth::OpenIDConnect::VersionTest < MiniTest::Test - def test_version_defined - refute_nil OmniAuth::OpenIDConnect::VERSION - end -end diff --git a/test/lib/omniauth/strategies/openid_connect_test.rb b/test/lib/omniauth/strategies/openid_connect_test.rb index 0deda94d..b72f6ce0 100644 --- a/test/lib/omniauth/strategies/openid_connect_test.rb +++ b/test/lib/omniauth/strategies/openid_connect_test.rb @@ -1,344 +1,620 @@ require_relative '../../../test_helper' -class OmniAuth::Strategies::OpenIDConnectTest < StrategyTestCase - def test_client_options_defaults - assert_equal 'https', strategy.options.client_options.scheme - assert_equal 443, strategy.options.client_options.port - assert_equal '/authorize', strategy.options.client_options.authorization_endpoint - assert_equal '/token', strategy.options.client_options.token_endpoint - end - - def test_request_phase - expected_redirect = /^https:\/\/example\.com\/authorize\?client_id=1234&nonce=[\w\d]{32}&response_type=code&scope=openid&state=[\w\d]{32}$/ - strategy.options.issuer = 'example.com' - strategy.options.client_options.host = 'example.com' - strategy.expects(:redirect).with(regexp_matches(expected_redirect)) - strategy.request_phase - end - - def test_request_phase_with_discovery - expected_redirect = /^https:\/\/example\.com\/authorization\?client_id=1234&nonce=[\w\d]{32}&response_type=code&scope=openid&state=[\w\d]{32}$/ - strategy.options.client_options.host = 'example.com' - strategy.options.discovery = true - - issuer = stub('OpenIDConnect::Discovery::Issuer') - issuer.stubs(:issuer).returns('https://example.com/') - ::OpenIDConnect::Discovery::Provider.stubs(:discover!).returns(issuer) - - config = stub('OpenIDConnect::Discovery::Provder::Config') - config.stubs(:authorization_endpoint).returns('https://example.com/authorization') - config.stubs(:token_endpoint).returns('https://example.com/token') - config.stubs(:userinfo_endpoint).returns('https://example.com/userinfo') - config.stubs(:jwks_uri).returns('https://example.com/jwks') - ::OpenIDConnect::Discovery::Provider::Config.stubs(:discover!).with('https://example.com/').returns(config) - - strategy.expects(:redirect).with(regexp_matches(expected_redirect)) - strategy.request_phase - - assert_equal strategy.options.issuer, 'https://example.com/' - assert_equal strategy.options.client_options.authorization_endpoint, 'https://example.com/authorization' - assert_equal strategy.options.client_options.token_endpoint, 'https://example.com/token' - assert_equal strategy.options.client_options.userinfo_endpoint, 'https://example.com/userinfo' - assert_equal strategy.options.client_options.jwks_uri, 'https://example.com/jwks' - end - - def test_uid - assert_equal user_info.sub, strategy.uid - end - - def test_callback_phase(session = {}, params = {}) - code = SecureRandom.hex(16) - state = SecureRandom.hex(16) - nonce = SecureRandom.hex(16) - request.stubs(:params).returns({'code' => code,'state' => state}) - request.stubs(:path_info).returns('') - - strategy.options.issuer = 'example.com' - strategy.options.client_signing_alg = :RS256 - strategy.options.client_jwk_signing_key = File.read('test/fixtures/jwks.json') - - id_token = stub('OpenIDConnect::ResponseObject::IdToken') - id_token.stubs(:verify!).with({:issuer => strategy.options.issuer, :client_id => @identifier, :nonce => nonce}).returns(true) - ::OpenIDConnect::ResponseObject::IdToken.stubs(:decode).returns(id_token) - - strategy.unstub(:user_info) - access_token = stub('OpenIDConnect::AccessToken') - access_token.stubs(:access_token) - access_token.stubs(:refresh_token) - access_token.stubs(:expires_in) - access_token.stubs(:scope) - access_token.stubs(:id_token).returns(File.read('test/fixtures/id_token.txt')) - client.expects(:access_token!).at_least_once.returns(access_token) - access_token.expects(:userinfo!).returns(user_info) - - strategy.call!({'rack.session' => {'omniauth.state' => state, 'omniauth.nonce' => nonce}}) - strategy.callback_phase - end - - def test_callback_phase_with_discovery - code = SecureRandom.hex(16) - state = SecureRandom.hex(16) - nonce = SecureRandom.hex(16) - jwks = JSON::JWK::Set.new(JSON.parse(File.read('test/fixtures/jwks.json'))['keys']) - - request.stubs(:params).returns({'code' => code,'state' => state}) - request.stubs(:path_info).returns('') - - strategy.options.client_options.host = 'example.com' - strategy.options.discovery = true - - issuer = stub('OpenIDConnect::Discovery::Issuer') - issuer.stubs(:issuer).returns('https://example.com/') - ::OpenIDConnect::Discovery::Provider.stubs(:discover!).returns(issuer) - - config = stub('OpenIDConnect::Discovery::Provder::Config') - config.stubs(:authorization_endpoint).returns('https://example.com/authorization') - config.stubs(:token_endpoint).returns('https://example.com/token') - config.stubs(:userinfo_endpoint).returns('https://example.com/userinfo') - config.stubs(:jwks_uri).returns('https://example.com/jwks') - config.stubs(:jwks).returns(jwks) - - ::OpenIDConnect::Discovery::Provider::Config.stubs(:discover!).with('https://example.com/').returns(config) - - id_token = stub('OpenIDConnect::ResponseObject::IdToken') - id_token.stubs(:verify!).with({:issuer => 'https://example.com/', :client_id => @identifier, :nonce => nonce}).returns(true) - ::OpenIDConnect::ResponseObject::IdToken.stubs(:decode).returns(id_token) - - strategy.unstub(:user_info) - access_token = stub('OpenIDConnect::AccessToken') - access_token.stubs(:access_token) - access_token.stubs(:refresh_token) - access_token.stubs(:expires_in) - access_token.stubs(:scope) - access_token.stubs(:id_token).returns(File.read('test/fixtures/id_token.txt')) - client.expects(:access_token!).at_least_once.returns(access_token) - access_token.expects(:userinfo!).returns(user_info) - - strategy.call!({'rack.session' => {'omniauth.state' => state, 'omniauth.nonce' => nonce}}) - strategy.callback_phase - end - - def test_callback_phase_with_error - state = SecureRandom.hex(16) - nonce = SecureRandom.hex(16) - request.stubs(:params).returns({'error' => 'invalid_request'}) - request.stubs(:path_info).returns('') - - strategy.call!({'rack.session' => {'omniauth.state' => state, 'omniauth.nonce' => nonce}}) - strategy.expects(:fail!) - strategy.callback_phase - end - - def test_callback_phase_with_invalid_state - code = SecureRandom.hex(16) - state = SecureRandom.hex(16) - nonce = SecureRandom.hex(16) - request.stubs(:params).returns({'code' => code,'state' => 'foobar'}) - request.stubs(:path_info).returns('') - - strategy.call!({'rack.session' => {'omniauth.state' => state, 'omniauth.nonce' => nonce}}) - result = strategy.callback_phase - - assert result.kind_of?(Array) - assert result.first == 401, "Expecting unauthorized" - end - - def test_callback_phase_with_timeout - code = SecureRandom.hex(16) - state = SecureRandom.hex(16) - nonce = SecureRandom.hex(16) - request.stubs(:params).returns({'code' => code,'state' => state}) - request.stubs(:path_info).returns('') - - strategy.options.issuer = 'example.com' - - strategy.stubs(:access_token).raises(::Timeout::Error.new('error')) - strategy.call!({'rack.session' => {'omniauth.state' => state, 'omniauth.nonce' => nonce}}) - strategy.expects(:fail!) - strategy.callback_phase - end - - def test_callback_phase_with_etimeout - code = SecureRandom.hex(16) - state = SecureRandom.hex(16) - nonce = SecureRandom.hex(16) - request.stubs(:params).returns({'code' => code,'state' => state}) - request.stubs(:path_info).returns('') - - strategy.options.issuer = 'example.com' - - strategy.stubs(:access_token).raises(::Errno::ETIMEDOUT.new('error')) - strategy.call!({'rack.session' => {'omniauth.state' => state, 'omniauth.nonce' => nonce}}) - strategy.expects(:fail!) - strategy.callback_phase - end - - def test_callback_phase_with_socket_error - code = SecureRandom.hex(16) - state = SecureRandom.hex(16) - nonce = SecureRandom.hex(16) - request.stubs(:params).returns({'code' => code,'state' => state}) - request.stubs(:path_info).returns('') - - strategy.options.issuer = 'example.com' - - strategy.stubs(:access_token).raises(::SocketError.new('error')) - strategy.call!({'rack.session' => {'omniauth.state' => state, 'omniauth.nonce' => nonce}}) - strategy.expects(:fail!) - strategy.callback_phase - end - - def test_info - info = strategy.info - assert_equal user_info.name, info[:name] - assert_equal user_info.email, info[:email] - assert_equal user_info.preferred_username, info[:nickname] - assert_equal user_info.given_name, info[:first_name] - assert_equal user_info.family_name, info[:last_name] - assert_equal user_info.gender, info[:gender] - assert_equal user_info.picture, info[:image] - assert_equal user_info.phone_number, info[:phone] - assert_equal({ website: user_info.website }, info[:urls]) - end - - def test_extra - assert_equal({ raw_info: user_info.as_json }, strategy.extra) - end - - def test_credentials - strategy.options.issuer = 'example.com' - strategy.options.client_signing_alg = :RS256 - strategy.options.client_jwk_signing_key = File.read('test/fixtures/jwks.json') - - id_token = stub('OpenIDConnect::ResponseObject::IdToken') - id_token.stubs(:verify!).returns(true) - ::OpenIDConnect::ResponseObject::IdToken.stubs(:decode).returns(id_token) - - access_token = stub('OpenIDConnect::AccessToken') - access_token.stubs(:access_token).returns(SecureRandom.hex(16)) - access_token.stubs(:refresh_token).returns(SecureRandom.hex(16)) - access_token.stubs(:expires_in).returns(Time.now) - access_token.stubs(:scope).returns('openidconnect') - access_token.stubs(:id_token).returns(File.read('test/fixtures/id_token.txt')) - - client.expects(:access_token!).returns(access_token) - access_token.expects(:refresh_token).returns(access_token.refresh_token) - access_token.expects(:expires_in).returns(access_token.expires_in) - - assert_equal({ id_token: access_token.id_token, - token: access_token.access_token, - refresh_token: access_token.refresh_token, - expires_in: access_token.expires_in, - scope: access_token.scope - }, strategy.credentials) - end - - def test_option_send_nonce - strategy.options.client_options[:host] = "foobar.com" - - assert(strategy.authorize_uri =~ /nonce=/, "URI must contain nonce") - - strategy.options.send_nonce = false - assert(!(strategy.authorize_uri =~ /nonce=/), "URI must not contain nonce") - end - - def test_failure_endpoint_redirect - OmniAuth.config.stubs(:failure_raise_out_environments).returns([]) - strategy.stubs(:env).returns({}) - request.stubs(:params).returns({"error" => "access denied"}) - - result = strategy.callback_phase - - assert(result.is_a? Array) - assert(result[0] == 302, "Redirect") - assert(result[1]["Location"] =~ /\/auth\/failure/) - end - - def test_state - strategy.options.state = lambda { 42 } - session = { "state" => 42 } - - expected_redirect = /&state=/ - strategy.options.issuer = 'example.com' - strategy.options.client_options.host = "example.com" - strategy.expects(:redirect).with(regexp_matches(expected_redirect)) - strategy.request_phase - - # this should succeed as the correct state is passed with the request - test_callback_phase(session, { "state" => 42 }) - - # the following should fail because the wrong state is passed to the callback - code = SecureRandom.hex(16) - request.stubs(:params).returns({"code" => code, "state" => 43}) - request.stubs(:path_info).returns("") - strategy.call!({"rack.session" => session}) - - result = strategy.callback_phase - - assert result.kind_of?(Array) - assert result.first == 401, "Expecting unauthorized" - end - - def test_option_client_auth_method - code = SecureRandom.hex(16) - state = SecureRandom.hex(16) - nonce = SecureRandom.hex(16) - - opts = strategy.options.client_options - opts[:host] = "foobar.com" - strategy.options.issuer = "foobar.com" - strategy.options.client_auth_method = :not_basic - strategy.options.client_signing_alg = :RS256 - strategy.options.client_jwk_signing_key = File.read('test/fixtures/jwks.json') - - json_response = {access_token: 'test_access_token', - id_token: File.read('test/fixtures/id_token.txt'), - token_type: 'Bearer', - }.to_json - success = Struct.new(:status, :body).new(200, json_response) - - request.stubs(:path_info).returns('') - strategy.call!({'rack.session' => {'omniauth.state' => state, 'omniauth.nonce' => nonce}}) - - id_token = stub('OpenIDConnect::ResponseObject::IdToken') - id_token.stubs(:verify!).with({:issuer => strategy.options.issuer, :client_id => @identifier, :nonce => nonce}).returns(true) - ::OpenIDConnect::ResponseObject::IdToken.stubs(:decode).returns(id_token) - - HTTPClient.any_instance.stubs(:post).with( - "#{opts.scheme}://#{opts.host}:#{opts.port}#{opts.token_endpoint}", - {scope: 'openid', :grant_type => :client_credentials, :client_id => @identifier, :client_secret => @secret}, - {} - ).returns(success) - - assert(strategy.send :access_token) - end - - def test_public_key_with_jwks - strategy.options.client_signing_alg = :RS256 - strategy.options.client_jwk_signing_key = File.read('./test/fixtures/jwks.json') - - assert_equal JSON::JWK::Set, strategy.public_key.class - end - - def test_public_key_with_jwk - strategy.options.client_signing_alg = :RS256 - jwks_str = File.read('./test/fixtures/jwks.json') - jwks = JSON.parse(jwks_str) - jwk = jwks['keys'].first - strategy.options.client_jwk_signing_key = jwk.to_json - - assert_equal JSON::JWK, strategy.public_key.class - end - - def test_public_key_with_x509 - strategy.options.client_signing_alg = :RS256 - strategy.options.client_x509_signing_key = File.read('./test/fixtures/test.crt') - assert_equal OpenSSL::PKey::RSA, strategy.public_key.class - end - - def test_public_key_with_hmac - strategy.options.client_options.secret = 'secret' - strategy.options.client_signing_alg = :HS256 - assert_equal strategy.options.client_options.secret, strategy.public_key +module OmniAuth + module Strategies + class OpenIDConnectTest < StrategyTestCase + def test_client_options_defaults + assert_equal 'https', strategy.options.client_options.scheme + assert_equal 443, strategy.options.client_options.port + assert_equal '/authorize', strategy.options.client_options.authorization_endpoint + assert_equal '/token', strategy.options.client_options.token_endpoint + end + + def test_request_phase + expected_redirect = /^https:\/\/example\.com\/authorize\?client_id=1234&nonce=\w{32}&response_type=code&scope=openid&state=\w{32}$/ + strategy.options.issuer = 'example.com' + strategy.options.client_options.host = 'example.com' + strategy.expects(:redirect).with(regexp_matches(expected_redirect)) + strategy.request_phase + end + + def test_logout_phase_with_discovery + expected_redirect = %r{^https:\/\/example\.com\/logout$} + strategy.options.client_options.host = 'example.com' + strategy.options.discovery = true + + issuer = stub('OpenIDConnect::Discovery::Issuer') + issuer.stubs(:issuer).returns('https://example.com/') + ::OpenIDConnect::Discovery::Provider.stubs(:discover!).returns(issuer) + + config = stub('OpenIDConnect::Discovery::Provder::Config') + config.stubs(:authorization_endpoint).returns('https://example.com/authorization') + config.stubs(:token_endpoint).returns('https://example.com/token') + config.stubs(:userinfo_endpoint).returns('https://example.com/userinfo') + config.stubs(:jwks_uri).returns('https://example.com/jwks') + config.stubs(:end_session_endpoint).returns('https://example.com/logout') + ::OpenIDConnect::Discovery::Provider::Config.stubs(:discover!).with('https://example.com/').returns(config) + + request.stubs(:path_info).returns('/auth/openid_connect/logout') + + strategy.expects(:redirect).with(regexp_matches(expected_redirect)) + strategy.other_phase + end + + def test_logout_phase_with_discovery_and_post_logout_redirect_uri + expected_redirect = 'https://example.com/logout?post_logout_redirect_uri=https%3A%2F%2Fmysite.com' + strategy.options.client_options.host = 'example.com' + strategy.options.discovery = true + strategy.options.post_logout_redirect_uri = 'https://mysite.com' + + issuer = stub('OpenIDConnect::Discovery::Issuer') + issuer.stubs(:issuer).returns('https://example.com/') + ::OpenIDConnect::Discovery::Provider.stubs(:discover!).returns(issuer) + + config = stub('OpenIDConnect::Discovery::Provder::Config') + config.stubs(:authorization_endpoint).returns('https://example.com/authorization') + config.stubs(:token_endpoint).returns('https://example.com/token') + config.stubs(:userinfo_endpoint).returns('https://example.com/userinfo') + config.stubs(:jwks_uri).returns('https://example.com/jwks') + config.stubs(:end_session_endpoint).returns('https://example.com/logout') + ::OpenIDConnect::Discovery::Provider::Config.stubs(:discover!).with('https://example.com/').returns(config) + + request.stubs(:path_info).returns('/auth/openid_connect/logout') + + strategy.expects(:redirect).with(expected_redirect) + strategy.other_phase + end + + def test_logout_phase + strategy.options.issuer = 'example.com' + strategy.options.client_options.host = 'example.com' + + request.stubs(:path_info).returns('/auth/openid_connect/logout') + + strategy.expects(:call_app!) + strategy.other_phase + end + + def test_request_phase_with_params + expected_redirect = /^https:\/\/example\.com\/authorize\?claims_locales=es&client_id=1234&login_hint=john.doe%40example.com&nonce=\w{32}&response_type=code&scope=openid&state=\w{32}&ui_locales=en$/ + strategy.options.issuer = 'example.com' + strategy.options.client_options.host = 'example.com' + request.stubs(:params).returns('login_hint' => 'john.doe@example.com', 'ui_locales' => 'en', 'claims_locales' => 'es') + + strategy.expects(:redirect).with(regexp_matches(expected_redirect)) + strategy.request_phase + end + + def test_request_phase_with_discovery + expected_redirect = /^https:\/\/example\.com\/authorization\?client_id=1234&nonce=\w{32}&response_type=code&scope=openid&state=\w{32}$/ + strategy.options.client_options.host = 'example.com' + strategy.options.discovery = true + + issuer = stub('OpenIDConnect::Discovery::Issuer') + issuer.stubs(:issuer).returns('https://example.com/') + ::OpenIDConnect::Discovery::Provider.stubs(:discover!).returns(issuer) + + config = stub('OpenIDConnect::Discovery::Provder::Config') + config.stubs(:authorization_endpoint).returns('https://example.com/authorization') + config.stubs(:token_endpoint).returns('https://example.com/token') + config.stubs(:userinfo_endpoint).returns('https://example.com/userinfo') + config.stubs(:jwks_uri).returns('https://example.com/jwks') + ::OpenIDConnect::Discovery::Provider::Config.stubs(:discover!).with('https://example.com/').returns(config) + + strategy.expects(:redirect).with(regexp_matches(expected_redirect)) + strategy.request_phase + + assert_equal strategy.options.issuer, 'https://example.com/' + assert_equal strategy.options.client_options.authorization_endpoint, 'https://example.com/authorization' + assert_equal strategy.options.client_options.token_endpoint, 'https://example.com/token' + assert_equal strategy.options.client_options.userinfo_endpoint, 'https://example.com/userinfo' + assert_equal strategy.options.client_options.jwks_uri, 'https://example.com/jwks' + assert_nil strategy.options.client_options.end_session_endpoint + end + + def test_request_phase_with_response_mode + expected_redirect = /^https:\/\/example\.com\/authorize\?client_id=1234&nonce=\w{32}&response_mode=form_post&response_type=id_token&scope=openid&state=\w{32}$/ + strategy.options.issuer = 'example.com' + strategy.options.response_mode = 'form_post' + strategy.options.response_type = 'id_token' + strategy.options.client_options.host = 'example.com' + + strategy.expects(:redirect).with(regexp_matches(expected_redirect)) + strategy.request_phase + end + + def test_request_phase_with_response_mode_symbol + expected_redirect = /^https:\/\/example\.com\/authorize\?client_id=1234&nonce=\w{32}&response_mode=form_post&response_type=id_token&scope=openid&state=\w{32}$/ + strategy.options.issuer = 'example.com' + strategy.options.response_mode = 'form_post' + strategy.options.response_type = :id_token + strategy.options.client_options.host = 'example.com' + + strategy.expects(:redirect).with(regexp_matches(expected_redirect)) + strategy.request_phase + end + + def test_option_acr_values + strategy.options.client_options[:host] = 'foobar.com' + + assert(!(strategy.authorize_uri =~ /acr_values=/), 'URI must not contain acr_values') + + strategy.options.acr_values = 'urn:some:acr:values:value' + assert(strategy.authorize_uri =~ /acr_values=/, 'URI must contain acr_values') + end + + def test_option_custom_attributes + strategy.options.client_options[:host] = 'foobar.com' + strategy.options.extra_authorize_params = {resource: 'xyz'} + + assert(strategy.authorize_uri =~ /resource=xyz/, 'URI must contain custom params') + end + + def test_uid + assert_equal user_info.sub, strategy.uid + + strategy.options.uid_field = 'preferred_username' + assert_equal user_info.preferred_username, strategy.uid + + strategy.options.uid_field = 'something' + assert_equal user_info.sub, strategy.uid + end + + def test_callback_phase(session = {}, params = {}) + code = SecureRandom.hex(16) + state = SecureRandom.hex(16) + nonce = SecureRandom.hex(16) + request.stubs(:params).returns('code' => code, 'state' => state) + request.stubs(:path_info).returns('') + + strategy.options.issuer = 'example.com' + strategy.options.client_signing_alg = :RS256 + strategy.options.client_jwk_signing_key = File.read('test/fixtures/jwks.json') + strategy.options.response_type = 'code' + + strategy.unstub(:user_info) + access_token = stub('OpenIDConnect::AccessToken') + access_token.stubs(:access_token) + access_token.stubs(:refresh_token) + access_token.stubs(:expires_in) + access_token.stubs(:scope) + access_token.stubs(:id_token).returns(File.read('test/fixtures/id_token.txt')) + client.expects(:access_token!).at_least_once.returns(access_token) + access_token.expects(:userinfo!).returns(user_info) + + id_token = stub('OpenIDConnect::ResponseObject::IdToken') + id_token.stubs(:raw_attributes).returns('sub' => 'sub', 'name' => 'name', 'email' => 'email') + id_token.stubs(:verify!).with(issuer: strategy.options.issuer, client_id: @identifier, nonce: nonce).returns(true) + id_token.expects(:verify!) + + strategy.expects(:decode_id_token).twice.with(access_token.id_token).returns(id_token) + strategy.call!('rack.session' => { 'omniauth.state' => state, 'omniauth.nonce' => nonce }) + strategy.callback_phase + end + + def test_callback_phase_with_id_token + code = SecureRandom.hex(16) + state = SecureRandom.hex(16) + nonce = SecureRandom.hex(16) + request.stubs(:params).returns('id_token' => code, 'state' => state) + request.stubs(:path_info).returns('') + + strategy.options.issuer = 'example.com' + strategy.options.client_signing_alg = :RS256 + strategy.options.client_jwk_signing_key = File.read('test/fixtures/jwks.json') + strategy.options.response_type = 'id_token' + + strategy.unstub(:user_info) + access_token = stub('OpenIDConnect::AccessToken') + access_token.stubs(:access_token) + access_token.stubs(:refresh_token) + access_token.stubs(:expires_in) + access_token.stubs(:scope) + access_token.stubs(:id_token).returns(File.read('test/fixtures/id_token.txt')) + + id_token = stub('OpenIDConnect::ResponseObject::IdToken') + id_token.stubs(:raw_attributes).returns('sub' => 'sub', 'name' => 'name', 'email' => 'email') + id_token.stubs(:verify!).with(issuer: strategy.options.issuer, client_id: @identifier, nonce: nonce).returns(true) + ::OpenIDConnect::ResponseObject::IdToken.stubs(:decode).returns(id_token) + id_token.expects(:verify!) + + strategy.call!('rack.session' => { 'omniauth.state' => state, 'omniauth.nonce' => nonce }) + strategy.callback_phase + end + + def test_callback_phase_with_discovery + code = SecureRandom.hex(16) + state = SecureRandom.hex(16) + nonce = SecureRandom.hex(16) + jwks = JSON::JWK::Set.new(JSON.parse(File.read('test/fixtures/jwks.json'))['keys']) + + request.stubs(:params).returns('code' => code, 'state' => state) + request.stubs(:path_info).returns('') + + strategy.options.client_options.host = 'example.com' + strategy.options.discovery = true + + issuer = stub('OpenIDConnect::Discovery::Issuer') + issuer.stubs(:issuer).returns('https://example.com/') + ::OpenIDConnect::Discovery::Provider.stubs(:discover!).returns(issuer) + + config = stub('OpenIDConnect::Discovery::Provder::Config') + config.stubs(:authorization_endpoint).returns('https://example.com/authorization') + config.stubs(:token_endpoint).returns('https://example.com/token') + config.stubs(:userinfo_endpoint).returns('https://example.com/userinfo') + config.stubs(:jwks_uri).returns('https://example.com/jwks') + config.stubs(:jwks).returns(jwks) + + ::OpenIDConnect::Discovery::Provider::Config.stubs(:discover!).with('https://example.com/').returns(config) + + id_token = stub('OpenIDConnect::ResponseObject::IdToken') + id_token.stubs(:raw_attributes).returns('sub' => 'sub', 'name' => 'name', 'email' => 'email') + id_token.stubs(:verify!).with(issuer: 'https://example.com/', client_id: @identifier, nonce: nonce).returns(true) + ::OpenIDConnect::ResponseObject::IdToken.stubs(:decode).returns(id_token) + + strategy.unstub(:user_info) + access_token = stub('OpenIDConnect::AccessToken') + access_token.stubs(:access_token) + access_token.stubs(:refresh_token) + access_token.stubs(:expires_in) + access_token.stubs(:scope) + access_token.stubs(:id_token).returns(File.read('test/fixtures/id_token.txt')) + client.expects(:access_token!).at_least_once.returns(access_token) + access_token.expects(:userinfo!).returns(user_info) + + strategy.call!('rack.session' => { 'omniauth.state' => state, 'omniauth.nonce' => nonce }) + strategy.callback_phase + end + + def test_callback_phase_with_error + state = SecureRandom.hex(16) + nonce = SecureRandom.hex(16) + request.stubs(:params).returns('error' => 'invalid_request') + request.stubs(:path_info).returns('') + + strategy.call!({'rack.session' => {'omniauth.state' => state, 'omniauth.nonce' => nonce}}) + strategy.expects(:fail!) + strategy.callback_phase + end + + def test_callback_phase_with_invalid_state + code = SecureRandom.hex(16) + state = SecureRandom.hex(16) + nonce = SecureRandom.hex(16) + request.stubs(:params).returns('code' => code, 'state' => 'foobar') + request.stubs(:path_info).returns('') + + strategy.call!('rack.session' => { 'omniauth.state' => state, 'omniauth.nonce' => nonce }) + strategy.expects(:fail!) + strategy.callback_phase + end + + def test_callback_phase_without_code + state = SecureRandom.hex(16) + nonce = SecureRandom.hex(16) + request.stubs(:params).returns('state' => state) + request.stubs(:path_info).returns('') + + strategy.call!('rack.session' => { 'omniauth.state' => state, 'omniauth.nonce' => nonce }) + + strategy.expects(:fail!).with(:missing_code, is_a(OmniAuth::OpenIDConnect::MissingCodeError)) + strategy.callback_phase + end + + def test_callback_phase_without_id_token + state = SecureRandom.hex(16) + nonce = SecureRandom.hex(16) + request.stubs(:params).returns('state' => state) + request.stubs(:path_info).returns('') + strategy.options.response_type = 'id_token' + + strategy.call!('rack.session' => { 'omniauth.state' => state, 'omniauth.nonce' => nonce }) + + strategy.expects(:fail!).with(:missing_id_token, is_a(OmniAuth::OpenIDConnect::MissingIdTokenError)) + strategy.callback_phase + end + + def test_callback_phase_without_id_token_symbol + state = SecureRandom.hex(16) + nonce = SecureRandom.hex(16) + request.stubs(:params).returns('state' => state) + request.stubs(:path_info).returns('') + strategy.options.response_type = :id_token + + strategy.call!('rack.session' => { 'omniauth.state' => state, 'omniauth.nonce' => nonce }) + + strategy.expects(:fail!).with(:missing_id_token, is_a(OmniAuth::OpenIDConnect::MissingIdTokenError)) + strategy.callback_phase + end + + def test_callback_phase_with_timeout + code = SecureRandom.hex(16) + state = SecureRandom.hex(16) + nonce = SecureRandom.hex(16) + request.stubs(:params).returns('code' => code, 'state' => state) + request.stubs(:path_info).returns('') + + strategy.options.issuer = 'example.com' + + strategy.stubs(:access_token).raises(::Timeout::Error.new('error')) + strategy.call!('rack.session' => { 'omniauth.state' => state, 'omniauth.nonce' => nonce }) + strategy.expects(:fail!) + + id_token = stub('OpenIDConnect::ResponseObject::IdToken') + id_token.stubs(:verify!).with(issuer: 'example.com', client_id: @identifier, nonce: nonce).returns(true) + ::OpenIDConnect::ResponseObject::IdToken.stubs(:decode).returns(id_token) + + strategy.callback_phase + end + + def test_callback_phase_with_etimeout + code = SecureRandom.hex(16) + state = SecureRandom.hex(16) + nonce = SecureRandom.hex(16) + request.stubs(:params).returns('code' => code, 'state' => state) + request.stubs(:path_info).returns('') + + strategy.options.issuer = 'example.com' + + strategy.stubs(:access_token).raises(::Errno::ETIMEDOUT.new('error')) + strategy.call!('rack.session' => { 'omniauth.state' => state, 'omniauth.nonce' => nonce }) + strategy.expects(:fail!) + + id_token = stub('OpenIDConnect::ResponseObject::IdToken') + id_token.stubs(:verify!).with(issuer: 'example.com', client_id: @identifier, nonce: nonce).returns(true) + ::OpenIDConnect::ResponseObject::IdToken.stubs(:decode).returns(id_token) + + strategy.callback_phase + end + + def test_callback_phase_with_socket_error + code = SecureRandom.hex(16) + state = SecureRandom.hex(16) + nonce = SecureRandom.hex(16) + request.stubs(:params).returns('code' => code, 'state' => state) + request.stubs(:path_info).returns('') + + strategy.options.issuer = 'example.com' + + strategy.stubs(:access_token).raises(::SocketError.new('error')) + strategy.call!('rack.session' => { 'omniauth.state' => state, 'omniauth.nonce' => nonce }) + strategy.expects(:fail!) + + id_token = stub('OpenIDConnect::ResponseObject::IdToken') + id_token.stubs(:verify!).with(issuer: 'example.com', client_id: @identifier, nonce: nonce).returns(true) + ::OpenIDConnect::ResponseObject::IdToken.stubs(:decode).returns(id_token) + + strategy.callback_phase + end + + def test_callback_phase_with_rack_oauth2_client_error + code = SecureRandom.hex(16) + state = SecureRandom.hex(16) + nonce = SecureRandom.hex(16) + request.stubs(:params).returns('code' => code, 'state' => state) + request.stubs(:path_info).returns('') + + strategy.options.issuer = 'example.com' + + strategy.stubs(:access_token).raises(::Rack::OAuth2::Client::Error.new('error', error: 'Unknown')) + strategy.call!('rack.session' => { 'omniauth.state' => state, 'omniauth.nonce' => nonce }) + strategy.expects(:fail!) + + id_token = stub('OpenIDConnect::ResponseObject::IdToken') + id_token.stubs(:verify!).with(issuer: 'example.com', client_id: @identifier, nonce: nonce).returns(true) + ::OpenIDConnect::ResponseObject::IdToken.stubs(:decode).returns(id_token) + + strategy.callback_phase + end + + def test_info + info = strategy.info + assert_equal user_info.name, info[:name] + assert_equal user_info.email, info[:email] + assert_equal user_info.preferred_username, info[:nickname] + assert_equal user_info.given_name, info[:first_name] + assert_equal user_info.family_name, info[:last_name] + assert_equal user_info.gender, info[:gender] + assert_equal user_info.picture, info[:image] + assert_equal user_info.phone_number, info[:phone] + assert_equal({ website: user_info.website }, info[:urls]) + end + + def test_extra + assert_equal({ raw_info: user_info.as_json }, strategy.extra) + end + + def test_credentials + strategy.options.issuer = 'example.com' + strategy.options.client_signing_alg = :RS256 + strategy.options.client_jwk_signing_key = File.read('test/fixtures/jwks.json') + + id_token = stub('OpenIDConnect::ResponseObject::IdToken') + id_token.stubs(:verify!).returns(true) + ::OpenIDConnect::ResponseObject::IdToken.stubs(:decode).returns(id_token) + + access_token = stub('OpenIDConnect::AccessToken') + access_token.stubs(:access_token).returns(SecureRandom.hex(16)) + access_token.stubs(:refresh_token).returns(SecureRandom.hex(16)) + access_token.stubs(:expires_in).returns(Time.now) + access_token.stubs(:scope).returns('openidconnect') + access_token.stubs(:id_token).returns(File.read('test/fixtures/id_token.txt')) + + client.expects(:access_token!).returns(access_token) + access_token.expects(:refresh_token).returns(access_token.refresh_token) + access_token.expects(:expires_in).returns(access_token.expires_in) + + assert_equal( + { + id_token: access_token.id_token, + token: access_token.access_token, + refresh_token: access_token.refresh_token, + expires_in: access_token.expires_in, + scope: access_token.scope + }, + strategy.credentials + ) + end + + def test_option_send_nonce + strategy.options.client_options[:host] = 'foobar.com' + + assert(strategy.authorize_uri =~ /nonce=/, 'URI must contain nonce') + + strategy.options.send_nonce = false + assert(!(strategy.authorize_uri =~ /nonce=/), 'URI must not contain nonce') + end + + def test_failure_endpoint_redirect + OmniAuth.config.stubs(:failure_raise_out_environments).returns([]) + strategy.stubs(:env).returns({}) + request.stubs(:params).returns('error' => 'access denied') + + result = strategy.callback_phase + + assert(result.is_a? Array) + assert(result[0] == 302, 'Redirect') + assert(result[1]["Location"] =~ /\/auth\/failure/) + end + + def test_state + strategy.options.state = -> { 42 } + + expected_redirect = /&state=42/ + strategy.options.issuer = 'example.com' + strategy.options.client_options.host = 'example.com' + strategy.expects(:redirect).with(regexp_matches(expected_redirect)) + strategy.request_phase + + session = { 'state' => 42 } + # this should succeed as the correct state is passed with the request + test_callback_phase(session, { 'state' => 42 }) + + # the following should fail because the wrong state is passed to the callback + code = SecureRandom.hex(16) + request.stubs(:params).returns('code' => code, 'state' => 43) + request.stubs(:path_info).returns('') + + strategy.call!('rack.session' => session) + strategy.expects(:fail!) + strategy.callback_phase + end + + def test_dynamic_state + # Stub request parameters + request.stubs(:path_info).returns('') + strategy.call!('rack.session' => { }, QUERY_STRING: { state: 'abc', client_id: '123' } ) + + strategy.options.state = lambda { |env| + # Get params from request, e.g. CGI.parse(env['QUERY_STRING']) + env[:QUERY_STRING][:state] + env[:QUERY_STRING][:client_id] + } + + expected_redirect = /&state=abc123/ + strategy.options.issuer = 'example.com' + strategy.options.client_options.host = 'example.com' + strategy.expects(:redirect).with(regexp_matches(expected_redirect)) + strategy.request_phase + end + + def test_option_client_auth_method + state = SecureRandom.hex(16) + nonce = SecureRandom.hex(16) + + opts = strategy.options.client_options + opts[:host] = 'foobar.com' + strategy.options.issuer = 'foobar.com' + strategy.options.client_auth_method = :not_basic + strategy.options.client_signing_alg = :RS256 + strategy.options.client_jwk_signing_key = File.read('test/fixtures/jwks.json') + + json_response = { + access_token: 'test_access_token', + id_token: File.read('test/fixtures/id_token.txt'), + token_type: 'Bearer', + }.to_json + success = Struct.new(:status, :body).new(200, json_response) + + request.stubs(:path_info).returns('') + strategy.call!('rack.session' => { 'omniauth.state' => state, 'omniauth.nonce' => nonce }) + + id_token = stub('OpenIDConnect::ResponseObject::IdToken') + id_token.stubs(:verify!).with(issuer: strategy.options.issuer, client_id: @identifier, nonce: nonce).returns(true) + ::OpenIDConnect::ResponseObject::IdToken.stubs(:decode).returns(id_token) + + HTTPClient.any_instance.stubs(:post).with( + "#{ opts.scheme }://#{ opts.host }:#{ opts.port }#{ opts.token_endpoint }", + { scope: 'openid', grant_type: :client_credentials, client_id: @identifier, client_secret: @secret }, + {} + ).returns(success) + + assert(strategy.send :access_token) + end + + def test_public_key_with_jwks + strategy.options.client_signing_alg = :RS256 + strategy.options.client_jwk_signing_key = File.read('./test/fixtures/jwks.json') + + assert_equal JSON::JWK::Set, strategy.public_key.class + end + + def test_public_key_with_jwk + strategy.options.client_signing_alg = :RS256 + jwks_str = File.read('./test/fixtures/jwks.json') + jwks = JSON.parse(jwks_str) + jwk = jwks['keys'].first + strategy.options.client_jwk_signing_key = jwk.to_json + + assert_equal JSON::JWK, strategy.public_key.class + end + + def test_public_key_with_x509 + strategy.options.client_signing_alg = :RS256 + strategy.options.client_x509_signing_key = File.read('./test/fixtures/test.crt') + assert_equal OpenSSL::PKey::RSA, strategy.public_key.class + end + + def test_public_key_with_hmac + strategy.options.client_options.secret = 'secret' + strategy.options.client_signing_alg = :HS256 + assert_equal strategy.options.client_options.secret, strategy.public_key + end + + def test_id_token_auth_hash + state = SecureRandom.hex(16) + nonce = SecureRandom.hex(16) + strategy.options.response_type = 'id_token' + strategy.options.issuer = 'example.com' + + id_token = stub('OpenIDConnect::ResponseObject::IdToken') + id_token.stubs(:verify!).returns(true) + id_token.stubs(:raw_attributes, :to_h).returns( + { + "iss": "http://server.example.com", + "sub": "248289761001", + "aud": "s6BhdRkqt3", + "nonce": "n-0S6_WzA2Mj", + "exp": 1311281970, + "iat": 1311280970, + } + ) + + request.stubs(:params).returns('state' => state, 'nounce' => nonce, 'id_token' => id_token) + request.stubs(:path_info).returns('') + + strategy.stubs(:decode_id_token).returns(id_token) + strategy.stubs(:stored_state).returns(state) + + strategy.call!('rack.session' => { 'omniauth.state' => state, 'omniauth.nonce' => nonce }) + strategy.callback_phase + + auth_hash = strategy.send(:env)['omniauth.auth'] + assert auth_hash.key?('provider') + assert auth_hash.key?('uid') + assert auth_hash.key?('info') + assert auth_hash.key?('extra') + assert auth_hash['extra'].key?('raw_info') + end + end end end diff --git a/test/strategy_test_case.rb b/test/strategy_test_case.rb new file mode 100644 index 00000000..6e7b15ef --- /dev/null +++ b/test/strategy_test_case.rb @@ -0,0 +1,51 @@ +class StrategyTestCase < MiniTest::Test + class DummyApp + def call(env); end + end + + attr_accessor :identifier, :secret + + def setup + @identifier = '1234' + @secret = '1234asdgat3' + end + + def client + strategy.client + end + + def user_info + @user_info ||= OpenIDConnect::ResponseObject::UserInfo.new( + sub: SecureRandom.hex(16), + name: Faker::Name.name, + email: Faker::Internet.email, + nickname: Faker::Name.first_name, + preferred_username: Faker::Internet.user_name, + given_name: Faker::Name.first_name, + family_name: Faker::Name.last_name, + gender: 'female', + picture: Faker::Internet.url + '.png', + phone_number: Faker::PhoneNumber.phone_number, + website: Faker::Internet.url, + ) + end + + def request + @request ||= stub('Request').tap do |request| + request.stubs(:params).returns({}) + request.stubs(:cookies).returns({}) + request.stubs(:env).returns({}) + request.stubs(:scheme).returns({}) + request.stubs(:ssl?).returns(false) + end + end + + def strategy + @strategy ||= OmniAuth::Strategies::OpenIDConnect.new(DummyApp.new).tap do |strategy| + strategy.options.client_options.identifier = @identifier + strategy.options.client_options.secret = @secret + strategy.stubs(:request).returns(request) + strategy.stubs(:user_info).returns(user_info) + end + end +end diff --git a/test/test_helper.rb b/test/test_helper.rb index 03b5d879..6c665484 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -1,66 +1,16 @@ -require 'simplecov' -SimpleCov.command_name 'test' -SimpleCov.start +lib = File.expand_path('../../lib', __FILE__) +$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) +require 'simplecov' require 'coveralls' -Coveralls.wear! - require 'minitest/autorun' -require 'mocha/mini_test' +require 'mocha/minitest' require 'faker' require 'active_support' -require_relative '../lib/omniauth-openid-connect' +require 'omniauth_openid_connect' +require_relative 'strategy_test_case' +SimpleCov.command_name 'test' +SimpleCov.start +Coveralls.wear! OmniAuth.config.test_mode = true - -class StrategyTestCase < MiniTest::Test - class DummyApp - def call(env); end - end - - attr_accessor :identifier, :secret - - def setup - @identifier = "1234" - @secret = "1234asdgat3" - end - - def client - strategy.client - end - - def user_info - @user_info ||= OpenIDConnect::ResponseObject::UserInfo.new( - sub: SecureRandom.hex(16), - name: Faker::Name.name, - email: Faker::Internet.email, - nickname: Faker::Name.first_name, - preferred_username: Faker::Internet.user_name, - given_name: Faker::Name.first_name, - family_name: Faker::Name.last_name, - gender: 'female', - picture: Faker::Internet.url + ".png", - phone_number: Faker::PhoneNumber.phone_number, - website: Faker::Internet.url, - ) - end - - def request - @request ||= stub('Request').tap do |request| - request.stubs(:params).returns({}) - request.stubs(:cookies).returns({}) - request.stubs(:env).returns({}) - request.stubs(:scheme).returns({}) - request.stubs(:ssl?).returns(false) - end - end - - def strategy - @strategy ||= OmniAuth::Strategies::OpenIDConnect.new(DummyApp.new).tap do |strategy| - strategy.options.client_options.identifier = @identifier - strategy.options.client_options.secret = @secret - strategy.stubs(:request).returns(request) - strategy.stubs(:user_info).returns(user_info) - end - end -end