Skip to content

Commit ed46c28

Browse files
committed
Add initial fastapi usage example
1 parent b74837c commit ed46c28

2 files changed

Lines changed: 86 additions & 0 deletions

File tree

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
from fastapi import FastAPI, Depends, HTTPException, status
2+
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
3+
from jose import jwt, jwk
4+
import httpx
5+
from functools import lru_cache
6+
7+
OIDC_DISCOVERY_URL = "http://192.168.1.41:8081/oidc/keyline/.well-known/openid-configuration"
8+
EXPECTED_AUDIENCE = "example"
9+
10+
app = FastAPI()
11+
security = HTTPBearer()
12+
13+
14+
@lru_cache
15+
def get_oidc_config():
16+
"""Fetch OIDC discovery and JWKS data once and cache it."""
17+
config = httpx.get(OIDC_DISCOVERY_URL, timeout=5).json()
18+
jwks_uri = config["jwks_uri"]
19+
jwks = httpx.get(jwks_uri, timeout=5).json()
20+
return {
21+
"issuer": config["issuer"],
22+
"jwks": {key["kid"]: key for key in jwks["keys"]},
23+
}
24+
25+
26+
def verify_jwt(token: str):
27+
config = get_oidc_config()
28+
29+
try:
30+
header = jwt.get_unverified_header(token)
31+
jwk_data = config["jwks"].get(header["kid"])
32+
except Exception as e:
33+
raise HTTPException(status_code=401, detail=f"Invalid token header: {e}")
34+
35+
if not jwk_data:
36+
raise HTTPException(status_code=401, detail="Unknown key ID")
37+
38+
key = jwk.construct(jwk_data)
39+
40+
try:
41+
payload = jwt.decode(
42+
token,
43+
key,
44+
algorithms=[jwk_data.get("alg", "RS256")],
45+
issuer=config["issuer"],
46+
audience=EXPECTED_AUDIENCE, # ✅ Enforce correct audience
47+
)
48+
return payload
49+
except jwt.ExpiredSignatureError:
50+
raise HTTPException(status_code=401, detail="Token expired")
51+
except jwt.JWTClaimsError as e:
52+
raise HTTPException(status_code=401, detail=f"Invalid claims: {e}")
53+
except jwt.JWTError as e:
54+
raise HTTPException(status_code=401, detail=f"Invalid token: {e}")
55+
56+
57+
def get_current_user(credentials: HTTPAuthorizationCredentials = Depends(security)):
58+
return verify_jwt(credentials.credentials)
59+
60+
61+
def require_role(role: str):
62+
def checker(user: dict = Depends(get_current_user)):
63+
roles = user.get("roles") or user.get("realm_access", {}).get("roles", [])
64+
if role not in roles:
65+
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Forbidden")
66+
return user
67+
return checker
68+
69+
70+
@app.get("/public")
71+
def public():
72+
return {"message": "This is a public endpoint"}
73+
74+
75+
@app.get("/me")
76+
def me(user: dict = Depends(get_current_user)):
77+
return {"subject": user.get("sub"), "claims": user}
78+
79+
80+
@app.get("/subscriber")
81+
def subscriber(user: dict = Depends(require_role("subscriber"))):
82+
return {"message": "Welcome, Keyline subscriber!"}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
fastapi
2+
uvicorn[standard]
3+
python-jose[cryptography]
4+
httpx

0 commit comments

Comments
 (0)