diff --git a/lib/omniauth/strategies/openid_connect.rb b/lib/omniauth/strategies/openid_connect.rb index 424f0be4..69e299f6 100644 --- a/lib/omniauth/strategies/openid_connect.rb +++ b/lib/omniauth/strategies/openid_connect.rb @@ -24,7 +24,8 @@ class OpenIDConnect authorization_endpoint: '/authorize', token_endpoint: '/token', userinfo_endpoint: '/userinfo', - jwks_uri: '/jwk' + jwks_uri: '/jwk', + end_session_endpoint: nil } option :issuer option :discovery, false @@ -45,6 +46,7 @@ class OpenIDConnect option :send_nonce, true option :send_scope_to_token_endpoint, true option :client_auth_method + option :post_logout_redirect_uri uid { user_info.sub } @@ -85,8 +87,8 @@ def config end def request_phase - options.issuer = issuer if options.issuer.blank? - discover! if options.discovery + options.issuer = issuer if options.issuer.nil? || options.issuer.empty? + discover! redirect authorize_uri end @@ -99,8 +101,8 @@ def callback_phase elsif !params['code'] return fail!(:missing_code, OmniAuth::OpenIDConnect::MissingCodeError.new(params['error'])) else - options.issuer = issuer if options.issuer.blank? - discover! if options.discovery + options.issuer = issuer if options.issuer.nil? || options.issuer.empty? + discover! client.redirect_uri = redirect_uri client.authorization_code = authorization_code access_token @@ -114,10 +116,26 @@ def callback_phase fail!(:failed_to_connect, e) end + def other_phase + if logout_path_pattern.match?(current_path) + options.issuer = issuer if options.issuer.nil? || options.issuer.empty? + discover! + return redirect(end_session_uri) if end_session_uri + end + call_app! + end + def authorization_code params['code'] end + def end_session_uri + return unless end_session_endpoint_is_valid? + end_session_uri = URI(client_options.end_session_endpoint) + end_session_uri.query = encoded_post_logout_redirect_uri + end_session_uri.to_s + end + def authorize_uri client.redirect_uri = redirect_uri opts = { @@ -148,10 +166,12 @@ def issuer end def discover! + return unless options.discovery client_options.authorization_endpoint = config.authorization_endpoint client_options.token_endpoint = config.token_endpoint client_options.userinfo_endpoint = config.userinfo_endpoint client_options.jwks_uri = config.jwks_uri + client_options.end_session_endpoint = config.end_session_endpoint if config.respond_to?(:end_session_endpoint) end def user_info @@ -240,6 +260,22 @@ def redirect_uri "#{ client_options.redirect_uri }?redirect_uri=#{ CGI.escape(params['redirect_uri']) }" end + def encoded_post_logout_redirect_uri + return unless options.post_logout_redirect_uri + URI.encode_www_form( + post_logout_redirect_uri: options.post_logout_redirect_uri + ) + end + + def end_session_endpoint_is_valid? + client_options.end_session_endpoint && + client_options.end_session_endpoint =~ URI::DEFAULT_PARSER.make_regexp + end + + def logout_path_pattern + @logout_path_pattern ||= %r{\A#{Regexp.quote(request_path)}(/logout)} + end + class CallbackError < StandardError attr_accessor :error, :error_reason, :error_uri diff --git a/test/lib/omniauth/strategies/openid_connect_test.rb b/test/lib/omniauth/strategies/openid_connect_test.rb index 16c6d978..cc7ef46f 100644 --- a/test/lib/omniauth/strategies/openid_connect_test.rb +++ b/test/lib/omniauth/strategies/openid_connect_test.rb @@ -18,6 +18,63 @@ def test_request_phase 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/openidconnect/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/openidconnect/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/openidconnect/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' @@ -52,6 +109,7 @@ def test_request_phase_with_discovery 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_uid