Skip to content
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

How not to send the state parameter? #174

Open
amessinger opened this issue Mar 5, 2024 · 21 comments · Fixed by #181
Open

How not to send the state parameter? #174

amessinger opened this issue Mar 5, 2024 · 21 comments · Fixed by #181

Comments

@amessinger
Copy link

amessinger commented Mar 5, 2024

Hi there! Thanks for creating & maintaining this gem :)

I'd like to prevent this client from sending the state parameter during the request phase because in some cases I can't rely on the session to store it.

I thought the require_state config option set to false would do the trick but it seems to be used only for validation during the callback phase :

invalid_state = (options.require_state && params['state'].to_s.empty?) || params['state'] != stored_state
)

From what I understand, the state parameter is still sent during the request phase, the identity provider forwards it back to the callback URL and this state value in the URL's parameters, though valid, can't be matched to the session's state value since the session is not available.

Couldn't the creation of the state parameter be made conditionally to the value of the require_state config option?

          [..]
          state: (new_state if options.require_state),
          [...]

Any help would be appreciated!

@stanhu
Copy link
Contributor

stanhu commented Jun 25, 2024

@skycocker @syakovyn @bufferoverflow I think require_state was added in #127. Do you see any reason why we couldn't simplify this via:

diff --git a/lib/omniauth/strategies/openid_connect.rb b/lib/omniauth/strategies/openid_connect.rb
index ebfaaa1..6d80a66 100644
--- a/lib/omniauth/strategies/openid_connect.rb
+++ b/lib/omniauth/strategies/openid_connect.rb
@@ -120,7 +120,12 @@ module OmniAuth
       def callback_phase
         error = params['error_reason'] || params['error']
         error_description = params['error_description'] || params['error_reason']
-        invalid_state = (options.require_state && params['state'].to_s.empty?) || params['state'] != stored_state
+        invalid_state =
+          if options.require_state
+            params['state'].to_s.empty? || params['state'] != stored_state
+          else
+            false
+          end
 
         raise CallbackError, error: params['error'], reason: error_description, uri: params['error_uri'] if error
         raise CallbackError, error: :csrf_detected, reason: "Invalid 'state' parameter" if invalid_state

I'm not sure if state is being used somewhere else, but if it's just forwarded back, there's no use in generating it?

@stanhu
Copy link
Contributor

stanhu commented Jun 25, 2024

Oh, I see that was proposed in #127 (comment).

The issue here is that if the state is generated by the authorize_uri, chances are the OpenID provider will pass it along. So maybe we should have gone with the original implementation?

@amessinger
Copy link
Author

amessinger commented Jun 25, 2024

Hi! Thanks for looking into this.

In the mean time, I've gone ahead and made a working though naive implementation of a solution introducing an additional send_state config parameter.

I went with this solution because I didn't want to alter the behavior of the require_state config parameter.

amessinger@2a2510c

Hope this helps!

@stanhu
Copy link
Contributor

stanhu commented Jun 25, 2024

Yeah, I'm trying to avoid adding yet another config parameter here, plus clear up some confusion about how require_state is supposed to work. I'm not sure how require_state ever worked as false because stored_state will always have a value since new_state always sets the value to some random value.

@stanhu
Copy link
Contributor

stanhu commented Jun 27, 2024

I submitted #181 to fix require_state. Please feel to review.

@amessinger
Copy link
Author

amessinger commented Jun 28, 2024

Thank you @stanhu for this!

The fix you're proposing allows me to complete my scenario of a stateless user agent which is not able to forward the state parameter for reasons out of my reach (aka my client's app :) ).

I think my initial intention was to prevent sending the state parameter during the request phase and make it so that it would not be expected during the callback phase.

I realize the "prevent sending de state parameter" part is more of a nice to have for me. It appears to be something the OIDC specs you mentioned in your PR aligns with me about base on the use of the "RECOMMENDED" key word.

That being said I understand that this library might wish to very slightly deviate from the specs to enforce good practices.

Please forgive me if I appear picky, I think this research and write up also serve me as a learning exercise on the subject.

Have a great rest of your day and thanks again!

@stanhu
Copy link
Contributor

stanhu commented Jun 28, 2024

I think my initial intention was to prevent sending the state parameter during the request phase and make it so that it would not be expected during the callback phase.

@amessinger Thanks, we could consider using require_state to stop sending state as well. https://openid.net/specs/openid-connect-core-1_0.html says this about the authentication response:

state
    OAuth 2.0 state value. REQUIRED if the state parameter is present in the Authorization Request. Clients MUST verify that the state value is equal to the value of state parameter in the Authorization Request. 

A OIDC provider that drops the state from the response is thus not conforming to the spec.

@amessinger
Copy link
Author

I think your interpretation of the specs is correct.

Maybe require_state could then be renamed send_state since, as I understand, the action of checking the state parameter would become a consequence of the client sending it in the first place?

Or maybe a more encompassing name would be more appropriate such as use_state or enable_state. I understand such a change would be breaking the API of the library so this might not be worth the hassle...

@BobbyMcWho
Copy link
Member

Let me know whenever y'all are ready for the PR to be merged. I'll have to double check to see if this is one of the Omniauth libraries that I actually have rubygems release privileges for.

@syakovyn
Copy link

