Skip to main content

Keycloak Core Concepts

HttpOnly Cookie prevents JavaScript from directly reading cookies, protecting against XSS attacks.

Browser receives Set-Cookie: token=xxx; HttpOnly
↓
document.cookie cannot read this cookie
↓
Even if malicious JS is injected via XSS, it cannot steal the token

Refresh Token Strategy​

Before calling an API, check the token's exp (expiration). If expired, refresh first then proceed:

def get_valid_token():
if time.time() >= token_expiration:
refresh_token()
return access_token

SSO Session Parameters​

ParameterDescriptionEffect
SSO Session IdleIf the user has no activity beyond this time, the session expiresControls auto-logout after inactivity
SSO Session MaxRegardless of activity, the session expires after this duration from loginControls maximum single-session duration

Permission Propagation Delay​

After changing user permissions, they don't always take effect immediately:

SystemDelay
Google Cloud IAMUsually 2–7 min, group changes may take hours
Azure AD / Office 365Minutes to 1 hour, up to 24 hours
SAPRequires re-login
Keycloak (custom)Controlled by access token expiry, usually within 2 minutes

RBAC Design Pattern​

Design roles at the page/feature level:

home_read → can view the Home page
home_write → can perform actions on the Home page

admin_read → can view the Admin page
admin_write → can perform actions on the Admin page

Use a decorator for API permission checking:

@require_roles(["home_read", "admin_read"])
async def get_dashboard():
...

Validates that the JWT in the request contains the required role.

Two ways to handle permission changes:

  1. Wait for the access token to expire and refresh automatically (set 2-minute expiry — wait at most 2 minutes)
  2. Ask the user to log out and log back in

Singleton Pattern for Token Management​

Share one token across the whole app, using Singleton to manage auto-refresh:

class OAuthClient:
def __init__(self, host: str, realm: str, client_id: str, client_secret: str):
self.host = host
self.realm = realm
self.client_id = client_id
self.client_secret = client_secret
self.access_token = None
self.token_expiration = 0

def get_header(self):
return {
"Content-Type": "application/json",
"Authorization": f"Bearer {self._get_access_token()}",
}

def _get_access_token(self):
if self.access_token is None or time.time() >= self.token_expiration:
self._refresh_token()
return self.access_token

def _refresh_token(self):
response = requests.post(
f"{self.host}/realms/{self.realm}/protocol/openid-connect/token",
data={
"grant_type": "client_credentials",
"client_id": self.client_id,
"client_secret": self.client_secret,
},
headers={"Content-Type": "application/x-www-form-urlencoded"},
timeout=10,
)
response.raise_for_status()
token_data = response.json()
self.access_token = token_data.get("access_token")
expires_in = token_data.get("expires_in", 60)
self.token_expiration = time.time() + expires_in - 60 # refresh 60 seconds early


# Whole app shares one instance
oauth_client = OAuthClient(
host=settings.OAUTH_HOST,
realm=settings.OAUTH_REALM,
client_id=settings.CLIENT_ID,
client_secret=settings.CLIENT_SECRET,
)