omniauth_openid_connect 0.5.0 → 0.6.1

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: 34218d7218e2aca766623a1252b641baa20d81330cfd38c67a733f9221d76aad
4
- data.tar.gz: 54f874832122f3cc1444280d8820d222a4dff7ef4c3580eb899d4da1efb38051
3
+ metadata.gz: 446a75e37d0a98638c32b054b7e4e1443b54c8d067025381ca340e2a80d5db05
4
+ data.tar.gz: fe1895242ce7bd7d1910d9db085678cc5cadc9757b62a7660a232462105d21fe
5
5
  SHA512:
6
- metadata.gz: e4465213a06e82fd61d9997d0ec1b9f993620dead092add6ba2f07b34aa08dca53bcb63d92a256839c7310478c89ecff9359778e5721cf1cc6dcc300e5d780f8
7
- data.tar.gz: 8fdbda1f579f271f6273a6380a7a9cf581b4b1239b589a1e2cb98799b8731056c3ec222c519e445d95387749ab62b58839e4005906938e0e1f76b9d396e76565
6
+ metadata.gz: c102811330e2e73ea3a76940c4c275799ef01b8b4640a3e49203a9febec8ecdcb16d9f2a48d2df67deb402f65914842964b656e82c71e1676108db7d54577252
7
+ data.tar.gz: 2c6e454846927acd7f2456b7b9653f356ac96d3a196b7299ca348c5bcb9f4853a802154da2548bfc86bc65ec3088abcabb16ea1ab37dc7cc0285cfae0c938a9c
@@ -14,12 +14,12 @@ jobs:
14
14
  strategy:
15
15
  fail-fast: false
16
16
  matrix:
17
- ruby: ["2.5", "2.6", "2.7", "3.0", "3.1"]
17
+ ruby: ["2.5", "2.6", "2.7", "3.0", "3.1", "3.2"]
18
18
  name: Ruby ${{ matrix.ruby }}
19
19
 
20
20
  steps:
21
21
  - name: Checkout code
22
- uses: actions/checkout@v2
22
+ uses: actions/checkout@v3
23
23
 
24
24
  - name: Setup Ruby
25
25
  uses: ruby/setup-ruby@v1
@@ -51,7 +51,7 @@ jobs:
51
51
  runs-on: ubuntu-latest
52
52
  steps:
53
53
  - name: Checkout code
54
- uses: actions/checkout@v2
54
+ uses: actions/checkout@v3
55
55
 
56
56
  - name: Setup Ruby
57
57
  uses: ruby/setup-ruby@v1
