Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Integrate Keycloak with FastAPI

I have an app that is written with FastAPI and SvelteKit, both these is running in separate containers, the current solution is Svelte sends the username and password to the FastAPI server, the FastAPI server then returns a signed JWT to svelte which is used to authenticate with FastAPI.

Heres the login flow Svelte -> username + password -> FastAPI

FastAPI -> JWT -> Svelte(stored in cookie)

Here's what happens when a request is made Svelte(Authorization: Bearer) -> FastAPI

I want to get rid of my own username and password and use KeyCloak for my authentication. I'm super new to OAuth so I have no idea what I'm supposed to do or even what terms to search.

Here's what I understand I want: Svelte(goto "/login") -> redirects to keycloak -> login and get a token (somehow get the token to FastAPI so I can sign my own token and send it to svelte)

And when I make a request Svelte -> FastAPI Token -> FastAPI

like image 261
MaoMaoCake Avatar asked May 12 '26 18:05

MaoMaoCake


1 Answers

I crafted some Python code for fastAPI with keycloak integration, it may be helpful to share it.

Declare auth functions

#/auth.py
from fastapi.security import OAuth2AuthorizationCodeBearer
from keycloak import KeycloakOpenID # pip require python-keycloak
from config import settings
from fastapi import Security, HTTPException, status,Depends
from pydantic import Json
from models import User

# This is used for fastapi docs authentification
oauth2_scheme = OAuth2AuthorizationCodeBearer(
    authorizationUrl=settings.authorization_url, # https://sso.example.com/auth/
    tokenUrl=settings.token_url, # https://sso.example.com/auth/realms/example-realm/protocol/openid-connect/token
)

# This actually does the auth checks
# client_secret_key is not mandatory if the client is public on keycloak
keycloak_openid = KeycloakOpenID(
    server_url=settings.server_url, # https://sso.example.com/auth/
    client_id=settings.client_id, # backend-client-id
    realm_name=settings.realm, # example-realm
    client_secret_key=settings.client_secret, # your backend client secret
    verify=True
)

async def get_idp_public_key():
    return (
        "-----BEGIN PUBLIC KEY-----\n"
        f"{keycloak_openid.public_key()}"
        "\n-----END PUBLIC KEY-----"
    )

# Get the payload/token from keycloak
async def get_payload(token: str = Security(oauth2_scheme)) -> dict:
    try:
        return keycloak_openid.decode_token(
            token,
            key= await get_idp_public_key(),
            options={
                "verify_signature": True,
                "verify_aud": False,
                "exp": True
            }
        )
    except Exception as e:
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail=str(e), # "Invalid authentication credentials",
            headers={"WWW-Authenticate": "Bearer"},
        )
    
# Get user infos from the payload
async def get_user_info(payload: dict = Depends(get_payload)) -> User:
    try:
        return User(
            id=payload.get("sub"),
            username=payload.get("preferred_username"),
            email=payload.get("email"),
            first_name=payload.get("given_name"),
            last_name=payload.get("family_name"),
            realm_roles=payload.get("realm_access", {}).get("roles", []),
            client_roles=payload.get("realm_access", {}).get("roles", [])
        )
    except Exception as e:
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail=str(e), # "Invalid authentication credentials",
            headers={"WWW-Authenticate": "Bearer"},
        )

Define your models , you can use what ever model you need :

#/models.py
from pydantic import BaseModel, EmailStr

class User(BaseModel):
    id: str
    username: str
    email: str
    first_name: str
    last_name: str
    realm_roles: list
    client_roles: list

class authConfiguration(BaseModel):
        server_url: str
        realm: str
        client_id: str
        client_secret: str
        authorization_url: str
        token_url: str

Define your config:

#/config.py
from models import authConfiguration


settings = authConfiguration(
    server_url="http://localhost:8080/",
    realm="roc",
    client_id="rns:roc:portal",
    client_secret="",
    authorization_url="http://localhost:8080/realms/roc/protocol/openid-connect/auth",
    token_url="http://localhost:8080/realms/roc/protocol/openid-connect/token",
)

and finally define your routes and secure them:

#/main.py
import uvicorn
from fastapi import FastAPI,Depends
from models import User
from auth import get_user_info

app = FastAPI()

@app.get("/")
async def root():
    return {"message": "Hello World"}

@app.get("/secure")
async def root(user: User = Depends(get_user_info)):
    return {"message": f"Hello {user.username} you have the following service: {user.realm_roles}"}


if __name__ == '__main__':
    uvicorn.run('main:app', host="127.0.0.1", port=8081)

On the keycloak side create a realm and within this real create a client. Create a test user to use it for authentification and that's it!

References:

  • https://github.com/tiangolo/fastapi/discussions/9066
  • https://pypi.org/project/python-keycloak/
like image 98
ilyesAj Avatar answered May 14 '26 09:05

ilyesAj



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!