Move browser authentication behind a generated-contract BFF

This commit is contained in:
ms2k 2026-07-19 18:21:17 +09:00
commit fa7220ee33
2 changed files with 7259 additions and 2825 deletions

View file

@ -1,6 +1,7 @@
import { createPrivateKey, sign } from 'node:crypto'
import { createPrivateKey, randomBytes, sign } from 'node:crypto'
import { expect, test } from '@playwright/test'
import { createClient } from 'redis'
test('authenticates and completes a deployed job lifecycle through ingress', async ({
page,
@ -8,28 +9,49 @@ test('authenticates and completes a deployed job lifecycle through ingress', asy
const token = await mintZitadelToken()
const claims = jwtClaims(token)
const issuer = requiredEnv('REMORA_OIDC_ISSUER')
const sessionId = randomBytes(32).toString('base64url')
const csrfToken = randomBytes(24).toString('base64url')
const coordination = createClient({
url: requiredEnv('REMORA_COORDINATION_URL'),
})
await coordination.connect()
await coordination.set(
`${requiredEnv('REMORA_COORDINATION_NAMESPACE')}web-session:${sessionId}`,
JSON.stringify({
kind: 'authenticated',
identity: `${issuer}\u0000${stringValue(claims.sub, 'subject')}`,
csrfToken,
accessToken: token,
refreshToken: null,
idToken: 'qualification-session',
accessExpiresAt: Number(claims.exp),
}),
{ EX: 3600 },
)
await coordination.quit()
await page.context().addCookies([
{
name: 'remora-qualification-session',
value: sessionId,
url: requiredEnv('REMORA_SYSTEM_BASE_URL'),
sameSite: 'Strict',
},
])
const assetResponse = page.waitForResponse((response) => {
const path = new URL(response.url()).pathname
return path.startsWith('/assets/') && path.endsWith('.js')
})
await page.goto('/')
await page.evaluate(
({ accessToken, authority, profile }) => {
sessionStorage.setItem(
`oidc.user:${authority}:remora-web`,
JSON.stringify({
access_token: accessToken,
token_type: 'Bearer',
scope: 'openid profile',
profile,
expires_at: Number(profile.exp),
}),
)
},
{ accessToken: token, authority: issuer, profile: claims },
expect(['br', 'gzip']).toContain(
(await assetResponse).headers()['content-encoding'],
)
await page.reload()
await expect(page.getByRole('heading', { name: 'Remora' })).toBeVisible()
await expect(page.getByRole('button', { name: 'Sign out' })).toBeVisible()
await expect(page.getByLabel('Synchronization status')).toHaveText('live')
await expect(page.getByLabel('Synchronization status')).toHaveText('live', {
timeout: 30_000,
})
await expect(page.getByText('deployment-coordination-rebuild')).toBeVisible()
await page.getByRole('button', { name: 'Submit job' }).click()
@ -39,7 +61,7 @@ test('authenticates and completes a deployed job lifecycle through ingress', asy
const submitted = page.waitForResponse(
(response) =>
response.request().method() === 'POST' &&
new URL(response.url()).pathname === '/v1/jobs',
new URL(response.url()).pathname === '/bff/api/jobs',
)
await page.getByRole('button', { name: 'Submit job now' }).click()
const response = await submitted

View file

@ -5,13 +5,14 @@ const upstreamHost = 'qualification-ingress'
const upstreamPort = 80
const server = http.createServer((request, response) => {
const headers = upstreamHeaders(request.headers)
const upstream = http.request(
{
host: upstreamHost,
port: upstreamPort,
method: request.method,
path: request.url,
headers: { ...request.headers, host: upstreamHost },
headers,
},
(upstreamResponse) => {
response.writeHead(
@ -30,7 +31,7 @@ const server = http.createServer((request, response) => {
server.on('upgrade', (request, socket, head) => {
const upstream = net.connect(upstreamPort, upstreamHost, () => {
const headers = Object.entries(request.headers)
const headers = Object.entries(upstreamHeaders(request.headers))
.filter(([name]) => name.toLowerCase() !== 'host')
.map(([name, value]) => `${name}: ${String(value)}`)
.join('\r\n')
@ -41,6 +42,25 @@ server.on('upgrade', (request, socket, head) => {
socket.pipe(upstream).pipe(socket)
})
upstream.on('error', () => socket.destroy())
socket.on('error', () => upstream.destroy())
})
function upstreamHeaders(incoming) {
const headers = { ...incoming, host: upstreamHost }
const session = qualificationSession(headers.cookie)
if (typeof session === 'string' && /^[A-Za-z0-9_-]{43}$/.test(session))
headers.cookie = `__Host-remora-session=${session}`
else delete headers.cookie
return headers
}
function qualificationSession(cookieHeader) {
if (typeof cookieHeader !== 'string') return null
for (const cookie of cookieHeader.split(';')) {
const [name, value] = cookie.trim().split('=', 2)
if (name === 'remora-qualification-session') return value
}
return null
}
server.listen(4174, '127.0.0.1')