Move browser authentication behind a generated-contract BFF
This commit is contained in:
parent
6f0f20f5cc
commit
fa7220ee33
1 changed files with 7259 additions and 2825 deletions
149
web/server/security.test.mjs
Normal file
149
web/server/security.test.mjs
Normal file
|
|
@ -0,0 +1,149 @@
|
|||
import assert from 'node:assert/strict'
|
||||
import { Readable } from 'node:stream'
|
||||
import { test } from 'node:test'
|
||||
|
||||
import { validBrowserContext, validUnsafeRequest } from './oauth.mjs'
|
||||
import { ApiProxy } from './proxy.mjs'
|
||||
import {
|
||||
SESSION_COOKIE,
|
||||
expiredSessionCookie,
|
||||
sessionCookie,
|
||||
sessionIdentifier,
|
||||
} from './sessions.mjs'
|
||||
|
||||
const config = {
|
||||
publicOrigin: 'https://remora.example.com',
|
||||
secureCookie: true,
|
||||
}
|
||||
|
||||
test('session cookies are host-only, opaque, secure, and not script-readable', () => {
|
||||
const cookie = sessionCookie('opaque-session', config)
|
||||
assert.equal(
|
||||
cookie,
|
||||
`${SESSION_COOKIE}=opaque-session; Path=/; HttpOnly; Secure; SameSite=Strict`,
|
||||
)
|
||||
assert.doesNotMatch(cookie, /Domain=/)
|
||||
assert.match(expiredSessionCookie(config), /Max-Age=0/)
|
||||
assert.equal(
|
||||
sessionIdentifier({
|
||||
headers: { cookie: `unrelated=1; ${SESSION_COOKIE}=opaque-session` },
|
||||
}),
|
||||
'opaque-session',
|
||||
)
|
||||
})
|
||||
|
||||
test('unsafe requests require both same-origin context and the CSRF value', () => {
|
||||
assert.equal(
|
||||
validUnsafeRequest(
|
||||
{
|
||||
headers: {
|
||||
origin: config.publicOrigin,
|
||||
'sec-fetch-site': 'same-origin',
|
||||
'x-remora-csrf': 'csrf-value',
|
||||
},
|
||||
},
|
||||
config,
|
||||
'csrf-value',
|
||||
),
|
||||
true,
|
||||
)
|
||||
assert.equal(
|
||||
validUnsafeRequest(
|
||||
{
|
||||
headers: {
|
||||
origin: 'https://attacker.invalid',
|
||||
'sec-fetch-site': 'cross-site',
|
||||
'x-remora-csrf': 'csrf-value',
|
||||
},
|
||||
},
|
||||
config,
|
||||
'csrf-value',
|
||||
),
|
||||
false,
|
||||
)
|
||||
assert.equal(
|
||||
validUnsafeRequest(
|
||||
{
|
||||
headers: {
|
||||
origin: config.publicOrigin,
|
||||
'sec-fetch-site': 'same-origin',
|
||||
'x-remora-csrf': 'wrong',
|
||||
},
|
||||
},
|
||||
config,
|
||||
'csrf-value',
|
||||
),
|
||||
false,
|
||||
)
|
||||
})
|
||||
|
||||
test('browser WebSocket context must be same-origin', () => {
|
||||
assert.equal(
|
||||
validBrowserContext(
|
||||
{
|
||||
headers: {
|
||||
origin: config.publicOrigin,
|
||||
'sec-fetch-site': 'same-origin',
|
||||
},
|
||||
},
|
||||
config,
|
||||
),
|
||||
true,
|
||||
)
|
||||
assert.equal(
|
||||
validBrowserContext(
|
||||
{
|
||||
headers: {
|
||||
origin: 'https://attacker.invalid',
|
||||
'sec-fetch-site': 'cross-site',
|
||||
},
|
||||
},
|
||||
config,
|
||||
),
|
||||
false,
|
||||
)
|
||||
})
|
||||
|
||||
test('event ticket requests use the validated public origin upstream', async () => {
|
||||
let upstreamOptions
|
||||
const proxy = new ApiProxy(
|
||||
{
|
||||
...config,
|
||||
apiUpstream: new URL('https://api.remora.invalid/v1/'),
|
||||
eventsUpstream: new URL('wss://api.remora.invalid/v1/events'),
|
||||
},
|
||||
{
|
||||
authenticated: async () => ({
|
||||
rotated: false,
|
||||
session: {
|
||||
accessToken: 'server-token',
|
||||
csrfToken: 'csrf-value',
|
||||
},
|
||||
}),
|
||||
},
|
||||
async (_url, options) => {
|
||||
upstreamOptions = options
|
||||
return new Response('{}', {
|
||||
status: 201,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
})
|
||||
},
|
||||
)
|
||||
const request = Readable.from([])
|
||||
request.method = 'POST'
|
||||
request.headers = {
|
||||
origin: config.publicOrigin,
|
||||
'sec-fetch-site': 'same-origin',
|
||||
'x-remora-csrf': 'csrf-value',
|
||||
}
|
||||
const response = {
|
||||
writeHead: () => undefined,
|
||||
end: () => undefined,
|
||||
}
|
||||
await proxy.route(
|
||||
request,
|
||||
response,
|
||||
new URL(`${config.publicOrigin}/bff/api/event-tickets`),
|
||||
)
|
||||
assert.equal(upstreamOptions.headers.Origin, config.publicOrigin)
|
||||
})
|
||||
Loading…
Reference in a new issue