Skip to content

Commit 39d5e2f

Browse files
committed
Complete protection of /hello endpoint
Signed-off-by: Federico Busetti <[email protected]>
1 parent 28d1dd9 commit 39d5e2f

File tree

7 files changed

+114
-34
lines changed

7 files changed

+114
-34
lines changed

local.env

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
1+
AUTH__JWKS_URL: "http://oathkeeper:4456/.well-known/jwks.json"
12
DRAMATIQ__REDIS_URL: "redis://redis:6379/0"
23
OTEL_EXPORTER_OTLP_ENDPOINT: "http://otel-collector:4317"

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ dependencies = [
3232

3333
[dependency-groups]
3434
http = [
35+
"cryptography>=44.0.0",
3536
"fastapi>=0.99.0",
3637
"jinja2<4.0.0,>=3.1.2",
3738
# We use the generic ASGI instrumentation, so that if we decide to change

src/common/config.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ class DramatiqConfig(BaseModel):
1313

1414

1515
class AuthConfig(BaseModel):
16-
JWT_ALGORITHM: str = "HS256"
16+
JWT_ALGORITHM: str = "RS256"
1717
JWKS_URL: Optional[str] = None
1818

1919

src/http_app/jinja_templates/hello.html

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,7 @@
44
</head>
55
<body>
66
<h1>Hello world</h1>
7+
<p>Your JWT token payload:</p>
8+
<pre>{{ token_payload | tojson(4) }}</pre>
79
</body>
810
</html>

src/http_app/routes/auth.py

Lines changed: 11 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
1-
import time
21
from typing import Annotated, Any, Optional
32

43
import jwt
54
from fastapi import Depends, HTTPException, Request, status
6-
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
5+
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer, SecurityScopes
76

87
from common import AppConfig
98
from http_app.dependencies import app_config
@@ -35,35 +34,21 @@ def _jwks_client(config: Annotated[AppConfig, Depends(app_config)]) -> jwt.PyJWK
3534
return jwt.PyJWKClient(config.AUTH.JWKS_URL)
3635

3736

38-
class JWTBearer(HTTPBearer):
39-
async def __call__(
40-
self,
41-
request: Request,
42-
) -> Optional[HTTPAuthorizationCredentials]:
43-
credentials = await super(JWTBearer, self).__call__(request)
44-
45-
await self.decode(request)
37+
class JWTDecoder:
38+
"""Does all the token verification using PyJWT"""
4639

47-
return credentials
48-
49-
async def decode(
40+
async def __call__(
5041
self,
51-
request: Request,
52-
jwks_client: jwt.PyJWKClient = Depends(_jwks_client),
42+
security_scopes: SecurityScopes,
5343
config: AppConfig = Depends(app_config),
54-
) -> dict[str, Any]:
55-
credentials = await super(JWTBearer, self).__call__(request)
56-
57-
if not credentials:
44+
jwks_client: jwt.PyJWKClient = Depends(_jwks_client),
45+
token: Optional[HTTPAuthorizationCredentials] = Depends(HTTPBearer()),
46+
):
47+
if token is None:
5848
raise UnauthenticatedException()
5949

60-
if not credentials.scheme == "Bearer":
61-
raise UnauthorizedException("Invalid authentication scheme.")
62-
6350
try:
64-
signing_key = jwks_client.get_signing_key_from_jwt(
65-
credentials.credentials
66-
).key
51+
signing_key = jwks_client.get_signing_key_from_jwt(token.credentials).key
6752
except jwt.exceptions.PyJWKClientError as error:
6853
raise UnauthorizedException(str(error))
6954
except jwt.exceptions.DecodeError as error:
@@ -73,7 +58,7 @@ async def decode(
7358
# TODO: Review decode setup and verifications
7459
# https://pyjwt.readthedocs.io/en/stable/api.html#jwt.decode
7560
payload = jwt.decode(
76-
jwt=credentials.credentials,
61+
jwt=token.credentials,
7762
key=signing_key,
7863
algorithms=[config.AUTH.JWT_ALGORITHM],
7964
)

src/http_app/routes/hello.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
1-
from fastapi import APIRouter, Request
1+
from fastapi import APIRouter, Request, Security
22
from fastapi.responses import HTMLResponse
33

44
from http_app.templates import templates
55

6+
from .auth import JWTDecoder
7+
68
router = APIRouter(prefix="/hello")
79

810

9-
@router.get("/", response_class=HTMLResponse, include_in_schema=False)
10-
async def hello(request: Request):
11-
return templates.TemplateResponse("hello.html", {"request": request})
11+
@router.get("/", response_class=HTMLResponse, include_in_schema=True)
12+
async def hello(request: Request, jwt_token=Security(JWTDecoder())):
13+
return templates.TemplateResponse(
14+
"hello.html", {"request": request, "token_payload": jwt_token}
15+
)

0 commit comments

Comments
 (0)