OAuth2 Cross Site Request Forgery, and state parameter

Let's walk through how this attack works.

The Attack

  1. I visit some client's website and start the process of authorizing that client to access some service provider using OAuth

  2. The client asks the service provider for permission to request access on my behalf, which is granted

  3. I am redirected to the service provider's website, where I would normally enter my username/password in order to authorize access

  4. Instead, I trap/prevent this request and save its URL

  5. I somehow get you to visit that URL instead. If you are logged-in to the service provider with your own account, then your credentials will be used to issue an authorization code

  6. The authorization code is exchanged for an access token

  7. Now my account on the client is authorized to access your account on the service provider

So, how do we prevent this using the state parameter?

Prevention

  1. The client should create a value that is somehow based on the original user's account (a hash of the user's session key, for example). It doesn't matter what it is as long as it's unique and generated using some private information about the original user.

  2. This value is passed to the service provider in the redirect from step three above

  3. Now, I get you to visit the URL I saved (step five above)

  4. The authorization code is issued and sent back to the client in your session along with the state parameter

  5. The client generates a state value based on your session information and compares it to the state value that was sent back from the authorization request to the service provider. This value does not match the state parameter on the request, because that state value was generated based on my session information, so it is rejected.

Your Questions

  • Should the randomly generated state be hashed or can same value be stored and sent to OAuth2 provider?

The point is that the attacker should not be able to generate a state value that is specific to a given user. It should be unguessable.

  • Is there a difference here, if session back-end is secure-cookies or a server-side storage (in GAE Memcache or database)?

I don't think this matters (if I understand you correctly)

  • Should state be stored as a key as suggested?

I don't know what this means.

  • Should state has validity period, or session (if there is one) lifetime is enough?

Yes, state should have an expiration. It doesn't necessarily have to be tied to the session, but it could be.


I will simplify this problem. Cross-Site Request Forgery and Clikjacking attacks are useful because it can force a victim's browser into performing actions against their will.

The mention of 10.12. Cross-Site Request Forgery and 10.13. Clickjacking in the OAuth v2 RFC have fundamentally the same concern. If an attacker can force a victim's browser into authenticating, then it is a useful step in forcing the victim's browser into performing other actions.

   in a clickjacking attack, an attacker registers a legitimate client
   and then constructs a malicious site in which it loads the
   authorization server's authorization endpoint web page in a
   transparent iframe overlaid on top of a set of dummy buttons, which
   are carefully constructed to be placed directly under important
   buttons on the authorization page.  When an end-user clicks a
   misleading visible button, the end-user is actually clicking an
   invisible button on the authorization page (such as an "Authorize"
   button).  This allows an attacker to trick a resource owner into
   granting its client access without their knowledge.

Source: 10.13. Clickjacking

For example, Stack Overflow uses OAuth and is vulnerable to this attack. If you visit StackOverflow and you are currently logged into your OAuth provider, you will be automatically logged in to StackOverflow. Therefor an attacker could automatically log in a victim by loading Stack Oveflow within an iframe. If Stack Overflow also had a CSRF vulnerability (and it has had them!), then an attacker could automatically authenticate a victim's browser and carry out a CSRF (Session Riding), Clickjacking, or XSS attack against stackoverflow.com in a Chained Attack.


Taken from The Importance of the state parameter in OAuth2:

This is where the "state" object in OAuth 2 comes into play. By always submitting a non-­guessable state when POSTing to the authorization endpoint, the client application can be certain that the Access Code obtained from the Authorization Server are in response to requests made by it rather than some other client application.

Example:

https://example.com/as/authorization?client_id=client1&response_type=code&scope=openid &state=7tvPJiv8StrAqo9IQE9xsJaDso4

For the state parameter to be useful in preventing CSRF attacks like this, all requests made to the OAuth server must include a state parameter that the client can use to authenticate itself. When sending a state parameter, the OAuth spec stipulates that the Authorization Server must return it to the client verbatim. This will be done by tacking it onto the client's call-back URL. The client must receive this state and be programmed to only accept redirects with a verifiable state. If this is a dictionary kept in memory or a re­-computable value is up to the client programmer.

When deciding how to implement this, one suggestion is to use a private key together with some easily verifiable variables, for example, client ID and a session cookie, to compute a hashed value. This will result in a byte value that will be infeasibility difficult to guess without the private key. After computing such an HMAC, base-64 encode it and pass it to the OAuth server as state parameter. Another suggestion is to hash the current date and time. This requires your application to save the time of transmission in order to verify it or to allow a sliding period of validity (e.g., using TOTP).

Hers is a simple Python example that uses the second suggestion using datetime:

def generate_state_parameter(client_id, private_key):
    date = datetime.datetime.today()
    raw_state = str(date) + client_id
    hashed = hmac.new(private_key, raw_state, sha1) state = base64.b64encode(hashed.digest())
    return (state, date)