omniauth_openid_connect 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7b5060fd9a0536e2740b340c56459f76b628912dea1ad54da415c2453921e6e3
4
- data.tar.gz: ada9bd68c69837538cd485ce0595525b2109feb0f79611a3ceb06fa1bf7eed96
3
+ metadata.gz: 34218d7218e2aca766623a1252b641baa20d81330cfd38c67a733f9221d76aad
4
+ data.tar.gz: 54f874832122f3cc1444280d8820d222a4dff7ef4c3580eb899d4da1efb38051
5
5
  SHA512:
6
- metadata.gz: 24c424fb608e45beba966b6914accdfad6e5af277c9428c94d3aec4ab3f4c010bfa66117a42d6ad1f57de9ea6d6d3ff99eaa5a33bdcbc753d1b7db568f3e870b
7
- data.tar.gz: f2ebfcd68b35425cc656ecaa58151b962d0bc96a4023f1bfcf312e363e2e0878c298bc90d7fc70104220b71cdbc77346b3f64c4653222b8df1110a12bb249dd0
6
+ metadata.gz: e4465213a06e82fd61d9997d0ec1b9f993620dead092add6ba2f07b34aa08dca53bcb63d92a256839c7310478c89ecff9359778e5721cf1cc6dcc300e5d780f8
7
+ data.tar.gz: 8fdbda1f579f271f6273a6380a7a9cf581b4b1239b589a1e2cb98799b8731056c3ec222c519e445d95387749ab62b58839e4005906938e0e1f76b9d396e76565
@@ -9,12 +9,12 @@ on:
9
9
  types: [opened, synchronize, reopened]
10
10
 
11
11
  jobs:
12
- base:
12
+ test:
13
13
  runs-on: ubuntu-latest
14
14
  strategy:
15
15
  fail-fast: false
16
16
  matrix:
17
- ruby: ["2.5", "2.6", "2.7", "3.0"]
17
+ ruby: ["2.5", "2.6", "2.7", "3.0", "3.1"]
18
18
  name: Ruby ${{ matrix.ruby }}
19
19
 
20
20
  steps:
@@ -29,3 +29,35 @@ jobs:
29
29
 
30
30
  - name: Run tests
31
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@v2
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
data/.rubocop.yml CHANGED
@@ -58,7 +58,4 @@ Metrics/MethodLength:
58
58
 
59
59
  AllCops:
60
60
  Exclude:
