Skip to content

Auth Login Smoke Test #87

Auth Login Smoke Test

Auth Login Smoke Test #87

name: Auth Login Smoke Test
# Verifies the auth login contract by building and running the Go backend
# in dev mode. No external dependencies — everything runs against localhost.
#
# #6590 contract (enforced by #8107): the refreshed JWT is delivered
# EXCLUSIVELY via the HttpOnly `kc_auth` cookie. The JSON body carries
# only `{ refreshed: true, onboarded }` — any `token` field in the body
# would be an XSS/extension-readable leak and must fail the smoke test.
#
# What it checks:
# 1. /health returns JSON with status and oauth_configured fields
# 2. GET /auth/github (dev mode, no OAuth client) sets a kc_auth cookie
# 3. POST /auth/refresh (cookie + CSRF header, NO Authorization) returns
# - response body with { refreshed: true, onboarded }
# - Set-Cookie: kc_auth=... (refreshed JWT)
# - NO `token` field in the body
#
# The test uses DEV_MODE (no GitHub OAuth credentials) so the backend
# auto-creates a dev-user on /auth/github. This takes ~60 seconds.
on:
schedule:
# Every hour
- cron: "0 * * * *"
workflow_dispatch:
permissions:
contents: read
issues: write
concurrency:
group: auth-login-smoke
cancel-in-progress: true
jobs:
auth-smoke:
if: github.repository == 'kubestellar/console'
name: Auth Login Smoke
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version-file: go.mod
# ── 1. Build the Go backend ──────────────────────────────────
- name: Build backend
run: go build -o console-bin ./cmd/console
# ── 2. Start backend in dev mode ─────────────────────────────
- name: Start backend
run: |
JWT_SECRET="smoke-test-$(openssl rand -hex 16)" \
DEV_MODE=true \
./console-bin --dev --port 8081 &
echo $! > /tmp/backend.pid
# ── 3. Wait for health ───────────────────────────────────────
- name: Wait for backend health
run: |
MAX_WAIT_SECONDS=30
for i in $(seq 1 "$MAX_WAIT_SECONDS"); do
if curl -sf http://localhost:8081/health > /dev/null 2>&1; then
echo "Backend healthy after ${i}s"
exit 0
fi
sleep 1
done
echo "::error::Backend did not become healthy within ${MAX_WAIT_SECONDS}s"
exit 1
# ── 4. Test /health returns JSON with expected fields ────────
- name: Test /health endpoint
run: |
echo "Checking /health endpoint..."
HEALTH_RESP=$(curl -sS --max-time 5 http://localhost:8081/health)
echo "Response: $HEALTH_RESP"
STATUS=$(echo "$HEALTH_RESP" | jq -r '.status // empty')
if [ -z "$STATUS" ]; then
echo "::error::/health response missing 'status' field"
exit 1
fi
echo "status=$STATUS"
# In dev mode with no OAuth creds, oauth_configured should be false
OAUTH=$(echo "$HEALTH_RESP" | jq -r '.oauth_configured // empty')
echo "oauth_configured=$OAUTH"
echo "/health OK"
# ── 5. Dev-mode login via /auth/github ───────────────────────
- name: Get dev-mode auth cookie
run: |
echo "Calling GET /auth/github (dev mode login)..."
# Dev mode redirects and sets kc_auth HttpOnly cookie.
# Use -c to capture the cookie jar, -L to NOT follow redirects
# (we just need the Set-Cookie header).
COOKIE_JAR=/tmp/cookies.txt
HTTP_CODE=$(curl -sS --max-time 5 \
-o /dev/null -w '%{http_code}' \
-c "$COOKIE_JAR" \
http://localhost:8081/auth/github)
echo "HTTP status: $HTTP_CODE (expect 307 redirect)"
# Extract the kc_auth cookie value
KC_AUTH=$(grep 'kc_auth' "$COOKIE_JAR" | awk '{print $NF}')
if [ -z "$KC_AUTH" ]; then
echo "::error::Dev-mode login did not set kc_auth cookie"
echo "Cookie jar contents:"
cat "$COOKIE_JAR"
exit 1
fi
echo "Got kc_auth cookie (${#KC_AUTH} chars)"
echo "$KC_AUTH" > /tmp/kc_auth_token.txt
# ── 6. Test /auth/refresh contract (#6590) ───────────────────
# The refreshed JWT is delivered via the HttpOnly kc_auth cookie
# ONLY. The JSON body must NOT contain a `token` field — any such
# field is readable by XSS/extension content scripts and is the
# regression #8107 fixed. We drive this call exactly like the
# frontend does: cookie jar + CSRF header, no Authorization.
- name: Test /auth/refresh contract
run: |
COOKIE_JAR=/tmp/cookies.txt
REFRESH_COOKIE_JAR=/tmp/cookies-after-refresh.txt
echo "Testing POST /auth/refresh..."
REFRESH_RESP=$(curl -sS --max-time 5 \
-X POST \
-b "$COOKIE_JAR" \
-c "$REFRESH_COOKIE_JAR" \
-H "X-Requested-With: XMLHttpRequest" \
http://localhost:8081/auth/refresh)
echo "Response: $REFRESH_RESP"
# #6590 CONTRACT: response body MUST NOT contain a `token` field.
# Presence of `token` means the refreshed JWT is JS-readable,
# which is the XSS leak that #8107 fixed.
HAS_TOKEN=$(echo "$REFRESH_RESP" | jq 'has("token")')
if [ "$HAS_TOKEN" != "false" ]; then
echo "::error::CONTRACT VIOLATION: /auth/refresh response includes 'token' field — this is the #6590 XSS leak that #8107 fixed. Refreshed JWT must live in the HttpOnly kc_auth cookie only."
echo "Response was: $REFRESH_RESP"
exit 1
fi
echo "token field absent from body (correct)"
# #6590 CONTRACT: response body MUST contain `onboarded` (boolean)
HAS_ONBOARDED=$(echo "$REFRESH_RESP" | jq 'has("onboarded")')
if [ "$HAS_ONBOARDED" != "true" ]; then
echo "::error::CONTRACT VIOLATION: /auth/refresh response missing 'onboarded' field"
echo "Response was: $REFRESH_RESP"
exit 1
fi
echo "onboarded field present"
# #6590 CONTRACT: response body MUST contain `refreshed: true`
HAS_REFRESHED=$(echo "$REFRESH_RESP" | jq -r '.refreshed // empty')
if [ "$HAS_REFRESHED" != "true" ]; then
echo "::error::CONTRACT VIOLATION: /auth/refresh response missing 'refreshed' field"
echo "Response was: $REFRESH_RESP"
exit 1
fi
echo "refreshed field present"
# #6590 CONTRACT: Set-Cookie must refresh the kc_auth cookie.
# curl writes Netscape-format cookie jars. HttpOnly cookies are prefixed
# with "#HttpOnly_" on the line (e.g. "#HttpOnly_localhost\tFALSE\t/\tTRUE\t...\tkc_auth\t<token>").
# We WANT the HttpOnly form — it proves the HttpOnly flag is set — so we
# match the cookie name as a tab-separated field regardless of leading
# host/HttpOnly prefix, then separately assert the HttpOnly line exists.
REFRESHED_KC_AUTH=$(grep -E $'(^|\t)kc_auth\t' "$REFRESH_COOKIE_JAR" 2>/dev/null | awk -F'\t' '{print $NF}' | tail -n1)
if [ -z "$REFRESHED_KC_AUTH" ]; then
echo "::error::CONTRACT VIOLATION: /auth/refresh did not set a new kc_auth cookie. The refreshed JWT must be delivered exclusively via the HttpOnly cookie."
echo "Cookie jar contents:"
cat "$REFRESH_COOKIE_JAR" || true
exit 1
fi
echo "kc_auth cookie refreshed (${#REFRESHED_KC_AUTH} chars)"
# #6590 CONTRACT: the refreshed kc_auth cookie must be HttpOnly.
# curl's Netscape jar encodes this by prefixing the line with "#HttpOnly_".
# Assert that a line starting with "#HttpOnly_" contains "kc_auth" — this
# proves the Set-Cookie response included HttpOnly (the #8107 XSS fix).
if ! grep -E '^#HttpOnly_.*[[:space:]]kc_auth[[:space:]]' "$REFRESH_COOKIE_JAR" >/dev/null 2>&1; then
echo "::error::CONTRACT VIOLATION: refreshed kc_auth cookie is NOT HttpOnly. The #8107 XSS fix requires Set-Cookie: kc_auth=...; HttpOnly."
echo "Cookie jar contents:"
cat "$REFRESH_COOKIE_JAR" || true
exit 1
fi
echo "kc_auth cookie is HttpOnly (correct)"
echo ""
echo "Auth login smoke test PASSED"
# ── 7. Cleanup ───────────────────────────────────────────────
- name: Stop backend
if: always()
run: |
if [ -f /tmp/backend.pid ]; then
kill "$(cat /tmp/backend.pid)" 2>/dev/null || true
fi
# ── 8. Alert on failure ──────────────────────────────────────
- name: Create issue on failure
if: failure()
uses: actions/github-script@v7
with:
script: |
const title = `Auth login smoke test failed — OAuth login contract may be broken`;
const body = [
'## Auth Login Smoke Test Failure',
'',
`**Run:** ${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`,
`**Time:** ${new Date().toISOString()}`,
'',
'The auth contract test failed against a local backend in dev mode.',
'This means the `/auth/refresh` endpoint is not returning the expected response shape.',
'',
'### #6590 contract (enforced by #8107)',
'The refreshed JWT must be delivered EXCLUSIVELY via the HttpOnly `kc_auth` cookie.',
'The JSON body carries only `{ refreshed: true, onboarded }`. Any `token` field in the body',
'is an XSS/extension-readable leak and MUST fail this smoke test.',
'',
'### Action required',
'1. Check the workflow run logs above for the specific failure',
'2. Verify /auth/refresh:',
' - body contains `refreshed: true` and `onboarded`',
' - body does NOT contain `token`',
' - response sets a fresh `kc_auth` HttpOnly cookie',
'3. Run `go test ./pkg/api/handlers/ -run TestAuthRefreshContract` locally',
'4. DO NOT "fix" this by re-adding `token` to the response body — that is the #6590 regression',
' and will be reverted.',
].join('\n');
// Don't create duplicate issues
const existing = await github.rest.issues.listForRepo({
owner: context.repo.owner,
repo: context.repo.repo,
state: 'open',
labels: 'auth-smoke-failure',
per_page: 1,
});
if (existing.data.length > 0) {
console.log(`Issue already open: #${existing.data[0].number}`);
return;
}
await github.rest.issues.create({
owner: context.repo.owner,
repo: context.repo.repo,
title,
body,
labels: ['auth-smoke-failure', 'priority/critical', 'ai-needs-human'],
});