gitlab-omniauth-openid-connect 0.4.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: ba1a1f85a4f4302aa277a818b0c6f1a19f0aa09bd10815a6cc339cbc8e19b630
4
- data.tar.gz: 91ded3eb14016b4ee15823b34e8c2b6960e7bafbcb5812596722c469b101e91f
3
+ metadata.gz: 13c881e6fc6d97b86a4608afaac9f44737d83650de5f00bb102deab1dc723c89
4
+ data.tar.gz: 3f116b19d3759309dfd6369671dffdddafe5362c3ecf9c4db16cc25c58b6c4ee
5
5
  SHA512:
6
- metadata.gz: 7f89ac2119d55244bec0c7a5f1b155b082e2e8c8d9f66ff54dbda173e167872847898d53b0d6cf6eb5d670c3d262ebfe1a0632bc25cfaf5ef178b586cae62de8
7
- data.tar.gz: e29da0221ea0895ce5951239ffc347669ceff53b8574a9e463ee00392c5aec032d61ea9544b0f4ee47395e440c3c2ddde0ed42ecfe54daa78e4cb204d1c8f2d9
6
+ metadata.gz: 80d59151cc0657817732e4d85bdee536fa328c40fc0a16b379172c88b62fc7bd25bfc156a7e10885c16277a0a83984f6e34e6223c47cc746cceaa9c264c7d20f
7
+ data.tar.gz: 251bbd0f19557183b39c72cc679579ce1550573786bfd41fa213f265e19158f27d491309a6199cdebf081c8ba72bffc6a11d9a25a68d0d7b7bfeac113880c061
data/CHANGELOG.md CHANGED
@@ -1,3 +1,13 @@
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
+
5
+ # v0.5.0 (05.07.2021)
6
+
7
+ - [Add email_verified field to info dict](https://gitlab.com/gitlab-org/gitlab-omniauth-openid-connect/-/merge_requests/7)
8
+ - [Simplify error handling for decoding individual keys](https://gitlab.com/gitlab-org/gitlab-omniauth-openid-connect/-/merge_requests/6)
9
+ - [Always convert client_signing_alg to be a symbol](https://gitlab.com/gitlab-org/gitlab-omniauth-openid-connect/-/merge_requests/5)
10
+
1
11
  # v0.4.0 (04.23.2021)
2
12
 
3
13
  - [Fetch key from JWKS URI if available](https://gitlab.com/gitlab-org/gitlab-omniauth-openid-connect/-/merge_requests/3)
@@ -2,6 +2,6 @@
2
2
 
3
3
  module OmniAuth
4
4
  module OpenIDConnect
5
- VERSION = '0.4.0'
5
+ VERSION = '0.6.0'
6
6
  end
7
7
  end
@@ -65,6 +65,7 @@ module OmniAuth
65
65
  {
66
66
  name: user_info.name,
67
67
  email: user_info.email,
68
+ email_verified: user_info.email_verified,
68
69
  nickname: user_info.preferred_username,
69
70
  first_name: user_info.given_name,
70
71
  last_name: user_info.family_name,
@@ -235,31 +236,55 @@ module OmniAuth
235
236
  @access_token
236
237
  end
237
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
238
247
  def decode_id_token(id_token)
239
- 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)
240
261
  rescue JSON::JWK::Set::KidNotFound
241
- # Either the JWT doesn't have kid specified or the set of keys doesn't
242
- # have a matching key. Since we can't tell the first case from the second,
243
- # 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:
244
266
  # https://github.com/nov/json-jwt/pull/92#issuecomment-824654949
245
- decoded = decode_with_each_key!(id_token)
267
+ raise if decoded&.header&.key?('kid')
268
+
269
+ decoded = decode_with_each_key!(id_token, keyset)
246
270
 
247
271
  raise unless decoded
248
272
 
249
273
  decoded
274
+
250
275
  end
251
276
 
252
277
  def decode!(id_token, key)
253
278
  ::OpenIDConnect::ResponseObject::IdToken.decode(id_token, key)
254
279
  end
255
280
 
256
- def decode_with_each_key!(id_token)
257
- 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)
258
283
 
259
- public_key.each do |key|
284
+ keyset.each do |key|
260
285
  begin
261
286
  decoded = decode!(id_token, key)
262
- rescue JSON::JWK::Set::KidNotFound, JSON::JWS::VerificationFailed
287
+ rescue JSON::JWS::VerificationFailed, JSON::JWS::UnexpectedAlgorithm, JSON::JWS::UnknownAlgorithm
263
288
  next
264
289
  end
265
290
 
@@ -303,8 +328,8 @@ module OmniAuth
303
328
  end
304
329
 
305
330
  def key_or_secret
306
- @key_or_secret ||=
307
- case options.client_signing_alg
331
+ @key_or_secret ||= begin
332
+ case options.client_signing_alg&.to_sym
308
333
  when :HS256, :HS384, :HS512
309
334
  client_options.secret
310
335
  when :RS256, :RS384, :RS512
@@ -314,6 +339,7 @@ module OmniAuth
314
339
  parse_x509_key(options.client_x509_signing_key)
315
340
  end
316
341
  end
342
+ end
317
343
  end
318
344
 
319
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')
@@ -477,6 +531,7 @@ module OmniAuth
477
531
  info = strategy.info
478
532
  assert_equal user_info.name, info[:name]
479
533
  assert_equal user_info.email, info[:email]
534
+ assert_equal user_info.email_verified, info[:email_verified]
480
535
  assert_equal user_info.preferred_username, info[:nickname]
481
536
  assert_equal user_info.given_name, info[:first_name]
482
537
  assert_equal user_info.family_name, info[:last_name]
@@ -493,7 +548,7 @@ module OmniAuth
493
548
  def test_credentials
494
549
  strategy.options.issuer = 'example.com'
495
550
  strategy.options.client_signing_alg = :RS256
496
- strategy.options.client_jwk_signing_key = File.read('test/fixtures/jwks.json')
551
+ strategy.options.client_jwk_signing_key = jwks.to_json
497
552
 
498
553
  id_token = stub('OpenIDConnect::ResponseObject::IdToken')
499
554
  id_token.stubs(:verify!).returns(true)
@@ -504,7 +559,7 @@ module OmniAuth
504
559
  access_token.stubs(:refresh_token).returns(SecureRandom.hex(16))
505
560
  access_token.stubs(:expires_in).returns(Time.now)
506
561
  access_token.stubs(:scope).returns('openidconnect')
507
- access_token.stubs(:id_token).returns(File.read('test/fixtures/id_token.txt'))
562
+ access_token.stubs(:id_token).returns(jwt.to_s)
508
563
 
509
564
  client.expects(:access_token!).returns(access_token)
510
565
  access_token.expects(:refresh_token).returns(access_token.refresh_token)
@@ -591,11 +646,11 @@ module OmniAuth
591
646
  strategy.options.issuer = 'foobar.com'
592
647
  strategy.options.client_auth_method = :not_basic
593
648
  strategy.options.client_signing_alg = :RS256
594
- strategy.options.client_jwk_signing_key = File.read('test/fixtures/jwks.json')
649
+ strategy.options.client_jwk_signing_key = jwks.to_json
595
650
 
596
651
  json_response = {
597
652
  access_token: 'test_access_token',
598
- id_token: File.read('test/fixtures/id_token.txt'),
653
+ id_token: jwt.to_s,
599
654
  token_type: 'Bearer',
600
655
  }.to_json
601
656
  success = Struct.new(:status, :body).new(200, json_response)
@@ -618,16 +673,14 @@ module OmniAuth
618
673
 
619
674
  def test_public_key_with_jwks
620
675
  strategy.options.client_signing_alg = :RS256
621
- strategy.options.client_jwk_signing_key = File.read('./test/fixtures/jwks.json')
676
+ strategy.options.client_jwk_signing_key = jwks.to_json
622
677
 
623
678
  assert_equal JSON::JWK::Set, strategy.public_key.class
624
679
  end
625
680
 
626
681
  def test_public_key_with_jwk
627
682
  strategy.options.client_signing_alg = :RS256
628
- jwks_str = File.read('./test/fixtures/jwks.json')
629
- jwks = JSON.parse(jwks_str)
630
- jwk = jwks['keys'].first
683
+ jwk = jwks[:keys].first
631
684
  strategy.options.client_jwk_signing_key = jwk.to_json
632
685
 
633
686
  assert_equal JSON::JWK, strategy.public_key.class
@@ -652,16 +705,7 @@ module OmniAuth
652
705
 
653
706
  id_token = stub('OpenIDConnect::ResponseObject::IdToken')
654
707
  id_token.stubs(:verify!).returns(true)
655
- id_token.stubs(:raw_attributes, :to_h).returns(
656
- {
657
- "iss": "http://server.example.com",
658
- "sub": "248289761001",
659
- "aud": "s6BhdRkqt3",
660
- "nonce": "n-0S6_WzA2Mj",
661
- "exp": 1311281970,
662
- "iat": 1311280970,
663
- }
664
- )
708
+ id_token.stubs(:raw_attributes, :to_h).returns(payload)
665
709
 
666
710
  request.stubs(:params).returns('state' => state, 'nounce' => nonce, 'id_token' => id_token)
667
711
  request.stubs(:path_info).returns('')
@@ -27,11 +27,36 @@ 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),
33
57
  name: Faker::Name.name,
34
58
  email: Faker::Internet.email,
59
+ email_verified: Faker::Boolean.boolean,
35
60
  nickname: Faker::Name.first_name,
36
61
  preferred_username: Faker::Internet.user_name,
37
62
  given_name: Faker::Name.first_name,
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.4.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-04-24 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