2 Commits

Author SHA1 Message Date
herel afcd230c3a fix(api): Send PKCE code_verifier in token exchange and secure OIDC cookies
The /exchange-token endpoint was not sending the code_verifier to the
token endpoint, rendering PKCE protection ineffective. Add the verifier
to the POST body so the IdP can validate the authorization code exchange.

Also add secure=True and samesite='Lax' to all OIDC cookies on /login
to improve cookie security per the recommended remediation.

Refs vuln-0004
2026-06-29 19:49:07 +02:00
herel 4593f9e8fb fix: use tailwindcss cli 2026-06-29 18:02:10 +02:00
8 changed files with 109 additions and 7 deletions
+5 -1
View File
@@ -3,7 +3,11 @@ myice.ini
schedule.json
.envrc
cookies.txt
# Byte-compiled / optimized / DLL files
# Tailwind CSS generated artifacts
style.css
tailwindcss
# Python bytecode
__pycache__/
*.py[cod]
*$py.class
+15
View File
@@ -48,6 +48,21 @@ pre-commit run ruff-format --files myice/webapi.py
pre-commit run mypy --files myice/myice.py
```
### CSS/Frontend Build
When modifying `index.html` or the Tailwind CSS setup:
```bash
# One-off build of style.css
./build-css.sh
# Watch mode for development
./build-css.sh --watch
# Or manually with the standalone CLI (if already downloaded)
./tailwindcss -i ./style-input.css -o ./style.css --minify
```
### Running the Web API
```bash
+14 -2
View File
@@ -4,7 +4,12 @@ FROM python:3.13-slim AS builder
# Create working directory
WORKDIR /app
# poetry export -f requirements.txt --output requirements.txt --without-hashes
# Install tools needed for Tailwind CSS build
RUN apt-get update && apt-get install -y --no-install-recommends \
ca-certificates \
curl \
&& rm -rf /var/lib/apt/lists/*
# Copy dependency files
COPY requirements.txt ./
@@ -12,6 +17,12 @@ COPY requirements.txt ./
RUN --mount=type=cache,target=/root/.cache/pip \
pip install --no-deps --disable-pip-version-check -r requirements.txt
# Build Tailwind CSS
COPY index.html style-input.css tailwind.config.js ./
RUN curl -sL https://github.com/tailwindlabs/tailwindcss/releases/download/v3.4.17/tailwindcss-linux-x64 -o tailwindcss \
&& chmod +x tailwindcss \
&& ./tailwindcss -i ./style-input.css -o ./style.css --minify
# Runtime stage
FROM python:3.13-slim AS runtime
@@ -29,8 +40,9 @@ RUN useradd --home-dir /app --no-create-home --uid 1000 myice
# Copy installed packages from builder stage
COPY --from=builder /usr/local/lib/python3.13/site-packages /usr/local/lib/python3.13/site-packages
# Copy application code
# Copy application code and compiled assets
COPY index.html favicon.ico ./
COPY --from=builder /app/style.css ./style.css
COPY myice ./myice
# Change ownership of copied files
Executable
+41
View File
@@ -0,0 +1,41 @@
#!/bin/bash
set -euo pipefail
# Build CSS using Tailwind CSS standalone CLI
# For local development, run:
# ./build-css.sh # one-off build
# ./build-css.sh --watch # watch mode
version="v3.4.17"
os="$(uname -s)"
arch="$(uname -m)"
case "$os" in
Linux*) platform="linux";;
Darwin*) platform="macos";;
*) echo "Unsupported OS: $os"; exit 1;;
esac
case "$arch" in
x86_64) arch="x64";;
arm64) arch="arm64";;
aarch64) arch="arm64";;
*) echo "Unsupported architecture: $arch"; exit 1;;
esac
binary="tailwindcss-${platform}-${arch}"
url="https://github.com/tailwindlabs/tailwindcss/releases/download/${version}/${binary}"
if [ ! -f "tailwindcss" ]; then
echo "Downloading Tailwind CSS standalone CLI (${version} ${platform}/${arch})..."
curl -sL "$url" -o tailwindcss
chmod +x tailwindcss
fi
if [ "${1:-}" == "--watch" ]; then
echo "Building CSS in watch mode..."
./tailwindcss -i ./style-input.css -o ./style.css --watch
else
echo "Building CSS..."
./tailwindcss -i ./style-input.css -o ./style.css --minify
fi
+1 -1
View File
@@ -5,7 +5,7 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>MyIce - Games</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="/style.css">
<link rel="icon" href="favicon.ico" type="image/x-icon">
</head>
+22 -3
View File
@@ -140,6 +140,11 @@ def get_health() -> HealthCheck:
return HealthCheck(status="OK")
@app.get("/style.css")
async def style_css():
return FileResponse("style.css")
@app.get("/favicon.ico")
async def favico():
return FileResponse("favicon.ico")
@@ -177,9 +182,20 @@ async def login(request: Request):
response = RedirectResponse(auth_url)
# Store state and nonce in secure http-only cookies for validation on callback
response.set_cookie("oidc_state", state, httponly=True, max_age=300)
response.set_cookie("oidc_nonce", nonce, httponly=True, max_age=300)
response.set_cookie("oidc_verifier", code_verifier, httponly=True, max_age=300)
response.set_cookie(
"oidc_state", state, httponly=True, secure=True, samesite="Lax", max_age=300
)
response.set_cookie(
"oidc_nonce", nonce, httponly=True, secure=True, samesite="Lax", max_age=300
)
response.set_cookie(
"oidc_verifier",
code_verifier,
httponly=True,
secure=True,
samesite="Lax",
max_age=300,
)
return response
@@ -252,6 +268,7 @@ async def callback(request: Request, code: str, state: str):
@app.get("/exchange-token")
async def exchange_token(
request: Request,
code: str,
headers: Annotated[AuthHeaders, Header()],
):
@@ -259,6 +276,7 @@ async def exchange_token(
if not headers.authorized():
raise HTTPException(401, detail="get out")
code_verifier = request.cookies.get("oidc_verifier")
# Exchange authorization code for access token
token_data = {
"grant_type": "authorization_code",
@@ -266,6 +284,7 @@ async def exchange_token(
"client_secret": CLIENT_SECRET,
"code": code,
"redirect_uri": REDIRECT_URI,
"code_verifier": code_verifier,
}
response = requests.post(TOKEN_ENDPOINT, data=token_data)
+3
View File
@@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
+8
View File
@@ -0,0 +1,8 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: ["./index.html"],
theme: {
extend: {},
},
plugins: [],
}