I am sorry, but I have an issue with the decision made.

I have the 2 scenarios:

  1. The service provider sends a state to the IdP and expects it back. I'm expecting the IdP to return the sent state back.
  2. The IdP makes a direct request to the service provider without the state. No state check is expected here.

With the previous implementation, I could cover both cases with require_state = false.
Now, there is no check in the first case with require_state = false, or the check fails in the second scenario when require_state = true.

I would be happy with any suggestion that can cover both cases.
Probably, send_state or verify_state?

Thanks,
Sergii

@stanhu
Copy link
Contributor

stanhu commented Jun 28, 2024

@syakovyn Thanks for raising your concerns here.

The service provider sends a state to the IdP and expects it back. I'm expecting the IdP to return the sent state back.

As discussed above, if we modified require_state NOT to send the state parameter in the first place, would this handle this case?

The IdP makes a direct request to the service provider without the state. No state check is expected here.

I'm a little confused about this case. In this case, the callback_phase is invoked directly from the IdP without authorize_uri? Perhaps a flow diagram would help.

@stanhu stanhu reopened this Jun 28, 2024
@syakovyn
Copy link

syakovyn commented Jun 28, 2024

As discussed above, if we modified require_state NOT to send the state parameter in the first place, would this handle this case?

I'd like to keep sending the state parameter by my service provider and expect the 3rd party IdP to return it. I'm pretty happy with require_state = true as it does what I need here.

I'm a little confused about this case. In this case, the callback_phase is invoked directly from the IdP without authorize_uri?

Exactly. The IdP keeps registered service providers and allows users to sign into a service provider directly from the IdP site. I am required to support that flow and, as the IdP doesn't have the state to send it fails with an error when require_state = true.

I thought having require_state = false would be reasonable to work in both cases meaning the gem doesn't require the state to be present in the response but validates if it matches if present.

The problem is that, in the first case, I want to send the state and validate it as suggested by the OIDC specification, and, in the second case, it looks like I didn't send the state though it's because the flow starts right away from the callback_phase.

@stanhu
Copy link
Contributor

stanhu commented Jun 28, 2024

@syakovyn How does the IdP gain authorization to make this callback, though? Does this mean that anyone could spoof the IdP? UPDATE: I suppose the IdP somehow has a valid ID token?

I see https://openid.net/specs/openid-connect-core-1_0.html#ThirdPartyInitiatedLogin touches on this.

@syakovyn
Copy link

syakovyn commented Jun 28, 2024

@stanhu, the IdP (in my case it's Clever) uses the first redirect URI and sends the ID token of the user that initiated that request.

UPDATE: It is similar to Send ID Token directly to app (Okta Simplified) described on https://support.okta.com/help/s/article/How-to-make-the-OIDCOAuth-App-visible-in-Okta-dashboard-and-what-are-the-login-flows-available?language=en_US

My case is also described on https://stackoverflow.com/a/51050953/419735

@stanhu
Copy link
Contributor

stanhu commented Jul 2, 2024

@syakovyn Interesting, thanks. I suppose we should revert #181 and then introduce a send_state parameter. The require_state setting is confusing, though.

@syakovyn
Copy link

syakovyn commented Jul 2, 2024

Actually, I don't need require_state if the condition is invalid_state = params['state'] != stored_state because, in the 1st scenario, I have a stored state and want it to match the one returned by IdP and in the 2nd scenario I have it neither stored nor passed by an IdP.

Thus, having some option not to have/generate the state should cover my 2 scenarios and this one. E.g. allowing state = nil as a configuration parameter in addition to state = Proc.new { SecureRandom.hex(32) }, storing and sending the generated state only if it can be generated.

I would opt for generating the state (probably should be a default setting) and @amessinger will opt out of generating the state.

Or even reusing the current state option with state = Proc.new { nil } and not sending nil state.
What do you think of it, @stanhu?

@amessinger
Copy link
Author

hi @syakovyn, that's fine by me since I've been doing it on my fork 😉 specifically I do agree that sending the state should done by default.

@stanhu
Copy link
Contributor

stanhu commented Jul 3, 2024

I think think an explicit send_state is less confusing than requiring state = Proc.new { nil } and allowing a blank state.

require_state is confusing because in the most common case where you expect the authorize step to run, I would expect thatstate be omitted entirely from the request and thus ignored in the response.

stanhu added a commit that referenced this issue Jul 3, 2024
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, so
`require_state` to fail the request would break the existing flow.

2. If `state` isn't used, it shouldn't be sent in the first place.

`send_state` will now disable the sending of a `state` in the
authorize phase.
stanhu added a commit that referenced this issue Jul 3, 2024
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.
@stanhu
Copy link
Contributor

stanhu commented Jul 3, 2024

Please take a look at #182.

stanhu added a commit that referenced this issue Jul 3, 2024
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.
stanhu added a commit that referenced this issue Jul 3, 2024
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.
@amessinger
Copy link
Author

amessinger commented Jul 4, 2024

Just tested the changes shipped in #182. Works like a charm on my local environment (didn't have to change a thing appart from the Gemfile since the API is the same as on my forked implementation).

Thanks a lot, I'd say this issue can be closed.

@syakovyn
Copy link

syakovyn commented Jul 8, 2024

The change works for me too. Thanks @stanhu

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

4 participants