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 +4 -4
- data/.github/workflows/main.yml +3 -3
- data/CHANGELOG.md +8 -0
- data/README.md +1 -0
- data/lib/omniauth/openid_connect/version.rb +1 -1
- data/lib/omniauth/strategies/openid_connect.rb +90 -15
- data/test/lib/omniauth/strategies/openid_connect_test.rb +133 -1
- data/test/strategy_test_case.rb +12 -0
- metadata +5 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 446a75e37d0a98638c32b054b7e4e1443b54c8d067025381ca340e2a80d5db05
|
4
|
+
data.tar.gz: fe1895242ce7bd7d1910d9db085678cc5cadc9757b62a7660a232462105d21fe
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c102811330e2e73ea3a76940c4c275799ef01b8b4640a3e49203a9febec8ecdcb16d9f2a48d2df67deb402f65914842964b656e82c71e1676108db7d54577252
|
7
|
+
data.tar.gz: 2c6e454846927acd7f2456b7b9653f356ac96d3a196b7299ca348c5bcb9f4853a802154da2548bfc86bc65ec3088abcabb16ea1ab37dc7cc0285cfae0c938a9c
|
data/.github/workflows/main.yml
CHANGED
@@ -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@
|
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@
|
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
|
|
@@ -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
|
204
|
-
|
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
|
-
::
|
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
|
312
|
-
@
|
313
|
-
|
314
|
-
|
315
|
-
|
316
|
-
|
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.
|
834
|
+
assert_equal strategy.options.client_options.secret, strategy.secret
|
703
835
|
end
|
704
836
|
|
705
837
|
def test_id_token_auth_hash
|
data/test/strategy_test_case.rb
CHANGED
@@ -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.
|
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:
|
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.
|
223
|
-
source_code_uri: https://github.com/m0n9oose/omniauth_openid_connect/tree/v0.
|
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.
|
240
|
+
rubygems_version: 3.4.7
|
241
241
|
signing_key:
|
242
242
|
specification_version: 4
|
243
243
|
summary: OpenID Connect Strategy for OmniAuth
|