OAuth Security Cheatsheet
State-Of-The-Art OAuth2 Homakov Edition
Taking into account poor quality of official OAuth2 spec and the number of vulnerable provider and client integrations, here's a secure-by-default OAuth Homakov edition
TL;DR: protect initial
/connect/provider endpoint with CSRF, always use and require
state, never allow dynamic redirect_uri, do not allow response_type=hash (formerly
token) by default, get rid of refresh_token & code, instead authenticate every access_token with appsecret_proof, double-check with user what provider account they want to connect, never expire access_token (think of it as public user_id).
The user clicks "Connect Provider" button. This either initiates a POST form submission (protected with CSRF token) to
/connect/provider_nameor navigates with GET to
/connect/provider?csrf_token=TOKEN. The initial step MUST be protected from CSRF, it shouldn't be possible to trigger connection from other origins, there must be an explicit confirmation from the user.
On the server side
/connect/providerclient's controller should verify that csrf_token is equal the token from the session, then generate
statetoken (CSRF token to ensure OAuth flow integrity) and store it in the session as
<provider>-state. Then redirect to
scopeparameter is optional (some critical scopes should only be allowed to verified clients).
Provider checks that given redirect_uri is inside of whitelist of redirect URIs (host, path, query and hash must be static). Every redirect_uri must have a corresponding response type,
hashfor mobile apps. It MUST NOT be a parameter.
stateparameter must be required by the provider (despite spec says it is optional - for most developers it is synonim for "not needed").
The user sees Approve/Deny buttons on provider domain. After clicking Approve POST form is submitted to the same URL, protected with CSRF token (don't be like Outlook or Doorkeeper or tons of other providers with CSRF on /authorize)
queryresponse_type redirect to
CLIENT/callback?access_token=TOKEN&state=STATE(all tokens are stored on central server and every API request is signed with appsecret_proof), for
CLIENT/callback#access_token=TOKEN&state=STATE(tokens are stored in local applications, appsecret_proof is not required). In both cases clients must check STATE is equal provider-state from the session.
access_tokennever expires? Because we don't need refresh_token/code anymore. With refresh token attackers get a window when they can use access_tokens to send spam or download social graph of all victims. Only when access_tokens expire, according to spec, clients should use refresh_token and client_secret to get a new token. Instead of this security theater, we are going to require appsecret_proof which was first introduced by Facebook. Every request to Provider MUST have access_token and appsecret_proof=
hash_hmac('sha256', $access_token, $client_secret);if the token was returned with response_type=
query. Now if the attacker leaks access_token, tokens are worthless. Even if they steal your client_secret, you can simply rotate it.
Optional, but recommended. Before connecting the account you should ask the user if they want to connect for example Facebook of "firstname.lastname@example.org". Because there are lots of attacks such as cookie forcing, login/logout CSRF, RECONNECT that can silently relogin user into malicious account on the provider.
About this document
This document describes common OAuth/Single Sign On/OpenID-related vulnerabilities. Many cross-site interactions are vulnerable to different kinds of leakings and hijackings.
Both hackers and developers can benefit from reading it.
Because of OAuth many startups including Soundcloud, Foursquare, Airbnb, About.me, Bit.ly, Pinterest, Digg, Stumbleupon, Songkick had an account hijacking vulnerability. And a lot of websites are still vulnerable. Our motivation is to make people aware of "Social login" risks, and we encourage you to use OAuth very carefully.
The cheatsheet does not explain how OAuth flows work, please look for it on the official website.
Authorization Code flow
Client account hijacking by connecting attacker's provider account.
Also known as The Most Common OAuth2 Vulnerability. In other words, CSRF.
code by redirecting user-agent to
Now the client must send
code along with client credentials and
redirect_uri to obtain
If the client implementation doesn't use
state parameter to mitigate CSRF, we can easily connect our provider account to the victim's client account.
It works for clients with social login and ability to add a login option to existing master account (screenshots of pinterest.com below).
Remediation: Before sending user to the provider generate a random nonce and save it in cookies or session. When user is back make sure
state you received is equal one from cookies.
This is another OAuth design issue - sometimes developers want to use
state for own purposes. Although you can send both values concatenated
state=welcome_landing.random_nonce, but no doubt it looks ugly. A neat solution is to use JSON Web Token as state
Client account hijacking through abusing session fixation on the provider
Even if the client properly validates the
state we are able to replace auth cookies on the provider with the attacker's account: using CSRF on login (VK, Facebook), header injection, or cookie forcing or tossing.
Then we just load a GET request triggering connect (
/user/auth/facebook in omniauth), Facebook will respond with the attacker's user info (uid=attacker's uid) and it will eventually connect the attacker's provider account to the victim's client account.
Remediation: make sure that adding a new social connection requires a valid csrf_token, so it is not possible to trigger the process with CSRF. Ideally, use POST instead of GET.
Facebook refused to fix CSRF on login from their side, and many libraries remain vulnerable. Do not expect providers to give you reliable authentication data.
Account hijacking by leaking authorization code.
OAuth documentation makes it clear that providers must check the first
redirect_uri is equal
redirect_uri the client uses to obtain
We didn't really check this because it looked too hard to get it wrong.
Surprisingly many providers got it wrong: Foursquare (reported), VK (report, in Russian), Github (could be used to leak tokens to private repos), and a lot of "home made" Single Sign Ons.
The attack is straightforward: find a leaking page on the client's domain, insert cross domain image or a link to your website, then use this page as
When your victim will load crafted URL it will send him to
leaking_page?code=CODE and victim's user-agent will expose the code in the Referrer header.
Now you can re-use leaked authorization code on the actual
redirect_uri to log in the victim account.
redirect_uri is a bad practise. But if you need it, store redirect_uri for every code you issue and verify it on access_token creation.
Leaking access_token/signed_request with an open redirect.
It was a media hype called "cover redirect" but in fact it was known for years. You simply need to find an open redirect on the client's domain or its subdomains, send it as
redirect_uri and replace
access_token can be used for spam and ruining your privacy.
signed_request is even more sensitive data. By finding an open redirect on the client you compromise Login with Facebook completely.
Remediation: whitelist only one redirect_uri in app's settings:
Account hijacking by using access_token issued for the attacker's client.
Also known as One Token to Rule Them All. This bug is relevant to mobile and client-side apps, because they often use access_token directly supplied by the user.
Imagine, user has many "authorization rings" and gives a ring to every new website where he wants to log in. A malicious website admin can use rings of its users to log in other websites his customers have accounts on.
Remediation: Before accepting user supplied access_token check if it was issued for your
Transport and JS SDK bugs
For better support of client-side applications (or browser apps) some of OAuth 2 providers added an out-of-specs custom variant of implicit flow, involving a proxy/relay page as a sort of router for access tokens. This proxy communicates with the client page of application either through standard HTML5 postMessage API, or using legacy ways: manipulating window name and url (known as Fragment transport, rmr) and Adobe Flash LocalConnection API.
Regardless of the selected method, it is important to ensure proxy can exchange messages only with the target origin, to disallow malicious page from impersonating the application page, and finally to verify these client-side checks are consistent with server-side checks. Fail properly secure client-side transport usually results in two kinds of problems:
- leaking data to an attacker (authorization and authentication bypass , )
- trusting data from an attacker (session fixation , )
- Completely disable all legacy client-side communication methods (Flash or Fragment), use postMessage
- Check origins of all incoming and outgoing messages, allow only the target application domain
- Ensure that all client-side origin checks are consistent with server-side origin checks
- Use a cryptographic nonce validation before starting data transmission with another party
Leaked client credentials threat
Client credetials are not as important as it sounds. All you can do is using leaking pages to leak auth code, then manually getting an access_token for them (providing leaking redirect_uri instead of actual). Even this threat can be mitigated when providers use static redirect_uri.
Session fixation (OAuth1.0)
Main difference between OAuth2 and 1 is the way you transfer parameters to providers. In the first version you send all parameters to the provider and obtain according request_token. Then you navigate user to
provider?request_token=TOKEN and after authorization user is redirected back to
The idea of fixation here is we can trick user into accepting Token1 supplied by us which was issued for us, then re-use Token1 on the client's callback.
This is not a severe vulnerability because it is mostly based on phishing. FYI Paypal express checkout has this bug
Provider In The Middle.
Many startups have Facebook Connect, and at the same time they are providers too. Being providers, they must redirect users to 3rd party websites, and those are "open redirects" you just cannot fix. It makes this chain possible: Facebook -> Middleware Provider -> Client's callback, leading to FB token leakage.
To fix this problem Facebook adds
#_=_ in the end of callback URLs. Your startup should "kill" fragment to prevent leaking. Redirect this way:
Tricks to bypass redirect_uri validation
If you are allowed to set subdirectory here are path traversal tricks:
/old/path/.%0a./.%0d./new/path (For Rails, because it strips \n\d\0)
code is sent via GET and potentionally will be stored in the logs. Providers must delete it after use or expire in 5 minutes.
Leaking a code with an open redirect
Usually you need a referrer-leaking page to leak ?query parameters. There are two tricks to do it with an open redirect though:
- When redirect uses
<meta>tag instead of 302 status and Location header. It will leak redirecting page's referrer to in the next request.
- When you managed to add
%23(#) in the end of
redirect_uri. It will result in sending the code in the fragment