forked from kubestellar/console
-
Notifications
You must be signed in to change notification settings - Fork 0
258 lines (231 loc) · 11.5 KB
/
auth-login-smoke.yml
File metadata and controls
258 lines (231 loc) · 11.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
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'],
});