OAuth (OAuth 2.0) - Cheatsheet
Elevator pitch:
OAuth is an agreed-upon standard around securing API usage, that is a (scoped) access token exchange based scheme, as an alternative to hardcoded passwords, or auth tokens that are scoped to the entire account.
A key differentiator of OAuth is that it is a form of delegated access - you are accessing protected resources that a user has granted you limited access to, as opposed to accessing them as the user.
Image Overview:
Resources:
What | Type | Link |
---|---|---|
Short video overview | Video | Link |
Digital Ocean Intro (excellent overview) | Tutorial / Post | Link |
Testing playground | Interactive | Link |
Steps to OAuth (2.0)
App registration
This is usually done far in advance of the main auth flow, so I wouldn't really consider it part of the main steps. It is more a pre-requisite to using OAuth.
- Register your application with the API / service:
- By providing...
- Application Name
- Application Website
- Redirect URI / Callback URL (more on this later)
- in order to obtain...
- Client ID (Public)
- Client Secret (Private)
- By providing...
Main Auth Flow: (assuming Auth Code)
Summary:
- Authorization Request (Outgoing)
- Authorization Grant (Incoming)
- Token Exchange / Request Access Token (Outgoing)
- Receive Access Token (Incoming)
- Use the token! (Outgoing, repeatable)
Detailed:
- Authorization Request
- WHAT:
- Your app asks the user to authorize it, and grant scoped access to parts of their account
- HOW
- Your app should construct a URL that is the API/service endpoint + ...
client_id
- Previously obtained
redirect_uri
- Where should the service send the user back to, along with the auth code, if the user approves
- This is also called a callback url
example.com/authCallback.php
scope
*- String that represents what you want the user to authorize your app to access
- For Google, this might be
profile
andemail
response_type
**- Corresponds with the grant type
state
**
- * = unique per API
- ** = sometimes optional, and unique per API
response_type
is often assumed based on endpoint, or maybe the API only supports one type of grantstate
is a randomized string that can serve multiple purposes:- Use a session identifier (instead of Cookie)
- Protection against CSRF (if you make sure to check that the callback
state
=== requeststate
)
- You then redirect the user to this constructed URL, in the same tab / webview / etc.
- You wouldn't normally open in new tab or try AJAX, since new tab would require polling, and AJAX doesn't make sense since they have to login and approve your app
- Your app should construct a URL that is the API/service endpoint + ...
- EXAMPLES
- Example: Ask Google user for read-only access to see basic account identity stuff (name, email, ID) to prove identity
- This is basically how Google Sign-in For Websites works.
- Example: Ask Twitter user for "Read and Write" access to account, so you can build an App that automates scheduled posting.
- Example: Ask Github user for access to
notifications
scope, so you can relay notifications from Github to Slack
- Example: Ask Google user for read-only access to see basic account identity stuff (name, email, ID) to prove identity
- CODE
https://github.com/login/oauth/authorize&client_id={{CLIENT_ID}}&redirect_uri={{https://example.com/authCallback.php}}&scope={{notifications}}&state={{asld!f93Cb20}}
- WHAT:
- Authorization grant (or deny) --- INCOMING - receiving the response
- WHAT
- After the user either approves or denies your request for granted access (see: The Authorization Grant Approval Screen), the user will be redirected to the redirect URI that you specified, with information from the third-party service appended as a querystring.
- HOW
- This should happen automatically after the user clicks either the approve or deny button on the approval screen.
- EXAMPLES
- User approves request and is redirected to our callback page at
https://example.com/authCallback.php?code={{AUTH_CODE}}&scope={{notifications}}&state={{asld!f93Cb20}}
- User denies request and is redirected to our callback page at
https://example.com/authCallback.php?error=access_denied&state={{asld!f93Cb20}}
- User approves request and is redirected to our callback page at
- CODE
- Grabbing querystrings param is pretty easy regardless of language... quick examples:
- PHP:
$authCode = $_GET['code'];
- JS:
var authCode = document.location.search.indexOf('code=') > 0 ? (/code=([^&]+)/.exec(document.location.search)[1]) : null;
- WHAT
- Token exchange / Access token request --- OUTGOING
- WHAT
- In this step, we are taking the auth code that we just received via GET query parameters, and using it as part of a GET or POST request, to exchange the granted auth code for a access token. The access token is the meat of OAUTH and what will be used all by future API requests by our app.
- You might be wondering why this step is necessary, and why couldn't step 2 simply return the access token instead of an auth code? The short answer is security - see my sidenote
- HOW
- We will take the auth code we just received, bundle it with our client ID and Client Secret, and make a request to the endpoint of the API to get an access token
- EXAMPLES
- For GitHub, you POST to
https://github.com/login/oauth/access_token
, which returns a token that should be valid indefinitely, until the user revokes the app
- For GitHub, you POST to
- CODE
- Since this is using a
client_secret
, any of these examples should only be running server-side - PHP: gist
- Since this is using a
- WHAT
- Receive Access Token / Access Token grant --- INCOMING
- WHAT
- In response to the GET or POST request in step 3, in which we sent our temporary
auth code
,client_secret
, andclient_id
, we should get back a response that includes the finalaccess_token
. It might also include extra information, like when the token expires, the granted scope,token_type
, etc.
- In response to the GET or POST request in step 3, in which we sent our temporary
- HOW
- Usually APIs return this response as
json
, so grabbing the finalaccess_token
should only take a line or two of code, given how almost every programming language has a JSON parser either natively or through a common lib. - As a dev, you will want to persist this token, usually in a DB, session, or some other storage, since tokens usually don't expire for a long time, but browser sessions do. If you don't persist the token and instead keep it with your user's session, that would mean that every time they log out and then back into your app, they would have to re-authorize OAuth as well!
- Usually APIs return this response as
- EXAMPLES
- The GitHub API currently returns
access_token
,scope
(comma separated), andtoken_type
("bearer").
- The GitHub API currently returns
- CODE
- Should be self-explanatory
- WHAT
- Use the token!
- WHAT
- Now, every time we need to make an API call to our third-party service, we will need to include the
access_token
in some form or another
- Now, every time we need to make an API call to our third-party service, we will need to include the
- HOW
- This depends on the API, but usually the token is included inside of a special HTTP header field - the 'Authorization' header.
Authorization: <type> <credentials>
- Sometimes an API will allow you to include the token as a querystring instead of a header, but this is not advisable due to security
- This depends on the API, but usually the token is included inside of a special HTTP header field - the 'Authorization' header.
- EXAMPLES
- For GitHub, you can include the token either as a header or a GET query param. And for GH, the name of the auth type is "token" for the HTTP header
- CODE
- The generic auth header looks like this:
Authorization: <type> <credentials>
- Here is a GitHub request, using cURL:
curl -H "Authorization: token OAUTH-TOKEN" https://api.github.com
- WHAT
Some Other Grant Types
Grant Type | Diff compared to authorization code |
Why use it |
---|---|---|
implicit |
Instead of asking for a code and then exchanging for a token , the API returns a token immediately. Basically step 3 is skipped. |
Useful for apps with no backend (e.g. a SPA served via static files), since no client_secret is ever exchanged (which would be impossible to keep safe on a front-end only app). The downside is that the access_token is completely exposed to client side code. |
password |
The user's unencrypted username and password are sent in a POST request, which returns the access_token . Essentially steps 1-3 are skipped. |
INCREDIBLY insecure, for multiple reasons, including you would have to store username & pass in plaintext in order to re-authenticate when necessary. Not many good reasons to use this, except maybe converting legacy apps. |
client |
Similar to password grant, except instead of user's username & pass, you send your app's client_id and client_secret (analogous to user & pass) in POST and get back token. |
This is useful when an API, or a subset of the API, is not scoped to individual users, but rather to clients. For example, an API might restrict WRITE endpoints to using the authorization code grant type, but an endpoint that returns the operational status of the entire platform might be restricted with client , since everyone sees the same data. |
The Authorization Grant Approval Screen
- WHAT
- This part of the flow is beyond your control as the dev, but I'm including it since it is a crucial part of the process. This is the screen that the user sees, if they are logged into the third party service and asked to authorize your app.
- EXAMPLES
Some security considerations
Why can't the Access Token be returned directly when user approves access?
First, it can be - that is actually how the implicit
grant type works, without using client_secret
.
However, back to the question of why this is not used with the authorization code
grant type, which is the more common protocol. I'm going to oversimply here, but the basic reason seems to be due to the high-visibility of URLs in GET requests and how they are logged. For example, any Javascript running on a webpage can read and store the current URL of the page, including the query string. If the OAuth protocol allowed for access tokens to be returned to the redirect_uri via a query param, such as callback.php?accesstoken=123ABC
, then a third party malicious script might grab it, and later use it to get access to our user's accounts.
The current flow, receiving the auth code in GET and then exchanging it for a token via GET/POST as a secondary request is much better, for several reasons.
- The exchange to receive the token is secured in two ways
- It should originate server-side, so client-side scripts, such as Google Analytics or malicious third-party JS scripts, will not see the request and log it.
- Some API's allow for this to be a POST request, which is a small further guard against MITM attacks, since the important params like
client_secret
should be in the body of the request, not the URL, and therefore encrypted with SSL.
- Although the auth
code
is visible as a query param, typically it expires within minutes of being granted. Even if a third party grabbed it within that short time span, they would need to know your client-secret in order to exchange it for a long-lived access token.