Skip to content

Support response_type: ["code", "id_token"] #139

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ config.omniauth :openid_connect, {
| 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 |
| client_auth_method | Which authentication method to use to authenticate your app with the authorization server | no | Sym: basic | "basic", "jwks" |
| scope | Which OpenID scopes to include (:openid is always required) | no | Array<sym> [:openid] | [:openid, :profile, :email] |
| response_type | Which OAuth2 response type to use with the authorization request | no | String: code | one of: 'code', 'id_token' |
| response_type | Which OAuth2 response type to use with the authorization request | no | String or Array: code | 'code', 'id_token', or ['code', 'id_token'] |
| 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) } |
| require_state | Should state param be verified - this is recommended, not required by the OIDC specification | no | true | false |
| 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 |
Expand Down
14 changes: 8 additions & 6 deletions lib/omniauth/strategies/openid_connect.rb
Original file line number Diff line number Diff line change
Expand Up @@ -125,11 +125,11 @@ def callback_phase

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

verify_id_token!(params['id_token']) if configured_response_type == 'id_token'
verify_id_token!(params['id_token']) if configured_response_types.include?('id_token')
discover!
client.redirect_uri = redirect_uri

return id_token_callback_phase if configured_response_type == 'id_token'
return id_token_callback_phase if configured_response_types.include?('id_token')

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

@access_token = client.access_token!(token_request_params)
verify_id_token!(@access_token.id_token) if configured_response_type == 'code'
verify_id_token!(@access_token.id_token) if configured_response_types.include?('code')

@access_token
end
Expand Down Expand Up @@ -372,16 +372,18 @@ def id_token_callback_phase
end

def valid_response_type?
return true if params.key?(configured_response_type)
return true if configured_response_types.all? { |key| params[key].present? }
Copy link
Contributor

@stanhu stanhu Jan 23, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In looking at https://openid.net/specs/oauth-v2-multiple-response-types-1_0.html#MultiValueResponseTypes and https://www.rfc-editor.org/rfc/rfc6749 closely, is this the correct mapping?

response_type Expected parameters
code token code, access_token, token_type
code id_token code, id_token
id_token token id_token, access_token, token_type
code id_token token code, id_token, access_token, token_type

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So while this change is correct for code id_token, I think we probably want to handle these cases here. It looks to me that every time token shows up, access_token and token_type come along.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for laying this out @stanhu. I don't have the bandwidth to round out the other combinations, but this changeset covers our use case.

Could we merge this change, then subsequently cover the rest?

If not, feel free to take this over.

Thank you!


configured_response_type, * = configured_response_types

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

false
end

def configured_response_type
@configured_response_type ||= options.response_type.to_s
def configured_response_types
@configured_response_types ||= Array(options.response_type).map(&:to_s)
end

def verify_id_token!(id_token)
Expand Down
46 changes: 46 additions & 0 deletions test/lib/omniauth/strategies/openid_connect_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,28 @@ def test_request_phase_with_response_mode_symbol
strategy.request_phase
end

def test_request_phase_with_response_mode_array
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}$}
strategy.options.issuer = 'example.com'
strategy.options.response_mode = 'form_post'
strategy.options.response_type = %w[code id_token]
strategy.options.client_options.host = 'example.com'

strategy.expects(:redirect).with(regexp_matches(expected_redirect))
strategy.request_phase
end

def test_request_phase_with_response_mode_symbol_array
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}$}
strategy.options.issuer = 'example.com'
strategy.options.response_mode = 'form_post'
strategy.options.response_type = %i[code id_token]
strategy.options.client_options.host = 'example.com'

strategy.expects(:redirect).with(regexp_matches(expected_redirect))
strategy.request_phase
end

def test_option_acr_values
strategy.options.client_options[:host] = 'foobar.com'

Expand Down Expand Up @@ -456,6 +478,30 @@ def test_callback_phase_without_id_token_symbol
strategy.callback_phase
end

def test_callback_phase_without_code_and_id_token
state = SecureRandom.hex(16)
request.stubs(:params).returns('state' => state)
request.stubs(:path).returns('')
strategy.options.response_type = %w[code id_token]

strategy.call!('rack.session' => { 'omniauth.state' => state, 'omniauth.nonce' => nonce })

strategy.expects(:fail!).with(:missing_code, is_a(OmniAuth::OpenIDConnect::MissingCodeError))
strategy.callback_phase
end

def test_callback_phase_without_code_and_id_token_symbol
state = SecureRandom.hex(16)
request.stubs(:params).returns('state' => state)
request.stubs(:path).returns('')
strategy.options.response_type = %i[code id_token]

strategy.call!('rack.session' => { 'omniauth.state' => state, 'omniauth.nonce' => nonce })

strategy.expects(:fail!).with(:missing_code, is_a(OmniAuth::OpenIDConnect::MissingCodeError))
strategy.callback_phase
end

def test_callback_phase_with_timeout
code = SecureRandom.hex(16)
state = SecureRandom.hex(16)
Expand Down