├── .github ├── stale.yml └── workflows │ └── main.yml ├── .gitignore ├── .rubocop.yml ├── CHANGELOG.md ├── Gemfile ├── Guardfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── lib ├── omniauth │ ├── openid_connect.rb │ ├── openid_connect │ │ ├── errors.rb │ │ └── version.rb │ └── strategies │ │ └── openid_connect.rb └── omniauth_openid_connect.rb ├── omniauth_openid_connect.gemspec └── test ├── fixtures └── test.crt ├── lib └── omniauth │ └── strategies │ └── openid_connect_test.rb ├── strategy_test_case.rb └── test_helper.rb /.github/stale.yml: -------------------------------------------------------------------------------- 1 | # Number of days of inactivity before an issue becomes stale 2 | daysUntilStale: 60 3 | # Number of days of inactivity before a stale issue is closed 4 | daysUntilClose: 7 5 | # Issues with these labels will never be considered stale 6 | exemptLabels: 7 | - pinned 8 | - security 9 | # Label to use when marking an issue as stale 10 | staleLabel: wontfix 11 | # Comment to post when marking an issue as stale. Set to `false` to disable 12 | markComment: > 13 | This issue has been automatically marked as stale because it has not had 14 | recent activity. It will be closed if no further activity occurs. Thank you 15 | for your contributions. 16 | # Comment to post when closing a stale issue. Set to `false` to disable 17 | closeComment: false 18 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Main 2 | on: 3 | push: 4 | branches: 5 | - main 6 | - master 7 | 8 | pull_request: 9 | types: [opened, synchronize, reopened] 10 | 11 | jobs: 12 | test: 13 | runs-on: ubuntu-latest 14 | strategy: 15 | fail-fast: false 16 | matrix: 17 | ruby: ["2.7", "3.0", "3.1", "3.2", "3.3", "3.4"] 18 | name: Ruby ${{ matrix.ruby }} 19 | 20 | steps: 21 | - name: Checkout code 22 | uses: actions/checkout@v3 23 | 24 | - name: Setup Ruby 25 | uses: ruby/setup-ruby@v1 26 | with: 27 | ruby-version: ${{ matrix.ruby }} 28 | bundler-cache: true 29 | 30 | - name: Run tests 31 | run: bundle exec rake 32 | 33 | - name: Coveralls Parallel 34 | uses: coverallsapp/github-action@master 35 | with: 36 | github-token: ${{ secrets.github_token }} 37 | flag-name: ruby-${{ matrix.ruby }} 38 | parallel: true 39 | 40 | finish: 41 | needs: test 42 | runs-on: ubuntu-latest 43 | steps: 44 | - name: Coveralls Finished 45 | uses: coverallsapp/github-action@master 46 | with: 47 | github-token: ${{ secrets.github_token }} 48 | parallel-finished: true 49 | 50 | rubocop: 51 | runs-on: ubuntu-latest 52 | steps: 53 | - name: Checkout code 54 | uses: actions/checkout@v3 55 | 56 | - name: Setup Ruby 57 | uses: ruby/setup-ruby@v1 58 | with: 59 | bundler-cache: true 60 | ruby-version: "2.7" 61 | 62 | - name: rubocop 63 | run: bundle exec rubocop --parallel 64 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | .bundle 4 | .config 5 | .idea 6 | .yardoc 7 | InstalledFiles 8 | _yardoc 9 | coverage 10 | doc/ 11 | lib/bundler/man 12 | pkg 13 | rdoc 14 | spec/reports 15 | test/tmp 16 | test/version_tmp 17 | tmp 18 | .ruby-version 19 | .ruby-gemset 20 | Gemfile.lock 21 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | Gemspec/RequiredRubyVersion: 2 | Enabled: false 3 | 4 | LineLength: 5 | Description: 'Limit lines to 130 characters.' 6 | Max: 130 7 | 8 | Layout/SpaceInsideStringInterpolation: 9 | Enabled: false 10 | 11 | Layout/MultilineOperationIndentation: 12 | EnforcedStyle: indented 13 | 14 | StringLiterals: 15 | EnforcedStyle: single_quotes 16 | 17 | Style/TrailingCommaInArrayLiteral: 18 | EnforcedStyleForMultiline: comma 19 | Style/TrailingCommaInHashLiteral: 20 | EnforcedStyleForMultiline: comma 21 | 22 | Style/SafeNavigation: 23 | Enabled: false 24 | 25 | Style/EmptyMethod: 26 | Description: 'Checks the formatting of empty method definitions.' 27 | StyleGuide: '#no-single-line-methods' 28 | Enabled: false 29 | 30 | HashSyntax: 31 | Description: "Prefer Ruby 1.9 hash syntax { a: 1, b: 2 } over 1.8 syntax\n{ :a => 1, :b => 2 }" 32 | EnforcedStyle: ruby19 33 | Enabled: true 34 | 35 | RedundantBegin: 36 | Enabled: true 37 | 38 | Documentation: 39 | Enabled: false 40 | 41 | Metrics/AbcSize: 42 | Max: 60 43 | 44 | Metrics/ClassLength: 45 | Max: 300 46 | 47 | Metrics/CyclomaticComplexity: 48 | Max: 50 49 | 50 | Metrics/PerceivedComplexity: 51 | Max: 15 52 | 53 | Metrics/BlockLength: 54 | Max: 40 55 | 56 | Metrics/MethodLength: 57 | Max: 45 58 | 59 | AllCops: 60 | Exclude: 61 | - vendor/bundle/**/* 62 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Unreleased 2 | 3 | # v0.8.0 (2024-07-04) 4 | 5 | - Add `send_state` parameter to disable sending of state (https://github.com/omniauth/omniauth_openid_connect/pull/182) 6 | 7 | # v0.7.1 (2023-04-26) 8 | 9 | - Fix handling of JWKS response (https://github.com/omniauth/omniauth_openid_connect/pull/157) 10 | 11 | # v0.7.0 (2023-04-25) 12 | 13 | - Update openid_connect to 2.2 (https://github.com/omniauth/omniauth_openid_connect/pull/153) 14 | - Drop Ruby 2.5 and 2.6 CI support (https://github.com/omniauth/omniauth_openid_connect/pull/154) 15 | - Improvements to README (https://github.com/omniauth/omniauth_openid_connect/pull/152, https://github.com/omniauth/omniauth_openid_connect/pull/151) 16 | - Add option `logout_path` (https://github.com/omniauth/omniauth_openid_connect/pull/143) 17 | 18 | # v0.6.1 (2023-02-22) 19 | 20 | - Fix uninitialized constant error (https://github.com/omniauth/omniauth_openid_connect/pull/147) 21 | 22 | # v0.6.0 (2023-01-22) 23 | 24 | - Support verification of HS256-signed JWTs (https://github.com/omniauth/omniauth_openid_connect/pull/134) 25 | 26 | # v0.5.0 (2022-12-26) 27 | 28 | - Support the "nonce" parameter forwarding without a session [#130](https://github.com/omniauth/omniauth_openid_connect/pull/130) 29 | - Fetch key from JWKS URI if available [#133](https://github.com/omniauth/omniauth_openid_connect/pull/133) 30 | - Make the state parameter verification optional [#122](https://github.com/omniauth/omniauth_openid_connect/pull/122) 31 | - Add email_verified claim in user info [#131](https://github.com/omniauth/omniauth_openid_connect/pull/131) 32 | - Add PKCE verification support [#128](https://github.com/omniauth/omniauth_openid_connect/pull/128) 33 | 34 | # v0.4.0 (2022-02-06) 35 | 36 | - Support dynamic parameters to the authorize URI [#90](https://github.com/omniauth/omniauth_openid_connect/pull/90) 37 | - Upgrade Faker and replace Travis with Github Actions [#102](https://github.com/omniauth/omniauth_openid_connect/pull/102) 38 | - Make `omniauth_openid_connect` gem compatible with `omniauth v2.0` [#95](https://github.com/omniauth/omniauth_openid_connect/pull/95) 39 | - Fall back to the discovered jwks when no key specified [#97](https://github.com/omniauth/omniauth_openid_connect/pull/97) 40 | - Allow updating to omniauth v2 [#88](https://github.com/omniauth/omniauth_openid_connect/pull/88) 41 | 42 | # v0.3.5 (2020-06-07) 43 | 44 | - bugfix: Info from decoded id_token is not exposed into `request.env['omniauth.auth']` [#61](https://github.com/m0n9oose/omniauth_openid_connect/pull/61) 45 | - bugfix: NoMethodError (`undefined method 'count' for #`) [#60](https://github.com/m0n9oose/omniauth_openid_connect/pull/60) 46 | 47 | # v0.3.4 (2020-05-21) 48 | 49 | - Try to verify id_token when response_type is code [#44](https://github.com/m0n9oose/omniauth_openid_connect/pull/44) 50 | - Provide more information on error [#49](https://github.com/m0n9oose/omniauth_openid_connect/pull/49) 51 | - Update configuration documentation [#53](https://github.com/m0n9oose/omniauth_openid_connect/pull/53) 52 | - Add documentation about the send_scope_to_token_endpoint config property [#52](https://github.com/m0n9oose/omniauth_openid_connect/pull/52) 53 | - refactor: take uid_field from raw_attributes [#54](https://github.com/m0n9oose/omniauth_openid_connect/pull/54) 54 | - chore(ci): add 2.7, ruby-head and jruby-head [#55](https://github.com/m0n9oose/omniauth_openid_connect/pull/55) 55 | 56 | # v0.3.3 (2019-11-09) 57 | 58 | - Pass `acr_values` to authorize url [#43](https://github.com/m0n9oose/omniauth_openid_connect/pull/43) 59 | - Add raw info for id token [#42](https://github.com/m0n9oose/omniauth_openid_connect/pull/42) 60 | - Fixed `id_token` verification when `id_token` is not used [#41](https://github.com/m0n9oose/omniauth_openid_connect/pull/41) 61 | - Cast `response_type` to string when checking if it is set in params [#36](https://github.com/m0n9oose/omniauth_openid_connect/pull/36) 62 | - Support both symbol and string version of `response_type` option [#35](https://github.com/m0n9oose/omniauth_openid_connect/pull/35) 63 | - Fix gemspec homepage [#33](https://github.com/m0n9oose/omniauth_openid_connect/pull/33) 64 | - Add support for `response_type` `id_token` [#32](https://github.com/m0n9oose/omniauth_openid_connect/pull/32) 65 | 66 | # v0.3.2 (2019-08-03) 67 | 68 | - Use response_mode in `authorize_uri` if the option is defined [#30](https://github.com/m0n9oose/omniauth_openid_connect/pull/30) 69 | - Move verification of `id_token` to before accessing tokens [#28](https://github.com/m0n9oose/omniauth_openid_connect/pull/28) 70 | - Update omniauth dependency [#26](https://github.com/m0n9oose/omniauth_openid_connect/pull/26) 71 | 72 | # v0.3.1 (2019-06-08) 73 | 74 | - Set default OmniAuth name to openid_connect [#23](https://github.com/m0n9oose/omniauth_openid_connect/pull/23) 75 | 76 | # v0.3.0 (2019-04-07) 77 | 78 | - RP-Initiated Logout phase [#5](https://github.com/m0n9oose/omniauth_openid_connect/pull/5) 79 | - Allows `ui_locales`, `claims_locales` and `login_hint` as request params [#6](https://github.com/m0n9oose/omniauth_openid_connect/pull/6) 80 | - Make uid label configurable [#11](https://github.com/m0n9oose/omniauth_openid_connect/pull/11) 81 | - Allow rails applications to handle state mismatch [#14](https://github.com/m0n9oose/omniauth_openid_connect/pull/14) 82 | - Handle errors when fetching access_token at callback_phase [#17](https://github.com/m0n9oose/omniauth_openid_connect/pull/17) 83 | - Allow state method to receive env [#19](https://github.com/m0n9oose/omniauth_openid_connect/pull/19) 84 | 85 | # v0.2.4 (2019-01-06) 86 | 87 | - Prompt and login hint [#4](https://github.com/m0n9oose/omniauth_openid_connect/pull/4) 88 | - Bump openid_connect dependency [#9](https://github.com/m0n9oose/omniauth_openid_connect/pull/9) 89 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source 'https://rubygems.org' 4 | gemspec 5 | 6 | if Gem::Version.new(RUBY_VERSION.dup) >= Gem::Version.new('3.1') 7 | gem 'net-imap', require: false 8 | gem 'net-pop', require: false 9 | gem 'net-smtp', require: false 10 | end 11 | -------------------------------------------------------------------------------- /Guardfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # A sample Guardfile 4 | # More info at https://github.com/guard/guard#readme 5 | 6 | guard 'minitest' do 7 | # with Minitest::Unit 8 | watch(%r{^test/(.*)/(.*)_test\.rb}) 9 | watch(%r{^lib/(.*)\.rb}) { |m| "test/lib/#{m[1]}_test.rb" } 10 | watch(%r{^test/test_helper\.rb}) { 'test' } 11 | end 12 | 13 | guard :bundler do 14 | watch('Gemfile') 15 | watch(/^.+\.gemspec/) 16 | end 17 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 John Bohn 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # OmniAuth::OpenIDConnect 2 | 3 | Originally was [omniauth-openid-connect](https://github.com/jjbohn/omniauth-openid-connect) 4 | 5 | I've forked this repository and launch as separate gem because maintaining of original was dropped. 6 | 7 | [![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) 8 | [![Coverage Status](https://coveralls.io/repos/github/omniauth/omniauth_openid_connect/badge.svg)](https://coveralls.io/github/omniauth/omniauth_openid_connect) 9 | 10 | ## Installation 11 | 12 | Add this line to your application's Gemfile: 13 | 14 | gem 'omniauth_openid_connect' 15 | 16 | And then execute: 17 | 18 | $ bundle 19 | 20 | Or install it yourself as: 21 | 22 | $ gem install omniauth_openid_connect 23 | 24 | ## Supported Ruby Versions 25 | 26 | OmniAuth::OpenIDConnect is tested under 2.7, 3.0, 3.1, 3.2, 3.3, 3.4 27 | 28 | ## Usage 29 | 30 | Example configuration 31 | 32 | ```ruby 33 | Rails.application.config.middleware.use OmniAuth::Builder do 34 | provider :openid_connect, { 35 | name: :my_provider, 36 | scope: [:openid, :email, :profile, :address], 37 | response_type: :code, 38 | uid_field: "preferred_username", 39 | client_options: { 40 | port: 443, 41 | scheme: "https", 42 | host: "myprovider.com", 43 | identifier: ENV["OP_CLIENT_ID"], 44 | secret: ENV["OP_SECRET_KEY"], 45 | redirect_uri: "http://myapp.com/users/auth/openid_connect/callback", 46 | }, 47 | } 48 | end 49 | ``` 50 | 51 | ### with Devise 52 | ```ruby 53 | Devise.setup do |config| 54 | config.omniauth :openid_connect, { 55 | name: :my_provider, 56 | scope: [:openid, :email, :profile, :address], 57 | response_type: :code, 58 | uid_field: "preferred_username", 59 | client_options: { 60 | port: 443, 61 | scheme: "https", 62 | host: "myprovider.com", 63 | identifier: ENV["OP_CLIENT_ID"], 64 | secret: ENV["OP_SECRET_KEY"], 65 | redirect_uri: "http://myapp.com/users/auth/openid_connect/callback", 66 | }, 67 | } 68 | end 69 | ``` 70 | 71 | ### Options Overview 72 | 73 | | Field | Description | Required | Default | Example/Options | 74 | |------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------|-------------------------------|-----------------------------------------------------| 75 | | name | Arbitrary string to identify connection and identify it from other openid_connect providers | no | String: openid_connect | :my_idp | 76 | | issuer | Root url for the authorization server | yes | | https://myprovider.com | 77 | | 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 | 78 | | client_auth_method | Which authentication method to use to authenticate your app with the authorization server | no | Sym: basic | "basic", "jwks" | 79 | | scope | Which OpenID scopes to include (:openid is always required) | no | Array [:openid] | [:openid, :profile, :email] | 80 | | response_type | Which OAuth2 response type to use with the authorization request | no | String: code | one of: 'code', 'id_token' | 81 | | 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) } | 82 | | require_state | Should the callback phase require that a state is present. If `send_state` is true, then the callback state must match the authorize state. This is recommended, not required by the OIDC specification. | no | true | false | 83 | | send_state | Should the authorize phase send a `state` parameter - this is recommended, not required by the OIDC specification | no | true | false | 84 | | 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 | 85 | | display | An optional parameter to the authorization request to determine how the authorization and consent page | no | nil | one of: :page, :popup, :touch, :wap | 86 | | prompt | An optional parameter to the authorization request to determine what pages the user will be shown | no | nil | one of: :none, :login, :consent, :select_account | 87 | | send_scope_to_token_endpoint | Should the scope parameter be sent to the authorization token endpoint? | no | true | one of: true, false | 88 | | 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 | 89 | | uid_field | The field of the user info response to be used as a unique id | no | 'sub' | "sub", "preferred_username" | 90 | | extra_authorize_params | A hash of extra fixed parameters that will be merged to the authorization request | no | Hash | {"tenant" => "common"} | 91 | | allow_authorize_params | A list of allowed dynamic parameters that will be merged to the authorization request | no | Array | [:screen_name] | 92 | | pkce | Enable [PKCE flow](https://oauth.net/2/pkce/) | no | false | one of: true, false | 93 | | pkce_verifier | Specify a custom PKCE verifier code. | no | A random 128-char string | Proc.new { SecureRandom.hex(64) } | 94 | | 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 | 95 | | client_options | A hash of client options detailed in its own section | yes | | | 96 | | 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" | 97 | | logout_path | The log out is only triggered when the request path ends on this path | no | '/logout' | '/sign_out' | 98 | | acr_values | Authentication Class Reference (ACR) values to be passed to the authorize_uri to enforce a specific level, see [RFC9470](https://www.rfc-editor.org/rfc/rfc9470.html) | no | nil | "c1 c2" | 99 | 100 | ### Client Config Options 101 | 102 | These are the configuration options for the client_options hash of the configuration. 103 | 104 | | Field | Description | Default | Replaced by discovery? | 105 | |------------------------|-----------------------------------------------------------------|------------|------------------------| 106 | | identifier | The OAuth2 client_id | | | 107 | | secret | The OAuth2 client secret | | | 108 | | redirect_uri | The OAuth2 authorization callback url in your app | | | 109 | | scheme | The http scheme to use | https | | 110 | | host | The host of the authorization server | nil | | 111 | | port | The port for the authorization server | 443 | | 112 | | audience | The intended consumer (`aud` field) of the id_token | nil | | 113 | | authorization_endpoint | The authorize endpoint on the authorization server | /authorize | yes | 114 | | token_endpoint | The token endpoint on the authorization server | /token | yes | 115 | | userinfo_endpoint | The user info endpoint on the authorization server | /userinfo | yes | 116 | | jwks_uri | The jwks_uri on the authorization server | /jwk | yes | 117 | | end_session_endpoint | The url to call to log the user out at the authorization server | nil | yes | 118 | 119 | ### Additional Configuration Notes 120 | * `name` is arbitrary, I recommend using the name of your provider. The name 121 | configuration exists because you could be using multiple OpenID Connect 122 | providers in a single app. 123 | 124 | **NOTE**: if you use this gem with Devise you should use `:openid_connect` name, 125 | or Devise would route to 'users/auth/:provider' rather than 'users/auth/openid_connect' 126 | 127 | * `response_type` tells the authorization server which grant type the application wants to use, 128 | currently, only `:code` (Authorization Code grant) and `:id_token` (Implicit grant) are valid. 129 | * If you want to pass `state` parameter by yourself. You can set Proc Object. 130 | e.g. `state: Proc.new { SecureRandom.hex(32) }` 131 | * `nonce` is optional. If don't want to pass "nonce" parameter to provider, You should specify 132 | `false` to `send_nonce` option. (default true) 133 | * Support for other client authentication methods. If don't specified 134 | `:client_auth_method` option, automatically set `:basic`. 135 | * Use "OpenID Connect Discovery", You should specify `true` to `discovery` option. (default false) 136 | * In "OpenID Connect Discovery", generally provider should have Webfinger endpoint. 137 | If provider does not have Webfinger endpoint, You can specify "Issuer" to option. 138 | e.g. `issuer: "https://myprovider.com"` 139 | It means to get configuration from "https://myprovider.com/.well-known/openid-configuration". 140 | * The uid is by default using the `sub` value from the `user_info` response, 141 | which in some applications is not the expected value. To avoid such limitations, the uid label can be 142 | configured by providing the omniauth `uid_field` option to a different label (i.e. `preferred_username`) 143 | that appears in the `user_info` details. 144 | * The `issuer` property should exactly match the provider's issuer link. 145 | * The `response_mode` option is optional and specifies how the result of the authorization request is formatted. 146 | * Some OpenID Connect providers require the `scope` attribute in requests to the token endpoint, even if 147 | this is not in the protocol specifications. In those cases, the `send_scope_to_token_endpoint` 148 | property can be used to add the attribute to the token request. Initial value is `true`, which means that the 149 | scope attribute is included by default. 150 | 151 | ## Additional notes 152 | * 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. 153 | In such example, the session is empty, so you have to forward certain parameters received from the client. 154 | Currently supported ones are `code_verifier` and `nonce` - simply provide them as the `/callback` request parameters. 155 | 156 | For the full low down on OpenID Connect, please check out 157 | [the spec](http://openid.net/specs/openid-connect-core-1_0.html). 158 | 159 | ## Contributing 160 | 161 | 1. Fork it ( http://github.com/omniauth/omniauth_openid_connect/fork ) 162 | 2. Create your feature branch (`git checkout -b my-new-feature`) 163 | 3. Cover your changes with tests and make sure they're green (`bundle install && bundle exec rake test`) 164 | 4. Commit your changes (`git commit -am 'Add some feature'`) 165 | 5. Push to the branch (`git push origin my-new-feature`) 166 | 6. Create new Pull Request 167 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'bundler/gem_tasks' 4 | require 'rake/testtask' 5 | 6 | Rake::TestTask.new do |t| 7 | t.libs << 'test' 8 | t.test_files = FileList['test/lib/omniauth/**/*_test.rb'] 9 | t.verbose = true 10 | end 11 | 12 | task default: :test 13 | -------------------------------------------------------------------------------- /lib/omniauth/openid_connect.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'omniauth/openid_connect/errors' 4 | require 'omniauth/openid_connect/version' 5 | require 'omniauth/strategies/openid_connect' 6 | -------------------------------------------------------------------------------- /lib/omniauth/openid_connect/errors.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module OmniAuth 4 | module OpenIDConnect 5 | class Error < RuntimeError; end 6 | 7 | class MissingCodeError < Error; end 8 | 9 | class MissingIdTokenError < Error; end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/omniauth/openid_connect/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module OmniAuth 4 | module OpenIDConnect 5 | VERSION = '0.8.0' 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /lib/omniauth/strategies/openid_connect.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'base64' 4 | require 'timeout' 5 | require 'net/http' 6 | require 'open-uri' 7 | require 'omniauth' 8 | require 'openid_connect' 9 | require 'forwardable' 10 | 11 | module OmniAuth 12 | module Strategies 13 | class OpenIDConnect # rubocop:disable Metrics/ClassLength 14 | include OmniAuth::Strategy 15 | extend Forwardable 16 | 17 | RESPONSE_TYPE_EXCEPTIONS = { 18 | 'id_token' => { exception_class: OmniAuth::OpenIDConnect::MissingIdTokenError, key: :missing_id_token }.freeze, 19 | 'code' => { exception_class: OmniAuth::OpenIDConnect::MissingCodeError, key: :missing_code }.freeze, 20 | }.freeze 21 | 22 | def_delegator :request, :params 23 | 24 | option :name, 'openid_connect' 25 | option(:client_options, identifier: nil, 26 | secret: nil, 27 | redirect_uri: nil, 28 | scheme: 'https', 29 | host: nil, 30 | port: 443, 31 | audience: nil, 32 | authorization_endpoint: '/authorize', 33 | token_endpoint: '/token', 34 | userinfo_endpoint: '/userinfo', 35 | jwks_uri: '/jwk', 36 | end_session_endpoint: nil) 37 | 38 | option :issuer 39 | option :discovery, false 40 | option :client_signing_alg 41 | option :jwt_secret_base64 42 | option :client_jwk_signing_key 43 | option :client_x509_signing_key 44 | option :scope, [:openid] 45 | option :response_type, 'code' # ['code', 'id_token'] 46 | option :send_state, true 47 | option :require_state, true 48 | option :state 49 | option :response_mode # [:query, :fragment, :form_post, :web_message] 50 | option :display, nil # [:page, :popup, :touch, :wap] 51 | option :prompt, nil # [:none, :login, :consent, :select_account] 52 | option :hd, nil 53 | option :max_age 54 | option :ui_locales 55 | option :id_token_hint 56 | option :acr_values 57 | option :send_nonce, true 58 | option :send_scope_to_token_endpoint, true 59 | option :client_auth_method 60 | option :post_logout_redirect_uri 61 | option :extra_authorize_params, {} 62 | option :allow_authorize_params, [] 63 | option :uid_field, 'sub' 64 | option :pkce, false 65 | option :pkce_verifier, nil 66 | option :pkce_options, { 67 | code_challenge: proc { |verifier| 68 | Base64.urlsafe_encode64(Digest::SHA2.digest(verifier), padding: false) 69 | }, 70 | code_challenge_method: 'S256', 71 | } 72 | 73 | option :logout_path, '/logout' 74 | 75 | def uid 76 | user_info.raw_attributes[options.uid_field.to_sym] || user_info.sub 77 | end 78 | 79 | info do 80 | { 81 | name: user_info.name, 82 | email: user_info.email, 83 | email_verified: user_info.email_verified, 84 | nickname: user_info.preferred_username, 85 | first_name: user_info.given_name, 86 | last_name: user_info.family_name, 87 | gender: user_info.gender, 88 | image: user_info.picture, 89 | phone: user_info.phone_number, 90 | urls: { website: user_info.website }, 91 | } 92 | end 93 | 94 | extra do 95 | { raw_info: user_info.raw_attributes } 96 | end 97 | 98 | credentials do 99 | { 100 | id_token: access_token.id_token, 101 | token: access_token.access_token, 102 | refresh_token: access_token.refresh_token, 103 | expires_in: access_token.expires_in, 104 | scope: access_token.scope, 105 | } 106 | end 107 | 108 | def client 109 | @client ||= ::OpenIDConnect::Client.new(client_options) 110 | end 111 | 112 | def config 113 | @config ||= ::OpenIDConnect::Discovery::Provider::Config.discover!(options.issuer) 114 | end 115 | 116 | def request_phase 117 | options.issuer = issuer if options.issuer.to_s.empty? 118 | discover! 119 | redirect authorize_uri 120 | end 121 | 122 | def callback_phase 123 | error = params['error_reason'] || params['error'] 124 | error_description = params['error_description'] || params['error_reason'] 125 | invalid_state = 126 | if options.send_state 127 | (options.require_state && params['state'].to_s.empty?) || params['state'] != stored_state 128 | else 129 | false 130 | end 131 | 132 | raise CallbackError, error: params['error'], reason: error_description, uri: params['error_uri'] if error 133 | raise CallbackError, error: :csrf_detected, reason: "Invalid 'state' parameter" if invalid_state 134 | 135 | return unless valid_response_type? 136 | 137 | options.issuer = issuer if options.issuer.nil? || options.issuer.empty? 138 | 139 | verify_id_token!(params['id_token']) if configured_response_type == 'id_token' 140 | discover! 141 | client.redirect_uri = redirect_uri 142 | 143 | return id_token_callback_phase if configured_response_type == 'id_token' 144 | 145 | client.authorization_code = authorization_code 146 | access_token 147 | super 148 | rescue CallbackError => e 149 | fail!(e.error, e) 150 | rescue ::Rack::OAuth2::Client::Error => e 151 | fail!(e.response[:error], e) 152 | rescue ::Timeout::Error, ::Errno::ETIMEDOUT => e 153 | fail!(:timeout, e) 154 | rescue ::SocketError => e 155 | fail!(:failed_to_connect, e) 156 | end 157 | 158 | def other_phase 159 | if logout_path_pattern.match?(current_path) 160 | options.issuer = issuer if options.issuer.to_s.empty? 161 | discover! 162 | return redirect(end_session_uri) if end_session_uri 163 | end 164 | call_app! 165 | end 166 | 167 | def authorization_code 168 | params['code'] 169 | end 170 | 171 | def end_session_uri 172 | return unless end_session_endpoint_is_valid? 173 | 174 | end_session_uri = URI(client_options.end_session_endpoint) 175 | end_session_uri.query = encoded_post_logout_redirect_uri 176 | end_session_uri.to_s 177 | end 178 | 179 | def authorize_uri # rubocop:disable Metrics/AbcSize 180 | client.redirect_uri = redirect_uri 181 | opts = { 182 | response_type: options.response_type, 183 | response_mode: options.response_mode, 184 | scope: options.scope, 185 | login_hint: params['login_hint'], 186 | ui_locales: params['ui_locales'], 187 | claims_locales: params['claims_locales'], 188 | prompt: options.prompt, 189 | nonce: (new_nonce if options.send_nonce), 190 | hd: options.hd, 191 | acr_values: options.acr_values, 192 | } 193 | 194 | opts[:state] = new_state if options.send_state 195 | opts.merge!(options.extra_authorize_params) unless options.extra_authorize_params.empty? 196 | 197 | options.allow_authorize_params.each do |key| 198 | opts[key] = request.params[key.to_s] unless opts.key?(key) 199 | end 200 | 201 | if options.pkce 202 | verifier = options.pkce_verifier ? options.pkce_verifier.call : SecureRandom.hex(64) 203 | 204 | opts.merge!(pkce_authorize_params(verifier)) 205 | session['omniauth.pkce.verifier'] = verifier 206 | end 207 | 208 | client.authorization_uri(opts.reject { |_k, v| v.nil? }) 209 | end 210 | 211 | def public_key 212 | @public_key ||= if options.discovery 213 | config.jwks 214 | elsif configured_public_key 215 | configured_public_key 216 | elsif client_options.jwks_uri 217 | fetch_key 218 | end 219 | end 220 | 221 | # Some OpenID providers use the OAuth2 client secret as the shared secret, but 222 | # Keycloak uses a separate key that's stored inside the database. 223 | def secret 224 | base64_decoded_jwt_secret || client_options.secret 225 | end 226 | 227 | def pkce_authorize_params(verifier) 228 | # NOTE: see https://tools.ietf.org/html/rfc7636#appendix-A 229 | { 230 | code_challenge: options.pkce_options[:code_challenge].call(verifier), 231 | code_challenge_method: options.pkce_options[:code_challenge_method], 232 | } 233 | end 234 | 235 | private 236 | 237 | def fetch_key 238 | @fetch_key ||= parse_jwk_key(::OpenIDConnect.http_client.get(client_options.jwks_uri).body) 239 | end 240 | 241 | def base64_decoded_jwt_secret 242 | return unless options.jwt_secret_base64 243 | 244 | Base64.decode64(options.jwt_secret_base64) 245 | end 246 | 247 | def issuer 248 | resource = "#{ client_options.scheme }://#{ client_options.host }" 249 | resource = "#{ resource }:#{ client_options.port }" if client_options.port 250 | ::OpenIDConnect::Discovery::Provider.discover!(resource).issuer 251 | end 252 | 253 | def discover! 254 | return unless options.discovery 255 | 256 | client_options.authorization_endpoint = config.authorization_endpoint 257 | client_options.token_endpoint = config.token_endpoint 258 | client_options.userinfo_endpoint = config.userinfo_endpoint 259 | client_options.jwks_uri = config.jwks_uri 260 | client_options.end_session_endpoint = config.end_session_endpoint if config.respond_to?(:end_session_endpoint) 261 | end 262 | 263 | def user_info 264 | return @user_info if @user_info 265 | 266 | if access_token.id_token 267 | decoded = decode_id_token(access_token.id_token).raw_attributes 268 | 269 | @user_info = ::OpenIDConnect::ResponseObject::UserInfo.new access_token.userinfo!.raw_attributes.merge(decoded) 270 | else 271 | @user_info = access_token.userinfo! 272 | end 273 | end 274 | 275 | def access_token 276 | return @access_token if @access_token 277 | 278 | token_request_params = { 279 | scope: (options.scope if options.send_scope_to_token_endpoint), 280 | client_auth_method: options.client_auth_method, 281 | } 282 | 283 | token_request_params[:code_verifier] = params['code_verifier'] || session.delete('omniauth.pkce.verifier') if options.pkce 284 | 285 | @access_token = client.access_token!(token_request_params) 286 | verify_id_token!(@access_token.id_token) if configured_response_type == 'code' 287 | 288 | @access_token 289 | end 290 | 291 | # Unlike ::OpenIDConnect::ResponseObject::IdToken.decode, this 292 | # method splits the decoding and verification of JWT into two 293 | # steps. First, we decode the JWT without verifying it to 294 | # determine the algorithm used to sign. Then, we verify it using 295 | # the appropriate public key (e.g. if algorithm is RS256) or 296 | # shared secret (e.g. if algorithm is HS256). This works around a 297 | # limitation in the openid_connect gem: 298 | # https://github.com/nov/openid_connect/issues/61 299 | def decode_id_token(id_token) 300 | decoded = JSON::JWT.decode(id_token, :skip_verification) 301 | algorithm = decoded.algorithm.to_sym 302 | 303 | validate_client_algorithm!(algorithm) 304 | 305 | keyset = 306 | case algorithm 307 | when :HS256, :HS384, :HS512 308 | secret 309 | else 310 | public_key 311 | end 312 | 313 | decoded.verify!(keyset) 314 | ::OpenIDConnect::ResponseObject::IdToken.new(decoded) 315 | rescue JSON::JWK::Set::KidNotFound 316 | # If the JWT has a key ID (kid), then we know that the set of 317 | # keys supplied doesn't contain the one we want, and we're 318 | # done. However, if there is no kid, then we try each key 319 | # individually to see if one works: 320 | # https://github.com/nov/json-jwt/pull/92#issuecomment-824654949 321 | raise if decoded&.header&.key?('kid') 322 | 323 | decoded = decode_with_each_key!(id_token, keyset) 324 | 325 | raise unless decoded 326 | 327 | decoded 328 | end 329 | 330 | # If client_signing_alg is specified, we check that the returned JWT 331 | # matches the expected algorithm. If not, we reject it. 332 | def validate_client_algorithm!(algorithm) 333 | client_signing_alg = options.client_signing_alg&.to_sym 334 | 335 | return unless client_signing_alg 336 | return if algorithm == client_signing_alg 337 | 338 | reason = "Received JWT is signed with #{algorithm}, but client_singing_alg is configured for #{client_signing_alg}" 339 | raise CallbackError, error: :invalid_jwt_algorithm, reason: reason, uri: params['error_uri'] 340 | end 341 | 342 | def decode!(id_token, key) 343 | ::OpenIDConnect::ResponseObject::IdToken.decode(id_token, key) 344 | end 345 | 346 | def decode_with_each_key!(id_token, keyset) 347 | return unless keyset.is_a?(JSON::JWK::Set) 348 | 349 | keyset.each do |key| 350 | begin 351 | decoded = decode!(id_token, key) 352 | rescue JSON::JWS::VerificationFailed, JSON::JWS::UnexpectedAlgorithm, JSON::JWK::UnknownAlgorithm 353 | next 354 | end 355 | 356 | return decoded if decoded 357 | end 358 | 359 | nil 360 | end 361 | 362 | def client_options 363 | options.client_options 364 | end 365 | 366 | def new_state 367 | state = if options.state.respond_to?(:call) 368 | if options.state.arity == 1 369 | options.state.call(env) 370 | else 371 | options.state.call 372 | end 373 | end 374 | session['omniauth.state'] = state || SecureRandom.hex(16) 375 | end 376 | 377 | def stored_state 378 | session.delete('omniauth.state') 379 | end 380 | 381 | def new_nonce 382 | session['omniauth.nonce'] = SecureRandom.hex(16) 383 | end 384 | 385 | def stored_nonce 386 | session.delete('omniauth.nonce') 387 | end 388 | 389 | def script_name 390 | return '' if @env.nil? 391 | 392 | super 393 | end 394 | 395 | def session 396 | return {} if @env.nil? 397 | 398 | super 399 | end 400 | 401 | def configured_public_key 402 | @configured_public_key ||= if options.client_jwk_signing_key 403 | parse_jwk_key(options.client_jwk_signing_key) 404 | elsif options.client_x509_signing_key 405 | parse_x509_key(options.client_x509_signing_key) 406 | end 407 | end 408 | 409 | def parse_x509_key(key) 410 | OpenSSL::X509::Certificate.new(key).public_key 411 | end 412 | 413 | def parse_jwk_key(key) 414 | json = key.is_a?(String) ? JSON.parse(key) : key 415 | return JSON::JWK::Set.new(json['keys']) if json.key?('keys') 416 | 417 | JSON::JWK.new(json) 418 | end 419 | 420 | def decode(str) 421 | UrlSafeBase64.decode64(str).unpack1('B*').to_i(2).to_s 422 | end 423 | 424 | def redirect_uri 425 | return client_options.redirect_uri unless params['redirect_uri'] 426 | 427 | "#{ client_options.redirect_uri }?redirect_uri=#{ CGI.escape(params['redirect_uri']) }" 428 | end 429 | 430 | def encoded_post_logout_redirect_uri 431 | return unless options.post_logout_redirect_uri 432 | 433 | URI.encode_www_form( 434 | post_logout_redirect_uri: options.post_logout_redirect_uri 435 | ) 436 | end 437 | 438 | def end_session_endpoint_is_valid? 439 | client_options.end_session_endpoint && 440 | client_options.end_session_endpoint =~ URI::DEFAULT_PARSER.make_regexp 441 | end 442 | 443 | def logout_path_pattern 444 | @logout_path_pattern ||= /\A#{Regexp.quote(request_path)}#{options.logout_path}/ 445 | end 446 | 447 | def id_token_callback_phase 448 | user_data = decode_id_token(params['id_token']).raw_attributes 449 | env['omniauth.auth'] = AuthHash.new( 450 | provider: name, 451 | uid: user_data['sub'], 452 | info: { name: user_data['name'], email: user_data['email'] }, 453 | extra: { raw_info: user_data } 454 | ) 455 | call_app! 456 | end 457 | 458 | def valid_response_type? 459 | return true if params.key?(configured_response_type) 460 | 461 | error_attrs = RESPONSE_TYPE_EXCEPTIONS[configured_response_type] 462 | fail!(error_attrs[:key], error_attrs[:exception_class].new(params['error'])) 463 | 464 | false 465 | end 466 | 467 | def configured_response_type 468 | @configured_response_type ||= options.response_type.to_s 469 | end 470 | 471 | def verify_id_token!(id_token) 472 | return unless id_token 473 | 474 | verify_kwargs = { 475 | issuer: options.issuer, 476 | client_id: client_options.identifier, 477 | nonce: params['nonce'].presence || stored_nonce, 478 | } 479 | verify_kwargs.merge!(audience: client_options.audience) if client_options.audience 480 | 481 | decode_id_token(id_token).verify!(**verify_kwargs) 482 | end 483 | 484 | class CallbackError < StandardError 485 | attr_accessor :error, :error_reason, :error_uri 486 | 487 | def initialize(data) 488 | super 489 | self.error = data[:error] 490 | self.error_reason = data[:reason] 491 | self.error_uri = data[:uri] 492 | end 493 | 494 | def message 495 | [error, error_reason, error_uri].compact.join(' | ') 496 | end 497 | end 498 | end 499 | end 500 | end 501 | 502 | OmniAuth.config.add_camelization 'openid_connect', 'OpenIDConnect' 503 | -------------------------------------------------------------------------------- /lib/omniauth_openid_connect.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'omniauth/openid_connect' 4 | -------------------------------------------------------------------------------- /omniauth_openid_connect.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | lib = File.expand_path('lib', __dir__) 4 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 5 | require 'omniauth/openid_connect/version' 6 | 7 | Gem::Specification.new do |spec| 8 | spec.name = 'omniauth_openid_connect' 9 | spec.version = OmniAuth::OpenIDConnect::VERSION 10 | spec.authors = ['John Bohn', 'Ilya Shcherbinin'] 11 | spec.email = ['jjbohn@gmail.com', 'm0n9oose@gmail.com'] 12 | spec.summary = 'OpenID Connect Strategy for OmniAuth' 13 | spec.description = 'OpenID Connect Strategy for OmniAuth.' 14 | spec.homepage = 'https://github.com/omniauth/omniauth_openid_connect' 15 | spec.license = 'MIT' 16 | 17 | spec.files = `git ls-files -z`.split("\x0") 18 | spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } 19 | spec.test_files = spec.files.grep(%r{^(test|spec|features)/}) 20 | spec.require_paths = ['lib'] 21 | 22 | spec.metadata = { 23 | 'bug_tracker_uri' => 'https://github.com/omniauth/omniauth_openid_connect/issues', 24 | 'changelog_uri' => 'https://github.com/omniauth/omniauth_openid_connect/releases', 25 | 'documentation_uri' => "https://github.com/omniauth/omniauth_openid_connect/tree/v#{spec.version}#readme", 26 | 'source_code_uri' => "https://github.com/omniauth/omniauth_openid_connect/tree/v#{spec.version}", 27 | 'rubygems_mfa_required' => 'true', 28 | } 29 | 30 | spec.add_dependency 'omniauth', '>= 1.9', '< 3' 31 | spec.add_dependency 'openid_connect', '~> 2.2' 32 | spec.add_development_dependency 'faker', '~> 2.0' 33 | spec.add_development_dependency 'guard', '~> 2.14' 34 | spec.add_development_dependency 'guard-bundler', '~> 2.2' 35 | spec.add_development_dependency 'guard-minitest', '~> 2.4' 36 | spec.add_development_dependency 'minitest', '~> 5.20' 37 | spec.add_development_dependency 'mocha', '~> 2.1' 38 | spec.add_development_dependency 'rake', '~> 12.0' 39 | spec.add_development_dependency 'rubocop', '~> 1.12' 40 | spec.add_development_dependency 'simplecov', '~> 0.21' 41 | spec.add_development_dependency 'simplecov-lcov', '~> 0.8' 42 | spec.add_development_dependency 'webmock', '~> 3.18' 43 | end 44 | -------------------------------------------------------------------------------- /test/fixtures/test.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDJDCCAgwCCQC57Ob2JfXb+DANBgkqhkiG9w0BAQUFADBUMQswCQYDVQQGEwJK 3 | UDEOMAwGA1UECBMFVG9reW8xITAfBgNVBAoTGEludGVybmV0IFdpZGdpdHMgUHR5 4 | IEx0ZDESMBAGA1UEAxMJbG9jYWxob3N0MB4XDTE0MDgwMTA4NTAxM1oXDTE1MDgw 5 | MTA4NTAxM1owVDELMAkGA1UEBhMCSlAxDjAMBgNVBAgTBVRva3lvMSEwHwYDVQQK 6 | ExhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQxEjAQBgNVBAMTCWxvY2FsaG9zdDCC 7 | ASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAN+7czSGHN2087T+oX2kBCY/ 8 | XN6UOS/mdU2Gn//omZlyxsQXIqvgBLNWeCVt4QdlFUbgPLggfXUelECV/RUOCIIi 9 | F2Th4t3x1LviN2XkUiva0DZBnOycqEaJdkyreEuGL1CLVZgZjKmSzNqLl0Yci3D0 10 | zgVsXFZSadQebietm4CCmfJYREt9NJxXcrLxVDgat/Xm/KJBsohs3f+cbBT8EXer 11 | 7+2oZjZoVUgw1hu0alaOvAfE4mxsVwjn3g2mjDqRJLbbuWqgDobjMHah+d4zwJvN 12 | ePK8E0hfaz/XBLsJ4e6bQA3M3bANEgSvsicup/qb/0th4gUdc/kj4aJGj0RP7oEC 13 | AwEAATANBgkqhkiG9w0BAQUFAAOCAQEADuVec/8u2qJiq6K2W/gSLGYCBZq64OrA 14 | s7L2+S82m9/3gAb62wGcDNZjIGFDQubXmO6RhHv7JUT5YZqv9/kRGTJcHDUrwwoN 15 | IE99CIPizp7VfnrZ6GsYeszSsw3m+mKTETm+6ELmaSDbYAsrCg4IpGwUF0L88ATv 16 | CJ8QzW4X7b9dYVc7UAYyCie2N65GXfesBbRlSwFLuVqIzZfMdNpNijTIUwUqGSME 17 | b8IjLYzvekP53CO4wEBRrAVIPNXgftorxIE30OLWua2Qw3y6Pn+Qp5fLe47025S7 18 | Lcec18/FbHG0Vbq0qO9cKQw80XyK31N6z556wr2GN2WyixkzVRddXA== 19 | -----END CERTIFICATE----- 20 | -------------------------------------------------------------------------------- /test/lib/omniauth/strategies/openid_connect_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative '../../../test_helper' 4 | 5 | module OmniAuth 6 | module Strategies 7 | class OpenIDConnectTest < StrategyTestCase # rubocop:disable Metrics/ClassLength 8 | def test_client_options_defaults 9 | assert_equal 'https', strategy.options.client_options.scheme 10 | assert_equal 443, strategy.options.client_options.port 11 | assert_equal '/authorize', strategy.options.client_options.authorization_endpoint 12 | assert_equal '/token', strategy.options.client_options.token_endpoint 13 | end 14 | 15 | def test_request_phase 16 | expected_redirect = %r{^https://example\.com/authorize\?client_id=1234&nonce=\w{32}&response_type=code&scope=openid&state=\w{32}$} 17 | strategy.options.issuer = 'example.com' 18 | strategy.options.client_options.host = 'example.com' 19 | strategy.expects(:redirect).with(regexp_matches(expected_redirect)) 20 | strategy.request_phase 21 | end 22 | 23 | def test_logout_phase_with_discovery 24 | expected_redirect = %r{^https://example\.com/logout$} 25 | strategy.options.client_options.host = 'example.com' 26 | strategy.options.discovery = true 27 | 28 | issuer = stub('OpenIDConnect::Discovery::Issuer') 29 | issuer.stubs(:issuer).returns('https://example.com/') 30 | ::OpenIDConnect::Discovery::Provider.stubs(:discover!).returns(issuer) 31 | 32 | config = stub('OpenIDConnect::Discovery::Provder::Config') 33 | config.stubs(:authorization_endpoint).returns('https://example.com/authorization') 34 | config.stubs(:token_endpoint).returns('https://example.com/token') 35 | config.stubs(:userinfo_endpoint).returns('https://example.com/userinfo') 36 | config.stubs(:jwks_uri).returns('https://example.com/jwks') 37 | config.stubs(:end_session_endpoint).returns('https://example.com/logout') 38 | ::OpenIDConnect::Discovery::Provider::Config.stubs(:discover!).with('https://example.com/').returns(config) 39 | 40 | request.stubs(:path_info).returns('/auth/openid_connect/logout') 41 | request.stubs(:path).returns('/auth/openid_connect/logout') 42 | 43 | strategy.expects(:redirect).with(regexp_matches(expected_redirect)) 44 | strategy.other_phase 45 | end 46 | 47 | def test_logout_phase_with_discovery_and_post_logout_redirect_uri 48 | expected_redirect = 'https://example.com/logout?post_logout_redirect_uri=https%3A%2F%2Fmysite.com' 49 | strategy.options.client_options.host = 'example.com' 50 | strategy.options.discovery = true 51 | strategy.options.post_logout_redirect_uri = 'https://mysite.com' 52 | 53 | issuer = stub('OpenIDConnect::Discovery::Issuer') 54 | issuer.stubs(:issuer).returns('https://example.com/') 55 | ::OpenIDConnect::Discovery::Provider.stubs(:discover!).returns(issuer) 56 | 57 | config = stub('OpenIDConnect::Discovery::Provder::Config') 58 | config.stubs(:authorization_endpoint).returns('https://example.com/authorization') 59 | config.stubs(:token_endpoint).returns('https://example.com/token') 60 | config.stubs(:userinfo_endpoint).returns('https://example.com/userinfo') 61 | config.stubs(:jwks_uri).returns('https://example.com/jwks') 62 | config.stubs(:end_session_endpoint).returns('https://example.com/logout') 63 | ::OpenIDConnect::Discovery::Provider::Config.stubs(:discover!).with('https://example.com/').returns(config) 64 | 65 | request.stubs(:path_info).returns('/auth/openid_connect/logout') 66 | request.stubs(:path).returns('/auth/openid_connect/logout') 67 | 68 | strategy.expects(:redirect).with(expected_redirect) 69 | strategy.other_phase 70 | end 71 | 72 | def test_logout_phase_with_logout_path 73 | strategy.options.issuer = 'example.com' 74 | strategy.options.client_options.host = 'example.com' 75 | strategy.options.logout_path = '/sign_out' 76 | 77 | request.stubs(:path).returns('/auth/openid_connect/sign_out') 78 | 79 | strategy.expects(:call_app!) 80 | strategy.other_phase 81 | end 82 | 83 | def test_logout_phase 84 | strategy.options.issuer = 'example.com' 85 | strategy.options.client_options.host = 'example.com' 86 | 87 | request.stubs(:path).returns('/auth/openid_connect/logout') 88 | 89 | strategy.expects(:call_app!) 90 | strategy.other_phase 91 | end 92 | 93 | def test_request_phase_with_params 94 | 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$} 95 | strategy.options.issuer = 'example.com' 96 | strategy.options.client_options.host = 'example.com' 97 | request.stubs(:params).returns('login_hint' => 'john.doe@example.com', 'ui_locales' => 'en', 'claims_locales' => 'es') 98 | 99 | strategy.expects(:redirect).with(regexp_matches(expected_redirect)) 100 | strategy.request_phase 101 | end 102 | 103 | def test_request_phase_with_discovery 104 | expected_redirect = %r{^https://example\.com/authorization\?client_id=1234&nonce=\w{32}&response_type=code&scope=openid&state=\w{32}$} 105 | strategy.options.client_options.host = 'example.com' 106 | strategy.options.discovery = true 107 | 108 | issuer = stub('OpenIDConnect::Discovery::Issuer') 109 | issuer.stubs(:issuer).returns('https://example.com/') 110 | ::OpenIDConnect::Discovery::Provider.stubs(:discover!).returns(issuer) 111 | 112 | config = stub('OpenIDConnect::Discovery::Provder::Config') 113 | config.stubs(:authorization_endpoint).returns('https://example.com/authorization') 114 | config.stubs(:token_endpoint).returns('https://example.com/token') 115 | config.stubs(:userinfo_endpoint).returns('https://example.com/userinfo') 116 | config.stubs(:jwks_uri).returns('https://example.com/jwks') 117 | ::OpenIDConnect::Discovery::Provider::Config.stubs(:discover!).with('https://example.com/').returns(config) 118 | 119 | strategy.expects(:redirect).with(regexp_matches(expected_redirect)) 120 | strategy.request_phase 121 | 122 | assert_equal strategy.options.issuer, 'https://example.com/' 123 | assert_equal strategy.options.client_options.authorization_endpoint, 'https://example.com/authorization' 124 | assert_equal strategy.options.client_options.token_endpoint, 'https://example.com/token' 125 | assert_equal strategy.options.client_options.userinfo_endpoint, 'https://example.com/userinfo' 126 | assert_equal strategy.options.client_options.jwks_uri, 'https://example.com/jwks' 127 | assert_nil strategy.options.client_options.end_session_endpoint 128 | end 129 | 130 | def test_request_phase_with_response_mode 131 | 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}$} 132 | strategy.options.issuer = 'example.com' 133 | strategy.options.response_mode = 'form_post' 134 | strategy.options.response_type = 'id_token' 135 | strategy.options.client_options.host = 'example.com' 136 | 137 | strategy.expects(:redirect).with(regexp_matches(expected_redirect)) 138 | strategy.request_phase 139 | end 140 | 141 | def test_request_phase_with_response_mode_symbol 142 | 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}$} 143 | strategy.options.issuer = 'example.com' 144 | strategy.options.response_mode = 'form_post' 145 | strategy.options.response_type = :id_token 146 | strategy.options.client_options.host = 'example.com' 147 | 148 | strategy.expects(:redirect).with(regexp_matches(expected_redirect)) 149 | strategy.request_phase 150 | end 151 | 152 | def test_option_acr_values 153 | strategy.options.client_options[:host] = 'foobar.com' 154 | 155 | refute_match(/acr_values=/, strategy.authorize_uri, 'URI must not contain acr_values') 156 | 157 | strategy.options.acr_values = 'urn:some:acr:values:value' 158 | assert_match(/acr_values=/, strategy.authorize_uri, 'URI must contain acr_values') 159 | end 160 | 161 | def test_option_custom_attributes 162 | strategy.options.client_options[:host] = 'foobar.com' 163 | strategy.options.extra_authorize_params = { resource: 'xyz' } 164 | 165 | assert(strategy.authorize_uri =~ /resource=xyz/, 'URI must contain custom params') 166 | end 167 | 168 | def test_request_phase_with_allowed_params 169 | strategy.options.issuer = 'example.com' 170 | strategy.options.allow_authorize_params = %i[name logo resource] 171 | strategy.options.extra_authorize_params = { resource: 'xyz' } 172 | strategy.options.client_options.host = 'example.com' 173 | request.stubs(:params).returns('name' => 'example', 'logo' => 'example_logo', 'resource' => 'abc', 174 | 'not_allowed' => 'filter_me') 175 | 176 | assert(strategy.authorize_uri =~ /resource=xyz/, 'URI must contain fixed param resource') 177 | assert(strategy.authorize_uri =~ /name=example/, 'URI must contain dynamic param name') 178 | assert(strategy.authorize_uri =~ /logo=example_logo/, 'URI must contain dynamic param logo') 179 | refute(strategy.authorize_uri =~ /not_allowed=filter_me/, 'URI must filter not allowed param') 180 | end 181 | 182 | def test_uid 183 | assert_equal user_info.sub, strategy.uid 184 | 185 | strategy.options.uid_field = 'preferred_username' 186 | assert_equal user_info.preferred_username, strategy.uid 187 | 188 | strategy.options.uid_field = 'something' 189 | assert_equal user_info.sub, strategy.uid 190 | end 191 | 192 | def test_callback_phase(_session = {}, _params = {}) # rubocop:disable Metrics/AbcSize 193 | code = SecureRandom.hex(16) 194 | state = SecureRandom.hex(16) 195 | request.stubs(:params).returns('code' => code, 'state' => state) 196 | request.stubs(:path).returns('') 197 | 198 | strategy.options.issuer = 'example.com' 199 | strategy.options.client_signing_alg = :RS256 200 | strategy.options.client_jwk_signing_key = jwks.to_s 201 | strategy.options.response_type = 'code' 202 | 203 | strategy.unstub(:user_info) 204 | access_token = stub('OpenIDConnect::AccessToken') 205 | access_token.stubs(:access_token) 206 | access_token.stubs(:refresh_token) 207 | access_token.stubs(:expires_in) 208 | access_token.stubs(:scope) 209 | access_token.stubs(:id_token).returns(jwt.to_s) 210 | client.expects(:access_token!).at_least_once.returns(access_token) 211 | access_token.expects(:userinfo!).returns(user_info) 212 | 213 | id_token = stub('OpenIDConnect::ResponseObject::IdToken') 214 | id_token.stubs(:raw_attributes).returns('sub' => 'sub', 'name' => 'name', 'email' => 'email') 215 | id_token.stubs(:verify!).with(issuer: strategy.options.issuer, client_id: @identifier, nonce: nonce).returns(true) 216 | id_token.expects(:verify!) 217 | 218 | strategy.expects(:decode_id_token).twice.with(access_token.id_token).returns(id_token) 219 | strategy.call!('rack.session' => { 'omniauth.state' => state, 'omniauth.nonce' => nonce }) 220 | strategy.callback_phase 221 | end 222 | 223 | def test_callback_phase_with_id_token 224 | state = SecureRandom.hex(16) 225 | request.stubs(:params).returns('id_token' => jwt.to_s, 'state' => state) 226 | request.stubs(:path).returns('') 227 | 228 | strategy.options.issuer = 'example.com' 229 | strategy.options.client_signing_alg = :RS256 230 | strategy.options.client_jwk_signing_key = jwks.to_json 231 | strategy.options.response_type = 'id_token' 232 | 233 | strategy.unstub(:user_info) 234 | access_token = stub('OpenIDConnect::AccessToken') 235 | access_token.stubs(:access_token) 236 | access_token.stubs(:refresh_token) 237 | access_token.stubs(:expires_in) 238 | access_token.stubs(:scope) 239 | access_token.stubs(:id_token).returns(jwt.to_s) 240 | 241 | id_token = stub('OpenIDConnect::ResponseObject::IdToken') 242 | id_token.stubs(:raw_attributes).returns('sub' => 'sub', 'name' => 'name', 'email' => 'email') 243 | id_token.stubs(:verify!).with(issuer: strategy.options.issuer, client_id: @identifier, nonce: nonce).returns(true) 244 | ::OpenIDConnect::ResponseObject::IdToken.stubs(:decode).returns(id_token) 245 | id_token.expects(:verify!) 246 | 247 | strategy.call!('rack.session' => { 'omniauth.state' => state, 'omniauth.nonce' => nonce }) 248 | strategy.callback_phase 249 | end 250 | 251 | def test_callback_phase_with_audience 252 | state = SecureRandom.hex(16) 253 | strategy.options.response_type = 'id_token' 254 | strategy.options.issuer = 'example.com' 255 | strategy.options.client_options.audience = 'my_audience' 256 | 257 | id_token = stub('OpenIDConnect::ResponseObject::IdToken') 258 | id_token.expects(:verify!).with(issuer: strategy.options.issuer, client_id: @identifier, audience: 'my_audience', 259 | nonce: nonce).returns(true) 260 | id_token.stubs(:raw_attributes, :to_h).returns(payload) 261 | 262 | request.stubs(:params).returns('state' => state, 'nounce' => nonce, 'id_token' => id_token) 263 | request.stubs(:path).returns('') 264 | 265 | strategy.stubs(:decode_id_token).returns(id_token) 266 | strategy.stubs(:stored_state).returns(state) 267 | 268 | strategy.call!('rack.session' => { 'omniauth.state' => state, 'omniauth.nonce' => nonce }) 269 | strategy.callback_phase 270 | end 271 | 272 | def test_callback_phase_with_id_token_and_param_provided_nonce # rubocop:disable Metrics/AbcSize 273 | code = SecureRandom.hex(16) 274 | state = SecureRandom.hex(16) 275 | nonce = SecureRandom.hex(16) 276 | request.stubs(:params).returns('code' => code, 'state' => state, 'nonce' => nonce) 277 | request.stubs(:path).returns('') 278 | 279 | strategy.options.issuer = 'example.com' 280 | strategy.options.client_signing_alg = :RS256 281 | strategy.options.client_jwk_signing_key = jwks.to_s 282 | strategy.options.response_type = 'code' 283 | 284 | strategy.unstub(:user_info) 285 | access_token = stub('OpenIDConnect::AccessToken') 286 | access_token.stubs(:access_token) 287 | access_token.stubs(:refresh_token) 288 | access_token.stubs(:expires_in) 289 | access_token.stubs(:scope) 290 | access_token.stubs(:id_token).returns(jwt.to_s) 291 | client.expects(:access_token!).at_least_once.returns(access_token) 292 | access_token.expects(:userinfo!).returns(user_info) 293 | 294 | id_token = stub('OpenIDConnect::ResponseObject::IdToken') 295 | id_token.stubs(:raw_attributes).returns('sub' => 'sub', 'name' => 'name', 'email' => 'email') 296 | id_token.stubs(:verify!).with(issuer: strategy.options.issuer, client_id: @identifier, nonce: nonce).returns(true) 297 | id_token.expects(:verify!) 298 | 299 | strategy.expects(:decode_id_token).twice.with(access_token.id_token).returns(id_token) 300 | strategy.call!('rack.session' => { 'omniauth.state' => state }) 301 | strategy.callback_phase 302 | end 303 | 304 | def test_callback_phase_with_id_token_no_kid 305 | other_rsa_private = OpenSSL::PKey::RSA.generate(2048) 306 | 307 | key = JSON::JWK.new(private_key) 308 | other_key = JSON::JWK.new(other_rsa_private) 309 | state = SecureRandom.hex(16) 310 | request.stubs(:params).returns('id_token' => jwt.to_s, 'state' => state) 311 | request.stubs(:path_info).returns('') 312 | 313 | strategy.options.issuer = issuer 314 | strategy.options.client_signing_alg = :RS256 315 | strategy.options.client_jwk_signing_key = { 'keys' => [other_key, key] }.to_json 316 | strategy.options.response_type = 'id_token' 317 | 318 | strategy.unstub(:user_info) 319 | strategy.call!('rack.session' => { 'omniauth.state' => state, 'omniauth.nonce' => nonce }) 320 | strategy.callback_phase 321 | end 322 | 323 | def test_callback_phase_with_id_token_with_kid 324 | other_rsa_private = OpenSSL::PKey::RSA.generate(2048) 325 | 326 | key = JSON::JWK.new(private_key) 327 | other_key = JSON::JWK.new(other_rsa_private) 328 | state = SecureRandom.hex(16) 329 | jwt_with_kid = JSON::JWT.new(payload).sign(key, :RS256) 330 | request.stubs(:params).returns('id_token' => jwt_with_kid.to_s, 'state' => state) 331 | request.stubs(:path_info).returns('') 332 | 333 | strategy.options.issuer = issuer 334 | strategy.options.client_signing_alg = :RS256 335 | strategy.options.client_jwk_signing_key = { 'keys' => [other_key, key] }.to_json 336 | strategy.options.response_type = 'id_token' 337 | 338 | strategy.unstub(:user_info) 339 | strategy.call!('rack.session' => { 'omniauth.state' => state, 'omniauth.nonce' => nonce }) 340 | strategy.callback_phase 341 | end 342 | 343 | def test_callback_phase_with_id_token_with_kid_and_no_matching_kid 344 | other_rsa_private = OpenSSL::PKey::RSA.generate(2048) 345 | 346 | key = JSON::JWK.new(private_key) 347 | other_key = JSON::JWK.new(other_rsa_private) 348 | state = SecureRandom.hex(16) 349 | jwt_with_kid = JSON::JWT.new(payload).sign(key, :RS256) 350 | request.stubs(:params).returns('id_token' => jwt_with_kid.to_s, 'state' => state) 351 | request.stubs(:path_info).returns('') 352 | 353 | strategy.options.issuer = issuer 354 | strategy.options.client_signing_alg = :RS256 355 | # We use private_key here instead of the wrapped key, which contains a kid 356 | strategy.options.client_jwk_signing_key = { 'keys' => [other_key, private_key] }.to_json 357 | strategy.options.response_type = 'id_token' 358 | 359 | strategy.unstub(:user_info) 360 | strategy.call!('rack.session' => { 'omniauth.state' => state, 'omniauth.nonce' => nonce }) 361 | 362 | assert_raises JSON::JWK::Set::KidNotFound do 363 | strategy.callback_phase 364 | end 365 | end 366 | 367 | def test_callback_phase_with_id_token_with_hs256 368 | state = SecureRandom.hex(16) 369 | request.stubs(:params).returns('id_token' => jwt_with_hs256.to_s, 'state' => state) 370 | request.stubs(:path_info).returns('') 371 | 372 | strategy.options.issuer = issuer 373 | strategy.options.client_options.secret = hmac_secret 374 | strategy.options.client_signing_alg = :HS256 375 | strategy.options.response_type = 'id_token' 376 | 377 | strategy.unstub(:user_info) 378 | strategy.call!('rack.session' => { 'omniauth.state' => state, 'omniauth.nonce' => nonce }) 379 | strategy.callback_phase 380 | end 381 | 382 | def test_callback_phase_with_hs256_base64_jwt_secret 383 | state = SecureRandom.hex(16) 384 | request.stubs(:params).returns('id_token' => jwt_with_hs256.to_s, 'state' => state) 385 | request.stubs(:path_info).returns('') 386 | 387 | strategy.options.issuer = issuer 388 | strategy.options.jwt_secret_base64 = Base64.encode64(hmac_secret) 389 | strategy.options.response_type = 'id_token' 390 | 391 | strategy.unstub(:user_info) 392 | strategy.call!('rack.session' => { 'omniauth.state' => state, 'omniauth.nonce' => nonce }) 393 | strategy.callback_phase 394 | end 395 | 396 | def test_callback_phase_with_mismatched_signing_algorithm 397 | state = SecureRandom.hex(16) 398 | request.stubs(:params).returns('id_token' => jwt_with_hs512.to_s, 'state' => state) 399 | request.stubs(:path_info).returns('') 400 | 401 | strategy.options.issuer = issuer 402 | strategy.options.client_options.secret = hmac_secret 403 | strategy.options.client_signing_alg = :HS256 404 | strategy.options.response_type = 'id_token' 405 | 406 | strategy.unstub(:user_info) 407 | strategy.call!('rack.session' => { 'omniauth.state' => state, 'omniauth.nonce' => nonce }) 408 | 409 | strategy.expects(:fail!).with(:invalid_jwt_algorithm, is_a(OmniAuth::Strategies::OpenIDConnect::CallbackError)) 410 | strategy.callback_phase 411 | end 412 | 413 | def test_callback_phase_with_id_token_no_matching_key 414 | rsa_private = OpenSSL::PKey::RSA.generate(2048) 415 | other_rsa_private = OpenSSL::PKey::RSA.generate(2048) 416 | 417 | other_key = JSON::JWK.new(other_rsa_private) 418 | token = JSON::JWT.new(payload).sign(rsa_private, :RS256).to_s 419 | state = SecureRandom.hex(16) 420 | request.stubs(:params).returns('id_token' => token, 'state' => state) 421 | request.stubs(:path_info).returns('') 422 | 423 | strategy.options.issuer = issuer 424 | strategy.options.client_signing_alg = :RS256 425 | strategy.options.client_jwk_signing_key = { 'keys' => [other_key] }.to_json 426 | strategy.options.response_type = 'id_token' 427 | 428 | strategy.unstub(:user_info) 429 | strategy.call!('rack.session' => { 'omniauth.state' => state, 'omniauth.nonce' => nonce }) 430 | 431 | assert_raises JSON::JWK::Set::KidNotFound do 432 | strategy.callback_phase 433 | end 434 | end 435 | 436 | def test_callback_phase_with_discovery # rubocop:disable Metrics/AbcSize 437 | state = SecureRandom.hex(16) 438 | 439 | request.stubs(:params).returns('code' => jwt.to_s, 'state' => state) 440 | request.stubs(:path).returns('') 441 | 442 | strategy.options.client_options.host = 'example.com' 443 | strategy.options.discovery = true 444 | 445 | issuer = stub('OpenIDConnect::Discovery::Issuer') 446 | issuer.stubs(:issuer).returns('https://example.com/') 447 | ::OpenIDConnect::Discovery::Provider.stubs(:discover!).returns(issuer) 448 | 449 | config = stub('OpenIDConnect::Discovery::Provder::Config') 450 | config.stubs(:authorization_endpoint).returns('https://example.com/authorization') 451 | config.stubs(:token_endpoint).returns('https://example.com/token') 452 | config.stubs(:userinfo_endpoint).returns('https://example.com/userinfo') 453 | config.stubs(:jwks_uri).returns('https://example.com/jwks') 454 | config.stubs(:jwks).returns(JSON::JWK::Set.new(jwks['keys'])) 455 | 456 | ::OpenIDConnect::Discovery::Provider::Config.stubs(:discover!).with('https://example.com/').returns(config) 457 | 458 | id_token = stub('OpenIDConnect::ResponseObject::IdToken') 459 | id_token.stubs(:raw_attributes).returns('sub' => 'sub', 'name' => 'name', 'email' => 'email') 460 | id_token.stubs(:verify!).with(issuer: 'https://example.com/', client_id: @identifier, nonce: nonce).returns(true) 461 | ::OpenIDConnect::ResponseObject::IdToken.stubs(:decode).returns(id_token) 462 | 463 | strategy.unstub(:user_info) 464 | access_token = stub('OpenIDConnect::AccessToken') 465 | access_token.stubs(:access_token) 466 | access_token.stubs(:refresh_token) 467 | access_token.stubs(:expires_in) 468 | access_token.stubs(:scope) 469 | access_token.stubs(:id_token).returns(jwt.to_s) 470 | client.expects(:access_token!).at_least_once.returns(access_token) 471 | access_token.expects(:userinfo!).returns(user_info) 472 | 473 | strategy.call!('rack.session' => { 'omniauth.state' => state, 'omniauth.nonce' => nonce }) 474 | strategy.callback_phase 475 | end 476 | 477 | def test_callback_phase_with_send_state_disabled # rubocop:disable Metrics/AbcSize 478 | code = SecureRandom.hex(16) 479 | 480 | strategy.options.client_options.host = 'example.com' 481 | strategy.options.require_state = true 482 | strategy.options.send_state = false 483 | strategy.options.discovery = true 484 | refute_match(/state/, strategy.authorize_uri, 'URI must not contain state') 485 | 486 | request.stubs(:params).returns('code' => code) 487 | request.stubs(:path).returns('') 488 | 489 | issuer = stub('OpenIDConnect::Discovery::Issuer') 490 | issuer.stubs(:issuer).returns('https://example.com/') 491 | ::OpenIDConnect::Discovery::Provider.stubs(:discover!).returns(issuer) 492 | 493 | config = stub('OpenIDConnect::Discovery::Provder::Config') 494 | config.stubs(:authorization_endpoint).returns('https://example.com/authorization') 495 | config.stubs(:token_endpoint).returns('https://example.com/token') 496 | config.stubs(:userinfo_endpoint).returns('https://example.com/userinfo') 497 | config.stubs(:jwks_uri).returns('https://example.com/jwks') 498 | config.stubs(:jwks).returns(JSON::JWK::Set.new(jwks['keys'])) 499 | 500 | ::OpenIDConnect::Discovery::Provider::Config.stubs(:discover!).with('https://example.com/').returns(config) 501 | 502 | id_token = stub('OpenIDConnect::ResponseObject::IdToken') 503 | id_token.stubs(:raw_attributes).returns('sub' => 'sub', 'name' => 'name', 'email' => 'email') 504 | id_token.stubs(:verify!).with(issuer: 'https://example.com/', client_id: @identifier, nonce: nonce).returns(true) 505 | ::OpenIDConnect::ResponseObject::IdToken.stubs(:decode).returns(id_token) 506 | 507 | strategy.unstub(:user_info) 508 | access_token = stub('OpenIDConnect::AccessToken') 509 | access_token.stubs(:access_token) 510 | access_token.stubs(:refresh_token) 511 | access_token.stubs(:expires_in) 512 | access_token.stubs(:scope) 513 | access_token.stubs(:id_token).returns(jwt.to_s) 514 | client.expects(:access_token!).at_least_once.returns(access_token) 515 | access_token.expects(:userinfo!).returns(user_info) 516 | 517 | strategy.call!('rack.session' => { 'omniauth.nonce' => nonce }) 518 | strategy.callback_phase 519 | end 520 | 521 | def test_callback_phase_with_no_state_without_state_verification # rubocop:disable Metrics/AbcSize 522 | code = SecureRandom.hex(16) 523 | 524 | strategy.options.require_state = false 525 | 526 | request.stubs(:params).returns('code' => code) 527 | request.stubs(:path).returns('') 528 | 529 | strategy.options.client_options.host = 'example.com' 530 | strategy.options.discovery = true 531 | 532 | issuer = stub('OpenIDConnect::Discovery::Issuer') 533 | issuer.stubs(:issuer).returns('https://example.com/') 534 | ::OpenIDConnect::Discovery::Provider.stubs(:discover!).returns(issuer) 535 | 536 | config = stub('OpenIDConnect::Discovery::Provder::Config') 537 | config.stubs(:authorization_endpoint).returns('https://example.com/authorization') 538 | config.stubs(:token_endpoint).returns('https://example.com/token') 539 | config.stubs(:userinfo_endpoint).returns('https://example.com/userinfo') 540 | config.stubs(:jwks_uri).returns('https://example.com/jwks') 541 | config.stubs(:jwks).returns(JSON::JWK::Set.new(jwks['keys'])) 542 | 543 | ::OpenIDConnect::Discovery::Provider::Config.stubs(:discover!).with('https://example.com/').returns(config) 544 | 545 | id_token = stub('OpenIDConnect::ResponseObject::IdToken') 546 | id_token.stubs(:raw_attributes).returns('sub' => 'sub', 'name' => 'name', 'email' => 'email') 547 | id_token.stubs(:verify!).with(issuer: 'https://example.com/', client_id: @identifier, nonce: nonce).returns(true) 548 | ::OpenIDConnect::ResponseObject::IdToken.stubs(:decode).returns(id_token) 549 | 550 | strategy.unstub(:user_info) 551 | access_token = stub('OpenIDConnect::AccessToken') 552 | access_token.stubs(:access_token) 553 | access_token.stubs(:refresh_token) 554 | access_token.stubs(:expires_in) 555 | access_token.stubs(:scope) 556 | access_token.stubs(:id_token).returns(jwt.to_s) 557 | client.expects(:access_token!).at_least_once.returns(access_token) 558 | access_token.expects(:userinfo!).returns(user_info) 559 | 560 | strategy.call!('rack.session' => { 'omniauth.nonce' => nonce }) 561 | strategy.callback_phase 562 | end 563 | 564 | def test_callback_phase_with_invalid_state_without_state_verification 565 | code = SecureRandom.hex(16) 566 | state = SecureRandom.hex(16) 567 | 568 | strategy.options.require_state = false 569 | 570 | request.stubs(:params).returns('code' => code, 'state' => 'foobar') 571 | request.stubs(:path).returns('') 572 | 573 | strategy.call!('rack.session' => { 'omniauth.state' => state, 'omniauth.nonce' => nonce }) 574 | strategy.expects(:fail!) 575 | strategy.callback_phase 576 | end 577 | 578 | def test_callback_phase_with_jwks_uri 579 | id_token = jwt.to_s 580 | state = SecureRandom.hex(16) 581 | request.stubs(:params).returns('id_token' => id_token, 'state' => state) 582 | request.stubs(:path_info).returns('') 583 | 584 | strategy.options.issuer = 'example.com' 585 | strategy.options.client_options.jwks_uri = 'https://jwks.example.com' 586 | strategy.options.response_type = 'id_token' 587 | 588 | stub_request(:get, strategy.options.client_options.jwks_uri).to_return( 589 | body: jwks.to_json, 590 | headers: { 'Content-Type' => 'application/json' } 591 | ) 592 | 593 | strategy.unstub(:user_info) 594 | access_token = stub('OpenIDConnect::AccessToken') 595 | access_token.stubs(:access_token) 596 | access_token.stubs(:refresh_token) 597 | access_token.stubs(:expires_in) 598 | access_token.stubs(:scope) 599 | access_token.stubs(:id_token).returns(id_token) 600 | 601 | id_token = stub('OpenIDConnect::ResponseObject::IdToken') 602 | id_token.stubs(:raw_attributes).returns('sub' => 'sub', 'name' => 'name', 'email' => 'email') 603 | id_token.stubs(:verify!).with(issuer: strategy.options.issuer, client_id: @identifier, nonce: nonce).returns(true) 604 | ::OpenIDConnect::ResponseObject::IdToken.stubs(:decode).returns(id_token) 605 | id_token.expects(:verify!) 606 | 607 | strategy.call!('rack.session' => { 'omniauth.state' => state, 'omniauth.nonce' => nonce }) 608 | strategy.callback_phase 609 | end 610 | 611 | def test_callback_phase_with_error 612 | state = SecureRandom.hex(16) 613 | request.stubs(:params).returns('error' => 'invalid_request') 614 | request.stubs(:path).returns('') 615 | 616 | strategy.call!({ 'rack.session' => { 'omniauth.state' => state, 'omniauth.nonce' => nonce } }) 617 | strategy.expects(:fail!) 618 | strategy.callback_phase 619 | end 620 | 621 | def test_callback_phase_with_invalid_state 622 | code = SecureRandom.hex(16) 623 | state = SecureRandom.hex(16) 624 | request.stubs(:params).returns('code' => code, 'state' => 'foobar') 625 | request.stubs(:path).returns('') 626 | 627 | strategy.call!('rack.session' => { 'omniauth.state' => state, 'omniauth.nonce' => nonce }) 628 | strategy.expects(:fail!) 629 | strategy.callback_phase 630 | end 631 | 632 | def test_callback_phase_without_code 633 | state = SecureRandom.hex(16) 634 | request.stubs(:params).returns('state' => state) 635 | request.stubs(:path).returns('') 636 | 637 | strategy.call!('rack.session' => { 'omniauth.state' => state, 'omniauth.nonce' => nonce }) 638 | 639 | strategy.expects(:fail!).with(:missing_code, is_a(OmniAuth::OpenIDConnect::MissingCodeError)) 640 | strategy.callback_phase 641 | end 642 | 643 | def test_callback_phase_without_id_token 644 | state = SecureRandom.hex(16) 645 | request.stubs(:params).returns('state' => state) 646 | request.stubs(:path).returns('') 647 | strategy.options.response_type = 'id_token' 648 | 649 | strategy.call!('rack.session' => { 'omniauth.state' => state, 'omniauth.nonce' => nonce }) 650 | 651 | strategy.expects(:fail!).with(:missing_id_token, is_a(OmniAuth::OpenIDConnect::MissingIdTokenError)) 652 | strategy.callback_phase 653 | end 654 | 655 | def test_callback_phase_without_id_token_symbol 656 | state = SecureRandom.hex(16) 657 | request.stubs(:params).returns('state' => state) 658 | request.stubs(:path).returns('') 659 | strategy.options.response_type = :id_token 660 | 661 | strategy.call!('rack.session' => { 'omniauth.state' => state, 'omniauth.nonce' => nonce }) 662 | 663 | strategy.expects(:fail!).with(:missing_id_token, is_a(OmniAuth::OpenIDConnect::MissingIdTokenError)) 664 | strategy.callback_phase 665 | end 666 | 667 | def test_callback_phase_with_timeout 668 | code = SecureRandom.hex(16) 669 | state = SecureRandom.hex(16) 670 | request.stubs(:params).returns('code' => code, 'state' => state) 671 | request.stubs(:path).returns('') 672 | 673 | strategy.options.issuer = 'example.com' 674 | 675 | strategy.stubs(:access_token).raises(::Timeout::Error.new('error')) 676 | strategy.call!('rack.session' => { 'omniauth.state' => state, 'omniauth.nonce' => nonce }) 677 | strategy.expects(:fail!) 678 | 679 | id_token = stub('OpenIDConnect::ResponseObject::IdToken') 680 | id_token.stubs(:verify!).with(issuer: 'example.com', client_id: @identifier, nonce: nonce).returns(true) 681 | ::OpenIDConnect::ResponseObject::IdToken.stubs(:decode).returns(id_token) 682 | 683 | strategy.callback_phase 684 | end 685 | 686 | def test_callback_phase_with_etimeout 687 | code = SecureRandom.hex(16) 688 | state = SecureRandom.hex(16) 689 | request.stubs(:params).returns('code' => code, 'state' => state) 690 | request.stubs(:path).returns('') 691 | 692 | strategy.options.issuer = 'example.com' 693 | 694 | strategy.stubs(:access_token).raises(::Errno::ETIMEDOUT.new('error')) 695 | strategy.call!('rack.session' => { 'omniauth.state' => state, 'omniauth.nonce' => nonce }) 696 | strategy.expects(:fail!) 697 | 698 | id_token = stub('OpenIDConnect::ResponseObject::IdToken') 699 | id_token.stubs(:verify!).with(issuer: 'example.com', client_id: @identifier, nonce: nonce).returns(true) 700 | ::OpenIDConnect::ResponseObject::IdToken.stubs(:decode).returns(id_token) 701 | 702 | strategy.callback_phase 703 | end 704 | 705 | def test_callback_phase_with_socket_error 706 | code = SecureRandom.hex(16) 707 | state = SecureRandom.hex(16) 708 | request.stubs(:params).returns('code' => code, 'state' => state) 709 | request.stubs(:path).returns('') 710 | 711 | strategy.options.issuer = 'example.com' 712 | 713 | strategy.stubs(:access_token).raises(::SocketError.new('error')) 714 | strategy.call!('rack.session' => { 'omniauth.state' => state, 'omniauth.nonce' => nonce }) 715 | strategy.expects(:fail!) 716 | 717 | id_token = stub('OpenIDConnect::ResponseObject::IdToken') 718 | id_token.stubs(:verify!).with(issuer: 'example.com', client_id: @identifier, nonce: nonce).returns(true) 719 | ::OpenIDConnect::ResponseObject::IdToken.stubs(:decode).returns(id_token) 720 | 721 | strategy.callback_phase 722 | end 723 | 724 | def test_callback_phase_with_rack_oauth2_client_error 725 | code = SecureRandom.hex(16) 726 | state = SecureRandom.hex(16) 727 | request.stubs(:params).returns('code' => code, 'state' => state) 728 | request.stubs(:path).returns('') 729 | 730 | strategy.options.issuer = 'example.com' 731 | 732 | strategy.stubs(:access_token).raises(::Rack::OAuth2::Client::Error.new('error', error: 'Unknown')) 733 | strategy.call!('rack.session' => { 'omniauth.state' => state, 'omniauth.nonce' => nonce }) 734 | strategy.expects(:fail!) 735 | 736 | id_token = stub('OpenIDConnect::ResponseObject::IdToken') 737 | id_token.stubs(:verify!).with(issuer: 'example.com', client_id: @identifier, nonce: nonce).returns(true) 738 | ::OpenIDConnect::ResponseObject::IdToken.stubs(:decode).returns(id_token) 739 | 740 | strategy.callback_phase 741 | end 742 | 743 | def test_info 744 | info = strategy.info 745 | assert_equal user_info.name, info[:name] 746 | assert_equal user_info.email, info[:email] 747 | assert_equal user_info.email_verified, info[:email_verified] 748 | assert_equal user_info.preferred_username, info[:nickname] 749 | assert_equal user_info.given_name, info[:first_name] 750 | assert_equal user_info.family_name, info[:last_name] 751 | assert_equal user_info.gender, info[:gender] 752 | assert_equal user_info.picture, info[:image] 753 | assert_equal user_info.phone_number, info[:phone] 754 | assert_equal({ website: user_info.website }, info[:urls]) 755 | end 756 | 757 | def test_extra 758 | assert_equal({ raw_info: user_info.as_json }, strategy.extra) 759 | end 760 | 761 | def test_credentials 762 | strategy.options.issuer = 'example.com' 763 | strategy.options.client_signing_alg = :RS256 764 | strategy.options.client_jwk_signing_key = jwks.to_json 765 | 766 | id_token = stub('OpenIDConnect::ResponseObject::IdToken') 767 | id_token.stubs(:verify!).returns(true) 768 | ::OpenIDConnect::ResponseObject::IdToken.stubs(:decode).returns(id_token) 769 | 770 | access_token = stub('OpenIDConnect::AccessToken') 771 | access_token.stubs(:access_token).returns(SecureRandom.hex(16)) 772 | access_token.stubs(:refresh_token).returns(SecureRandom.hex(16)) 773 | access_token.stubs(:expires_in).returns(Time.now) 774 | access_token.stubs(:scope).returns('openidconnect') 775 | access_token.stubs(:id_token).returns(jwt.to_s) 776 | 777 | client.expects(:access_token!).returns(access_token) 778 | access_token.expects(:refresh_token).returns(access_token.refresh_token) 779 | access_token.expects(:expires_in).returns(access_token.expires_in) 780 | 781 | assert_equal( 782 | { 783 | id_token: access_token.id_token, 784 | token: access_token.access_token, 785 | refresh_token: access_token.refresh_token, 786 | expires_in: access_token.expires_in, 787 | scope: access_token.scope, 788 | }, 789 | strategy.credentials 790 | ) 791 | end 792 | 793 | def test_option_send_nonce 794 | strategy.options.client_options[:host] = 'foobar.com' 795 | assert_match(/nonce/, strategy.authorize_uri, 'URI must contain nonce') 796 | 797 | strategy.options.send_nonce = false 798 | refute_match(/nonce/, strategy.authorize_uri, 'URI must not contain nonce') 799 | end 800 | 801 | def test_failure_endpoint_redirect 802 | OmniAuth.config.stubs(:failure_raise_out_environments).returns([]) 803 | strategy.stubs(:env).returns({}) 804 | request.stubs(:params).returns('error' => 'access denied') 805 | 806 | result = strategy.callback_phase 807 | 808 | assert(result.is_a?(Array)) 809 | assert(result[0] == 302, 'Redirect') 810 | assert(result[1]['Location'] =~ %r{/auth/failure}) 811 | end 812 | 813 | def test_state 814 | strategy.options.state = -> { 42 } 815 | 816 | expected_redirect = /&state=42/ 817 | strategy.options.issuer = 'example.com' 818 | strategy.options.client_options.host = 'example.com' 819 | strategy.expects(:redirect).with(regexp_matches(expected_redirect)) 820 | strategy.request_phase 821 | 822 | session = { 'state' => 42 } 823 | # this should succeed as the correct state is passed with the request 824 | test_callback_phase(session, { 'state' => 42 }) 825 | 826 | # the following should fail because the wrong state is passed to the callback 827 | code = SecureRandom.hex(16) 828 | request.stubs(:params).returns('code' => code, 'state' => 43) 829 | request.stubs(:path).returns('') 830 | 831 | strategy.call!('rack.session' => session) 832 | strategy.expects(:fail!) 833 | strategy.callback_phase 834 | end 835 | 836 | def test_dynamic_state 837 | # Stub request parameters 838 | request.stubs(:path).returns('') 839 | strategy.call!('rack.session' => {}, QUERY_STRING: { state: 'abc', client_id: '123' }) 840 | 841 | strategy.options.state = lambda { |env| 842 | # Get params from request, e.g. CGI.parse(env['QUERY_STRING']) 843 | env[:QUERY_STRING][:state] + env[:QUERY_STRING][:client_id] 844 | } 845 | 846 | expected_redirect = /&state=abc123/ 847 | strategy.options.issuer = 'example.com' 848 | strategy.options.client_options.host = 'example.com' 849 | strategy.expects(:redirect).with(regexp_matches(expected_redirect)) 850 | strategy.request_phase 851 | end 852 | 853 | def test_option_client_auth_method 854 | state = SecureRandom.hex(16) 855 | 856 | opts = strategy.options.client_options 857 | opts[:host] = 'foobar.com' 858 | strategy.options.issuer = 'foobar.com' 859 | strategy.options.client_auth_method = :not_basic 860 | strategy.options.client_signing_alg = :RS256 861 | strategy.options.client_jwk_signing_key = jwks.to_json 862 | 863 | json_response = { 864 | access_token: 'test_access_token', 865 | id_token: jwt.to_s, 866 | token_type: 'Bearer', 867 | } 868 | 869 | request.stubs(:path).returns('') 870 | strategy.call!('rack.session' => { 'omniauth.state' => state, 'omniauth.nonce' => nonce }) 871 | 872 | id_token = stub('OpenIDConnect::ResponseObject::IdToken') 873 | id_token.stubs(:verify!).with(issuer: strategy.options.issuer, client_id: @identifier, nonce: nonce).returns(true) 874 | ::OpenIDConnect::ResponseObject::IdToken.stubs(:decode).returns(id_token) 875 | 876 | url = "#{ opts.scheme }://#{ opts.host }:#{ opts.port }#{ opts.token_endpoint }" 877 | body = { scope: 'openid', grant_type: 'client_credentials', client_id: @identifier, client_secret: @secret } 878 | 879 | stub_request(:post, url).with(body: body).to_return( 880 | body: json_response.to_json, 881 | headers: { 'Content-Type' => 'application/json' } 882 | ) 883 | 884 | assert(strategy.send(:access_token)) 885 | end 886 | 887 | def test_public_key_with_jwks 888 | strategy.options.client_signing_alg = :RS256 889 | strategy.options.client_jwk_signing_key = jwks.to_json 890 | 891 | assert_equal JSON::JWK::Set, strategy.public_key.class 892 | end 893 | 894 | def test_public_key_with_jwk 895 | strategy.options.client_signing_alg = :RS256 896 | jwk = jwks[:keys].first 897 | strategy.options.client_jwk_signing_key = jwk.to_json 898 | 899 | assert_equal JSON::JWK, strategy.public_key.class 900 | end 901 | 902 | def test_public_key_with_x509 903 | strategy.options.client_signing_alg = :RS256 904 | strategy.options.client_x509_signing_key = File.read('./test/fixtures/test.crt') 905 | assert_equal OpenSSL::PKey::RSA, strategy.public_key.class 906 | end 907 | 908 | def test_public_key_with_hmac 909 | strategy.options.client_options.secret = 'secret' 910 | strategy.options.client_signing_alg = :HS256 911 | assert_equal strategy.options.client_options.secret, strategy.secret 912 | end 913 | 914 | def test_id_token_auth_hash 915 | state = SecureRandom.hex(16) 916 | strategy.options.response_type = 'id_token' 917 | strategy.options.issuer = 'example.com' 918 | 919 | id_token = stub('OpenIDConnect::ResponseObject::IdToken') 920 | id_token.stubs(:verify!).returns(true) 921 | id_token.stubs(:raw_attributes, :to_h).returns(payload) 922 | 923 | request.stubs(:params).returns('state' => state, 'nounce' => nonce, 'id_token' => id_token) 924 | request.stubs(:path).returns('') 925 | 926 | strategy.stubs(:decode_id_token).returns(id_token) 927 | strategy.stubs(:stored_state).returns(state) 928 | 929 | strategy.call!('rack.session' => { 'omniauth.state' => state, 'omniauth.nonce' => nonce }) 930 | strategy.callback_phase 931 | 932 | auth_hash = strategy.send(:env)['omniauth.auth'] 933 | assert auth_hash.key?('provider') 934 | assert auth_hash.key?('uid') 935 | assert auth_hash.key?('info') 936 | assert auth_hash.key?('extra') 937 | assert auth_hash['extra'].key?('raw_info') 938 | end 939 | 940 | def test_option_pkce 941 | strategy.options.client_options[:host] = 'example.com' 942 | 943 | # test pkce disabled 944 | strategy.options.pkce = false 945 | 946 | assert((strategy.authorize_uri !~ /code_challenge=/), 'URI must not contain code challenge param') 947 | assert((strategy.authorize_uri !~ /code_challenge_method=/), 'URI must not contain code challenge method param') 948 | 949 | # test pkce enabled with default opts 950 | strategy.options.pkce = true 951 | 952 | assert(strategy.authorize_uri =~ /code_challenge=/, 'URI must contain code challenge param') 953 | assert(strategy.authorize_uri =~ /code_challenge_method=/, 'URI must contain code challenge method param') 954 | 955 | # test pkce with custom verifier code 956 | strategy.options.pkce_verifier = proc { 'dummy_verifier' } 957 | code_challenge_value = Base64.urlsafe_encode64( 958 | Digest::SHA2.digest(strategy.options.pkce_verifier.call), 959 | padding: false 960 | ) 961 | 962 | assert(strategy.authorize_uri =~ /#{Regexp.quote(code_challenge_value)}/, 'URI must contain code challenge value') 963 | 964 | # test pkce with custom options and plain text code 965 | strategy.options.pkce_options = 966 | { 967 | code_challenge: proc { |verifier| verifier }, 968 | code_challenge_method: 'plain', 969 | } 970 | 971 | assert(strategy.authorize_uri =~ /#{Regexp.quote(strategy.options.pkce_verifier.call)}/, 972 | 'URI must contain code challenge value') 973 | end 974 | end 975 | end 976 | end 977 | -------------------------------------------------------------------------------- /test/strategy_test_case.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class StrategyTestCase < Minitest::Test 4 | class DummyApp 5 | def call(env); end 6 | end 7 | 8 | attr_accessor :identifier, :secret, :issuer, :nonce 9 | 10 | def setup 11 | @identifier = '1234' 12 | @secret = '1234asdgat3' 13 | @issuer = 'https://server.example.com' 14 | @nonce = SecureRandom.hex(16) 15 | end 16 | 17 | def client 18 | strategy.client 19 | end 20 | 21 | def payload 22 | { 23 | "iss": issuer, 24 | "aud": identifier, 25 | "sub": '248289761001', 26 | "nonce": nonce, 27 | "exp": Time.now.to_i + 1000, 28 | "iat": Time.now.to_i, 29 | } 30 | end 31 | 32 | def private_key 33 | @private_key ||= OpenSSL::PKey::RSA.generate(512) 34 | end 35 | 36 | def jwt 37 | @jwt ||= JSON::JWT.new(payload).sign(private_key, :RS256) 38 | end 39 | 40 | def hmac_secret 41 | @hmac_secret ||= SecureRandom.hex(16) 42 | end 43 | 44 | def jwt_with_hs256 45 | @jwt_with_hs256 ||= JSON::JWT.new(payload).sign(hmac_secret, :HS256) 46 | end 47 | 48 | def jwt_with_hs512 49 | @jwt_with_hs512 ||= JSON::JWT.new(payload).sign(hmac_secret, :HS512) 50 | end 51 | 52 | def jwks 53 | @jwks ||= begin 54 | key = JSON::JWK.new(private_key) 55 | keyset = JSON::JWK::Set.new(key) 56 | { keys: keyset } 57 | end 58 | end 59 | 60 | def user_info 61 | @user_info ||= OpenIDConnect::ResponseObject::UserInfo.new( 62 | sub: SecureRandom.hex(16), 63 | name: Faker::Name.name, 64 | email: Faker::Internet.email, 65 | email_verified: Faker::Boolean.boolean, 66 | nickname: Faker::Name.first_name, 67 | preferred_username: Faker::Internet.user_name, 68 | given_name: Faker::Name.first_name, 69 | family_name: Faker::Name.last_name, 70 | gender: 'female', 71 | picture: "#{Faker::Internet.url}.png", 72 | phone_number: Faker::PhoneNumber.phone_number, 73 | website: Faker::Internet.url 74 | ) 75 | end 76 | 77 | def request 78 | @request ||= stub('Request').tap do |request| 79 | request.stubs(:params).returns({}) 80 | request.stubs(:cookies).returns({}) 81 | request.stubs(:env).returns({}) 82 | request.stubs(:scheme).returns({}) 83 | request.stubs(:ssl?).returns(false) 84 | request.stubs(:path).returns('') 85 | end 86 | end 87 | 88 | def strategy 89 | @strategy ||= OmniAuth::Strategies::OpenIDConnect.new(DummyApp.new).tap do |strategy| 90 | strategy.options.client_options.identifier = @identifier 91 | strategy.options.client_options.secret = @secret 92 | strategy.stubs(:request).returns(request) 93 | strategy.stubs(:user_info).returns(user_info) 94 | strategy.stubs(:script_name).returns('') 95 | end 96 | end 97 | end 98 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'simplecov' 4 | require 'minitest/autorun' 5 | require 'mocha/minitest' 6 | require 'faker' 7 | require 'active_support' 8 | require 'webmock/minitest' 9 | 10 | SimpleCov.start do 11 | if ENV['CI'] 12 | require 'simplecov-lcov' 13 | 14 | SimpleCov::Formatter::LcovFormatter.config do |c| 15 | c.report_with_single_file = true 16 | c.single_report_path = 'coverage/lcov.info' 17 | end 18 | 19 | formatter SimpleCov::Formatter::LcovFormatter 20 | end 21 | end 22 | 23 | lib = File.expand_path('../lib', __dir__) 24 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 25 | require 'omniauth_openid_connect' 26 | require_relative 'strategy_test_case' 27 | OmniAuth.config.test_mode = true 28 | --------------------------------------------------------------------------------