Seamless Authentication: Integrating OpenID Connect with Django Using Auth0.
Data is everywhere; it’s all around us. In our schools, our industries, our workplaces, and our lives in general, there’s some form of data that concerns us, waiting to be requested or manipulated. In order to utilise our data effectively, our software applications need to know the identity of the person requesting for such data.
As developers, we can figure this out by asking users to always provide their emails/usernames and passwords whenever they need to access data. However, this may not be the best experience for them, as there could be some delays in the authentication process during the time the users are cooking up random “strong” passwords like “JohnDoe123” for account creation or while trying to remember a hint for their precious forgotten passwords.
To provide better user experience with seamless authentication, we could look to third-party methods that take control of the authentication process, popularly referred to as the authentication flow, while we focus on providing data to their rightful owners.
This is where OpenID Connect (OIDC) comes into play.
So, what is OpenID Connect?
OpenID Connect (OIDC) is an identity protocol built on OAuth 2.0, a delegated authorization framework, that’s used to define how users authenticate their identities across multiple software applications. Rather than requiring users to provide their credentials repeatedly to several applications during authentication, these applications just need to request for the authenticated user’s ID token, which contains a set of user claims (simply pieces of information about the end user) from an OpenID provider.
So if you’ve ever tried signing up or logging in to your account with a login page looking like this,
congratulations; you’ve mostly likely used OIDC.
For a complete OIDC authentication process, several components are required to play their roles.
- OpenID Provider (OP)—responsible for managing the user’s credentials and identity data. Popular examples of providers include Google, Microsoft, Twitter, and LinkedIn.
- The Relying Party (RP) or Client Application—the application requesting for the identity data of the user.
- End-User—needs access to the client application.
With these three bodies in place, here is an overview of the authentication process:
- The user makes an unauthenticated request to the client application.
- The client application redirects the user to an OP for authentication.
- The OP prompts the user to authenticate.
- After authentication, authorization for user’s data is granted to the client application
- With the authorization given, the client application requests for the user’s identity data from the OP in order to create or retrieve related custom data of the user from its own local data store.
In OAuth2.0 and OIDC, there are methods that determine the authentication and delegation flows in a software application; these methods are called grant types and they include:
- Client credentials: This grant type is used when the client application is acting on its own behalf, typically for server-to-server communication. It involves the client providing its credentials directly to the authorization server.
- Authorization code: This is the most common grant type for web applications. It involves the client first obtaining an authorization code from the authorization server, which is then exchanged for an access token.
- Resource owner credentials: This grant type involves the client obtaining the user’s credentials directly and using them to authenticate and obtain an access token from the authorization server. This method is less secure and is not recommended.
- Implicit: In this grant type, the client obtains an access token directly from the authorization server, without an intermediate authorization code exchange step.
- Refresh token: With this grant type, the client requests a new access token after the existing one is expired with a refresh token without needing the user to re-authenticate
- Hybrid flow: This grant type originating from OpenID Connect combines both the authorization code and implicit delegation flows, where the client receives both an authorization code and an ID token after the user authorizes it, but then the client can go ahead and request an access token with the authorization code.
Out of these grant types and authentication flows, the authorization code flow is the most implemented for web applications that involve an end user, due to its robust support for user authentication. In this flow, both authentication and authorization revolve around an authorization code.
There are components involved in this process.
- The authorization server: This is responsible for authenticating the end user.
- Client: The software application that’s in need of the user’s data.
- User: You, me, the individual that needs access to the client application.
Here’s how the authorization code delegation flow works:
- The client application initiates the delegation flow by sending a request to the authorization server’s authorization endpoint, along with a set of parameters that include:
- redirect_uri: This is the URI that the authorization server uses to redirect the user back to the client application after authentication.
- scope: This can be one or more permissions that need to be granted in order to enable the client application to access data or execute some functionalities on behalf of the user.
- response_type: Determines the grant type of the delegation flow, typically set to “code” for the Authorization Code flow.
- client_id: This is the public identifier that the authorization server uses to identify a client. It acts as the username of the client application.
2. The authorization server prompts the user to authenticate. After user authentication, it presents the requested scopes for approval, and the user grants the client access to the specified scope permissions.
3. The user is redirected to the client with an authorization code indicating that its permissions were granted.
4. The client exchanges the authorization code for an access token by sending a request to the authorization server’s token endpoint.
5. The authorization server validates the authorization code and issues an access token to the client.
6. With the access token, the client accesses the user’s data and resources for a limited period of time.
In an OIDC flow, the client application sends a request to three endpoints:
- Authorization endpoint: This endpoint is responsible for the authentication of the end user. This request is sent in this format:
GET <authorization endpoint>/?
response_type=code
&client_id=<client id>
&redirect_uri=<redirect uri>
&scope=openid%20profile%20email
2. Token endpoint: With this endpoint, the client application receives an ID token and an access token. The ID token is a JWT that contains both user and registered claims.
The registered claims contain details about the creation of the ID token, such as:
- iss: Issuer of the JWT
- sub: Subject of the JWT (the client ID)
- iat: Time at which the JWT was issued, which can be used to determine age of the JWT
- exp: Time after which the JWT expires
These claims appear as a result of the openid
scope.
Upon receiving a token response, the ID token must be validated before use in order to make sure that it has not been tampered with while in transit. Token validation is done by verifying the signature of the token with its public key.
We can either validate and decode the ID token with Python’s PyJWT package to get user claims, or we can send a request to the userinfo endpoint with the access token gotten from the token response as a Bearer
token in the Authorization
header, to selectively retrieve user claims.
3. UserInfo endpoint (Optional)
While OAuth2.0 delegates authorization rights to client applications, allowing them to execute functionalities on user data, OIDC focuses on facilitating the sharing of a user’s identity information across multiple applications. This depends on the nature of the scope specified during the authorization request. In the context of OIDC authentication, an authorization server is the OpenID provider. To make an OIDC authentication request, the openid
scope value should be included in the scope parameter, along with an optionalemail
or/and a profile
scope for user information.
The profile
scope returns claims about the user, containing the user’s
- family name
- given name
- middle name
- nickname
- profile picture
The email
scope returns claims about the user, containing their email address and a boolean value of email verification.
There are many OpenID providers that you can integrate into your software application, including Google, Microsoft, PayPal, Apple and Okta. For this article, we would be focusing on one, which is Auth0.
What is Auth0?
Firstly, I must say that Auth0 is different from OAuth2.0. I made this mistake when I first learned about them.
Auth0 is an identity platform used by developers to integrate various security protocols and frameworks into their applications before granting users access to these applications. So while OAuth2.0 is an authorization framework, Auth0 helps developers integrate this framework into software applications. While Auth0 is an identity provider, we should also know that it is an identity broker, connecting to external identity providers as well.
In order to work with Auth0, you must have an account with the platform. You can visit the homepage and sign up.
Now that we’ve seen how OIDC improves user experience and data security, as developers, how do we integrate it into our Django applications? Let’s find out.
Demo…
To begin with, I’ll be working with an existing demo GitHub repo. In this case, the repo will be our client application. When you create an account, a “room” is automatically created for your application. This is what Auth0 calls a tenant. A tenant holds all the Auth0 assets of your client application, such as connections, applications, and user profiles. However, if you want to create a new tenant for your application, here’s how you do it:
On the top left corner of your account’s dashboard, click the drop-down button and click the create tenant
button.
Input a tenant name that contributes to the Auth0 domain for our blog API and create a tenant.
On the next page, you will see two options that provide more details about Auth0.
Next, we register our client application with the tenant. On the side bar, click the Applications
drop-down button and select Applications
, then click the Create Application
button.
On the modal, type in your client application’s name, mine will be blog-ql
. Set the application type to Regular Web Applications
and click the create
button.
On the next page, click the settings
tab, and you will see the details needed to set your application as an Auth0 client.
Now, you need to store the details of your Domain
, Client ID
, and Client Secret
in your source code, which helps Auth0 recognize your application.
Recall that an end user is redirected to the client application after authentication, so we have to provide a redirect_uri
in our settings. Running on a localhost server, our redirect URI could look like this:http://127.0.0.1:8000/blog/auth0/callback
.
Add this to the list of Allowed Callback URLs
in the Applications URIs
section of the Settings
tab.
Once a tenant has been created, it is automatically linked to Google as an Identity Provider. We can find it in our Social Connections
by clicking the Social
button in the Authentication
drop-down menu.
You can choose to link your client application to more social connections by clicking the Create Connections
button.
Moving to our Django source code, we need to import three Python packages:
- Python-dotenv: to help read our client data in environment variables
- requests: to send HTTP requests from our source code.
- PyJWT: used to validate and decode JWTs
pip install python-dotenv requests pyjwt
In our views.py
file, create a view function that will be called by the user to authenticate.
from django.shortcuts import render, redirect
import requests
import os
from dotenv import load_dotenv
load_dotenv()
from django.http import JsonResponse
import json
import jwt
# Auth0 client details
auth0_domain = os.environ.get('AUTH0_DOMAIN')
client_id = os.environ.get('AUTH0_CLIENT_ID')
client_secret = os.environ.get('AUTH0_CLIENT_SECRET')
redirect_uri = "http://127.0.0.1:8000/blog/auth0/callback"
# user interacts with this function to be authenticated by
def oidc_authenticate(request):
auth0_authorization_url = f"http://{auth0_domain}/authorize" # authorization endpoint
params = {
"response_type": "code", # OIDC flow will be of the authorization code grant type
"client_id": client_id,
"redirect_uri": redirect_uri,
"scope": "openid profile email", # scopes
}
# redirect user to OpenID provider to be authenticated
return redirect( f"{auth0_authorization_url}?{'&'.join([f'{key}={value}' for key, value in params.items()])}" )
After the view function is called, our client application receives a response containing the keys,id_token
, access_token
, scope
, expires_in
and token_type
. In order to use our ID token, it must first be validated with its public key. In the process of token validation, only the recipient in possession of the public key can validate the token. This brings us to the question:
- How do we check for possession of the public key?
A token is made up of three parts: a header, a payload, and a signature. The header contains three details:
- alg: the algorithm that was used to sign the token.
- typ: the type of token
- kid: the key identifier of the public key
Here, the attribute needed to check for possession of a public key is the kid
, in the sense that it must be present in our tenant's JSON Web Key Set (JWKS) for the token to be considered valid. Every tenant has a set of JSON Web Keys (JWK), and each key makes up a public key. This set can be accessed with the URL format: https://{your_auth0_domain}/.well-known/jwks.json. For our demo application, it is https://blog-ql.us.auth0.com/.well-known/jwks.json.
Moving to the next step of the OIDC flow, we define another function for the redirect_uri. This will be used to redirect the user to the client application. Upon redirection, the client requests a token, which will be validated and decoded to access user claims:
def oidc_callback(request):
# https://blog-ql.us.auth0.com/.well-known/jwks.json
jwks_endpoint = os.environ.get('AUTH0_JWKS_ENDPOINT')
authorization_code = request.GET.get("code")
# exchanging code for token
token_url = f'https://{auth0_domain}/oauth/token'
token_data = {
"grant_type": "authorization_code",
"client_id": client_id,
"client_secret": client_secret,
"code": authorization_code,
"redirect_uri": redirect_uri
}
token_response = requests.post(token_url, data=token_data)
token_response_data = token_response.json()
id_token = token_response_data['id_token']
## token validation
# get ID token header
id_token_jwt_header = jwt.get_unverified_header(id_token)
jwks = requests.get(jwks_endpoint).json()
public_key = None
for jwk in jwks['keys']:
if jwk['kid'] == id_token_jwt_header['kid']:
public_key = jwt.algorithms.RSAAlgorithm.from_jwk(json.dumps(jwk))
if public_key is None:
raise Exception("Public key not found")
try:
decoded_data = jwt.decode(id_token, public_key, audience=client_id, issuer=issuer, algorithms=['RS256'])
return JsonResponse(decoded_data)
except jwt.exceptions.DecodeError as e:
print("Invalid Token: ", e)
Create URLs for these view functions in your app’s urls.py file.
from django.urls import path
from .views import oidc_authenticate, oidc_callback
urlpatterns = [
# http://127.0.0.1:8000/blog/auth/
path('auth/', oidc_authenticate, name="oidc_authenticate"),
# http://127.0.0.1:8000/blog/auth0/callback/
path('auth0/callback/', oidc_callback)
]
The app’s URLs must be contained in the project’s urlpatterns
.
urlpatterns = [
...
...
path('blog/', include('blog.urls'))
]
Run the localhost server with python3 manage.py runserver
and call the blog authentication URL, http://127.0.0.1:8000/blog/auth/, from your browser. At first, you should see a page where you’re asked to authorize the blog API to access your profile and email data.
Another time, you will be visited by a page with a “Sign in with Google” button because of the Google social connection.
You should get a response page with your user claims, where you can go ahead and use the data according to your application’s requirements.
In conclusion…
OpenID Connect (OIDC) offers a streamlined approach to authentication, enhancing user experience while maintaining security. Auth0 serves as an identity platform, integrating security protocols seamlessly into applications.
By understanding OIDC’s authentication flow and leveraging platforms like Auth0, developers can ensure both user convenience and robust data protection.
So next time you’re building an application, please think twice and reconsider your method of authentication, for the sake of your dear users.