gitlab-omniauth-openid-connect 0.5.0 → 0.6.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: cee603f7fbf49710ecee6e6953ca205feb9620c25dc9d865d7b44dda22a7b0dc
4
- data.tar.gz: 18ecc1ef7a21e86a2e2554d10d3a95dc2c9dcd26cf0a66509f931a62ce0b4e7d
3
+ metadata.gz: 13c881e6fc6d97b86a4608afaac9f44737d83650de5f00bb102deab1dc723c89
4
+ data.tar.gz: 3f116b19d3759309dfd6369671dffdddafe5362c3ecf9c4db16cc25c58b6c4ee
5
5
  SHA512:
6
- metadata.gz: 2959c9ee30ca4e8c84b57f578d142744fea140c94c60daafcefb83c6aabfb707610f47c9f1ae2186033bf98d992e799614c24760393a63ce03473a1fb4301e77
7
- data.tar.gz: 1fd6fb3c3d009d011ca4fd595756ea6d46d34d7ad8a0fc93d4921684f0e4cec92b1e20a4ecc1d49beaa28eff6731e02df616e2f7f99e3ec4419273a179aeb139
6
+ metadata.gz: 80d59151cc0657817732e4d85bdee536fa328c40fc0a16b379172c88b62fc7bd25bfc156a7e10885c16277a0a83984f6e34e6223c47cc746cceaa9c264c7d20f
7
+ data.tar.gz: 251bbd0f19557183b39c72cc679579ce1550573786bfd41fa213f265e19158f27d491309a6199cdebf081c8ba72bffc6a11d9a25a68d0d7b7bfeac113880c061
data/CHANGELOG.md CHANGED
@@ -1,3 +1,7 @@
1
+ # v0.6.0 (07.08.2021)
2
+
3
+ - [Support verification of HS256-signed JWTs](https://gitlab.com/gitlab-org/gitlab-omniauth-openid-connect/-/merge_requests/8)
4
+
1
5
  # v0.5.0 (05.07.2021)
2
6
 
3
7
  - [Add email_verified field to info dict](https://gitlab.com/gitlab-org/gitlab-omniauth-openid-connect/-/merge_requests/7)
@@ -2,6 +2,6 @@
2
2
 
3
3
  module OmniAuth
4
4
  module OpenIDConnect
5
- VERSION = '0.5.0'
5
+ VERSION = '0.6.0'
6
6
  end
7
7
  end
@@ -236,31 +236,55 @@ module OmniAuth
236
236
  @access_token
237
237
  end
238
238
 
239
+ # Unlike ::OpenIDConnect::ResponseObject::IdToken.decode, this
240
+ # method splits the decoding and verification of JWT into two
241
+ # steps. First, we decode the JWT without verifying it to
242
+ # determine the algorithm used to sign. Then, we verify it using
243
+ # the appropriate public key (e.g. if algorithm is RS256) or
244
+ # shared secret (e.g. if algorithm is HS256). This works around a
245
+ # limitation in the openid_connect gem:
246
+ # https://github.com/nov/openid_connect/issues/61
239
247
  def decode_id_token(id_token)
240
- decode!(id_token, public_key)
248
+ decoded = JSON::JWT.decode(id_token, :skip_verification)
249
+ algorithm = decoded.algorithm.to_sym
250
+
251
+ keyset =
252
+ case algorithm
253
+ when :RS256, :RS384, :RS512
254
+ public_key
255
+ when :HS256, :HS384, :HS512
256
+ client_options.secret
257
+ end
258
+
259
+ decoded.verify!(keyset)
260
+ ::OpenIDConnect::ResponseObject::IdToken.new(decoded)
241
261
  rescue JSON::JWK::Set::KidNotFound
242
- # Either the JWT doesn't have kid specified or the set of keys doesn't
243
- # have a matching key. Since we can't tell the first case from the second,
244
- # try each key individually to see if one works.
262
+ # If the JWT has a key ID (kid), then we know that the set of
263
+ # keys supplied doesn't contain the one we want, and we're
264
+ # done. However, if there is no kid, then we try each key
265
+ # individually to see if one works:
245
266
  # https://github.com/nov/json-jwt/pull/92#issuecomment-824654949
246
- decoded = decode_with_each_key!(id_token)
267
+ raise if decoded&.header&.key?('kid')
268
+
269
+ decoded = decode_with_each_key!(id_token, keyset)
247
270
 
248
271
  raise unless decoded
249
272
 
250
273
  decoded
274
+
251
275
  end
252
276
 
253
277
  def decode!(id_token, key)
254
278
  ::OpenIDConnect::ResponseObject::IdToken.decode(id_token, key)
255
279
  end
256
280
 
257
- def decode_with_each_key!(id_token)
258
- return unless public_key.is_a?(JSON::JWK::Set)
281
+ def decode_with_each_key!(id_token, keyset)
282
+ return unless keyset.is_a?(JSON::JWK::Set)
259
283
 
260
- public_key.each do |key|
284
+ keyset.each do |key|
261
285
  begin
262
286
  decoded = decode!(id_token, key)
263
- rescue JSON::JWS::VerificationFailed
287
+ rescue JSON::JWS::VerificationFailed, JSON::JWS::UnexpectedAlgorithm, JSON::JWS::UnknownAlgorithm
264
288
  next
265
289
  end
266
290
 
@@ -304,7 +328,7 @@ module OmniAuth
304
328
  end
305
329
 
306
330
  def key_or_secret
307
- @key_or_secret ||=
331
+ @key_or_secret ||= begin
308
332
  case options.client_signing_alg&.to_sym
309
333
  when :HS256, :HS384, :HS512
310
334
  client_options.secret
@@ -315,6 +339,7 @@ module OmniAuth
315
339
  parse_x509_key(options.client_x509_signing_key)
316
340
  end
317
341
  end
342
+ end
318
343
  end
319
344
 
320
345
  def parse_x509_key(key)
@@ -168,7 +168,7 @@ module OmniAuth
168
168
 
169
169
  strategy.options.issuer = 'example.com'
170
170
  strategy.options.client_signing_alg = :RS256
171
- strategy.options.client_jwk_signing_key = File.read('test/fixtures/jwks.json')
171
+ strategy.options.client_jwk_signing_key = jwks.to_s
172
172
  strategy.options.response_type = 'code'
173
173
 
174
174
  strategy.unstub(:user_info)
@@ -177,7 +177,7 @@ module OmniAuth
177
177
  access_token.stubs(:refresh_token)
178
178
  access_token.stubs(:expires_in)
179
179
  access_token.stubs(:scope)
180
- access_token.stubs(:id_token).returns(File.read('test/fixtures/id_token.txt'))
180
+ access_token.stubs(:id_token).returns(jwt.to_s)
181
181
  client.expects(:access_token!).at_least_once.returns(access_token)
182
182
  access_token.expects(:userinfo!).returns(user_info)
183
183
 
@@ -192,14 +192,13 @@ module OmniAuth
192
192
  end
193
193
 
194
194
  def test_callback_phase_with_id_token
195
- code = SecureRandom.hex(16)
196
195
  state = SecureRandom.hex(16)
197
- request.stubs(:params).returns('id_token' => code, 'state' => state)
196
+ request.stubs(:params).returns('id_token' => jwt.to_s, 'state' => state)
198
197
  request.stubs(:path_info).returns('')
199
198
 
200
199
  strategy.options.issuer = 'example.com'
201
200
  strategy.options.client_signing_alg = :RS256
202
- strategy.options.client_jwk_signing_key = File.read('test/fixtures/jwks.json')
201
+ strategy.options.client_jwk_signing_key = jwks.to_json
203
202
  strategy.options.response_type = 'id_token'
204
203
 
205
204
  strategy.unstub(:user_info)
@@ -208,7 +207,7 @@ module OmniAuth
208
207
  access_token.stubs(:refresh_token)
209
208
  access_token.stubs(:expires_in)
210
209
  access_token.stubs(:scope)
211
- access_token.stubs(:id_token).returns(File.read('test/fixtures/id_token.txt'))
210
+ access_token.stubs(:id_token).returns(jwt.to_s)
212
211
 
213
212
  id_token = stub('OpenIDConnect::ResponseObject::IdToken')
214
213
  id_token.stubs(:raw_attributes).returns('sub' => 'sub', 'name' => 'name', 'email' => 'email')
@@ -221,14 +220,32 @@ module OmniAuth
221
220
  end
222
221
 
223
222
  def test_callback_phase_with_id_token_no_kid
224
- rsa_private = OpenSSL::PKey::RSA.generate(2048)
225
223
  other_rsa_private = OpenSSL::PKey::RSA.generate(2048)
226
224
 
227
- key = JSON::JWK.new(rsa_private)
225
+ key = JSON::JWK.new(private_key)
228
226
  other_key = JSON::JWK.new(other_rsa_private)
229
- token = JSON::JWT.new(payload).sign(rsa_private, :RS256).to_s
230
227
  state = SecureRandom.hex(16)
231
- request.stubs(:params).returns('id_token' => token, 'state' => state)
228
+ request.stubs(:params).returns('id_token' => jwt.to_s, 'state' => state)
229
+ request.stubs(:path_info).returns('')
230
+
231
+ strategy.options.issuer = issuer
232
+ strategy.options.client_signing_alg = :RS256
233
+ strategy.options.client_jwk_signing_key = { 'keys' => [other_key, key] }.to_json
234
+ strategy.options.response_type = 'id_token'
235
+
236
+ strategy.unstub(:user_info)
237
+ strategy.call!('rack.session' => { 'omniauth.state' => state, 'omniauth.nonce' => nonce })
238
+ strategy.callback_phase
239
+ end
240
+
241
+ def test_callback_phase_with_id_token_with_kid
242
+ other_rsa_private = OpenSSL::PKey::RSA.generate(2048)
243
+
244
+ key = JSON::JWK.new(private_key)
245
+ other_key = JSON::JWK.new(other_rsa_private)
246
+ state = SecureRandom.hex(16)
247
+ jwt_with_kid = JSON::JWT.new(payload).sign(key, :RS256)
248
+ request.stubs(:params).returns('id_token' => jwt_with_kid.to_s, 'state' => state)
232
249
  request.stubs(:path_info).returns('')
233
250
 
234
251
  strategy.options.issuer = issuer
@@ -241,6 +258,45 @@ module OmniAuth
241
258
  strategy.callback_phase
242
259
  end
243
260
 
261
+ def test_callback_phase_with_id_token_with_kid_and_no_matching_kid
262
+ other_rsa_private = OpenSSL::PKey::RSA.generate(2048)
263
+
264
+ key = JSON::JWK.new(private_key)
265
+ other_key = JSON::JWK.new(other_rsa_private)
266
+ state = SecureRandom.hex(16)
267
+ jwt_with_kid = JSON::JWT.new(payload).sign(key, :RS256)
268
+ request.stubs(:params).returns('id_token' => jwt_with_kid.to_s, 'state' => state)
269
+ request.stubs(:path_info).returns('')
270
+
271
+ strategy.options.issuer = issuer
272
+ strategy.options.client_signing_alg = :RS256
273
+ # We use private_key here instead of the wrapped key, which contains a kid
274
+ strategy.options.client_jwk_signing_key = { 'keys' => [other_key, private_key] }.to_json
275
+ strategy.options.response_type = 'id_token'
276
+
277
+ strategy.unstub(:user_info)
278
+ strategy.call!('rack.session' => { 'omniauth.state' => state, 'omniauth.nonce' => nonce })
279
+
280
+ assert_raises JSON::JWK::Set::KidNotFound do
281
+ strategy.callback_phase
282
+ end
283
+ end
284
+
285
+ def test_callback_phase_with_id_token_with_hs256
286
+ state = SecureRandom.hex(16)
287
+ request.stubs(:params).returns('id_token' => jwt_with_hs256.to_s, 'state' => state)
288
+ request.stubs(:path_info).returns('')
289
+
290
+ strategy.options.issuer = issuer
291
+ strategy.options.client_options.secret = hmac_secret
292
+ strategy.options.client_signing_alg = :HS256
293
+ strategy.options.response_type = 'id_token'
294
+
295
+ strategy.unstub(:user_info)
296
+ strategy.call!('rack.session' => { 'omniauth.state' => state, 'omniauth.nonce' => nonce })
297
+ strategy.callback_phase
298
+ end
299
+
244
300
  def test_callback_phase_with_id_token_no_matching_key
245
301
  rsa_private = OpenSSL::PKey::RSA.generate(2048)
246
302
  other_rsa_private = OpenSSL::PKey::RSA.generate(2048)
@@ -266,11 +322,9 @@ module OmniAuth
266
322
  end
267
323
 
268
324
  def test_callback_phase_with_discovery
269
- code = SecureRandom.hex(16)
270
325
  state = SecureRandom.hex(16)
271
- jwks = JSON::JWK::Set.new(JSON.parse(File.read('test/fixtures/jwks.json'))['keys'])
272
326
 
273
- request.stubs(:params).returns('code' => code, 'state' => state)
327
+ request.stubs(:params).returns('code' => jwt.to_s, 'state' => state)
274
328
  request.stubs(:path_info).returns('')
275
329
 
276
330
  strategy.options.client_options.host = 'example.com'
@@ -285,7 +339,7 @@ module OmniAuth
285
339
  config.stubs(:token_endpoint).returns('https://example.com/token')
286
340
  config.stubs(:userinfo_endpoint).returns('https://example.com/userinfo')
287
341
  config.stubs(:jwks_uri).returns('https://example.com/jwks')
288
- config.stubs(:jwks).returns(jwks)
342
+ config.stubs(:jwks).returns(JSON::JWK::Set.new(jwks['keys']))
289
343
 
290
344
  ::OpenIDConnect::Discovery::Provider::Config.stubs(:discover!).with('https://example.com/').returns(config)
291
345
 
@@ -300,7 +354,7 @@ module OmniAuth
300
354
  access_token.stubs(:refresh_token)
301
355
  access_token.stubs(:expires_in)
302
356
  access_token.stubs(:scope)
303
- access_token.stubs(:id_token).returns(File.read('test/fixtures/id_token.txt'))
357
+ access_token.stubs(:id_token).returns(jwt.to_s)
304
358
  client.expects(:access_token!).at_least_once.returns(access_token)
305
359
  access_token.expects(:userinfo!).returns(user_info)
306
360
 
@@ -309,9 +363,9 @@ module OmniAuth
309
363
  end
310
364
 
311
365
  def test_callback_phase_with_jwks_uri
312
- code = SecureRandom.hex(16)
366
+ id_token = jwt.to_s
313
367
  state = SecureRandom.hex(16)
314
- request.stubs(:params).returns('id_token' => code, 'state' => state)
368
+ request.stubs(:params).returns('id_token' => id_token, 'state' => state)
315
369
  request.stubs(:path_info).returns('')
316
370
 
317
371
  strategy.options.issuer = 'example.com'
@@ -321,7 +375,7 @@ module OmniAuth
321
375
  HTTPClient
322
376
  .any_instance.stubs(:get_content)
323
377
  .with(strategy.options.client_options.jwks_uri)
324
- .returns(File.read('test/fixtures/jwks.json'))
378
+ .returns(jwks.to_json)
325
379
 
326
380
  strategy.unstub(:user_info)
327
381
  access_token = stub('OpenIDConnect::AccessToken')
@@ -329,7 +383,7 @@ module OmniAuth
329
383
  access_token.stubs(:refresh_token)
330
384
  access_token.stubs(:expires_in)
331
385
  access_token.stubs(:scope)
332
- access_token.stubs(:id_token).returns(File.read('test/fixtures/id_token.txt'))
386
+ access_token.stubs(:id_token).returns(id_token)
333
387
 
334
388
  id_token = stub('OpenIDConnect::ResponseObject::IdToken')
335
389
  id_token.stubs(:raw_attributes).returns('sub' => 'sub', 'name' => 'name', 'email' => 'email')
@@ -494,7 +548,7 @@ module OmniAuth
494
548
  def test_credentials
495
549
  strategy.options.issuer = 'example.com'
496
550
  strategy.options.client_signing_alg = :RS256
497
- strategy.options.client_jwk_signing_key = File.read('test/fixtures/jwks.json')
551
+ strategy.options.client_jwk_signing_key = jwks.to_json
498
552
 
499
553
  id_token = stub('OpenIDConnect::ResponseObject::IdToken')
500
554
  id_token.stubs(:verify!).returns(true)
@@ -505,7 +559,7 @@ module OmniAuth
505
559
  access_token.stubs(:refresh_token).returns(SecureRandom.hex(16))
506
560
  access_token.stubs(:expires_in).returns(Time.now)
507
561
  access_token.stubs(:scope).returns('openidconnect')
508
- access_token.stubs(:id_token).returns(File.read('test/fixtures/id_token.txt'))
562
+ access_token.stubs(:id_token).returns(jwt.to_s)
509
563
 
510
564
  client.expects(:access_token!).returns(access_token)
511
565
  access_token.expects(:refresh_token).returns(access_token.refresh_token)
@@ -592,11 +646,11 @@ module OmniAuth
592
646
  strategy.options.issuer = 'foobar.com'
593
647
  strategy.options.client_auth_method = :not_basic
594
648
  strategy.options.client_signing_alg = :RS256
595
- strategy.options.client_jwk_signing_key = File.read('test/fixtures/jwks.json')
649
+ strategy.options.client_jwk_signing_key = jwks.to_json
596
650
 
597
651
  json_response = {
598
652
  access_token: 'test_access_token',
599
- id_token: File.read('test/fixtures/id_token.txt'),
653
+ id_token: jwt.to_s,
600
654
  token_type: 'Bearer',
601
655
  }.to_json
602
656
  success = Struct.new(:status, :body).new(200, json_response)
@@ -619,16 +673,14 @@ module OmniAuth
619
673
 
620
674
  def test_public_key_with_jwks
621
675
  strategy.options.client_signing_alg = :RS256
622
- strategy.options.client_jwk_signing_key = File.read('./test/fixtures/jwks.json')
676
+ strategy.options.client_jwk_signing_key = jwks.to_json
623
677
 
624
678
  assert_equal JSON::JWK::Set, strategy.public_key.class
625
679
  end
626
680
 
627
681
  def test_public_key_with_jwk
628
682
  strategy.options.client_signing_alg = :RS256
629
- jwks_str = File.read('./test/fixtures/jwks.json')
630
- jwks = JSON.parse(jwks_str)
631
- jwk = jwks['keys'].first
683
+ jwk = jwks[:keys].first
632
684
  strategy.options.client_jwk_signing_key = jwk.to_json
633
685
 
634
686
  assert_equal JSON::JWK, strategy.public_key.class
@@ -653,16 +705,7 @@ module OmniAuth
653
705
 
654
706
  id_token = stub('OpenIDConnect::ResponseObject::IdToken')
655
707
  id_token.stubs(:verify!).returns(true)
656
- id_token.stubs(:raw_attributes, :to_h).returns(
657
- {
658
- "iss": "http://server.example.com",
659
- "sub": "248289761001",
660
- "aud": "s6BhdRkqt3",
661
- "nonce": "n-0S6_WzA2Mj",
662
- "exp": 1311281970,
663
- "iat": 1311280970,
664
- }
665
- )
708
+ id_token.stubs(:raw_attributes, :to_h).returns(payload)
666
709
 
667
710
  request.stubs(:params).returns('state' => state, 'nounce' => nonce, 'id_token' => id_token)
668
711
  request.stubs(:path_info).returns('')
@@ -27,6 +27,30 @@ class StrategyTestCase < MiniTest::Test
27
27
  }
28
28
  end
29
29
 
30
+ def private_key
31
+ @private_key ||= OpenSSL::PKey::RSA.generate(512)
32
+ end
33
+
34
+ def jwt
35
+ @jwt ||= JSON::JWT.new(payload).sign(private_key, :RS256)
36
+ end
37
+
38
+ def hmac_secret
39
+ @hmac_secret ||= SecureRandom.hex(16)
40
+ end
41
+
42
+ def jwt_with_hs256
43
+ @jwt_with_hs256 ||= JSON::JWT.new(payload).sign(hmac_secret, :HS256)
44
+ end
45
+
46
+ def jwks
47
+ @jwks ||= begin
48
+ key = JSON::JWK.new(private_key)
49
+ keyset = JSON::JWK::Set.new(key)
50
+ { keys: keyset }
51
+ end
52
+ end
53
+
30
54
  def user_info
31
55
  @user_info ||= OpenIDConnect::ResponseObject::UserInfo.new(
32
56
  sub: SecureRandom.hex(16),
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: gitlab-omniauth-openid-connect
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.0
4
+ version: 0.6.0
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: 2021-05-07 00:00:00.000000000 Z
12
+ date: 2021-07-08 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: addressable
@@ -220,7 +220,6 @@ files:
220
220
  - lib/omniauth/openid_connect/version.rb
221
221
  - lib/omniauth/strategies/openid_connect.rb
222
222
  - lib/omniauth_openid_connect.rb
223
- - test/fixtures/id_token.txt
224
223
  - test/fixtures/jwks.json
225
224
  - test/fixtures/test.crt
226
225
  - test/lib/omniauth/strategies/openid_connect_test.rb
@@ -250,7 +249,6 @@ signing_key:
250
249
  specification_version: 4
251
250
  summary: OpenID Connect Strategy for OmniAuth
252
251
  test_files:
253
- - test/fixtures/id_token.txt
254
252
  - test/fixtures/jwks.json
255
253
  - test/fixtures/test.crt
256
254
  - test/lib/omniauth/strategies/openid_connect_test.rb
@@ -1 +0,0 @@
1
- eyJhbGciOiJSUzI1NiIsImtpZCI6IjFlOWdkazcifQ.ewogImlzcyI6ICJodHRwOi8vc2VydmVyLmV4YW1wbGUuY29tIiwKICJzdWIiOiAiMjQ4Mjg5NzYxMDAxIiwKICJhdWQiOiAiczZCaGRSa3F0MyIsCiAibm9uY2UiOiAibi0wUzZfV3pBMk1qIiwKICJleHAiOiAxMzExMjgxOTcwLAogImlhdCI6IDEzMTEyODA5NzAKfQ.ggW8hZ1EuVLuxNuuIJKX_V8a_OMXzR0EHR9R6jgdqrOOF4daGU96Sr_P6qJp6IcmD3HP99Obi1PRs-cwh3LO-p146waJ8IhehcwL7F09JdijmBqkvPeB2T9CJNqeGpe-gccMg4vfKjkM8FcGvnzZUN4_KSP0aAp1tOJ1zZwgjxqGByKHiOtX7TpdQyHE5lcMiKPXfEIQILVq0pc_E2DzL7emopWoaoZTF_m0_N0YzFC6g6EJbOEoRoSK5hoDalrcvRYLSrQAZZKflyuVCyixEoV9GfNQC3_osjzw2PAithfubEEBLuVVk4XUVrWOLrLl0nx7RkKU8NXNHq-rvKMzqg