149 lines
3.4 KiB
JavaScript
149 lines
3.4 KiB
JavaScript
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)
|
|
})
|