gitlab-omniauth-openid-connect 0.5.0 → 0.6.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: 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