data/CHANGELOG.md CHANGED
@@ -1,3 +1,11 @@
1
+ # v0.6.1 (22.02.2023)
2
+
3
+ - Fix uninitialized constant error (https://github.com/omniauth/omniauth_openid_connect/pull/147)
4
+
5
+ # v0.6.0 (21.01.2023)
6
+
7
+ - Support verification of HS256-signed JWTs (https://github.com/omniauth/omniauth_openid_connect/pull/134)
8
+
1
9
  # v0.5.0 (26.12.2022)
2
10
 
3
11
  - Support the "nonce" parameter forwarding without a session [#130](https://github.com/omniauth/omniauth_openid_connect/pull/130)
data/README.md CHANGED
@@ -69,6 +69,7 @@ config.omniauth :openid_connect, {
69
69
  | pkce_verifier | Specify a custom PKCE verifier code. | no | A random 128-char string | Proc.new { SecureRandom.hex(64) } |
70
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
71
  | client_options | A hash of client options detailed in its own section | yes | | |
72
+ | jwt_secret_base64 | For HMAC with SHA2 (e.g. HS256) signing algorithms, specify the base64-encoded secret used to sign the JWT token. Defaults to the OAuth2 client secret if not specified. | no | client_options.secret | "bXlzZWNyZXQ=\n"
72
73
 
73
74
  ### Client Config Options
74
75
 
@@ -2,6 +2,6 @@
2
2
 
3
3
  module OmniAuth
4
4
  module OpenIDConnect
5
- VERSION = '0.5.0'
5
+ VERSION = '0.6.1'
6
6
  end
7
7
  end
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'base64'
3
4
  require 'timeout'
4
5
  require 'net/http'
5
6
  require 'open-uri'
@@ -36,6 +37,7 @@ module OmniAuth
36
37
  option :issuer
37
38
  option :discovery, false
38
39
  option :client_signing_alg
40
+ option :jwt_secret_base64
39
41
  option :client_jwk_signing_key
40
42
  option :client_x509_signing_key
41
43
  option :scope, [:openid]
@@ -200,13 +202,19 @@ module OmniAuth
200
202
  def public_key
201
203
  @public_key ||= if options.discovery
202
204
  config.jwks
203
- elsif key_or_secret
204
- key_or_secret
205
+ elsif configured_public_key
206
+ configured_public_key
205
207
  elsif client_options.jwks_uri
206
208
  fetch_key
207
209
  end
208
210
  end
209
211
 
212
+ # Some OpenID providers use the OAuth2 client secret as the shared secret, but
213
+ # Keycloak uses a separate key that's stored inside the database.
214
+ def secret
215
+ base64_decoded_jwt_secret || client_options.secret
216
+ end
217
+
210
218
  def pkce_authorize_params(verifier)
211
219
  # NOTE: see https://tools.ietf.org/html/rfc7636#appendix-A
212
220
  {
@@ -221,6 +229,12 @@ module OmniAuth
221
229
  @fetch_key ||= parse_jwk_key(::OpenIDConnect.http_client.get_content(client_options.jwks_uri))
222
230
  end
223
231
 
232
+ def base64_decoded_jwt_secret
233
+ return unless options.jwt_secret_base64
234
+
235
+ Base64.decode64(options.jwt_secret_base64)
236
+ end
237
+
224
238
  def issuer
225
239
  resource = "#{ client_options.scheme }://#{ client_options.host }"
226
240
  resource = "#{ resource }:#{ client_options.port }" if client_options.port
@@ -265,8 +279,75 @@ module OmniAuth
265
279
  @access_token
266
280
  end
267
281
 
282
+ # Unlike ::OpenIDConnect::ResponseObject::IdToken.decode, this
283
+ # method splits the decoding and verification of JWT into two
284
+ # steps. First, we decode the JWT without verifying it to
285
+ # determine the algorithm used to sign. Then, we verify it using
286
+ # the appropriate public key (e.g. if algorithm is RS256) or
287
+ # shared secret (e.g. if algorithm is HS256). This works around a
288
+ # limitation in the openid_connect gem:
289
+ # https://github.com/nov/openid_connect/issues/61
268
290
  def decode_id_token(id_token)
269
- ::OpenIDConnect::ResponseObject::IdToken.decode(id_token, public_key)
291
+ decoded = JSON::JWT.decode(id_token, :skip_verification)
292
+ algorithm = decoded.algorithm.to_sym
293
+
294
+ validate_client_algorithm!(algorithm)
295
+
296
+ keyset =
297
+ case algorithm
298
+ when :HS256, :HS384, :HS512
299
+ secret
300
+ else
301
+ public_key
302
+ end
303
+
304
+ decoded.verify!(keyset)
305
+ ::OpenIDConnect::ResponseObject::IdToken.new(decoded)
306
+ rescue JSON::JWK::Set::KidNotFound
307
+ # If the JWT has a key ID (kid), then we know that the set of
308
+ # keys supplied doesn't contain the one we want, and we're
309
+ # done. However, if there is no kid, then we try each key
310
+ # individually to see if one works:
311
+ # https://github.com/nov/json-jwt/pull/92#issuecomment-824654949
312
+ raise if decoded&.header&.key?('kid')
313
+
314
+ decoded = decode_with_each_key!(id_token, keyset)
315
+
316
+ raise unless decoded
317
+
318
+ decoded
319
+ end
320
+
321
+ # If client_signing_alg is specified, we check that the returned JWT
322
+ # matches the expected algorithm. If not, we reject it.
323
+ def validate_client_algorithm!(algorithm)
324
+ client_signing_alg = options.client_signing_alg&.to_sym
325
+
326
+ return unless client_signing_alg
327
+ return if algorithm == client_signing_alg
328
+
329
+ reason = "Received JWT is signed with #{algorithm}, but client_singing_alg is configured for #{client_signing_alg}"
330
+ raise CallbackError, error: :invalid_jwt_algorithm, reason: reason, uri: params['error_uri']
331
+ end
332
+
333
+ def decode!(id_token, key)
334
+ ::OpenIDConnect::ResponseObject::IdToken.decode(id_token, key)
335
+ end
336
+
337
+ def decode_with_each_key!(id_token, keyset)
338
+ return unless keyset.is_a?(JSON::JWK::Set)
339
+
340
+ keyset.each do |key|
341
+ begin
342
+ decoded = decode!(id_token, key)
343
+ rescue JSON::JWS::VerificationFailed, JSON::JWS::UnexpectedAlgorithm, JSON::JWK::UnknownAlgorithm
344
+ next
345
+ end
346
+
347
+ return decoded if decoded
348
+ end
349
+
350
+ nil
270
351
  end
271
352
 
272
353
  def client_options
@@ -308,18 +389,12 @@ module OmniAuth
308
389
  super
309
390
  end
310
391
 
311
- def key_or_secret
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
322
- end
392
+ def configured_public_key
393
+ @configured_public_key ||= if options.client_jwk_signing_key
394
+ parse_jwk_key(options.client_jwk_signing_key)
395
+ elsif options.client_x509_signing_key
396
+ parse_x509_key(options.client_x509_signing_key)
397
+ end
323
398
  end
324
399
 
325
400
  def parse_x509_key(key)
@@ -269,6 +269,138 @@ module OmniAuth
269
269
  strategy.callback_phase
270
270
  end
271
271
 
272
+ def test_callback_phase_with_id_token_no_kid
273
+ other_rsa_private = OpenSSL::PKey::RSA.generate(2048)
274
+
275
+ key = JSON::JWK.new(private_key)
276
+ other_key = JSON::JWK.new(other_rsa_private)
277
+ state = SecureRandom.hex(16)
278
+ request.stubs(:params).returns('id_token' => jwt.to_s, 'state' => state)
279
+ request.stubs(:path_info).returns('')
280
+
281
+ strategy.options.issuer = issuer
282
+ strategy.options.client_signing_alg = :RS256
283
+ strategy.options.client_jwk_signing_key = { 'keys' => [other_key, key] }.to_json
284
+ strategy.options.response_type = 'id_token'
285
+
286
+ strategy.unstub(:user_info)
287
+ strategy.call!('rack.session' => { 'omniauth.state' => state, 'omniauth.nonce' => nonce })
288
+ strategy.callback_phase
289
+ end
290
+
291
+ def test_callback_phase_with_id_token_with_kid
292
+ other_rsa_private = OpenSSL::PKey::RSA.generate(2048)
293
+
294
+ key = JSON::JWK.new(private_key)
295
+ other_key = JSON::JWK.new(other_rsa_private)
296
+ state = SecureRandom.hex(16)
297
+ jwt_with_kid = JSON::JWT.new(payload).sign(key, :RS256)
298
+ request.stubs(:params).returns('id_token' => jwt_with_kid.to_s, 'state' => state)
299
+ request.stubs(:path_info).returns('')
300
+
301
+ strategy.options.issuer = issuer
302
+ strategy.options.client_signing_alg = :RS256
303
+ strategy.options.client_jwk_signing_key = { 'keys' => [other_key, key] }.to_json
304
+ strategy.options.response_type = 'id_token'
305
+
306
+ strategy.unstub(:user_info)
307
+ strategy.call!('rack.session' => { 'omniauth.state' => state, 'omniauth.nonce' => nonce })
308
+ strategy.callback_phase
309
+ end
310
+
311
+ def test_callback_phase_with_id_token_with_kid_and_no_matching_kid
312
+ other_rsa_private = OpenSSL::PKey::RSA.generate(2048)
313
+
314
+ key = JSON::JWK.new(private_key)
315
+ other_key = JSON::JWK.new(other_rsa_private)
316
+ state = SecureRandom.hex(16)
317
+ jwt_with_kid = JSON::JWT.new(payload).sign(key, :RS256)
318
+ request.stubs(:params).returns('id_token' => jwt_with_kid.to_s, 'state' => state)
319
+ request.stubs(:path_info).returns('')
320
+
321
+ strategy.options.issuer = issuer
322
+ strategy.options.client_signing_alg = :RS256
323
+ # We use private_key here instead of the wrapped key, which contains a kid
324
+ strategy.options.client_jwk_signing_key = { 'keys' => [other_key, private_key] }.to_json
325
+ strategy.options.response_type = 'id_token'
326
+
327
+ strategy.unstub(:user_info)
328
+ strategy.call!('rack.session' => { 'omniauth.state' => state, 'omniauth.nonce' => nonce })
329
+
330
+ assert_raises JSON::JWK::Set::KidNotFound do
331
+ strategy.callback_phase
332
+ end
333
+ end
334
+
335
+ def test_callback_phase_with_id_token_with_hs256
336
+ state = SecureRandom.hex(16)
337
+ request.stubs(:params).returns('id_token' => jwt_with_hs256.to_s, 'state' => state)
338
+ request.stubs(:path_info).returns('')
339
+
340
+ strategy.options.issuer = issuer
341
+ strategy.options.client_options.secret = hmac_secret
342
+ strategy.options.client_signing_alg = :HS256
343
+ strategy.options.response_type = 'id_token'
344
+
345
+ strategy.unstub(:user_info)
346
+ strategy.call!('rack.session' => { 'omniauth.state' => state, 'omniauth.nonce' => nonce })
347
+ strategy.callback_phase
348
+ end
349
+
350
+ def test_callback_phase_with_hs256_base64_jwt_secret
351
+ state = SecureRandom.hex(16)
352
+ request.stubs(:params).returns('id_token' => jwt_with_hs256.to_s, 'state' => state)
353
+ request.stubs(:path_info).returns('')
354
+
355
+ strategy.options.issuer = issuer
356
+ strategy.options.jwt_secret_base64 = Base64.encode64(hmac_secret)
357
+ strategy.options.response_type = 'id_token'
358
+
359
+ strategy.unstub(:user_info)
360
+ strategy.call!('rack.session' => { 'omniauth.state' => state, 'omniauth.nonce' => nonce })
361
+ strategy.callback_phase
362
+ end
363
+
364
+ def test_callback_phase_with_mismatched_signing_algorithm
365
+ state = SecureRandom.hex(16)
366
+ request.stubs(:params).returns('id_token' => jwt_with_hs512.to_s, 'state' => state)
367
+ request.stubs(:path_info).returns('')
368
+
369
+ strategy.options.issuer = issuer
370
+ strategy.options.client_options.secret = hmac_secret
371
+ strategy.options.client_signing_alg = :HS256
372
+ strategy.options.response_type = 'id_token'
373
+
374
+ strategy.unstub(:user_info)
375
+ strategy.call!('rack.session' => { 'omniauth.state' => state, 'omniauth.nonce' => nonce })
376
+
377
+ strategy.expects(:fail!).with(:invalid_jwt_algorithm, is_a(OmniAuth::Strategies::OpenIDConnect::CallbackError))
378
+ strategy.callback_phase
379
+ end
380
+
381
+ def test_callback_phase_with_id_token_no_matching_key
382
+ rsa_private = OpenSSL::PKey::RSA.generate(2048)
383
+ other_rsa_private = OpenSSL::PKey::RSA.generate(2048)
384
+
385
+ other_key = JSON::JWK.new(other_rsa_private)
386
+ token = JSON::JWT.new(payload).sign(rsa_private, :RS256).to_s
387
+ state = SecureRandom.hex(16)
388
+ request.stubs(:params).returns('id_token' => token, 'state' => state)
389
+ request.stubs(:path_info).returns('')
390
+
391
+ strategy.options.issuer = issuer
392
+ strategy.options.client_signing_alg = :RS256
393
+ strategy.options.client_jwk_signing_key = { 'keys' => [other_key] }.to_json
394
+ strategy.options.response_type = 'id_token'
395
+
396
+ strategy.unstub(:user_info)
397
+ strategy.call!('rack.session' => { 'omniauth.state' => state, 'omniauth.nonce' => nonce })
398
+
399
+ assert_raises JSON::JWK::Set::KidNotFound do
400
+ strategy.callback_phase
401
+ end
402
+ end
403
+
272
404
  def test_callback_phase_with_discovery # rubocop:disable Metrics/AbcSize
273
405
  state = SecureRandom.hex(16)
274
406
 
@@ -699,7 +831,7 @@ module OmniAuth
699
831
  def test_public_key_with_hmac
700
832
  strategy.options.client_options.secret = 'secret'
701
833
  strategy.options.client_signing_alg = :HS256
702
- assert_equal strategy.options.client_options.secret, strategy.public_key
834
+ assert_equal strategy.options.client_options.secret, strategy.secret
703
835
  end
704
836
 
705
837
  def test_id_token_auth_hash
@@ -37,6 +37,18 @@ class StrategyTestCase < MiniTest::Test
37
37
  @jwt ||= JSON::JWT.new(payload).sign(private_key, :RS256)
38
38
  end
39
39
 
40
+ def hmac_secret
41
+ @hmac_secret ||= SecureRandom.hex(16)
42
+ end
43
+
44
+ def jwt_with_hs256
45
+ @jwt_with_hs256 ||= JSON::JWT.new(payload).sign(hmac_secret, :HS256)
46
+ end
47
+
48
+ def jwt_with_hs512
49
+ @jwt_with_hs512 ||= JSON::JWT.new(payload).sign(hmac_secret, :HS512)
50
+ end
51
+
40
52
  def jwks
41
53
  @jwks ||= begin
42
54
  key = JSON::JWK.new(private_key)
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: omniauth_openid_connect
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.0
4
+ version: 0.6.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - John Bohn
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2022-12-26 00:00:00.000000000 Z
12
+ date: 2023-02-23 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: omniauth
@@ -219,8 +219,8 @@ licenses:
219
219
  metadata:
220
220
  bug_tracker_uri: https://github.com/m0n9oose/omniauth_openid_connect/issues
221
221
  changelog_uri: https://github.com/m0n9oose/omniauth_openid_connect/releases
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
222
+ documentation_uri: https://github.com/m0n9oose/omniauth_openid_connect/tree/v0.6.1#readme
223
+ source_code_uri: https://github.com/m0n9oose/omniauth_openid_connect/tree/v0.6.1
224
224
  rubygems_mfa_required: 'true'
225
225
  post_install_message:
226
226
  rdoc_options: []
@@ -237,7 +237,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
237
237
  - !ruby/object:Gem::Version
238
238
  version: '0'
239
239
  requirements: []
240
- rubygems_version: 3.3.26
240
+ rubygems_version: 3.4.7
241
241
  signing_key:
242
242
  specification_version: 4
243
243
  summary: OpenID Connect Strategy for OmniAuth