OAuth Demystified: A Straightforward OAuth Tutorial
Incorporating OAuth (short for Open Authorization) into an application might seem somewhat intimidating and disheartening for many entry-level software engineers. After all, they need to spend hours—or even days—implementing a complicated authentication process with the correct configuration, only to realize they have merely completed a tiny feature. Despite all the hard work it entails, OAuth is a safe and efficient way for users to grant websites and applications access to their personal information. Therefore, it is worth the effort to comprehend how OAuth works. In this article, I will break down all the steps involved in the OAuth process to help you kick-start your OAuth journey.
Two things to note before we jump into the tutorial:
- The OAuth version I will use in this article is OAuth 2.0 since it is the current industry standard. `https` (TLS) is required to be secure.
- Although PKCE is optional, I have decided to include it in this tutorial because it makes the entire OAuth process more secure.
Let’s assume we have two applications: App A (client application) and App B (authorization server). App A wants to access User X’s information stored in App B. We need to establish an OAuth process so that App A can securely access this potentially sensitive information.
Step 1: Authorization URL Generation
To allow App A to access User X’s information in App B, App B has to verify User X’s identity first. The most straightforward way for App B to verify User X’s identity is to ask User X to log into App B. However, User X should NOT directly log into App B without involving App A. The login URL needs to include information from App A so that App B is cognizant of the destination, namely App A, to which it needs to send User X’s information. This is where the authorization URL comes into play. An Authorization URL is essentially a URL for informing App B about App A’s attempt to access User X’s information. App B redirects User X to its login page afterward. To create it, you need the following information:.
- Client ID
- Code Verifier
- Code Challenge
- Code Challenge Method
- Redirect URI
Client ID is a public identifier of App A issued by App B. It helps App B confirm App A’s identity. Without Client ID, App B would refuse to provide sensitive information to App A for lack of trust.
Both Code Verifier and Code Challenge are a part of the PKCE (abbreviation for Proof Key for Code Exchange) process that aims to make Authorization Code grants more secure. Code Verifier is a random code that meets some requirements, while Code Challenge is a transformation of the Code Verifier. In Step 1, we only need Code Challenge. We will not utilize Code Verifier until Step 2.
You can use the following code to create Code Verifiers and Code Challenges.
import re
import os
import base64
import hashlib
code_verifier = base64.urlsafe_b64encode(os.urandom(50)).decode("utf-8")
code_verifier = re.sub("[^a-zA-Z0-9]+", "", code_verifier)
code_challenge = hashlib.sha256(code_verifier.encode("utf-8")).digest()
code_challenge = base64.urlsafe_b64encode(code_challenge).decode("utf-8")
code_challenge = code_challenge.replace("=", "")
Code Challenge Method is the encoding method used to transform Code Verifier into Code Challenge. Usually, OAuth 2.0 uses SHA-256 as the code challenge method, just like in the above code example.
Redirect URI is a callback API in App B that App A triggers after User X logs in. I will explain its purpose in greater detail later.
After you gather all the above information, you can start to assemble the authorization URL. In Python, we can use a library called requests_oauthlib to do so.
from requests_oauthlib import OAuth2Session
app_b = OAuth2Session(CLIENT_ID, redirect_uri=REDIRECT_URI)
authorization_url, _ = app_b.authorization_url(AUTH_URL, code_challenge=code_challenge, code_challenge_method="S256")
In the above code example, App A uses the OAuth2Session
constructor to initialize the app_b
instance with Client ID and Redirect URI as parameters. Then, App A creates an Authorization URL using the authorization_url
method. The parameters include the base URL of App B’s authorization API, Code Challenge, and Code Challenge Method. The Authorization URL generated in the above code example looks like the following:
https://www.example.com/get_authorization_token?response_type=code&client_id=&code_challenge=&code_challenge_method=S256
Once the Authorization URL is ready, App A can send it to User X and ask them to open it. After User X opens the Authorization URL, App B records App A’s request to access User X’s information and redirects User X to App B’s login page.
Step 2: Fetch Tokens and Retrieving Information in Callback API
User X has logged into App B. Now what? App B then sends a redirect response to User X to redirect User X to App A’s Callback API specified in Redirect URI. The purposes of Callback API are twofold: fetching tokens and retrieving User X’s information from App B.
First, let’s start with the two tokens that App A needs to fetch from App B: Access Token and Refresh Token. Access Token is necessary for gaining access to protected resources. In our case, App A cannot access User X’s information in App B until it possesses an Access Token issued by App B. The Access Token is irrevocable, meaning that App B cannot revoke it after sending it to App A. Therefore, Access Tokens are powerful and dangerous by the same token (no pun intended). To prevent potential data breaches, Access Tokens usually have short lifespans.
Making Access Tokens short-lived plays a significant role in ensuring data security. However, it creates a different problem in terms of user experience: users have to re-authenticate frequently to get new Access Tokens because Access Tokens expire often. Fortunately, we have Refresh Token to help us out. Refresh Token serves the purpose of obtaining new Access Tokens when current Access Tokens expire. In our example, App B sends a Refresh Token along with the initial Access Token to App A. Instead of asking User X to log into App B again after the current Access Token expires, App A can use the Refresh Token to fetch a new Access Token from App B.
To fetch the initial Access Token and Refresh Token, we need the following information:
- Token URL
- Authorization Code
- Code Verifier
- Code Challenge Method
- Client ID
Token URL: Token URL is the API for issuing and renewing tokens on App B’s side.
Authorization Code: Authorization Code is a code provided by App B. App B attaches it to the end of App A’s Callback API as a URL parameter.
Code Verifier: Please refer to my previous explanation about Code Verifier.
Code Challenge: Please refer to my previous explanation about Code Challenge Method.
Client ID: Please refer to my previous explanation about Client ID.
The following code example makes an API call to App B to get the initial Access Token, Refresh Token, and information regarding when the Access Token will expire.
# from requests_oauthlib import OAuth2Session
# app_b = OAuth2Session(CLIENT_ID, redirect_uri=REDIRECT_URI)
token = app_b.fetch_token(
token_url=TOKEN_URL,
code=auth_code,
code_verifier=code_verifier,
code_challenge_method='S256',
client_id=CLIENT_ID,
)
access_token = token["access_token"]
refresh_token = token["refresh_token"]
expires_at = token["expires_at"]
expires_in = token["expires_in"]
Access Token: To refresh the Access Token using the Refresh Token, we need the following information:
- Token URL
- Refresh Token
- Client ID
- Client Secret
Token URL: Please refer to my previous explanation about Token URL.
Refresh Token: Please refer to my previous explanation about Refresh Token.
Client ID: Please refer to my previous explanation about Client ID.
Client Secret is a credential known only to the OAuth application and the authorization server. In our case, Client Secret acts as a password to help App B verify App A’s identity.
The following code example makes an API call to App B to refresh the Access Token.
# from requests_oauthlib import OAuth2Session
# app_b = OAuth2Session(CLIENT_ID, redirect_uri=REDIRECT_URI)
token = app_b.refresh_token(
token_url=TOKEN_URL,
refresh_token=refresh_token,
client_id=CLIENT_ID,
client_secret=CLIENT_SECRET
)
access_token = token["access_token"]
expires_at = token["expires_at"]
expires_in = token["expires_in"]
Armed with the Access Code, App A can finally retrieve User X’s information from App B on User X’s behalf. All App A needs to do is to make an API call to App B’s designated API for offering user information to external applications (typically ending with “/me”), with the Access Token as a part of the request’s headers.
response = requests.request(
"GET",
ME_URL,
headers={"Authorization": "Bearer {}".format(token["access_token"])},
)
Step 3: What’s Next?
Depending on App A’s specifications, App A can do the following in the Callback API:
- App A can store User X’s information in its database.
- App A can store the Access Token and Refresh Token in the session to keep track of User X’s authentication status in App B. Once the Refresh Token expires, App A can consider User X’s session expired and log User X out.
Disclaimers
Before I end this article, I would like to note that different authorization servers (App B) might have slightly different implementations. Therefore, I would kindly recommend you take the code examples in this article with a grain of salt, especially when it comes to determining what parameters to include in API calls to authorization servers. When implementing your client application (App A) and designing how it should interact with your target authorization server (App B), please consult the authorization server’s documentation or codebase to understand which variables are necessary for which API calls.
In this article, I have demonstrated how to set up an OAuth process in a client application to access protected resources in another application. I also explained what variables we need for each step in the OAuth process and why we need them. I hope this article has helped you understand OAuth a bit better.