Skip to content

Commit 09af6dc

Browse files
committed
Support response_type: ["code", "id_token"]
Closes [omniauth#105][] Similar to [omniauth#107][] Some OpenID compatible IdP support hybrid authorizations that accept a `response_type` with both `code` and `id_token`. For example, [Microsoft Azure B2C][] accepts them as a URL-encoded array: > `response_type`: Must include an ID token for OpenID Connect. If your web application also needs tokens for calling a web API, you can use `code+id_token`. This commit extends the `OmniAuth::Strategies::OpenIDConnect` to encode the `response_type` into the query parameter as space-delimited token list when provided as an array. Similarly, when checking for missing keys in the response, iterate over the values as if they're an array. For the originally supported single-value case, the previous behavior is maintained. [Microsoft Azure B2C]: https://learn.microsoft.com/en-us/azure/active-directory-b2c/openid-connect#send-authentication-requests [omniauth#105]: omniauth#105 [omniauth#107]: omniauth#107
1 parent 4e16f70 commit 09af6dc

File tree

3 files changed

+55
-7
lines changed

3 files changed

+55
-7
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ config.omniauth :openid_connect, {
5454
| discovery | Should OpenID discovery be used. This is recommended if the IDP provides a discovery endpoint. See client config for how to manually enter discovered values. | no | false | one of: true, false |
5555
| client_auth_method | Which authentication method to use to authenticate your app with the authorization server | no | Sym: basic | "basic", "jwks" |
5656
| scope | Which OpenID scopes to include (:openid is always required) | no | Array<sym> [:openid] | [:openid, :profile, :email] |
57-
| response_type | Which OAuth2 response type to use with the authorization request | no | String: code | one of: 'code', 'id_token' |
57+
| response_type | Which OAuth2 response type to use with the authorization request | no | String or Array: code | 'code', 'id_token', or ['code', 'id_token'] |
5858
| state | A value to be used for the OAuth2 state parameter on the authorization request. Can be a proc that generates a string. | no | Random 16 character string | Proc.new { SecureRandom.hex(32) } |
5959
| require_state | Should state param be verified - this is recommended, not required by the OIDC specification | no | true | false |
6060
| response_mode | The response mode per [spec](https://openid.net/specs/oauth-v2-form-post-response-mode-1_0.html) | no | nil | one of: :query, :fragment, :form_post, :web_message |

lib/omniauth/strategies/openid_connect.rb

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -125,11 +125,11 @@ def callback_phase
125125

126126
options.issuer = issuer if options.issuer.nil? || options.issuer.empty?
127127

128-
verify_id_token!(params['id_token']) if configured_response_type == 'id_token'
128+
verify_id_token!(params['id_token']) if configured_response_types.include?('id_token')
129129
discover!
130130
client.redirect_uri = redirect_uri
131131

132-
return id_token_callback_phase if configured_response_type == 'id_token'
132+
return id_token_callback_phase if configured_response_types.include?('id_token')
133133

134134
client.authorization_code = authorization_code
135135
access_token
@@ -260,7 +260,7 @@ def access_token
260260
token_request_params[:code_verifier] = params['code_verifier'] || session.delete('omniauth.pkce.verifier') if options.pkce
261261

262262
@access_token = client.access_token!(token_request_params)
263-
verify_id_token!(@access_token.id_token) if configured_response_type == 'code'
263+
verify_id_token!(@access_token.id_token) if configured_response_types.include?('code')
264264

265265
@access_token
266266
end
@@ -372,16 +372,18 @@ def id_token_callback_phase
372372
end
373373

374374
def valid_response_type?
375-
return true if params.key?(configured_response_type)
375+
return true if configured_response_types.all? { |key| params.key?(key) }
376+
377+
configured_response_type, * = configured_response_types
376378

377379
error_attrs = RESPONSE_TYPE_EXCEPTIONS[configured_response_type]
378380
fail!(error_attrs[:key], error_attrs[:exception_class].new(params['error']))
379381

380382
false
381383
end
382384

383-
def configured_response_type
384-
@configured_response_type ||= options.response_type.to_s
385+
def configured_response_types
386+
@configured_response_types ||= Array(options.response_type).map(&:to_s)
385387
end
386388

387389
def verify_id_token!(id_token)

test/lib/omniauth/strategies/openid_connect_test.rb

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,28 @@ def test_request_phase_with_response_mode_symbol
138138
strategy.request_phase
139139
end
140140

141+
def test_request_phase_with_response_mode_array
142+
expected_redirect = %r{^https://example\.com/authorize\?client_id=1234&nonce=\w{32}&response_mode=form_post&response_type=code%20id_token&scope=openid&state=\w{32}$}
143+
strategy.options.issuer = 'example.com'
144+
strategy.options.response_mode = 'form_post'
145+
strategy.options.response_type = ['code', 'id_token']
146+
strategy.options.client_options.host = 'example.com'
147+
148+
strategy.expects(:redirect).with(regexp_matches(expected_redirect))
149+
strategy.request_phase
150+
end
151+
152+
def test_request_phase_with_response_mode_symbol_array
153+
expected_redirect = %r{^https://example\.com/authorize\?client_id=1234&nonce=\w{32}&response_mode=form_post&response_type=code%20id_token&scope=openid&state=\w{32}$}
154+
strategy.options.issuer = 'example.com'
155+
strategy.options.response_mode = 'form_post'
156+
strategy.options.response_type = [:code, :id_token]
157+
strategy.options.client_options.host = 'example.com'
158+
159+
strategy.expects(:redirect).with(regexp_matches(expected_redirect))
160+
strategy.request_phase
161+
end
162+
141163
def test_option_acr_values
142164
strategy.options.client_options[:host] = 'foobar.com'
143165

@@ -456,6 +478,30 @@ def test_callback_phase_without_id_token_symbol
456478
strategy.callback_phase
457479
end
458480

481+
def test_callback_phase_without_code_and_id_token
482+
state = SecureRandom.hex(16)
483+
request.stubs(:params).returns('state' => state)
484+
request.stubs(:path).returns('')
485+
strategy.options.response_type = ['code', 'id_token']
486+
487+
strategy.call!('rack.session' => { 'omniauth.state' => state, 'omniauth.nonce' => nonce })
488+
489+
strategy.expects(:fail!).with(:missing_code, is_a(OmniAuth::OpenIDConnect::MissingCodeError))
490+
strategy.callback_phase
491+
end
492+
493+
def test_callback_phase_without_code_and_id_token_symbol
494+
state = SecureRandom.hex(16)
495+
request.stubs(:params).returns('state' => state)
496+
request.stubs(:path).returns('')
497+
strategy.options.response_type = [:code, :id_token]
498+
499+
strategy.call!('rack.session' => { 'omniauth.state' => state, 'omniauth.nonce' => nonce })
500+
501+
strategy.expects(:fail!).with(:missing_code, is_a(OmniAuth::OpenIDConnect::MissingCodeError))
502+
strategy.callback_phase
503+
end
504+
459505
def test_callback_phase_with_timeout
460506
code = SecureRandom.hex(16)
461507
state = SecureRandom.hex(16)

0 commit comments

Comments
 (0)