omniauth_openid_connect 0.4.0 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
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
- }