Skip to content

Commit 02839d7

Browse files
committed
feat: Add send_state parameter to disable sending of state
This reverts #181 and adds a `send_state` parameter instead to address #174. According to https://openid.net/specs/openid-connect-core-1_0.html#rfc.section.3.1.2.1, `state` is recommended but not required: ``` state RECOMMENDED. Opaque value used to maintain state between the request and the callback. Typically, Cross-Site Request Forgery (CSRF, XSRF) mitigation is done by cryptographically binding the value of this parameter with a browser cookie. ``` In #181 we attempted to make `require_state` skip the `state` verification if it were `true`, but this was reverted for two reasons: 1. If identity providers make direct requests to the callback phase with a valid token, no `state` is passed in the request. If `require_state` were `true`, this change fails the request and breaks existing flows. 2. If `state` isn't sent in the first place, it should not be verified. `send_state` will now disable the sending of a `state` in the authorize phase.
1 parent bd14191 commit 02839d7

File tree

3 files changed

+55
-4
lines changed

3 files changed

+55
-4
lines changed

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,8 @@ end
7979
| scope | Which OpenID scopes to include (:openid is always required) | no | Array<sym> [:openid] | [:openid, :profile, :email] |
8080
| response_type | Which OAuth2 response type to use with the authorization request | no | String: code | one of: 'code', 'id_token' |
8181
| 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) } |
82-
| require_state | Should state param be verified - this is recommended, not required by the OIDC specification | no | true | false |
82+
| require_state | Should the callback phase require that a state is present. If `send_state` is true, then the callback state must match the authorize state. This is recommended, not required by the OIDC specification. | no | true | false |
83+
| send_state | Should the authorize phase send a `state` parameter - this is recommended, not required by the OIDC specification | no | true | false |
8384
| 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 |
8485
| display | An optional parameter to the authorization request to determine how the authorization and consent page | no | nil | one of: :page, :popup, :touch, :wap |
8586
| prompt | An optional parameter to the authrization request to determine what pages the user will be shown | no | nil | one of: :none, :login, :consent, :select_account |

lib/omniauth/strategies/openid_connect.rb

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ class OpenIDConnect # rubocop:disable Metrics/ClassLength
4242
option :client_x509_signing_key
4343
option :scope, [:openid]
4444
option :response_type, 'code' # ['code', 'id_token']
45+
option :send_state, true
4546
option :require_state, true
4647
option :state
4748
option :response_mode # [:query, :fragment, :form_post, :web_message]
@@ -120,7 +121,12 @@ def request_phase
120121
def callback_phase
121122
error = params['error_reason'] || params['error']
122123
error_description = params['error_description'] || params['error_reason']
123-
invalid_state = (options.require_state && params['state'].to_s.empty?) || params['state'] != stored_state
124+
invalid_state =
125+
if options.send_state
126+
(options.require_state && params['state'].to_s.empty?) || params['state'] != stored_state
127+
else
128+
false
129+
end
124130

125131
raise CallbackError, error: params['error'], reason: error_description, uri: params['error_uri'] if error
126132
raise CallbackError, error: :csrf_detected, reason: "Invalid 'state' parameter" if invalid_state
@@ -169,13 +175,12 @@ def end_session_uri
169175
end_session_uri.to_s
170176
end
171177

172-
def authorize_uri
178+
def authorize_uri # rubocop:disable Metrics/AbcSize
173179
client.redirect_uri = redirect_uri
174180
opts = {
175181
response_type: options.response_type,
176182
response_mode: options.response_mode,
177183
scope: options.scope,
178-
state: new_state,
179184
login_hint: params['login_hint'],
180185
ui_locales: params['ui_locales'],
181186
claims_locales: params['claims_locales'],
@@ -185,6 +190,7 @@ def authorize_uri
185190
acr_values: options.acr_values,
186191
}
187192

193+
opts[:state] = new_state if options.send_state
188194
opts.merge!(options.extra_authorize_params) unless options.extra_authorize_params.empty?
189195

190196
options.allow_authorize_params.each do |key|

test/lib/omniauth/strategies/openid_connect_test.rb

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -453,6 +453,50 @@ def test_callback_phase_with_discovery # rubocop:disable Metrics/AbcSize
453453
strategy.callback_phase
454454
end
455455

456+
def test_callback_phase_with_send_state_disabled # rubocop:disable Metrics/AbcSize
457+
code = SecureRandom.hex(16)
458+
459+
strategy.options.client_options.host = 'example.com'
460+
strategy.options.require_state = true
461+
strategy.options.send_state = false
462+
strategy.options.discovery = true
463+
refute_match(/state/, strategy.authorize_uri, 'URI must not contain state')
464+
465+
request.stubs(:params).returns('code' => code)
466+
request.stubs(:path).returns('')
467+
468+
issuer = stub('OpenIDConnect::Discovery::Issuer')
469+
issuer.stubs(:issuer).returns('https://example.com/')
470+
::OpenIDConnect::Discovery::Provider.stubs(:discover!).returns(issuer)
471+
472+
config = stub('OpenIDConnect::Discovery::Provder::Config')
473+
config.stubs(:authorization_endpoint).returns('https://example.com/authorization')
474+
config.stubs(:token_endpoint).returns('https://example.com/token')
475+
config.stubs(:userinfo_endpoint).returns('https://example.com/userinfo')
476+
config.stubs(:jwks_uri).returns('https://example.com/jwks')
477+
config.stubs(:jwks).returns(JSON::JWK::Set.new(jwks['keys']))
478+
479+
::OpenIDConnect::Discovery::Provider::Config.stubs(:discover!).with('https://example.com/').returns(config)
480+
481+
id_token = stub('OpenIDConnect::ResponseObject::IdToken')
482+
id_token.stubs(:raw_attributes).returns('sub' => 'sub', 'name' => 'name', 'email' => 'email')
483+
id_token.stubs(:verify!).with(issuer: 'https://example.com/', client_id: @identifier, nonce: nonce).returns(true)
484+
::OpenIDConnect::ResponseObject::IdToken.stubs(:decode).returns(id_token)
485+
486+
strategy.unstub(:user_info)
487+
access_token = stub('OpenIDConnect::AccessToken')
488+
access_token.stubs(:access_token)
489+
access_token.stubs(:refresh_token)
490+
access_token.stubs(:expires_in)
491+
access_token.stubs(:scope)
492+
access_token.stubs(:id_token).returns(jwt.to_s)
493+
client.expects(:access_token!).at_least_once.returns(access_token)
494+
access_token.expects(:userinfo!).returns(user_info)
495+
496+
strategy.call!('rack.session' => { 'omniauth.nonce' => nonce })
497+
strategy.callback_phase
498+
end
499+
456500
def test_callback_phase_with_no_state_without_state_verification # rubocop:disable Metrics/AbcSize
457501
code = SecureRandom.hex(16)
458502

0 commit comments

Comments
 (0)