Implementing OAuth2 and OpenID Connect (OIDC) with Keycloak: A Python & React Guide

Learn how to implement OAuth2 and OpenID Connect using Keycloak, Docker, Python, and React with secure production-ready best practices.


Introduction

In today’s digital landscape, application security is paramount. Authentication and authorization are two of the most critical aspects of secure system design. OAuth2 is a widely adopted authorization framework, and OpenID Connect (OIDC) is an identity layer on top of OAuth2 that enables clients to verify user identities. Keycloak, a powerful open-source Identity and Access Management solution, allows you to implement OAuth2/OIDC effortlessly across your stack.

In this post, we will guide you step-by-step through deploying Keycloak using Docker, setting up your realm and clients, and integrating secure authentication flows into a Python (FastAPI) backend and a React frontend. We’ll also cover security hardening tips for production environments.

Table of Contents

  1. Installing Keycloak with Docker
  2. Initial Configuration
  3. Registering Applications (Clients)
  4. Integrating with Python (FastAPI)
  5. Integrating with React
  6. Security Best Practices for Production
  7. Conclusion
  8. References

You can find all codes for this post in reference section.

Installing Keycloak with Docker

Keycloak offers an official Docker image that’s ideal for rapid local development.

docker run -p 8080:8080 \
  -e KEYCLOAK_ADMIN=admin \
  -e KEYCLOAK_ADMIN_PASSWORD=admin \
  quay.io/keycloak/keycloak:24.0.3 \
  start-dev

or using docker-compose

version: '3.8'

services:
  keycloak:
    image: quay.io/keycloak/keycloak:24.0.3
    container_name: keycloak
    command: start-dev
    ports:
      - "8080:8080"
    environment:
      KEYCLOAK_ADMIN: admin
      KEYCLOAK_ADMIN_PASSWORD: admin
    networks:
      - keycloak-net

networks:
  keycloak-net:
    driver: bridge

Open http://localhost:8080 and login with the admin credentials.

Initial Configuration

  1. Create a Realm: Go to the admin console, select Create Realm, and name it (e.g., demo).
  2. Create Users: Add users under Users, assign them roles and set passwords.

Registering Applications (Clients)

We will registrer only one application called “demo”. And we’ll used it for frontend and backend. But that is not a recomendable practice.

To register an application, follow these steps:

  1. Go to Clients > Create.
  2. Set:
    • Client ID: demo
    • Protocol: openid-connect
    • Access Type: public (React)
  3. Add valid redirect URIs:
    • React: http://localhost:3000/*

Integrating with Python (FastAPI)

Use a custom middleware in FastAPI, to manage the access. This middleware has an interaction with keycloak in a simply way, only for retrieve the user information.

from fastapi import FastAPI


from fastapi.middleware.cors import CORSMiddleware
import os

from fastapi import Request
from fastapi.responses import JSONResponse
from keycloak import KeycloakOpenID
from starlette.middleware.base import BaseHTTPMiddleware, \
    RequestResponseEndpoint
from starlette.responses import Response

KEYCLOAK_OPENID = KeycloakOpenID(
    server_url=os.getenv('KEYCLOAK_URL', None),
    client_id=os.getenv('CLIENT_ID', None),
    realm_name=os.getenv('REALM', None),
)
the_user = None

class AuthMiddleware(BaseHTTPMiddleware):
    """

    This code defines an AuthMiddleware class that extends BaseHTTPMiddleware
    to handle authentication for incoming HTTP requests. It checks if the
    request URL is in a predefined list of paths that do not require
    authentication. If the URL is not in this list, it verifies the
    presence of a valid API key in the request headers before allowing
    the request to proceed.

     """
    __jump_paths__ = ['/docs', '/openapi.json', '/redoc',
                      '/health', '/favicon.ico', ]

    __name__api_key__ = 'API_KEY'
    __auth__ = 'authorization'

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

    @staticmethod
    def unauthorised(
            code: int = 401, msg: str = 'Unauthorised') -> JSONResponse:
        """
            Return a message of unauthorised
        """
        return JSONResponse(status_code=code, content=msg)

    def _is_jump_url_(self, request: Request) -> bool:
        return request.url.path in self.__jump_paths__

    def decode_token(self, token: str):
        token_ = token.replace('Bearer ', '')
        payload = KEYCLOAK_OPENID.decode_token(token_)
        global the_user
        the_user = payload
        return payload

    def get_header_token(self, request: Request):
        return request.headers.get(self.__auth__, '')

    def get_user_config(self, request: Request) -> dict:
        token = self.get_header_token(request)
        try:
            decode_token = self.decode_token(token)
            return decode_token
        except Exception:
            return {}

    def is_auth(self, request: Request) -> dict:
        """
        queda por implementar
        :param request:
        :return:
        """
        return self.get_user_config(request)

    async def dispatch(
        self, request: Request, call_next: RequestResponseEndpoint
    ) -> Response:
        """
        The dispatch method in the AuthMiddleware class is an asynchronous
        middleware function that processes incoming HTTP requests.
        It checks if the request URL is in a predefined list of paths
        that do not require authentication. If the URL is not in this
        list, it verifies the presence of a valid API key in the request
         headers before allowing the request to proceed.

        :param request:  An instance of Request representing the incoming
            HTTP request.
        :param call_next: A callable (RequestResponseEndpoint) that processes
            the next middleware or the actual request handler

        :return: Returns a Response object, either from the next
            middleware/request handler or an unauthorized response.
        """
        if self._is_jump_url_(request):
            return await call_next(request)

        response = self.unauthorised()

        if len(self.is_auth(request)) > 0:
            response = await call_next(request)

        return response

