diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 4efc513c..b5414a22 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -1,3 +1,4 @@ + name: Main on: push: diff --git a/LICENSE.txt b/LICENSE.txt index fb7b9071..825c8e7b 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,22 +1,26 @@ +Copyright (c) 2023 Estonian Internet Foundation Copyright (c) 2014 John Bohn -MIT License +The MIT/X11 License (MIT/X11) -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +Except as contained in this notice, the name(s) of the above copyright holders +shall not be used in advertising or otherwise to promote the sale, use or other +dealings in this Software without prior written authorization. diff --git a/README.md b/README.md index 6fc80304..6ce8724a 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,17 @@ -# OmniAuth::OpenIDConnect +# OmniAuth::Tara -Originally was [omniauth-openid-connect](https://github.com/jjbohn/omniauth-openid-connect) +Originally based on [omniauth_openid_connect](https://github.com/omniauth/omniauth_openid_connect), +with parts rewritten to fit TARA-Doku protocol. I've forked this repository and launch as separate gem because maintaining of original was dropped. -[![Build Status](https://github.com/omniauth/omniauth_openid_connect/actions/workflows/main.yml/badge.svg)](https://github.com/omniauth/omniauth_openid_connect/actions/workflows/main.yml) -[![Coverage Status](https://coveralls.io/repos/github/omniauth/omniauth_openid_connect/badge.svg)](https://coveralls.io/github/omniauth/omniauth_openid_connect) +[![Build Status](https://travis-ci.org/internetee/omniauth-tara.svg?branch=master)](https://travis-ci.org/internetee/omniauth-tara) ## Installation Add this line to your application's Gemfile: - gem 'omniauth_openid_connect' + gem 'omniauth-tara' And then execute: @@ -19,11 +19,11 @@ And then execute: Or install it yourself as: - $ gem install omniauth_openid_connect + $ gem install omniauth-tara ## Supported Ruby Versions -OmniAuth::OpenIDConnect is tested under 2.7, 3.0, 3.1, 3.2 +OmniAuth::Tara is tested under 2.7, 3.0, 3.1, 3.2 ## Usage @@ -31,9 +31,9 @@ Example configuration ```ruby Rails.application.config.middleware.use OmniAuth::Builder do - provider :openid_connect, { + provider :tara, { name: :my_provider, - scope: [:openid, :email, :profile, :address], + scope: [:openid, :idcard, :mid, :smartid], response_type: :code, uid_field: "preferred_username", client_options: { @@ -48,81 +48,12 @@ Rails.application.config.middleware.use OmniAuth::Builder do end ``` -### with Devise -```ruby -Devise.setup do |config| - config.omniauth :openid_connect, { - name: :my_provider, - scope: [:openid, :email, :profile, :address], - response_type: :code, - uid_field: "preferred_username", - client_options: { - port: 443, - scheme: "https", - host: "myprovider.com", - identifier: ENV["OP_CLIENT_ID"], - secret: ENV["OP_SECRET_KEY"], - redirect_uri: "http://myapp.com/users/auth/openid_connect/callback", - }, - } -end -``` - -### Options Overview - -| Field | Description | Required | Default | Example/Options | -|------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------|----------|-------------------------------|-----------------------------------------------------| -| name | Arbitrary string to identify connection and identify it from other openid_connect providers | no | String: openid_connect | :my_idp | -| issuer | Root url for the authorization server | yes | | https://myprovider.com | -| discovery | Should OpenID discovery be used. This is recommended if the IDP provides a discovery endpoint. See client config for how to manually enter discovered values. | no | false | one of: true, false | -| client_auth_method | Which authentication method to use to authenticate your app with the authorization server | no | Sym: basic | "basic", "jwks" | -| scope | Which OpenID scopes to include (:openid is always required) | no | Array [:openid] | [:openid, :profile, :email] | -| response_type | Which OAuth2 response type to use with the authorization request | no | String: code | one of: 'code', 'id_token' | -| state | A value to be used for the OAuth2 state parameter on the authorization request. Can be a proc that generates a string. | no | Random 16 character string | Proc.new { SecureRandom.hex(32) } | -| require_state | Should state param be verified - this is recommended, not required by the OIDC specification | no | true | false | -| response_mode | The response mode per [spec](https://openid.net/specs/oauth-v2-form-post-response-mode-1_0.html) | no | nil | one of: :query, :fragment, :form_post, :web_message | -| display | An optional parameter to the authorization request to determine how the authorization and consent page | no | nil | one of: :page, :popup, :touch, :wap | -| prompt | An optional parameter to the authrization request to determine what pages the user will be shown | no | nil | one of: :none, :login, :consent, :select_account | -| send_scope_to_token_endpoint | Should the scope parameter be sent to the authorization token endpoint? | no | true | one of: true, false | -| post_logout_redirect_uri | The logout redirect uri to use per the [session management draft](https://openid.net/specs/openid-connect-session-1_0.html) | no | empty | https://myapp.com/logout/callback | -| uid_field | The field of the user info response to be used as a unique id | no | 'sub' | "sub", "preferred_username" | -| extra_authorize_params | A hash of extra fixed parameters that will be merged to the authorization request | no | Hash | {"tenant" => "common"} | -| allow_authorize_params | A list of allowed dynamic parameters that will be merged to the authorization request | no | Array | [:screen_name] | -| pkce | Enable [PKCE flow](https://oauth.net/2/pkce/) | no | false | one of: true, false | -| pkce_verifier | Specify a custom PKCE verifier code. | no | A random 128-char string | Proc.new { SecureRandom.hex(64) } | -| pkce_options | Specify a custom implementation of the PKCE code challenge/method. | no | SHA256(code_challenge) in hex | Proc to customise the code challenge generation | -| client_options | A hash of client options detailed in its own section | yes | | | -| jwt_secret_base64 | For HMAC with SHA2 (e.g. HS256) signing algorithms, specify the base64-encoded secret used to sign the JWT token. Defaults to the OAuth2 client secret if not specified. | no | client_options.secret | "bXlzZWNyZXQ=\n" -| logout_path | The log out is only triggered when the request path ends on this path | no | '/logout' | '/sign_out' - -### Client Config Options - -These are the configuration options for the client_options hash of the configuration. - -| Field | Description | Default | Replaced by discovery? | -|------------------------|-----------------------------------------------------------------|------------|------------------------| -| identifier | The OAuth2 client_id | | | -| secret | The OAuth2 client secret | | | -| redirect_uri | The OAuth2 authorization callback url in your app | | | -| scheme | The http scheme to use | https | | -| host | The host of the authorization server | nil | | -| port | The port for the authorization server | 443 | | -| authorization_endpoint | The authorize endpoint on the authorization server | /authorize | yes | -| token_endpoint | The token endpoint on the authorization server | /token | yes | -| userinfo_endpoint | The user info endpoint on the authorization server | /userinfo | yes | -| jwks_uri | The jwks_uri on the authorization server | /jwk | yes | -| end_session_endpoint | The url to call to log the user out at the authorization server | nil | yes | - ### Additional Configuration Notes * `name` is arbitrary, I recommend using the name of your provider. The name configuration exists because you could be using multiple OpenID Connect providers in a single app. - - **NOTE**: if you use this gem with Devise you should use `:openid_connect` name, - or Devise would route to 'users/auth/:provider' rather than 'users/auth/openid_connect' - * `response_type` tells the authorization server which grant type the application wants to use, - currently, only `:code` (Authorization Code grant) and `:id_token` (Implicit grant) are valid. + currently, only `:code` (Authorization Code grant) is valid. * If you want to pass `state` parameter by yourself. You can set Proc Object. e.g. `state: Proc.new { SecureRandom.hex(32) }` * `nonce` is optional. If don't want to pass "nonce" parameter to provider, You should specify @@ -140,24 +71,6 @@ These are the configuration options for the client_options hash of the configura that appears in the `user_info` details. * The `issuer` property should exactly match the provider's issuer link. * The `response_mode` option is optional and specifies how the result of the authorization request is formatted. - * Some OpenID Connect providers require the `scope` attribute in requests to the token endpoint, even if - this is not in the protocol specifications. In those cases, the `send_scope_to_token_endpoint` - property can be used to add the attribute to the token request. Initial value is `true`, which means that the - scope attribute is included by default. - -## Additional notes - * In some cases, you may want to go straight to the callback phase - e.g. when requested by a stateless client, like a mobile app. - In such example, the session is empty, so you have to forward certain parameters received from the client. - Currently supported ones are `code_verifier` and `nonce` - simply provide them as the `/callback` request parameters. For the full low down on OpenID Connect, please check out [the spec](http://openid.net/specs/openid-connect-core-1_0.html). - -## Contributing - -1. Fork it ( http://github.com/omniauth/omniauth_openid_connect/fork ) -2. Create your feature branch (`git checkout -b my-new-feature`) -3. Cover your changes with tests and make sure they're green (`bundle install && bundle exec rake test`) -4. Commit your changes (`git commit -am 'Add some feature'`) -5. Push to the branch (`git push origin my-new-feature`) -6. Create new Pull Request diff --git a/lib/omniauth/openid_connect.rb b/lib/omniauth/openid_connect.rb deleted file mode 100644 index 6158e0f0..00000000 --- a/lib/omniauth/openid_connect.rb +++ /dev/null @@ -1,5 +0,0 @@ -# frozen_string_literal: true - -require 'omniauth/openid_connect/errors' -require 'omniauth/openid_connect/version' -require 'omniauth/strategies/openid_connect' diff --git a/lib/omniauth/strategies/openid_connect.rb b/lib/omniauth/strategies/tara.rb similarity index 91% rename from lib/omniauth/strategies/openid_connect.rb rename to lib/omniauth/strategies/tara.rb index ebfaaa17..196c372f 100644 --- a/lib/omniauth/strategies/openid_connect.rb +++ b/lib/omniauth/strategies/tara.rb @@ -10,28 +10,28 @@ module OmniAuth module Strategies - class OpenIDConnect # rubocop:disable Metrics/ClassLength + class Tara # rubocop:disable Metrics/ClassLength include OmniAuth::Strategy extend Forwardable RESPONSE_TYPE_EXCEPTIONS = { - 'id_token' => { exception_class: OmniAuth::OpenIDConnect::MissingIdTokenError, key: :missing_id_token }.freeze, - 'code' => { exception_class: OmniAuth::OpenIDConnect::MissingCodeError, key: :missing_code }.freeze, + 'code' => { exception_class: OmniAuth::Tara::MissingCodeError, + key: :missing_code }.freeze, }.freeze def_delegator :request, :params - option :name, 'openid_connect' + option :name, 'tara' option(:client_options, identifier: nil, secret: nil, redirect_uri: nil, scheme: 'https', host: nil, port: 443, - authorization_endpoint: '/authorize', - token_endpoint: '/token', - userinfo_endpoint: '/userinfo', - jwks_uri: '/jwk', + authorization_endpoint: '/oidc/authorize', + token_endpoint: '/oidc/token', + userinfo_endpoint: '/oidc/profile', + jwks_uri: '/oidc/jwks', end_session_endpoint: nil) option :issuer @@ -41,7 +41,7 @@ class OpenIDConnect # rubocop:disable Metrics/ClassLength option :client_jwk_signing_key option :client_x509_signing_key option :scope, [:openid] - option :response_type, 'code' # ['code', 'id_token'] + option :response_type, 'code' option :require_state, true option :state option :response_mode # [:query, :fragment, :form_post, :web_message] @@ -76,16 +76,18 @@ def uid info do { - name: user_info.name, + # name: user_info.name, email: user_info.email, email_verified: user_info.email_verified, - nickname: user_info.preferred_username, + # nickname: user_info.preferred_username, first_name: user_info.given_name, last_name: user_info.family_name, - gender: user_info.gender, - image: user_info.picture, - phone: user_info.phone_number, - urls: { website: user_info.website }, + # gender: user_info.gender, + # image: user_info.picture, + phone_number: user_info.phone_number, + phone_verified: user_info.phone_number_verified, + birthdate: user_info.birthdate, + # urls: { website: user_info.website }, } end @@ -129,12 +131,8 @@ def callback_phase options.issuer = issuer if options.issuer.nil? || options.issuer.empty? - verify_id_token!(params['id_token']) if configured_response_type == 'id_token' discover! client.redirect_uri = redirect_uri - - return id_token_callback_phase if configured_response_type == 'id_token' - client.authorization_code = authorization_code access_token super @@ -256,13 +254,9 @@ def discover! def user_info return @user_info if @user_info - if access_token.id_token - decoded = decode_id_token(access_token.id_token).raw_attributes - - @user_info = ::OpenIDConnect::ResponseObject::UserInfo.new access_token.userinfo!.raw_attributes.merge(decoded) - else - @user_info = access_token.userinfo! - end + decoded = decode_id_token(access_token.id_token).raw_attributes + # @user_info = ::OpenIDConnect::ResponseObject::UserInfo.new access_token.userinfo!.raw_attributes.merge(decoded) + @user_info = OmniAuth::Tara::UserInfo.new access_token.userinfo!.raw_attributes.merge(decoded) end def access_token @@ -276,7 +270,7 @@ def access_token token_request_params[:code_verifier] = params['code_verifier'] || session.delete('omniauth.pkce.verifier') if options.pkce @access_token = client.access_token!(token_request_params) - verify_id_token!(@access_token.id_token) if configured_response_type == 'code' + verify_id_token!(@access_token.id_token) @access_token end @@ -487,4 +481,4 @@ def message end end -OmniAuth.config.add_camelization 'openid_connect', 'OpenIDConnect' +OmniAuth.config.add_camelization 'tara', 'Tara' diff --git a/lib/omniauth/tara.rb b/lib/omniauth/tara.rb new file mode 100644 index 00000000..91107560 --- /dev/null +++ b/lib/omniauth/tara.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +require 'omniauth/tara/errors' +require 'omniauth/tara/version' +require 'omniauth/tara/user_info' +require 'omniauth/strategies/tara' diff --git a/lib/omniauth/openid_connect/errors.rb b/lib/omniauth/tara/errors.rb similarity index 88% rename from lib/omniauth/openid_connect/errors.rb rename to lib/omniauth/tara/errors.rb index 37c9da1c..0b076b9d 100644 --- a/lib/omniauth/openid_connect/errors.rb +++ b/lib/omniauth/tara/errors.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module OmniAuth - module OpenIDConnect + module Tara class Error < RuntimeError; end class MissingCodeError < Error; end diff --git a/lib/omniauth/tara/user_info.rb b/lib/omniauth/tara/user_info.rb new file mode 100644 index 00000000..1e920506 --- /dev/null +++ b/lib/omniauth/tara/user_info.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +require 'openid_connect' + +module OmniAuth + module Tara + class UserInfo < OpenIDConnect::ConnectObject + attr_optional( + :sub, + :name, + :nickname, + :preferred_username, + :profile, + :locale, + :email, + :email_verified, + :phone_number, + :phone_number_verified, + :profile_attributes + ) + alias subject sub + alias subject= sub= + + validates :email_verified, :phone_number_verified, allow_nil: true, inclusion: { in: [true, false] } + validates :email, allow_nil: true, email: true + validate :require_at_least_one_attributes + + def initialize(attributes = {}) + super + (all_attributes - %i[email_verified phone_number_verified profile_attributes]).each do |key| + send "#{key}=", send(key).try(:to_s) + end + end + + def given_name + raw_attributes.dig('profile_attributes', 'given_name') + end + + def family_name + raw_attributes.dig('profile_attributes', 'family_name') + end + + def birthdate + raw_attributes.dig('profile_attributes', 'date_of_birth') + end + end + end +end diff --git a/lib/omniauth/openid_connect/version.rb b/lib/omniauth/tara/version.rb similarity index 77% rename from lib/omniauth/openid_connect/version.rb rename to lib/omniauth/tara/version.rb index 78efcf6c..ee7187bf 100644 --- a/lib/omniauth/openid_connect/version.rb +++ b/lib/omniauth/tara/version.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module OmniAuth - module OpenIDConnect + module Tara VERSION = '0.7.1' end end diff --git a/lib/omniauth_openid_connect.rb b/lib/omniauth_openid_connect.rb deleted file mode 100644 index e04c2d37..00000000 --- a/lib/omniauth_openid_connect.rb +++ /dev/null @@ -1,3 +0,0 @@ -# frozen_string_literal: true - -require 'omniauth/openid_connect' diff --git a/lib/omniauth_tara.rb b/lib/omniauth_tara.rb new file mode 100644 index 00000000..882c0a85 --- /dev/null +++ b/lib/omniauth_tara.rb @@ -0,0 +1,3 @@ +# frozen_string_literal: true + +require 'omniauth/tara' diff --git a/omniauth_openid_connect.gemspec b/omniauth_tara.gemspec similarity index 51% rename from omniauth_openid_connect.gemspec rename to omniauth_tara.gemspec index 9d2b8d2b..aec34da8 100644 --- a/omniauth_openid_connect.gemspec +++ b/omniauth_tara.gemspec @@ -2,39 +2,36 @@ lib = File.expand_path('lib', __dir__) $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) -require 'omniauth/openid_connect/version' +require 'omniauth/tara/version' Gem::Specification.new do |spec| - spec.name = 'omniauth_openid_connect' - spec.version = OmniAuth::OpenIDConnect::VERSION - spec.authors = ['John Bohn', 'Ilya Shcherbinin'] - spec.email = ['jjbohn@gmail.com', 'm0n9oose@gmail.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.required_ruby_version = '>= 2.7' + spec.name = 'omniauth-tara' + spec.version = OmniAuth::Tara::VERSION + spec.authors = ['John Bohn', 'Ilya Shcherbinin', 'Artur Beljajev', + 'Maciej Szlosarczyk', + 'Sergei Tsõganov'] + spec.email = ['jjbohn@gmail.com', 'm0n9oose@gmail.com', 'artur.beljajev@internet.ee', + 'maciej.szlosarczyk@eestiinternet.ee', + 'sergei.tsoganov@internet.ee'] + spec.summary = 'TARA-Doku (https://github.com/e-gov/TARA-Doku) strategy for OmniAuth' + spec.description = 'TARA-Doku (https://github.com/e-gov/TARA-Doku) strategy for OmniAuth' + spec.homepage = 'https://github.com/internetee/omniauth-tara' + spec.license = 'MIT/X11' 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.metadata = { - 'bug_tracker_uri' => 'https://github.com/m0n9oose/omniauth_openid_connect/issues', - 'changelog_uri' => 'https://github.com/m0n9oose/omniauth_openid_connect/releases', - 'documentation_uri' => "https://github.com/m0n9oose/omniauth_openid_connect/tree/v#{spec.version}#readme", - 'source_code_uri' => "https://github.com/m0n9oose/omniauth_openid_connect/tree/v#{spec.version}", - 'rubygems_mfa_required' => 'true', - } - spec.add_dependency 'omniauth', '>= 1.9', '< 3' spec.add_dependency 'openid_connect', '~> 2.2' spec.add_development_dependency 'faker', '~> 2.0' 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 'minitest', '~> 5.8', '>= 5.8.4' + spec.add_development_dependency 'mocha', '~> 2.1' spec.add_development_dependency 'rake', '~> 12.0' spec.add_development_dependency 'rubocop', '~> 1.12' spec.add_development_dependency 'simplecov', '~> 0.21' diff --git a/test/lib/omniauth/strategies/openid_connect_test.rb b/test/lib/omniauth/strategies/tara_test.rb similarity index 69% rename from test/lib/omniauth/strategies/openid_connect_test.rb rename to test/lib/omniauth/strategies/tara_test.rb index 031e1e3c..8e05f891 100644 --- a/test/lib/omniauth/strategies/openid_connect_test.rb +++ b/test/lib/omniauth/strategies/tara_test.rb @@ -4,16 +4,16 @@ module OmniAuth module Strategies - class OpenIDConnectTest < StrategyTestCase # rubocop:disable Metrics/ClassLength + class TaraTest < StrategyTestCase # rubocop:disable Metrics/ClassLength 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 + assert_equal '/oidc/authorize', strategy.options.client_options.authorization_endpoint + assert_equal '/oidc/token', strategy.options.client_options.token_endpoint end def test_request_phase - expected_redirect = %r{^https://example\.com/authorize\?client_id=1234&nonce=\w{32}&response_type=code&scope=openid&state=\w{32}$} + expected_redirect = %r{^https://example\.com/oidc/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)) @@ -37,8 +37,8 @@ def test_logout_phase_with_discovery 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') - request.stubs(:path).returns('/auth/openid_connect/logout') + request.stubs(:path_info).returns('/auth/tara/logout') + request.stubs(:path).returns('/auth/tara/logout') strategy.expects(:redirect).with(regexp_matches(expected_redirect)) strategy.other_phase @@ -62,8 +62,8 @@ def test_logout_phase_with_discovery_and_post_logout_redirect_uri 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') - request.stubs(:path).returns('/auth/openid_connect/logout') + request.stubs(:path_info).returns('/auth/tara/logout') + request.stubs(:path).returns('/auth/tara/logout') strategy.expects(:redirect).with(expected_redirect) strategy.other_phase @@ -91,7 +91,7 @@ def test_logout_phase end def test_request_phase_with_params - expected_redirect = %r{^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$} + expected_redirect = %r{^https://example\.com/oidc/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') @@ -128,10 +128,10 @@ def test_request_phase_with_discovery end def test_request_phase_with_response_mode - expected_redirect = %r{^https://example\.com/authorize\?client_id=1234&nonce=\w{32}&response_mode=form_post&response_type=id_token&scope=openid&state=\w{32}$} + expected_redirect = %r{^https://example\.com/oidc/authorize\?client_id=1234&nonce=\w{32}&response_mode=form_post&response_type=code&scope=openid&state=\w{32}$} strategy.options.issuer = 'example.com' strategy.options.response_mode = 'form_post' - strategy.options.response_type = 'id_token' + strategy.options.response_type = 'code' strategy.options.client_options.host = 'example.com' strategy.expects(:redirect).with(regexp_matches(expected_redirect)) @@ -139,10 +139,10 @@ def test_request_phase_with_response_mode end def test_request_phase_with_response_mode_symbol - expected_redirect = %r{^https://example\.com/authorize\?client_id=1234&nonce=\w{32}&response_mode=form_post&response_type=id_token&scope=openid&state=\w{32}$} + expected_redirect = %r{^https://example\.com/oidc/authorize\?client_id=1234&nonce=\w{32}&response_mode=form_post&response_type=code&scope=openid&state=\w{32}$} strategy.options.issuer = 'example.com' strategy.options.response_mode = 'form_post' - strategy.options.response_type = :id_token + strategy.options.response_type = :code strategy.options.client_options.host = 'example.com' strategy.expects(:redirect).with(regexp_matches(expected_redirect)) @@ -220,34 +220,6 @@ def test_callback_phase(_session = {}, _params = {}) # rubocop:disable Metrics/A strategy.callback_phase end - def test_callback_phase_with_id_token - state = SecureRandom.hex(16) - request.stubs(:params).returns('id_token' => jwt.to_s, 'state' => state) - request.stubs(:path).returns('') - - strategy.options.issuer = 'example.com' - strategy.options.client_signing_alg = :RS256 - strategy.options.client_jwk_signing_key = jwks.to_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(jwt.to_s) - - 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_id_token_and_param_provided_nonce # rubocop:disable Metrics/AbcSize code = SecureRandom.hex(16) state = SecureRandom.hex(16) @@ -280,138 +252,6 @@ def test_callback_phase_with_id_token_and_param_provided_nonce # rubocop:disable strategy.callback_phase end - def test_callback_phase_with_id_token_no_kid - other_rsa_private = OpenSSL::PKey::RSA.generate(2048) - - key = JSON::JWK.new(private_key) - other_key = JSON::JWK.new(other_rsa_private) - state = SecureRandom.hex(16) - request.stubs(:params).returns('id_token' => jwt.to_s, 'state' => state) - request.stubs(:path_info).returns('') - - strategy.options.issuer = issuer - strategy.options.client_signing_alg = :RS256 - strategy.options.client_jwk_signing_key = { 'keys' => [other_key, key] }.to_json - strategy.options.response_type = 'id_token' - - strategy.unstub(:user_info) - strategy.call!('rack.session' => { 'omniauth.state' => state, 'omniauth.nonce' => nonce }) - strategy.callback_phase - end - - def test_callback_phase_with_id_token_with_kid - other_rsa_private = OpenSSL::PKey::RSA.generate(2048) - - key = JSON::JWK.new(private_key) - other_key = JSON::JWK.new(other_rsa_private) - state = SecureRandom.hex(16) - jwt_with_kid = JSON::JWT.new(payload).sign(key, :RS256) - request.stubs(:params).returns('id_token' => jwt_with_kid.to_s, 'state' => state) - request.stubs(:path_info).returns('') - - strategy.options.issuer = issuer - strategy.options.client_signing_alg = :RS256 - strategy.options.client_jwk_signing_key = { 'keys' => [other_key, key] }.to_json - strategy.options.response_type = 'id_token' - - strategy.unstub(:user_info) - strategy.call!('rack.session' => { 'omniauth.state' => state, 'omniauth.nonce' => nonce }) - strategy.callback_phase - end - - def test_callback_phase_with_id_token_with_kid_and_no_matching_kid - other_rsa_private = OpenSSL::PKey::RSA.generate(2048) - - key = JSON::JWK.new(private_key) - other_key = JSON::JWK.new(other_rsa_private) - state = SecureRandom.hex(16) - jwt_with_kid = JSON::JWT.new(payload).sign(key, :RS256) - request.stubs(:params).returns('id_token' => jwt_with_kid.to_s, 'state' => state) - request.stubs(:path_info).returns('') - - strategy.options.issuer = issuer - strategy.options.client_signing_alg = :RS256 - # We use private_key here instead of the wrapped key, which contains a kid - strategy.options.client_jwk_signing_key = { 'keys' => [other_key, private_key] }.to_json - strategy.options.response_type = 'id_token' - - strategy.unstub(:user_info) - strategy.call!('rack.session' => { 'omniauth.state' => state, 'omniauth.nonce' => nonce }) - - assert_raises JSON::JWK::Set::KidNotFound do - strategy.callback_phase - end - end - - def test_callback_phase_with_id_token_with_hs256 - state = SecureRandom.hex(16) - request.stubs(:params).returns('id_token' => jwt_with_hs256.to_s, 'state' => state) - request.stubs(:path_info).returns('') - - strategy.options.issuer = issuer - strategy.options.client_options.secret = hmac_secret - strategy.options.client_signing_alg = :HS256 - strategy.options.response_type = 'id_token' - - strategy.unstub(:user_info) - strategy.call!('rack.session' => { 'omniauth.state' => state, 'omniauth.nonce' => nonce }) - strategy.callback_phase - end - - def test_callback_phase_with_hs256_base64_jwt_secret - state = SecureRandom.hex(16) - request.stubs(:params).returns('id_token' => jwt_with_hs256.to_s, 'state' => state) - request.stubs(:path_info).returns('') - - strategy.options.issuer = issuer - strategy.options.jwt_secret_base64 = Base64.encode64(hmac_secret) - strategy.options.response_type = 'id_token' - - strategy.unstub(:user_info) - strategy.call!('rack.session' => { 'omniauth.state' => state, 'omniauth.nonce' => nonce }) - strategy.callback_phase - end - - def test_callback_phase_with_mismatched_signing_algorithm - state = SecureRandom.hex(16) - request.stubs(:params).returns('id_token' => jwt_with_hs512.to_s, 'state' => state) - request.stubs(:path_info).returns('') - - strategy.options.issuer = issuer - strategy.options.client_options.secret = hmac_secret - strategy.options.client_signing_alg = :HS256 - strategy.options.response_type = 'id_token' - - strategy.unstub(:user_info) - strategy.call!('rack.session' => { 'omniauth.state' => state, 'omniauth.nonce' => nonce }) - - strategy.expects(:fail!).with(:invalid_jwt_algorithm, is_a(OmniAuth::Strategies::OpenIDConnect::CallbackError)) - strategy.callback_phase - end - - def test_callback_phase_with_id_token_no_matching_key - rsa_private = OpenSSL::PKey::RSA.generate(2048) - other_rsa_private = OpenSSL::PKey::RSA.generate(2048) - - other_key = JSON::JWK.new(other_rsa_private) - token = JSON::JWT.new(payload).sign(rsa_private, :RS256).to_s - state = SecureRandom.hex(16) - request.stubs(:params).returns('id_token' => token, 'state' => state) - request.stubs(:path_info).returns('') - - strategy.options.issuer = issuer - strategy.options.client_signing_alg = :RS256 - strategy.options.client_jwk_signing_key = { 'keys' => [other_key] }.to_json - strategy.options.response_type = 'id_token' - - strategy.unstub(:user_info) - strategy.call!('rack.session' => { 'omniauth.state' => state, 'omniauth.nonce' => nonce }) - - assert_raises JSON::JWK::Set::KidNotFound do - strategy.callback_phase - end - end - def test_callback_phase_with_discovery # rubocop:disable Metrics/AbcSize state = SecureRandom.hex(16) @@ -510,39 +350,6 @@ def test_callback_phase_with_invalid_state_without_state_verification strategy.callback_phase end - def test_callback_phase_with_jwks_uri - id_token = jwt.to_s - state = SecureRandom.hex(16) - request.stubs(:params).returns('id_token' => id_token, 'state' => state) - request.stubs(:path_info).returns('') - - strategy.options.issuer = 'example.com' - strategy.options.client_options.jwks_uri = 'https://jwks.example.com' - strategy.options.response_type = 'id_token' - - stub_request(:get, strategy.options.client_options.jwks_uri).to_return( - body: jwks.to_json, - headers: { 'Content-Type' => 'application/json' } - ) - - 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(id_token) - - 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_error state = SecureRandom.hex(16) request.stubs(:params).returns('error' => 'invalid_request') @@ -571,31 +378,7 @@ def test_callback_phase_without_code 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) - request.stubs(:params).returns('state' => state) - request.stubs(:path).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) - request.stubs(:params).returns('state' => state) - request.stubs(:path).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.expects(:fail!).with(:missing_code, is_a(OmniAuth::Tara::MissingCodeError)) strategy.callback_phase end @@ -677,16 +460,11 @@ def test_callback_phase_with_rack_oauth2_client_error def test_info info = strategy.info - assert_equal user_info.name, info[:name] assert_equal user_info.email, info[:email] assert_equal user_info.email_verified, info[:email_verified] - 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]) + assert_equal user_info.phone_number, info[:phone_number] end def test_extra @@ -846,32 +624,6 @@ def test_public_key_with_hmac assert_equal strategy.options.client_options.secret, strategy.secret end - def test_id_token_auth_hash - state = 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(payload) - - request.stubs(:params).returns('state' => state, 'nounce' => nonce, 'id_token' => id_token) - request.stubs(:path).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 - def test_option_pkce strategy.options.client_options[:host] = 'example.com' diff --git a/test/strategy_test_case.rb b/test/strategy_test_case.rb index 1492d02b..72a1bd06 100644 --- a/test/strategy_test_case.rb +++ b/test/strategy_test_case.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -class StrategyTestCase < MiniTest::Test +class StrategyTestCase < Minitest::Test class DummyApp def call(env); end end @@ -58,19 +58,20 @@ def jwks end def user_info - @user_info ||= OpenIDConnect::ResponseObject::UserInfo.new( + @user_info ||= OmniAuth::Tara::UserInfo.new( sub: SecureRandom.hex(16), name: Faker::Name.name, email: Faker::Internet.email, email_verified: Faker::Boolean.boolean, - 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", + nickname: Faker::Name.first_name, + profile_attributes: { + given_name: Faker::Name.first_name, + family_name: Faker::Name.last_name, + date_of_birth: '1903-03-03', + }, phone_number: Faker::PhoneNumber.phone_number, - website: Faker::Internet.url + phone_number_verified: Faker::Boolean.boolean ) end @@ -86,7 +87,7 @@ def request end def strategy - @strategy ||= OmniAuth::Strategies::OpenIDConnect.new(DummyApp.new).tap do |strategy| + @strategy ||= OmniAuth::Strategies::Tara.new(DummyApp.new).tap do |strategy| strategy.options.client_options.identifier = @identifier strategy.options.client_options.secret = @secret strategy.stubs(:request).returns(request) diff --git a/test/test_helper.rb b/test/test_helper.rb index 9bf7ba94..fb57378b 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -22,6 +22,6 @@ lib = File.expand_path('../lib', __dir__) $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) -require 'omniauth_openid_connect' +require 'omniauth_tara' require_relative 'strategy_test_case' OmniAuth.config.test_mode = true