omniauth_openid_connect 0.5.0 → 0.6.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|