app = FastAPI()

app.add_middleware(AuthMiddleware)
app.add_middleware(
    CORSMiddleware,
    allow_origins=['*'],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

@app.get("/protected")
def read_protected(message: str = 'probe'):
    return {"message": message + "Received", "user": the_user}


if __name__ == '__main__':
    import uvicorn
    uvicorn.run(app=app, host="localhost", port=8000)

Integrating with React

Use keycloak-js to handle OIDC authentication in React. With this file you can manage the autentification using login portal from keycloak. We only include here the file that interacts with keycloak. You can find the full react project in the references.

import Keycloak from "keycloak-js";


const keycloakOptions = {
  url: import.meta.env.VITE_KEYCLOAK_URL,
  realm: import.meta.env.VITE_REALM, 
  clientId: import.meta.env.VITE_CLIENT_ID, 
};

const keycloakInstance = new Keycloak(keycloakOptions);
let authenticated = null;
/**
 * Initializes Keycloak and handles authentication.
 */
export const initKeycloak = async () => {
  try {
    if (!authenticated){
      authenticated = await keycloakInstance.init({
        onLoad: "login-required", 
        checkLoginIframe: false,
      });
    }

    if (authenticated) {

      setInterval(async () => {
        if (keycloakInstance.isTokenExpired()) {
          await keycloakInstance.updateToken(30);
        }
      }, 30000);

    }

    return authenticated;
  } catch (error) {
    console.error(`Error in Keycloak: ${error}`);
    return false;
  }
};

/**
 * Close and clean the session.
 */
export const handleLogout = () => {
  if (keycloakInstance) {
    sessionStorage.removeItem("token");
    keycloakInstance.logout();
  }
};

export default keycloakInstance;

For any request made to the Python API, you must include an Authorization Bearer token in the header, using the token you received when logging in. This token is used by your Python backend to validate the request.

Security Best Practices for Production

To prepare your system for production:

  • Enable HTTPS with a reverse proxy (e.g., NGINX + Let’s Encrypt or Traefik)
  • Enable Multi-Factor Authentication (MFA) using OTP apps
  • Enforce strong password policies in Keycloak settings
  • Configure token lifetimes to reduce risk of token theft
  • Enable audit logging and review security events
  • Regularly backup Keycloak DB and config

Conclusion

Implementing OAuth2 and OpenID Connect with Keycloak gives you a solid foundation for secure identity and access management. By using Docker, you can spin up Keycloak quickly for dev/testing, and integrating it with FastAPI and React helps secure full-stack applications. Always apply security hardening before moving to production.

What’s Next?

  • Role-Based Access Control (RBAC) with Keycloak
    Dive deeper into Keycloak by implementing fine-grained role and permission management across your frontend and backend.

  • Custom User Attributes and Claims in Tokens
    Learn how to enrich your tokens with custom claims and use them for authorization logic in your FastAPI backend.

  • Single Sign-On (SSO) Across Multiple Applications
    Explore how to configure SSO using Keycloak for multiple React apps or microservices sharing authentication.

  • User Self-Service Portals with Keycloak Themes
    Customize the Keycloak login, registration, and account management pages to match your application’s brand.

  • Deploying Keycloak Securely in Kubernetes with Ingress and TLS
    Take your deployment to the next level by running Keycloak in a Kubernetes cluster with secure ingress and scalability in mind.

References