61
- - bin/**/*
62
- - Rakefile
63
- - config/**/*
64
- - test/**/*
61
+ - vendor/bundle/**/*
data/CHANGELOG.md CHANGED
@@ -1,4 +1,12 @@
1
- # v0.4.0 (02.05.2021)
1
+ # v0.5.0 (26.12.2022)
2
+
3
+ - Support the "nonce" parameter forwarding without a session [#130](https://github.com/omniauth/omniauth_openid_connect/pull/130)
4
+ - Fetch key from JWKS URI if available [#133](https://github.com/omniauth/omniauth_openid_connect/pull/133)
5
+ - Make the state parameter verification optional [#122](https://github.com/omniauth/omniauth_openid_connect/pull/122)
6
+ - Add email_verified claim in user info [#131](https://github.com/omniauth/omniauth_openid_connect/pull/131)
7
+ - Add PKCE verification support [#128](https://github.com/omniauth/omniauth_openid_connect/pull/128)
8
+
9
+ # v0.4.0 (06.02.2022)
2
10
 
3
11
  - Support dynamic parameters to the authorize URI [#90](https://github.com/omniauth/omniauth_openid_connect/pull/90)
4
12
  - Upgrade Faker and replace Travis with Github Actions [#102](https://github.com/omniauth/omniauth_openid_connect/pull/102)
data/Gemfile CHANGED
@@ -2,3 +2,9 @@
2
2
 
3
3
  source 'https://rubygems.org'
4
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
data/README.md CHANGED
@@ -1,12 +1,3 @@
1
- # Maintainers Wanted
2
-
3
- This project is looking for maintainers.
4
-
5
- Due to lack of using this gem in my projects I have no time to keep it alive.
6
- Feel free to open an issue if you interested.
7
-
8
- _This project is built and maintained 100% voluntarily._
9
-
10
1
  # OmniAuth::OpenIDConnect
11
2
 
12
3
  Originally was [omniauth-openid-connect](https://github.com/jjbohn/omniauth-openid-connect)
@@ -14,6 +5,7 @@ Originally was [omniauth-openid-connect](https://github.com/jjbohn/omniauth-open
14
5
  I've forked this repository and launch as separate gem because maintaining of original was dropped.
15
6
 
16
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)
17
9
 
18
10
  ## Installation
19
11
 
@@ -31,7 +23,7 @@ Or install it yourself as:
31
23
 
32
24
  ## Supported Ruby Versions
33
25
 
34
- OmniAuth::OpenIDConnect is tested under 2.4, 2.5, 2.6, 2.7
26
+ OmniAuth::OpenIDConnect is tested under 2.5, 2.6, 2.7, 3.0, 3.1
35
27
 
36
28
  ## Usage
37
29
 
@@ -55,24 +47,28 @@ config.omniauth :openid_connect, {
55
47
 
56
48
  ### Options Overview
57
49
 
58
- | Field | Description | Required | Default | Example/Options |
59
- |------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------|----------|----------------------------|-----------------------------------------------------|
60
- | name | Arbitrary string to identify connection and identify it from other openid_connect providers | no | String: openid_connect | :my_idp |
61
- | issuer | Root url for the authorization server | yes | | https://myprovider.com |
62
- | 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 |
63
- | client_auth_method | Which authentication method to use to authenticate your app with the authorization server | no | Sym: basic | "basic", "jwks" |
64
- | scope | Which OpenID scopes to include (:openid is always required) | no | Array<sym> [:openid] | [:openid, :profile, :email] |
65
- | response_type | Which OAuth2 response type to use with the authorization request | no | String: code | one of: 'code', 'id_token' |
66
- | 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) } |
67
- | 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 |
68
- | display | An optional parameter to the authorization request to determine how the authorization and consent page | no | nil | one of: :page, :popup, :touch, :wap |
69
- | prompt | An optional parameter to the authrization request to determine what pages the user will be shown | no | nil | one of: :none, :login, :consent, :select_account |
70
- | send_scope_to_token_endpoint | Should the scope parameter be sent to the authorization token endpoint? | no | true | one of: true, false |
71
- | 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 |
72
- | uid_field | The field of the user info response to be used as a unique id | no | 'sub' | "sub", "preferred_username" |
73
- | extra_authorize_params | A hash of extra fixed parameters that will be merged to the authorization request | no | Hash | {"tenant" => "common"} |
74
- | allow_authorize_params | A list of allowed dynamic parameters that will be merged to the authorization request | no | Array | [:screen_name] |
75
- | client_options | A hash of client options detailed in its own section | yes | | |
50
+ | Field | Description | Required | Default | Example/Options |
51
+ |------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------|----------|-------------------------------|-----------------------------------------------------|
52
+ | name | Arbitrary string to identify connection and identify it from other openid_connect providers | no | String: openid_connect | :my_idp |
53
+ | issuer | Root url for the authorization server | yes | | https://myprovider.com |
54
+ | 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 |
55
+ | client_auth_method | Which authentication method to use to authenticate your app with the authorization server | no | Sym: basic | "basic", "jwks" |
56
+ | scope | Which OpenID scopes to include (:openid is always required) | no | Array<sym> [:openid] | [:openid, :profile, :email] |
57
+ | response_type | Which OAuth2 response type to use with the authorization request | no | String: code | one of: 'code', 'id_token' |
58
+ | 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) } |
59
+ | require_state | Should state param be verified - this is recommended, not required by the OIDC specification | no | true | false |
60
+ | 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 |
61
+ | display | An optional parameter to the authorization request to determine how the authorization and consent page | no | nil | one of: :page, :popup, :touch, :wap |
62
+ | prompt | An optional parameter to the authrization request to determine what pages the user will be shown | no | nil | one of: :none, :login, :consent, :select_account |
63
+ | send_scope_to_token_endpoint | Should the scope parameter be sent to the authorization token endpoint? | no | true | one of: true, false |
64
+ | 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 |
65
+ | uid_field | The field of the user info response to be used as a unique id | no | 'sub' | "sub", "preferred_username" |
66
+ | extra_authorize_params | A hash of extra fixed parameters that will be merged to the authorization request | no | Hash | {"tenant" => "common"} |
67
+ | allow_authorize_params | A list of allowed dynamic parameters that will be merged to the authorization request | no | Array | [:screen_name] |
68
+ | pkce | Enable [PKCE flow](https://oauth.net/2/pkce/) | no | false | one of: true, false |
69
+ | pkce_verifier | Specify a custom PKCE verifier code. | no | A random 128-char string | Proc.new { SecureRandom.hex(64) } |
70
+ | 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 |
71
+ | client_options | A hash of client options detailed in its own section | yes | | |
76
72
 
77
73
  ### Client Config Options
78
74
 
@@ -102,7 +98,7 @@ These are the configuration options for the client_options hash of the configura
102
98
 
103
99
  * `response_type` tells the authorization server which grant type the application wants to use,
104
100
  currently, only `:code` (Authorization Code grant) and `:id_token` (Implicit grant) are valid.
105
- * If you want to pass `state` paramete by yourself. You can set Proc Object.
101
+ * If you want to pass `state` parameter by yourself. You can set Proc Object.
106
102
  e.g. `state: Proc.new { SecureRandom.hex(32) }`
107
103
  * `nonce` is optional. If don't want to pass "nonce" parameter to provider, You should specify
108
104
  `false` to `send_nonce` option. (default true)
@@ -124,6 +120,11 @@ These are the configuration options for the client_options hash of the configura
124
120
  property can be used to add the attribute to the token request. Initial value is `true`, which means that the
125
121
  scope attribute is included by default.
126
122
 
123
+ ## Additional notes
124
+ * 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.
125
+ In such example, the session is empty, so you have to forward certain parameters received from the client.
126
+ Currently supported ones are `code_verifier` and `nonce` - simply provide them as the `/callback` request parameters.
127
+
127
128
  For the full low down on OpenID Connect, please check out
128
129
  [the spec](http://openid.net/specs/openid-connect-core-1_0.html).
129
130
 
data/Rakefile CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'bundler/gem_tasks'
2
4
  require 'rake/testtask'
3
5
 
@@ -2,6 +2,6 @@
2
2
 
3
3
  module OmniAuth
4
4
  module OpenIDConnect
5
- VERSION = '0.4.0'
5
+ VERSION = '0.5.0'
6
6
  end
7
7
  end
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'addressable/uri'
4
3
  require 'timeout'
5
4
  require 'net/http'
6
5
  require 'open-uri'
@@ -10,7 +9,7 @@ require 'forwardable'
10
9
 
11
10
  module OmniAuth
12
11
  module Strategies
13
- class OpenIDConnect
12
+ class OpenIDConnect # rubocop:disable Metrics/ClassLength
14
13
  include OmniAuth::Strategy
15
14
  extend Forwardable
16
15
 
@@ -41,6 +40,7 @@ module OmniAuth
41
40
  option :client_x509_signing_key
42
41
  option :scope, [:openid]
43
42
  option :response_type, 'code' # ['code', 'id_token']
43
+ option :require_state, true
44
44
  option :state
45
45
  option :response_mode # [:query, :fragment, :form_post, :web_message]
46
46
  option :display, nil # [:page, :popup, :touch, :wap]
@@ -57,6 +57,14 @@ module OmniAuth
57
57
  option :extra_authorize_params, {}
58
58
  option :allow_authorize_params, []
59
59
  option :uid_field, 'sub'
60
+ option :pkce, false
61
+ option :pkce_verifier, nil
62
+ option :pkce_options, {
63
+ code_challenge: proc { |verifier|
64
+ Base64.urlsafe_encode64(Digest::SHA2.digest(verifier), padding: false)
65
+ },
66
+ code_challenge_method: 'S256',
67
+ }
60
68
 
61
69
  def uid
62
70
  user_info.raw_attributes[options.uid_field.to_sym] || user_info.sub
@@ -66,6 +74,7 @@ module OmniAuth
66
74
  {
67
75
  name: user_info.name,
68
76
  email: user_info.email,
77
+ email_verified: user_info.email_verified,
69
78
  nickname: user_info.preferred_username,
70
79
  first_name: user_info.given_name,
71
80
  last_name: user_info.family_name,
@@ -107,7 +116,7 @@ module OmniAuth
107
116
  def callback_phase
108
117
  error = params['error_reason'] || params['error']
109
118
  error_description = params['error_description'] || params['error_reason']
110
- invalid_state = params['state'].to_s.empty? || params['state'] != stored_state
119
+ invalid_state = (options.require_state && params['state'].to_s.empty?) || params['state'] != stored_state
111
120
 
112
121
  raise CallbackError, error: params['error'], reason: error_description, uri: params['error_uri'] if error
113
122
  raise CallbackError, error: :csrf_detected, reason: "Invalid 'state' parameter" if invalid_state
@@ -178,17 +187,40 @@ module OmniAuth
178
187
  opts[key] = request.params[key.to_s] unless opts.key?(key)
179
188
  end
180
189
 
190
+ if options.pkce
191
+ verifier = options.pkce_verifier ? options.pkce_verifier.call : SecureRandom.hex(64)
192
+
193
+ opts.merge!(pkce_authorize_params(verifier))
194
+ session['omniauth.pkce.verifier'] = verifier
195
+ end
196
+
181
197
  client.authorization_uri(opts.reject { |_k, v| v.nil? })
182
198
  end
183
199
 
184
200
  def public_key
185
- return config.jwks if options.discovery
201
+ @public_key ||= if options.discovery
202
+ config.jwks
203
+ elsif key_or_secret
204
+ key_or_secret
205
+ elsif client_options.jwks_uri
206
+ fetch_key
207
+ end
208
+ end
186
209
 
187
- key_or_secret || config.jwks
210
+ def pkce_authorize_params(verifier)
211
+ # NOTE: see https://tools.ietf.org/html/rfc7636#appendix-A
212
+ {
213
+ code_challenge: options.pkce_options[:code_challenge].call(verifier),
214
+ code_challenge_method: options.pkce_options[:code_challenge_method],
215
+ }
188
216
  end
189
217
 
190
218
  private
191
219
 
220
+ def fetch_key
221
+ @fetch_key ||= parse_jwk_key(::OpenIDConnect.http_client.get_content(client_options.jwks_uri))
222
+ end
223
+
192
224
  def issuer
193
225
  resource = "#{ client_options.scheme }://#{ client_options.host }"
194
226
  resource = "#{ resource }:#{ client_options.port }" if client_options.port
@@ -220,11 +252,14 @@ module OmniAuth
220
252
  def access_token
221
253
  return @access_token if @access_token
222
254
 
223
- @access_token = client.access_token!(
255
+ token_request_params = {
224
256
  scope: (options.scope if options.send_scope_to_token_endpoint),
225
- client_auth_method: options.client_auth_method
226
- )
257
+ client_auth_method: options.client_auth_method,
258
+ }
259
+
260
+ token_request_params[:code_verifier] = params['code_verifier'] || session.delete('omniauth.pkce.verifier') if options.pkce
227
261
 
262
+ @access_token = client.access_token!(token_request_params)
228
263
  verify_id_token!(@access_token.id_token) if configured_response_type == 'code'
229
264
 
230
265
  @access_token
@@ -274,16 +309,17 @@ module OmniAuth
274
309
  end
275
310
 
276
311
  def key_or_secret
277
- case options.client_signing_alg
278
- when :HS256, :HS384, :HS512
279
- client_options.secret
280
- when :RS256, :RS384, :RS512
281
- if options.client_jwk_signing_key
282
- parse_jwk_key(options.client_jwk_signing_key)
283
- elsif options.client_x509_signing_key
284
- parse_x509_key(options.client_x509_signing_key)
312
+ @key_or_secret ||=
313
+ case options.client_signing_alg&.to_sym
314
+ when :HS256, :HS384, :HS512
315
+ client_options.secret
316
+ when :RS256, :RS384, :RS512
317
+ if options.client_jwk_signing_key
318
+ parse_jwk_key(options.client_jwk_signing_key)
319
+ elsif options.client_x509_signing_key
320
+ parse_x509_key(options.client_x509_signing_key)
321
+ end
285
322
  end
286
- end
287
323
  end
288
324
 
289
325
  def parse_x509_key(key)
@@ -353,7 +389,7 @@ module OmniAuth
353
389
 
354
390
  decode_id_token(id_token).verify!(issuer: options.issuer,
355
391
  client_id: client_options.identifier,
356
- nonce: stored_nonce)
392
+ nonce: params['nonce'].presence || stored_nonce)
357
393
  end
358
394
 
359
395
  class CallbackError < StandardError
@@ -27,10 +27,8 @@ Gem::Specification.new do |spec|
27
27
  'rubygems_mfa_required' => 'true',
28
28
  }
29
29
 
30
- spec.add_dependency 'addressable', '~> 2.5'
31
30
  spec.add_dependency 'omniauth', '>= 1.9', '< 3'
32
31
  spec.add_dependency 'openid_connect', '~> 1.1'
33
- spec.add_development_dependency 'coveralls', '~> 0.8'
34
32
  spec.add_development_dependency 'faker', '~> 2.0'
35
33
  spec.add_development_dependency 'guard', '~> 2.14'
36
34
  spec.add_development_dependency 'guard-bundler', '~> 2.2'
@@ -39,5 +37,6 @@ Gem::Specification.new do |spec|
39
37
  spec.add_development_dependency 'mocha', '~> 1.7'
40
38
  spec.add_development_dependency 'rake', '~> 12.0'
41
39
  spec.add_development_dependency 'rubocop', '~> 1.12'
42
- spec.add_development_dependency 'simplecov', '~> 0.12'
40
+ spec.add_development_dependency 'simplecov', '~> 0.21'
41
+ spec.add_development_dependency 'simplecov-lcov', '~> 0.8'
43
42
  end
@@ -1,8 +1,10 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative '../../../test_helper'
2
4
 
3
5
  module OmniAuth
4
6
  module Strategies
5
- class OpenIDConnectTest < StrategyTestCase
7
+ class OpenIDConnectTest < StrategyTestCase # rubocop:disable Metrics/ClassLength
6
8
  def test_client_options_defaults
7
9
  assert_equal 'https', strategy.options.client_options.scheme
8
10
  assert_equal 443, strategy.options.client_options.port
@@ -11,7 +13,7 @@ module OmniAuth
11
13
  end
12
14
 
13
15
  def test_request_phase
14
- expected_redirect = /^https:\/\/example\.com\/authorize\?client_id=1234&nonce=\w{32}&response_type=code&scope=openid&state=\w{32}$/
16
+ expected_redirect = %r{^https://example\.com/authorize\?client_id=1234&nonce=\w{32}&response_type=code&scope=openid&state=\w{32}$}
15
17
  strategy.options.issuer = 'example.com'
16
18
  strategy.options.client_options.host = 'example.com'
17
19
  strategy.expects(:redirect).with(regexp_matches(expected_redirect))
@@ -19,7 +21,7 @@ module OmniAuth
19
21
  end
20
22
 
21
23
  def test_logout_phase_with_discovery
22
- expected_redirect = %r{^https:\/\/example\.com\/logout$}
24
+ expected_redirect = %r{^https://example\.com/logout$}
23
25
  strategy.options.client_options.host = 'example.com'
24
26
  strategy.options.discovery = true
25
27
 
@@ -78,7 +80,7 @@ module OmniAuth
78
80
  end
79
81
 
80
82
  def test_request_phase_with_params
81
- expected_redirect = /^https:\/\/example\.com\/authorize\?claims_locales=es&client_id=1234&login_hint=john.doe%40example.com&nonce=\w{32}&response_type=code&scope=openid&state=\w{32}&ui_locales=en$/
83
+ 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$}
82
84
  strategy.options.issuer = 'example.com'
83
85
  strategy.options.client_options.host = 'example.com'
84
86
  request.stubs(:params).returns('login_hint' => 'john.doe@example.com', 'ui_locales' => 'en', 'claims_locales' => 'es')
@@ -88,7 +90,7 @@ module OmniAuth
88
90
  end
89
91
 
90
92
  def test_request_phase_with_discovery
91
- expected_redirect = /^https:\/\/example\.com\/authorization\?client_id=1234&nonce=\w{32}&response_type=code&scope=openid&state=\w{32}$/
93
+ expected_redirect = %r{^https://example\.com/authorization\?client_id=1234&nonce=\w{32}&response_type=code&scope=openid&state=\w{32}$}
92
94
  strategy.options.client_options.host = 'example.com'
93
95
  strategy.options.discovery = true
94
96
 
@@ -115,7 +117,7 @@ module OmniAuth
115
117
  end
116
118
 
117
119
  def test_request_phase_with_response_mode
118
- expected_redirect = /^https:\/\/example\.com\/authorize\?client_id=1234&nonce=\w{32}&response_mode=form_post&response_type=id_token&scope=openid&state=\w{32}$/
120
+ 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}$}
119
121
  strategy.options.issuer = 'example.com'
120
122
  strategy.options.response_mode = 'form_post'
121
123
  strategy.options.response_type = 'id_token'
@@ -126,7 +128,7 @@ module OmniAuth
126
128
  end
127
129
 
128
130
  def test_request_phase_with_response_mode_symbol
129
- expected_redirect = /^https:\/\/example\.com\/authorize\?client_id=1234&nonce=\w{32}&response_mode=form_post&response_type=id_token&scope=openid&state=\w{32}$/
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}$}
130
132
  strategy.options.issuer = 'example.com'
131
133
  strategy.options.response_mode = 'form_post'
132
134
  strategy.options.response_type = :id_token
@@ -139,25 +141,26 @@ module OmniAuth
139
141
  def test_option_acr_values
140
142
  strategy.options.client_options[:host] = 'foobar.com'
141
143
 
142
- assert(!(strategy.authorize_uri =~ /acr_values=/), 'URI must not contain acr_values')
144
+ refute_match(/acr_values=/, strategy.authorize_uri, 'URI must not contain acr_values')
143
145
 
144
146
  strategy.options.acr_values = 'urn:some:acr:values:value'
145
- assert(strategy.authorize_uri =~ /acr_values=/, 'URI must contain acr_values')
147
+ assert_match(/acr_values=/, strategy.authorize_uri, 'URI must contain acr_values')
146
148
  end
147
149
 
148
150
  def test_option_custom_attributes
149
151
  strategy.options.client_options[:host] = 'foobar.com'
150
- strategy.options.extra_authorize_params = {resource: 'xyz'}
152
+ strategy.options.extra_authorize_params = { resource: 'xyz' }
151
153
 
152
154
  assert(strategy.authorize_uri =~ /resource=xyz/, 'URI must contain custom params')
153
155
  end
154
156
 
155
157
  def test_request_phase_with_allowed_params
156
158
  strategy.options.issuer = 'example.com'
157
- strategy.options.allow_authorize_params = [:name, :logo, :resource]
158
- strategy.options.extra_authorize_params = {resource: 'xyz'}
159
+ strategy.options.allow_authorize_params = %i[name logo resource]
160
+ strategy.options.extra_authorize_params = { resource: 'xyz' }
159
161
  strategy.options.client_options.host = 'example.com'
160
- request.stubs(:params).returns('name' => 'example', 'logo' => 'example_logo', 'resource' => 'abc', 'not_allowed' => 'filter_me')
162
+ request.stubs(:params).returns('name' => 'example', 'logo' => 'example_logo', 'resource' => 'abc',
163
+ 'not_allowed' => 'filter_me')
161
164
 
162
165
  assert(strategy.authorize_uri =~ /resource=xyz/, 'URI must contain fixed param resource')
163
166
  assert(strategy.authorize_uri =~ /name=example/, 'URI must contain dynamic param name')
@@ -175,16 +178,15 @@ module OmniAuth
175
178
  assert_equal user_info.sub, strategy.uid
176
179
  end
177
180
 
178
- def test_callback_phase(session = {}, params = {})
181
+ def test_callback_phase(_session = {}, _params = {}) # rubocop:disable Metrics/AbcSize
179
182
  code = SecureRandom.hex(16)
180
183
  state = SecureRandom.hex(16)
181
- nonce = SecureRandom.hex(16)
182
184
  request.stubs(:params).returns('code' => code, 'state' => state)
183
185
  request.stubs(:path).returns('')
184
186
 
185
187
  strategy.options.issuer = 'example.com'
186
188
  strategy.options.client_signing_alg = :RS256
187
- strategy.options.client_jwk_signing_key = File.read('test/fixtures/jwks.json')
189
+ strategy.options.client_jwk_signing_key = jwks.to_s
188
190
  strategy.options.response_type = 'code'
189
191
 
190
192
  strategy.unstub(:user_info)
@@ -193,7 +195,7 @@ module OmniAuth
193
195
  access_token.stubs(:refresh_token)
194
196
  access_token.stubs(:expires_in)
195
197
  access_token.stubs(:scope)
196
- access_token.stubs(:id_token).returns(File.read('test/fixtures/id_token.txt'))
198
+ access_token.stubs(:id_token).returns(jwt.to_s)
197
199
  client.expects(:access_token!).at_least_once.returns(access_token)
198
200
  access_token.expects(:userinfo!).returns(user_info)
199
201
 
@@ -208,15 +210,13 @@ module OmniAuth
208
210
  end
209
211
 
210
212
  def test_callback_phase_with_id_token
211
- code = SecureRandom.hex(16)
212
213
  state = SecureRandom.hex(16)
213
- nonce = SecureRandom.hex(16)
214
- request.stubs(:params).returns('id_token' => code, 'state' => state)
214
+ request.stubs(:params).returns('id_token' => jwt.to_s, 'state' => state)
215
215
  request.stubs(:path).returns('')
216
216
 
217
217
  strategy.options.issuer = 'example.com'
218
218
  strategy.options.client_signing_alg = :RS256
219
- strategy.options.client_jwk_signing_key = File.read('test/fixtures/jwks.json')
219
+ strategy.options.client_jwk_signing_key = jwks.to_json
220
220
  strategy.options.response_type = 'id_token'
221
221
 
222
222
  strategy.unstub(:user_info)
@@ -225,7 +225,7 @@ module OmniAuth
225
225
  access_token.stubs(:refresh_token)
226
226
  access_token.stubs(:expires_in)
227
227
  access_token.stubs(:scope)
228
- access_token.stubs(:id_token).returns(File.read('test/fixtures/id_token.txt'))
228
+ access_token.stubs(:id_token).returns(jwt.to_s)
229
229
 
230
230
  id_token = stub('OpenIDConnect::ResponseObject::IdToken')
231
231
  id_token.stubs(:raw_attributes).returns('sub' => 'sub', 'name' => 'name', 'email' => 'email')
@@ -237,13 +237,85 @@ module OmniAuth
237
237
  strategy.callback_phase
238
238
  end
239
239
 
240
- def test_callback_phase_with_discovery
240
+ def test_callback_phase_with_id_token_and_param_provided_nonce # rubocop:disable Metrics/AbcSize
241
241
  code = SecureRandom.hex(16)
242
242
  state = SecureRandom.hex(16)
243
243
  nonce = SecureRandom.hex(16)
244
- jwks = JSON::JWK::Set.new(JSON.parse(File.read('test/fixtures/jwks.json'))['keys'])
244
+ request.stubs(:params).returns('code' => code, 'state' => state, 'nonce' => nonce)
245
+ request.stubs(:path).returns('')
245
246
 
246
- request.stubs(:params).returns('code' => code, 'state' => state)
247
+ strategy.options.issuer = 'example.com'
248
+ strategy.options.client_signing_alg = :RS256
249
+ strategy.options.client_jwk_signing_key = jwks.to_s
250
+ strategy.options.response_type = 'code'
251
+
252
+ strategy.unstub(:user_info)
253
+ access_token = stub('OpenIDConnect::AccessToken')
254
+ access_token.stubs(:access_token)
255
+ access_token.stubs(:refresh_token)
256
+ access_token.stubs(:expires_in)
257
+ access_token.stubs(:scope)
258
+ access_token.stubs(:id_token).returns(jwt.to_s)
259
+ client.expects(:access_token!).at_least_once.returns(access_token)
260
+ access_token.expects(:userinfo!).returns(user_info)
261
+
262
+ id_token = stub('OpenIDConnect::ResponseObject::IdToken')
263
+ id_token.stubs(:raw_attributes).returns('sub' => 'sub', 'name' => 'name', 'email' => 'email')
264
+ id_token.stubs(:verify!).with(issuer: strategy.options.issuer, client_id: @identifier, nonce: nonce).returns(true)
265
+ id_token.expects(:verify!)
266
+
267
+ strategy.expects(:decode_id_token).twice.with(access_token.id_token).returns(id_token)
268
+ strategy.call!('rack.session' => { 'omniauth.state' => state })
269
+ strategy.callback_phase
270
+ end
271
+
272
+ def test_callback_phase_with_discovery # rubocop:disable Metrics/AbcSize
273
+ state = SecureRandom.hex(16)
274
+
275
+ request.stubs(:params).returns('code' => jwt.to_s, 'state' => state)
276
+ request.stubs(:path).returns('')
277
+
278
+ strategy.options.client_options.host = 'example.com'
279
+ strategy.options.discovery = true
280
+
281
+ issuer = stub('OpenIDConnect::Discovery::Issuer')
282
+ issuer.stubs(:issuer).returns('https://example.com/')
283
+ ::OpenIDConnect::Discovery::Provider.stubs(:discover!).returns(issuer)
284
+
285
+ config = stub('OpenIDConnect::Discovery::Provder::Config')
286
+ config.stubs(:authorization_endpoint).returns('https://example.com/authorization')
287
+ config.stubs(:token_endpoint).returns('https://example.com/token')
288
+ config.stubs(:userinfo_endpoint).returns('https://example.com/userinfo')
289
+ config.stubs(:jwks_uri).returns('https://example.com/jwks')
290
+ config.stubs(:jwks).returns(JSON::JWK::Set.new(jwks['keys']))
291
+
292
+ ::OpenIDConnect::Discovery::Provider::Config.stubs(:discover!).with('https://example.com/').returns(config)
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: 'https://example.com/', client_id: @identifier, nonce: nonce).returns(true)
297
+ ::OpenIDConnect::ResponseObject::IdToken.stubs(:decode).returns(id_token)
298
+
299
+ strategy.unstub(:user_info)
300
+ access_token = stub('OpenIDConnect::AccessToken')
301
+ access_token.stubs(:access_token)
302
+ access_token.stubs(:refresh_token)
303
+ access_token.stubs(:expires_in)
304
+ access_token.stubs(:scope)
305
+ access_token.stubs(:id_token).returns(jwt.to_s)
306
+ client.expects(:access_token!).at_least_once.returns(access_token)
307
+ access_token.expects(:userinfo!).returns(user_info)
308
+
309
+ strategy.call!('rack.session' => { 'omniauth.state' => state, 'omniauth.nonce' => nonce })
310
+ strategy.callback_phase
311
+ end
312
+
313
+ def test_callback_phase_with_no_state_without_state_verification # rubocop:disable Metrics/AbcSize
314
+ code = SecureRandom.hex(16)
315
+
316
+ strategy.options.require_state = false
317
+
318
+ request.stubs(:params).returns('code' => code)
247
319
  request.stubs(:path).returns('')
248
320
 
249
321
  strategy.options.client_options.host = 'example.com'
@@ -258,7 +330,7 @@ module OmniAuth
258
330
  config.stubs(:token_endpoint).returns('https://example.com/token')
259
331
  config.stubs(:userinfo_endpoint).returns('https://example.com/userinfo')
260
332
  config.stubs(:jwks_uri).returns('https://example.com/jwks')
261
- config.stubs(:jwks).returns(jwks)
333
+ config.stubs(:jwks).returns(JSON::JWK::Set.new(jwks['keys']))
262
334
 
263
335
  ::OpenIDConnect::Discovery::Provider::Config.stubs(:discover!).with('https://example.com/').returns(config)
264
336
 
@@ -273,21 +345,67 @@ module OmniAuth
273
345
  access_token.stubs(:refresh_token)
274
346
  access_token.stubs(:expires_in)
275
347
  access_token.stubs(:scope)
276
- access_token.stubs(:id_token).returns(File.read('test/fixtures/id_token.txt'))
348
+ access_token.stubs(:id_token).returns(jwt.to_s)
277
349
  client.expects(:access_token!).at_least_once.returns(access_token)
278
350
  access_token.expects(:userinfo!).returns(user_info)
279
351
 
352
+ strategy.call!('rack.session' => { 'omniauth.nonce' => nonce })
353
+ strategy.callback_phase
354
+ end
355
+
356
+ def test_callback_phase_with_invalid_state_without_state_verification
357
+ code = SecureRandom.hex(16)
358
+ state = SecureRandom.hex(16)
359
+
360
+ strategy.options.require_state = false
361
+
362
+ request.stubs(:params).returns('code' => code, 'state' => 'foobar')
363
+ request.stubs(:path).returns('')
364
+
365
+ strategy.call!('rack.session' => { 'omniauth.state' => state, 'omniauth.nonce' => nonce })
366
+ strategy.expects(:fail!)
367
+ strategy.callback_phase
368
+ end
369
+
370
+ def test_callback_phase_with_jwks_uri
371
+ id_token = jwt.to_s
372
+ state = SecureRandom.hex(16)
373
+ request.stubs(:params).returns('id_token' => id_token, 'state' => state)
374
+ request.stubs(:path_info).returns('')
375
+
376
+ strategy.options.issuer = 'example.com'
377
+ strategy.options.client_options.jwks_uri = 'https://jwks.example.com'
378
+ strategy.options.response_type = 'id_token'
379
+
380
+ HTTPClient
381
+ .any_instance.stubs(:get_content)
382
+ .with(strategy.options.client_options.jwks_uri)
383
+ .returns(jwks.to_json)
384
+
385
+ strategy.unstub(:user_info)
386
+ access_token = stub('OpenIDConnect::AccessToken')
387
+ access_token.stubs(:access_token)
388
+ access_token.stubs(:refresh_token)
389
+ access_token.stubs(:expires_in)
390
+ access_token.stubs(:scope)
391
+ access_token.stubs(:id_token).returns(id_token)
392
+
393
+ id_token = stub('OpenIDConnect::ResponseObject::IdToken')
394
+ id_token.stubs(:raw_attributes).returns('sub' => 'sub', 'name' => 'name', 'email' => 'email')
395
+ id_token.stubs(:verify!).with(issuer: strategy.options.issuer, client_id: @identifier, nonce: nonce).returns(true)
396
+ ::OpenIDConnect::ResponseObject::IdToken.stubs(:decode).returns(id_token)
397
+ id_token.expects(:verify!)
398
+
280
399
  strategy.call!('rack.session' => { 'omniauth.state' => state, 'omniauth.nonce' => nonce })
281
400
  strategy.callback_phase
282
401
  end
283
402
 
284
403
  def test_callback_phase_with_error
285
404
  state = SecureRandom.hex(16)
286
- nonce = SecureRandom.hex(16)
287
405
  request.stubs(:params).returns('error' => 'invalid_request')
288
406
  request.stubs(:path).returns('')
289
407
 
290
- strategy.call!({'rack.session' => {'omniauth.state' => state, 'omniauth.nonce' => nonce}})
408
+ strategy.call!({ 'rack.session' => { 'omniauth.state' => state, 'omniauth.nonce' => nonce } })
291
409
  strategy.expects(:fail!)
292
410
  strategy.callback_phase
293
411
  end
@@ -295,7 +413,6 @@ module OmniAuth
295
413
  def test_callback_phase_with_invalid_state
296
414
  code = SecureRandom.hex(16)
297
415
  state = SecureRandom.hex(16)
298
- nonce = SecureRandom.hex(16)
299
416
  request.stubs(:params).returns('code' => code, 'state' => 'foobar')
300
417
  request.stubs(:path).returns('')
301
418
 
@@ -306,7 +423,6 @@ module OmniAuth
306
423
 
307
424
  def test_callback_phase_without_code
308
425
  state = SecureRandom.hex(16)
309
- nonce = SecureRandom.hex(16)
310
426
  request.stubs(:params).returns('state' => state)
311
427
  request.stubs(:path).returns('')
312
428
 
@@ -318,7 +434,6 @@ module OmniAuth
318
434
 
319
435
  def test_callback_phase_without_id_token
320
436
  state = SecureRandom.hex(16)
321
- nonce = SecureRandom.hex(16)
322
437
  request.stubs(:params).returns('state' => state)
323
438
  request.stubs(:path).returns('')
324
439
  strategy.options.response_type = 'id_token'
@@ -331,7 +446,6 @@ module OmniAuth
331
446
 
332
447
  def test_callback_phase_without_id_token_symbol
333
448
  state = SecureRandom.hex(16)
334
- nonce = SecureRandom.hex(16)
335
449
  request.stubs(:params).returns('state' => state)
336
450
  request.stubs(:path).returns('')
337
451
  strategy.options.response_type = :id_token
@@ -345,7 +459,6 @@ module OmniAuth
345
459
  def test_callback_phase_with_timeout
346
460
  code = SecureRandom.hex(16)
347
461
  state = SecureRandom.hex(16)
348
- nonce = SecureRandom.hex(16)
349
462
  request.stubs(:params).returns('code' => code, 'state' => state)
350
463
  request.stubs(:path).returns('')
351
464
 
@@ -365,7 +478,6 @@ module OmniAuth
365
478
  def test_callback_phase_with_etimeout
366
479
  code = SecureRandom.hex(16)
367
480
  state = SecureRandom.hex(16)
368
- nonce = SecureRandom.hex(16)
369
481
  request.stubs(:params).returns('code' => code, 'state' => state)
370
482
  request.stubs(:path).returns('')
371
483
 
@@ -385,7 +497,6 @@ module OmniAuth
385
497
  def test_callback_phase_with_socket_error
386
498
  code = SecureRandom.hex(16)
387
499
  state = SecureRandom.hex(16)
388
- nonce = SecureRandom.hex(16)
389
500
  request.stubs(:params).returns('code' => code, 'state' => state)
390
501
  request.stubs(:path).returns('')
391
502
 
@@ -405,7 +516,6 @@ module OmniAuth
405
516
  def test_callback_phase_with_rack_oauth2_client_error
406
517
  code = SecureRandom.hex(16)
407
518
  state = SecureRandom.hex(16)
408
- nonce = SecureRandom.hex(16)
409
519
  request.stubs(:params).returns('code' => code, 'state' => state)
410
520
  request.stubs(:path).returns('')
411
521
 
@@ -426,6 +536,7 @@ module OmniAuth
426
536
  info = strategy.info
427
537
  assert_equal user_info.name, info[:name]
428
538
  assert_equal user_info.email, info[:email]
539
+ assert_equal user_info.email_verified, info[:email_verified]
429
540
  assert_equal user_info.preferred_username, info[:nickname]
430
541
  assert_equal user_info.given_name, info[:first_name]
431
542
  assert_equal user_info.family_name, info[:last_name]
@@ -442,7 +553,7 @@ module OmniAuth
442
553
  def test_credentials
443
554
  strategy.options.issuer = 'example.com'
444
555
  strategy.options.client_signing_alg = :RS256
445
- strategy.options.client_jwk_signing_key = File.read('test/fixtures/jwks.json')
556
+ strategy.options.client_jwk_signing_key = jwks.to_json
446
557
 
447
558
  id_token = stub('OpenIDConnect::ResponseObject::IdToken')
448
559
  id_token.stubs(:verify!).returns(true)
@@ -453,7 +564,7 @@ module OmniAuth
453
564
  access_token.stubs(:refresh_token).returns(SecureRandom.hex(16))
454
565
  access_token.stubs(:expires_in).returns(Time.now)
455
566
  access_token.stubs(:scope).returns('openidconnect')
456
- access_token.stubs(:id_token).returns(File.read('test/fixtures/id_token.txt'))
567
+ access_token.stubs(:id_token).returns(jwt.to_s)
457
568
 
458
569
  client.expects(:access_token!).returns(access_token)
459
570
  access_token.expects(:refresh_token).returns(access_token.refresh_token)
@@ -465,7 +576,7 @@ module OmniAuth
465
576
  token: access_token.access_token,
466
577
  refresh_token: access_token.refresh_token,
467
578
  expires_in: access_token.expires_in,
468
- scope: access_token.scope
579
+ scope: access_token.scope,
469
580
  },
470
581
  strategy.credentials
471
582
  )
@@ -473,11 +584,10 @@ module OmniAuth
473
584
 
474
585
  def test_option_send_nonce
475
586
  strategy.options.client_options[:host] = 'foobar.com'
476
-
477
- assert(strategy.authorize_uri =~ /nonce=/, 'URI must contain nonce')
587
+ assert_match(/nonce/, strategy.authorize_uri, 'URI must contain nonce')
478
588
 
479
589
  strategy.options.send_nonce = false
480
- assert(!(strategy.authorize_uri =~ /nonce=/), 'URI must not contain nonce')
590
+ refute_match(/nonce/, strategy.authorize_uri, 'URI must not contain nonce')
481
591
  end
482
592
 
483
593
  def test_failure_endpoint_redirect
@@ -487,9 +597,9 @@ module OmniAuth
487
597
 
488
598
  result = strategy.callback_phase
489
599
 
490
- assert(result.is_a? Array)
600
+ assert(result.is_a?(Array))
491
601
  assert(result[0] == 302, 'Redirect')
492
- assert(result[1]["Location"] =~ /\/auth\/failure/)
602
+ assert(result[1]['Location'] =~ %r{/auth/failure})
493
603
  end
494
604
 
495
605
  def test_state
@@ -518,7 +628,7 @@ module OmniAuth
518
628
  def test_dynamic_state
519
629
  # Stub request parameters
520
630
  request.stubs(:path).returns('')
521
- strategy.call!('rack.session' => { }, QUERY_STRING: { state: 'abc', client_id: '123' } )
631
+ strategy.call!('rack.session' => {}, QUERY_STRING: { state: 'abc', client_id: '123' })
522
632
 
523
633
  strategy.options.state = lambda { |env|
524
634
  # Get params from request, e.g. CGI.parse(env['QUERY_STRING'])
@@ -534,18 +644,17 @@ module OmniAuth
534
644
 
535
645
  def test_option_client_auth_method
536
646
  state = SecureRandom.hex(16)
537
- nonce = SecureRandom.hex(16)
538
647
 
539
648
  opts = strategy.options.client_options
540
649
  opts[:host] = 'foobar.com'
541
650
  strategy.options.issuer = 'foobar.com'
542
651
  strategy.options.client_auth_method = :not_basic
543
652
  strategy.options.client_signing_alg = :RS256
544
- strategy.options.client_jwk_signing_key = File.read('test/fixtures/jwks.json')
653
+ strategy.options.client_jwk_signing_key = jwks.to_json
545
654
 
546
655
  json_response = {
547
656
  access_token: 'test_access_token',
548
- id_token: File.read('test/fixtures/id_token.txt'),
657
+ id_token: jwt.to_s,
549
658
  token_type: 'Bearer',
550
659
  }.to_json
551
660
  success = Struct.new(:status, :body).new(200, json_response)
@@ -563,21 +672,19 @@ module OmniAuth
563
672
  {}
564
673
  ).returns(success)
565
674
 
566
- assert(strategy.send :access_token)
675
+ assert(strategy.send(:access_token))
567
676
  end
568
677
 
569
678
  def test_public_key_with_jwks
570
679
  strategy.options.client_signing_alg = :RS256
571
- strategy.options.client_jwk_signing_key = File.read('./test/fixtures/jwks.json')
680
+ strategy.options.client_jwk_signing_key = jwks.to_json
572
681
 
573
682
  assert_equal JSON::JWK::Set, strategy.public_key.class
574
683
  end
575
684
 
576
685
  def test_public_key_with_jwk
577
686
  strategy.options.client_signing_alg = :RS256
578
- jwks_str = File.read('./test/fixtures/jwks.json')
579
- jwks = JSON.parse(jwks_str)
580
- jwk = jwks['keys'].first
687
+ jwk = jwks[:keys].first
581
688
  strategy.options.client_jwk_signing_key = jwk.to_json
582
689
 
583
690
  assert_equal JSON::JWK, strategy.public_key.class
@@ -597,22 +704,12 @@ module OmniAuth
597
704
 
598
705
  def test_id_token_auth_hash
599
706
  state = SecureRandom.hex(16)
600
- nonce = SecureRandom.hex(16)
601
707
  strategy.options.response_type = 'id_token'
602
708
  strategy.options.issuer = 'example.com'
603
709
 
604
710
  id_token = stub('OpenIDConnect::ResponseObject::IdToken')
605
711
  id_token.stubs(:verify!).returns(true)
606
- id_token.stubs(:raw_attributes, :to_h).returns(
607
- {
608
- "iss": "http://server.example.com",
609
- "sub": "248289761001",
610
- "aud": "s6BhdRkqt3",
611
- "nonce": "n-0S6_WzA2Mj",
612
- "exp": 1311281970,
613
- "iat": 1311280970,
614
- }
615
- )
712
+ id_token.stubs(:raw_attributes, :to_h).returns(payload)
616
713
 
617
714
  request.stubs(:params).returns('state' => state, 'nounce' => nonce, 'id_token' => id_token)
618
715
  request.stubs(:path).returns('')
@@ -630,6 +727,41 @@ module OmniAuth
630
727
  assert auth_hash.key?('extra')
631
728
  assert auth_hash['extra'].key?('raw_info')
632
729
  end
730
+
731
+ def test_option_pkce
732
+ strategy.options.client_options[:host] = 'example.com'
733
+
734
+ # test pkce disabled
735
+ strategy.options.pkce = false
736
+
737
+ assert((strategy.authorize_uri !~ /code_challenge=/), 'URI must not contain code challenge param')
738
+ assert((strategy.authorize_uri !~ /code_challenge_method=/), 'URI must not contain code challenge method param')
739
+
740
+ # test pkce enabled with default opts
741
+ strategy.options.pkce = true
742
+
743
+ assert(strategy.authorize_uri =~ /code_challenge=/, 'URI must contain code challenge param')
744
+ assert(strategy.authorize_uri =~ /code_challenge_method=/, 'URI must contain code challenge method param')
745
+
746
+ # test pkce with custom verifier code
747
+ strategy.options.pkce_verifier = proc { 'dummy_verifier' }
748
+ code_challenge_value = Base64.urlsafe_encode64(
749
+ Digest::SHA2.digest(strategy.options.pkce_verifier.call),
750
+ padding: false
751
+ )
752
+
753
+ assert(strategy.authorize_uri =~ /#{Regexp.quote(code_challenge_value)}/, 'URI must contain code challenge value')
754
+
755
+ # test pkce with custom options and plain text code
756
+ strategy.options.pkce_options =
757
+ {
758
+ code_challenge: proc { |verifier| verifier },
759
+ code_challenge_method: 'plain',
760
+ }
761
+
762
+ assert(strategy.authorize_uri =~ /#{Regexp.quote(strategy.options.pkce_verifier.call)}/,
763
+ 'URI must contain code challenge value')
764
+ end
633
765
  end
634
766
  end
635
767
  end
@@ -1,32 +1,64 @@
1
+ # frozen_string_literal: true
2
+
1
3
  class StrategyTestCase < MiniTest::Test
2
4
  class DummyApp
3
5
  def call(env); end
4
6
  end
5
7
 
6
- attr_accessor :identifier, :secret
8
+ attr_accessor :identifier, :secret, :issuer, :nonce
7
9
 
8
10
  def setup
9
11
  @identifier = '1234'
10
12
  @secret = '1234asdgat3'
13
+ @issuer = 'https://server.example.com'
14
+ @nonce = SecureRandom.hex(16)
11
15
  end
12
16
 
13
17
  def client
14
18
  strategy.client
15
19
  end
16
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 jwks
41
+ @jwks ||= begin
42
+ key = JSON::JWK.new(private_key)
43
+ keyset = JSON::JWK::Set.new(key)
44
+ { keys: keyset }
45
+ end
46
+ end
47
+
17
48
  def user_info
18
49
  @user_info ||= OpenIDConnect::ResponseObject::UserInfo.new(
19
50
  sub: SecureRandom.hex(16),
20
51
  name: Faker::Name.name,
21
52
  email: Faker::Internet.email,
53
+ email_verified: Faker::Boolean.boolean,
22
54
  nickname: Faker::Name.first_name,
23
55
  preferred_username: Faker::Internet.user_name,
24
56
  given_name: Faker::Name.first_name,
25
57
  family_name: Faker::Name.last_name,
26
58
  gender: 'female',
27
- picture: Faker::Internet.url + '.png',
59
+ picture: "#{Faker::Internet.url}.png",
28
60
  phone_number: Faker::PhoneNumber.phone_number,
29
- website: Faker::Internet.url,
61
+ website: Faker::Internet.url
30
62
  )
31
63
  end
32
64
 
data/test/test_helper.rb CHANGED
@@ -1,16 +1,26 @@
1
- lib = File.expand_path('../../lib', __FILE__)
2
- $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
1
+ # frozen_string_literal: true
3
2
 
4
3
  require 'simplecov'
5
- require 'coveralls'
6
4
  require 'minitest/autorun'
7
5
  require 'mocha/minitest'
8
6
  require 'faker'
9
7
  require 'active_support'
8
+
9
+ SimpleCov.start do
10
+ if ENV['CI']
11
+ require 'simplecov-lcov'
12
+
13
+ SimpleCov::Formatter::LcovFormatter.config do |c|
14
+ c.report_with_single_file = true
15
+ c.single_report_path = 'coverage/lcov.info'
16
+ end
17
+
18
+ formatter SimpleCov::Formatter::LcovFormatter
19
+ end
20
+ end
21
+
22
+ lib = File.expand_path('../lib', __dir__)
23
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
10
24
  require 'omniauth_openid_connect'
11
25
  require_relative 'strategy_test_case'
12
-
13
- SimpleCov.command_name 'test'
14
- SimpleCov.start
15
- Coveralls.wear!
16
26
  OmniAuth.config.test_mode = true
metadata CHANGED
@@ -1,30 +1,16 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: omniauth_openid_connect
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.0
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - John Bohn
8
8
  - Ilya Shcherbinin
9
- autorequire:
9
+ autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2022-02-06 00:00:00.000000000 Z
12
+ date: 2022-12-26 00:00:00.000000000 Z
13
13
  dependencies:
14
- - !ruby/object:Gem::Dependency
15
- name: addressable
16
- requirement: !ruby/object:Gem::Requirement
17
- requirements:
18
- - - "~>"
19
- - !ruby/object:Gem::Version
20
- version: '2.5'
21
- type: :runtime
22
- prerelease: false
23
- version_requirements: !ruby/object:Gem::Requirement
24
- requirements:
25
- - - "~>"
26
- - !ruby/object:Gem::Version
27
- version: '2.5'
28
14
  - !ruby/object:Gem::Dependency
29
15
  name: omniauth
30
16
  requirement: !ruby/object:Gem::Requirement
@@ -59,20 +45,6 @@ dependencies:
59
45
  - - "~>"
60
46
  - !ruby/object:Gem::Version
61
47
  version: '1.1'
62
- - !ruby/object:Gem::Dependency
63
- name: coveralls
64
- requirement: !ruby/object:Gem::Requirement
65
- requirements:
66
- - - "~>"
67
- - !ruby/object:Gem::Version
68
- version: '0.8'
69
- type: :development
70
- prerelease: false
71
- version_requirements: !ruby/object:Gem::Requirement
72
- requirements:
73
- - - "~>"
74
- - !ruby/object:Gem::Version
75
- version: '0.8'
76
48
  - !ruby/object:Gem::Dependency
77
49
  name: faker
78
50
  requirement: !ruby/object:Gem::Requirement
@@ -191,14 +163,28 @@ dependencies:
191
163
  requirements:
192
164
  - - "~>"
193
165
  - !ruby/object:Gem::Version
194
- version: '0.12'
166
+ version: '0.21'
167
+ type: :development
168
+ prerelease: false
169
+ version_requirements: !ruby/object:Gem::Requirement
170
+ requirements:
171
+ - - "~>"
172
+ - !ruby/object:Gem::Version
173
+ version: '0.21'
174
+ - !ruby/object:Gem::Dependency
175
+ name: simplecov-lcov
176
+ requirement: !ruby/object:Gem::Requirement
177
+ requirements:
178
+ - - "~>"
179
+ - !ruby/object:Gem::Version
180
+ version: '0.8'
195
181
  type: :development
196
182
  prerelease: false
197
183
  version_requirements: !ruby/object:Gem::Requirement
198
184
  requirements:
199
185
  - - "~>"
200
186
  - !ruby/object:Gem::Version
201
- version: '0.12'
187
+ version: '0.8'
202
188
  description: OpenID Connect Strategy for OmniAuth.
203
189
  email:
204
190
  - jjbohn@gmail.com
@@ -207,10 +193,8 @@ executables: []
207
193
  extensions: []
208
194
  extra_rdoc_files: []
209
195
  files:
210
- - ".github/config/rubocop_linter_action.yml"
211
196
  - ".github/stale.yml"
212
197
  - ".github/workflows/main.yml"
213
- - ".github/workflows/rubocop.yml"
214
198
  - ".gitignore"
215
199
  - ".rubocop.yml"
216
200
  - CHANGELOG.md
@@ -225,8 +209,6 @@ files:
225
209
  - lib/omniauth/strategies/openid_connect.rb
226
210
  - lib/omniauth_openid_connect.rb
227
211
  - omniauth_openid_connect.gemspec
228
- - test/fixtures/id_token.txt
229
- - test/fixtures/jwks.json
230
212
  - test/fixtures/test.crt
231
213
  - test/lib/omniauth/strategies/openid_connect_test.rb
232
214
  - test/strategy_test_case.rb
@@ -237,10 +219,10 @@ licenses:
237
219
  metadata:
238
220
  bug_tracker_uri: https://github.com/m0n9oose/omniauth_openid_connect/issues
239
221
  changelog_uri: https://github.com/m0n9oose/omniauth_openid_connect/releases
240
- documentation_uri: https://github.com/m0n9oose/omniauth_openid_connect/tree/v0.4.0#readme
241
- source_code_uri: https://github.com/m0n9oose/omniauth_openid_connect/tree/v0.4.0
222
+ documentation_uri: https://github.com/m0n9oose/omniauth_openid_connect/tree/v0.5.0#readme
223
+ source_code_uri: https://github.com/m0n9oose/omniauth_openid_connect/tree/v0.5.0
242
224
  rubygems_mfa_required: 'true'
243
- post_install_message:
225
+ post_install_message:
244
226
  rdoc_options: []
245
227
  require_paths:
246
228
  - lib
@@ -255,13 +237,11 @@ required_rubygems_version: !ruby/object:Gem::Requirement
255
237
  - !ruby/object:Gem::Version
256
238
  version: '0'
257
239
  requirements: []
258
- rubygems_version: 3.3.5
259
- signing_key:
240
+ rubygems_version: 3.3.26
241
+ signing_key:
260
242
  specification_version: 4
261
243
  summary: OpenID Connect Strategy for OmniAuth
262
244
  test_files:
263
- - test/fixtures/id_token.txt
264
- - test/fixtures/jwks.json
265
245
  - test/fixtures/test.crt
266
246
  - test/lib/omniauth/strategies/openid_connect_test.rb
267
247
  - test/strategy_test_case.rb
@@ -1,59 +0,0 @@
1
- # Description: The name of the check that will be created.
2
- # Valid Options: A reasonably sized string.
3
- # Default: 'Rubocop Action'
4
- check_name: 'Rubocop Results'
5
-
6
- # Description: Versions required to run your RuboCop checks.
7
- # Valid options: RuboCop and any RuboCop extension, by default the latest gem version will be used. You can explicitly state that
8
- # (not required) or use a version number, like '1.5.1'.
9
- # Default:
10
- # versions:
11
- # - rubocop: 'latest'
12
- versions:
13
- - rubocop
14
- - rubocop-minitest
15
- - rubocop-performance: '1.5.1'
16
-
17
- # Description: Rubocop configuration file path relative to the workspace.
18
- # Valid options: A valid file path inside of the workspace.
19
- # Default: nil
20
- # Note: This does not need to be filled out for Rubocop to still find your config.
21
- # Resource: https://rubocop.readthedocs.io/en/stable/configuration/
22
- rubocop_config_path: '.rubocop.yml'
23
-
24
- # Run all cops enabled by configuration except this list.
25
- # Valid options: list of valid cop(s) and/or departments.
26
- # Default: nil
27
- # Resource: https://rubocop.readthedocs.io/en/stable/cops/
28
- # rubocop_excluded_cops:
29
- # - 'Style/FrozenStringLiteralComment'
30
-
31
- # Minimum severity for exit with error code
32
- # Valid options: 'refactor', 'convention', 'warning', 'error', or 'fatal'.
33
- # Default: 'warning'
34
- # Resource: https://rubocop.readthedocs.io/en/stable/configuration/#severity
35
- # rubocop_fail_level: 'warning'
36
-
37
- # Whether or not to use --force-exclusion when building the rubocop command. Use this if you are only linting modified
38
- # files and typically excluded files have been changed. For example, if you exclude db/schema.rb in your rubocop.yml
39
- # but a change gets made, then with the check_scope config set to 'modified' rubocop will lint db/schema.rb. If you set
40
- # this to true, rubocop will ignore it.
41
- # Valid options: true || false
42
- # Default: false
43
-
44
- # Instead of installing gems from rubygems, we can run `bundle install` on your project,
45
- # you would need to do this if you are using something like 'rubocop-github' or if you don't
46
- # want to list out dependencies with the `versions` key.
47
- # Valid options: true || false
48
- # Default: false
49
- # bundle: false
50
-
51
- # The scope of code that Rubocop should lint. Use this if you only want to lint changed files. If this is not set
52
- # or not equal to 'modified', Rubocop is run against the entire codebase. Note that this will not work on the master branch.
53
- # Valid options: 'modified'
54
- # Default: nil
55
-
56
- # The base branch against which changes will be compared, if check_scope config is set to 'modified'.
57
- # This setting is not used if check_scope != 'modified'.
58
- # Valid options: 'origin/another_branch'
59
- # Default: 'origin/master'
@@ -1,22 +0,0 @@
1
- name: Rubocop check
2
-
3
- on:
4
- pull_request:
5
- branches:
6
- - "*"
7
- push:
8
- branches:
9
- - master
10
- jobs:
11
- build:
12
- name: RuboCop Action
13
- runs-on: ubuntu-latest
14
- steps:
15
- - name: Checkout Action
16
- uses: actions/checkout@v1
17
- - name: Rubocop Linter Action
18
- uses: andrewmcodes/rubocop-linter-action@v3.2.0
19
- with:
20
- action_config_path: '.github/config/rubocop_linter_action.yml'
21
- env:
22
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -1 +0,0 @@
1
- eyJhbGciOiJSUzI1NiIsImtpZCI6IjFlOWdkazcifQ.ewogImlzcyI6ICJodHRwOi8vc2VydmVyLmV4YW1wbGUuY29tIiwKICJzdWIiOiAiMjQ4Mjg5NzYxMDAxIiwKICJhdWQiOiAiczZCaGRSa3F0MyIsCiAibm9uY2UiOiAibi0wUzZfV3pBMk1qIiwKICJleHAiOiAxMzExMjgxOTcwLAogImlhdCI6IDEzMTEyODA5NzAKfQ.ggW8hZ1EuVLuxNuuIJKX_V8a_OMXzR0EHR9R6jgdqrOOF4daGU96Sr_P6qJp6IcmD3HP99Obi1PRs-cwh3LO-p146waJ8IhehcwL7F09JdijmBqkvPeB2T9CJNqeGpe-gccMg4vfKjkM8FcGvnzZUN4_KSP0aAp1tOJ1zZwgjxqGByKHiOtX7TpdQyHE5lcMiKPXfEIQILVq0pc_E2DzL7emopWoaoZTF_m0_N0YzFC6g6EJbOEoRoSK5hoDalrcvRYLSrQAZZKflyuVCyixEoV9GfNQC3_osjzw2PAithfubEEBLuVVk4XUVrWOLrLl0nx7RkKU8NXNHq-rvKMzqg
@@ -1,8 +0,0 @@
1
- {"keys": [{
2
- "kty": "RSA",
3
- "n": "0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx4cbbfAAtVT86zwu1RK7aPFFxuhDR1L6tSoc_BJECPebWKRXjBZCiFV4n3oknjhMstn64tZ_2W-5JsGY4Hc5n9yBXArwl93lqt7_RN5w6Cf0h4QyQ5v-65YGjQR0_FDW2QvzqY368QQMicAtaSqzs8KJZgnYb9c7d0zgdAZHzu6qMQvRL5hajrn1n91CbOpbISD08qNLyrdkt-bFTWhAI4vMQFh6WeZu0fM4lFd2NcRwr3XPksINHaQ-G_xBniIqbw0Ls1jF44-csFCur-kEgU8awapJzKnqDKgw",
4
- "e": "AQAB",
5
- "alg": "RS256",
6
- "kid": "1e9gdk7"
7
- }]
8
- }