Skip to content

Commit 8e971aa

Browse files
committed
Added a protected API
1 parent da2bd13 commit 8e971aa

File tree

2 files changed

+118
-1
lines changed

2 files changed

+118
-1
lines changed

pyapp/app/auth_dep.py

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import logging
2+
from typing import Any, Dict
3+
from urllib.parse import urljoin
4+
5+
import httpx
6+
from fastapi import HTTPException, Request, status
7+
from joserfc import jwt
8+
from joserfc.jwk import KeySet
9+
from pydantic import BaseModel, Field
10+
11+
logger = logging.getLogger(__name__)
12+
13+
_jwks_keyset_cache: Dict[str, KeySet] = {}
14+
15+
16+
class AuthenticatedUser(BaseModel):
17+
subject: str
18+
name: str | None = None
19+
preferred_username: str | None = None
20+
email: str | None = None
21+
roles: list[str] = Field(default_factory=list)
22+
# claims: Dict[str, Any]
23+
24+
def __str__(self) -> str:
25+
return self.preferred_username or self.subject
26+
27+
28+
async def get_jwks_keyset(request: Request) -> KeySet | None:
29+
"""
30+
Convert the JWKS dictionary into a key set usable for verification.
31+
"""
32+
cache_key = str(request.base_url)
33+
34+
if cache_key in _jwks_keyset_cache:
35+
return _jwks_keyset_cache[cache_key]
36+
37+
# url = urljoin(str(request.base_url), "auth/keys")
38+
url = "https://dev.id.scouterna.se/realms/jamboree26/protocol/openid-connect/certs"
39+
try:
40+
async with httpx.AsyncClient(timeout=5.0) as http_client:
41+
response = await http_client.get(url)
42+
response.raise_for_status()
43+
jwks_dict = response.json()
44+
except Exception as exc:
45+
logger.warning("Failed to fetch %s: %s", url, exc)
46+
return None
47+
48+
try:
49+
keyset = KeySet.import_key_set(jwks_dict)
50+
_jwks_keyset_cache[cache_key] = keyset
51+
return keyset
52+
except Exception as exc:
53+
logger.warning("Failed to parse JWKS: %s", exc)
54+
return None
55+
56+
57+
async def decode_access_token(token: str, request: Request) -> Dict[str, Any]:
58+
keyset = await get_jwks_keyset(request)
59+
if keyset is None:
60+
raise HTTPException(status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail="Token validation unavailable")
61+
62+
try:
63+
token_obj = jwt.decode(token, keyset)
64+
registry = jwt.JWTClaimsRegistry(leeway=30)
65+
registry.validate(token_obj.claims)
66+
return dict(token_obj.claims)
67+
except Exception as exc:
68+
logger.warning("Failed to validate JWT: %s", exc)
69+
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Unauthorized") from exc
70+
71+
72+
def _extract_roles(claims: Dict[str, Any]) -> list[str]:
73+
roles = set()
74+
realm_access = claims.get("realm_access") or {}
75+
realm_roles = realm_access.get("roles") or []
76+
roles.update(role for role in realm_roles if isinstance(role, str))
77+
78+
resource_access = claims.get("resource_access") or {}
79+
for resource in resource_access.values():
80+
resource_roles = resource.get("roles") if isinstance(resource, dict) else []
81+
roles.update(role for role in (resource_roles or []) if isinstance(role, str))
82+
83+
return sorted(roles)
84+
85+
86+
async def require_authenticated_user(request: Request) -> AuthenticatedUser:
87+
"""
88+
FastAPI dependency that validates the auth cookie and returns user info + roles.
89+
"""
90+
token = request.cookies.get("j26-auth_access-token")
91+
if not token:
92+
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Unauthorized")
93+
94+
claims = await decode_access_token(token, request)
95+
96+
return AuthenticatedUser(
97+
subject=claims.get("sub", ""),
98+
name=claims.get("name"),
99+
preferred_username=claims.get("preferred_username"),
100+
email=claims.get("email"),
101+
roles=_extract_roles(claims),
102+
# claims=claims,
103+
)

pyapp/app/info_api.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,10 @@
33
import os
44

55
import kubernetes
6-
from fastapi import APIRouter, Request, status
6+
from fastapi import APIRouter, Depends, Request, status
77
from kubernetes.client.models import V1Pod
88

9+
from .auth_dep import AuthenticatedUser, require_authenticated_user
910
from .config import get_settings
1011

1112
settings = get_settings()
@@ -78,3 +79,16 @@ async def get_cookies(request: Request):
7879
Return all cookies sent with the request as a dict.
7980
"""
8081
return dict(request.cookies)
82+
83+
84+
@router.get(
85+
"/user",
86+
response_model=dict,
87+
status_code=status.HTTP_200_OK,
88+
response_description="Returns the user object",
89+
)
90+
async def get_user(user: AuthenticatedUser = Depends(require_authenticated_user)):
91+
"""
92+
Returns an autenticated user object as a dict.
93+
"""
94+
return dict(user)

0 commit comments

Comments
 (0)