Secure your application with Keycloak
If you’re building a web application, chances are you will definitely need user management of some sort. Most web frameworks are batteries-included, meaning they come with their own authentication modules.
But it’s hard to beat Keycloak, which is a full fledged open source identity and access management system. You should use Keycloak if any of this is true:
- You want to integrate with external identity providers like Google, Github or Facebook.
- You want to connect to an existing LDAP or Active Directory for authenticating users
- You are using OIDC, Oauth 2.0 or SAML
Overall, it is a good practice to delegate your user management to Keycloak.
In this post, we shall build a simple Flask based web application which uses Keycloak to authenticate users.
Assuming you already have docker-compose installed on your laptop, you can clone the repository and do a docker-compose build
and docker-compose up
.
Running the application
The Flask web app lives in port 5000, and it has the following routes:
- a
/hello
endpoint which is authenticated. - A
/login
endpoint which performs the user login.
Let’s hit the login endpoint. This will perform a 3-legged oauth flow with Keycloak.
First, the application calls the authorize URL of Keycloak. This redirects to the login page of Keycloak. You will get a 404 not found as the redirect URL contains the docker hostname of the Keycloak service.
Replace the “keycloak.auth” with “localhost” and reload the page. It will prompt for the user credentials in the Keycloak login UI.
The user credentials for this demo are “admin” and “admin”. Once logged in successfully, Keycloak will call the callback URL of your application. In this step, the application will use the code
query parameter obtained from Keycloak and exchange it to get valid Oauth tokens(access token, ID token and refresh token to be precise).
The access token has a relatively short validity period(60 seconds) as shown in the above response. This is configurable in Keycloak. We can fetch a new access token by using the refresh token.
All 3 tokens are stored in the user session.
Difference between access token and ID token
The ID token is a JWT token which serves as a proof of successful user authentication. It can also be used to verify the issuer of the tokens and ensure that the token has indeed been issued by the correct issuer and hasn’t been tampered with. It also contains other meta information about the user like first name, last name, email, birthday etc.
The access token might or might not be a JWT token. This is the token which is sent in subsequent API calls to fetch resources on behalf of the user. It contains information about the scope of the token, i.e. what resources the user has access to and what they can do with them.
TLDR; ID token is used for authentication, access token is used for authorization.
How the application checks for authenticated user
We write a Flask before_request decorator that will, barring /login
and /callback
endpoints, look out for the ID token in the session, and decode the JWT. If the token is not to be found and the decoding is not successful, then it responds with a 401 status code.
Here’s a valid ID token payload.
{
"exp": 1675754079,
"iat": 1675754019,
"auth_time": 1675754019,
"jti": "9ae22e40-1900-41df-9d7d-27bc59493b89",
"iss": "http://localhost:8080/realms/master",
"aud": "account",
"sub": "6f08e71e-191a-478e-8e22-efd589158d90",
"typ": "ID",
"azp": "account",
"session_state": "57450fa7-d805-46c4-b127-73f2d6c336e0",
"at_hash": "HtLx_4l9-TTT9kBB8HgBsg",
"acr": "1",
"sid": "57450fa7-d805-46c4-b127-73f2d6c336e0",
"email_verified": false,
"preferred_username": "admin"
}
The before_request decorator checks for the “audience” parameter and the expiry of the JWT token.
Now, hitting the /hello endpoint will print the ID token on the screen.
Other points of note
The Python Oauth library works only with https endpoints. For the sake of our local demo, we have to override this behaviour by setting the OAUTHLIB_INSECURE_TRANSPORT environment variable in the Flask service.
We imported the “master” realm with its client configuration into Keycloak. This was done to minimize the number of steps required to get the setup up and running. To do so, first we configured the “account” client by adding a valid callback URL, generated credentials and exported this as a JSON file. When we boot up Keycloak, we import this JSON while starting the Keycloak server.
keycloak.auth:
image: quay.io/keycloak/keycloak:20.0
command: ["start-dev", "-Dkeycloak.migration.action=import", "-Dkeycloak.migration.provider=singleFile", "-Dkeycloak.migration.strategy=OVERWRITE_EXISTING", "-Dkeycloak.migration.file=/opt/keycloak/data/import/master.json"]
environment:
DB_VENDOR: postgres
DB_ADDR: database
DB_DATABASE: keycloak
DB_USER: keycloak
DB_SCHEMA: public
DB_PASSWORD: password
KEYCLOAK_ADMIN: admin
KEYCLOAK_ADMIN_PASSWORD: admin
ports:
- 8080:8080
depends_on:
- database
volumes:
- ./keycloak-data:/opt/keycloak/data/import
You can notice the container start command in the above code snippet.
Summary
We used KeycloakX(the quarkus based version of Keycloak) as the user and identity management service for a Flask web application and demonstrated their integration on a local setup. You can find the working copy of the code here.