feat(260610-k1z-01): wire persistSessionCookie into index.ts + add unit tests
- Mount persistSessionCookie() immediately after oidcAuthMiddleware() inside !devBypassActive block - Test A: truthy oidcAuthJwt produces Set-Cookie with Max-Age, SameSite=Lax, HttpOnly, Secure - Test B: falsy/absent oidcAuthJwt emits no oidc-auth cookie (no-resurrection guard)
This commit is contained in:
@@ -11,6 +11,7 @@ import { listsRouter, listItemsRouter } from './routes/lists.js'
|
|||||||
import { pushRouter } from './routes/push.js'
|
import { pushRouter } from './routes/push.js'
|
||||||
import { oidcAuthMiddleware, processOAuthCallback } from './auth/middleware.js'
|
import { oidcAuthMiddleware, processOAuthCallback } from './auth/middleware.js'
|
||||||
import { devAuthBypass } from './auth/devBypass.js'
|
import { devAuthBypass } from './auth/devBypass.js'
|
||||||
|
import { persistSessionCookie } from './auth/persistSessionCookie.js'
|
||||||
import { startBrokerPoller } from './broker/poller.js'
|
import { startBrokerPoller } from './broker/poller.js'
|
||||||
import { startOutboxWorker } from './broker/outboxWorker.js'
|
import { startOutboxWorker } from './broker/outboxWorker.js'
|
||||||
import { startReminderScheduler } from './broker/reminderScheduler.js'
|
import { startReminderScheduler } from './broker/reminderScheduler.js'
|
||||||
@@ -49,6 +50,8 @@ app.use('/api/*', devAuthBypass())
|
|||||||
// redirect_uri (Pitfall 1). Set it to https://familysync.<domain>.
|
// redirect_uri (Pitfall 1). Set it to https://familysync.<domain>.
|
||||||
if (!devBypassActive) {
|
if (!devBypassActive) {
|
||||||
app.use('/api/*', oidcAuthMiddleware())
|
app.use('/api/*', oidcAuthMiddleware())
|
||||||
|
// Re-issues the session-scoped oidc-auth cookie as persistent so PWA sessions survive close (AUTH-02).
|
||||||
|
app.use('/api/*', persistSessionCookie())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Protected API routes (behind oidcAuthMiddleware)
|
// Protected API routes (behind oidcAuthMiddleware)
|
||||||
|
|||||||
@@ -0,0 +1,122 @@
|
|||||||
|
/**
|
||||||
|
* persistSessionCookie() middleware — unit tests.
|
||||||
|
*
|
||||||
|
* Tests the two critical behavioral properties:
|
||||||
|
* A (persist path): when oidcAuthJwt is set on context, the response includes an
|
||||||
|
* oidc-auth Set-Cookie with Max-Age, SameSite=Lax, HttpOnly, Secure.
|
||||||
|
* B (guard path): when oidcAuthJwt is NOT set, no oidc-auth Set-Cookie is emitted
|
||||||
|
* (the no-resurrection security guard).
|
||||||
|
*
|
||||||
|
* Does NOT call @hono/oidc-auth — sets the context variable directly to keep this a
|
||||||
|
* pure unit test that runs without MariaDB or any external service.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { describe, it, expect, afterEach } from 'vitest'
|
||||||
|
import { Hono } from 'hono'
|
||||||
|
import { persistSessionCookie } from '../../src/auth/persistSessionCookie.js'
|
||||||
|
|
||||||
|
// Read the cookie name the same way the implementation does so the test stays correct
|
||||||
|
// if OIDC_COOKIE_NAME is overridden in the environment.
|
||||||
|
const COOKIE_NAME = process.env.OIDC_COOKIE_NAME ?? 'oidc-auth'
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
// Nothing to restore: these tests do not mutate process.env.
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('persistSessionCookie middleware', () => {
|
||||||
|
describe('Test A — persist path (oidcAuthJwt truthy)', () => {
|
||||||
|
it('emits an oidc-auth Set-Cookie with Max-Age, SameSite=Lax, HttpOnly, and Secure', async () => {
|
||||||
|
const app = new Hono()
|
||||||
|
|
||||||
|
// Simulate what @hono/oidc-auth does: set a signed session JWT on context.
|
||||||
|
app.use('/api/*', async (c, next) => {
|
||||||
|
c.set('oidcAuthJwt' as never, 'header.payload.sig')
|
||||||
|
await next()
|
||||||
|
})
|
||||||
|
|
||||||
|
app.use('/api/*', persistSessionCookie())
|
||||||
|
|
||||||
|
app.get('/api/test', (c) => c.json({ ok: true }))
|
||||||
|
|
||||||
|
const res = await app.request('/api/test')
|
||||||
|
expect(res.status).toBe(200)
|
||||||
|
|
||||||
|
const setCookieHeader = res.headers.get('set-cookie')
|
||||||
|
expect(setCookieHeader).not.toBeNull()
|
||||||
|
|
||||||
|
// Cookie name must appear in the Set-Cookie value.
|
||||||
|
expect(setCookieHeader).toContain(COOKIE_NAME)
|
||||||
|
|
||||||
|
// Max-Age must be present (persistent cookie, not session-scoped).
|
||||||
|
expect(setCookieHeader?.toLowerCase()).toContain('max-age')
|
||||||
|
|
||||||
|
// SameSite=Lax must be present.
|
||||||
|
expect(setCookieHeader?.toLowerCase()).toContain('samesite=lax')
|
||||||
|
|
||||||
|
// HttpOnly must be present.
|
||||||
|
expect(setCookieHeader?.toLowerCase()).toContain('httponly')
|
||||||
|
|
||||||
|
// Secure must be present.
|
||||||
|
expect(setCookieHeader?.toLowerCase()).toContain('secure')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('re-issues the same JWT value the library provided (no re-signing)', async () => {
|
||||||
|
const dummyJwt = 'header.payload.sig'
|
||||||
|
const app = new Hono()
|
||||||
|
|
||||||
|
app.use('/api/*', async (c, next) => {
|
||||||
|
c.set('oidcAuthJwt' as never, dummyJwt)
|
||||||
|
await next()
|
||||||
|
})
|
||||||
|
|
||||||
|
app.use('/api/*', persistSessionCookie())
|
||||||
|
app.get('/api/test', (c) => c.json({ ok: true }))
|
||||||
|
|
||||||
|
const res = await app.request('/api/test')
|
||||||
|
const setCookieHeader = res.headers.get('set-cookie') ?? ''
|
||||||
|
|
||||||
|
// The cookie value must contain the exact JWT string (URL-encoded = is fine but
|
||||||
|
// the JWT characters must all appear).
|
||||||
|
expect(setCookieHeader).toContain(dummyJwt)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('Test B — guard path (oidcAuthJwt falsy / absent)', () => {
|
||||||
|
it('emits NO oidc-auth Set-Cookie when oidcAuthJwt is not set (no resurrection)', async () => {
|
||||||
|
const app = new Hono()
|
||||||
|
|
||||||
|
// Do NOT set oidcAuthJwt — simulates a logged-out or unauthenticated request.
|
||||||
|
app.use('/api/*', persistSessionCookie())
|
||||||
|
app.get('/api/test', (c) => c.json({ ok: true }))
|
||||||
|
|
||||||
|
const res = await app.request('/api/test')
|
||||||
|
expect(res.status).toBe(200)
|
||||||
|
|
||||||
|
const setCookieHeader = res.headers.get('set-cookie')
|
||||||
|
|
||||||
|
// Either no Set-Cookie header at all, or it must not contain the oidc-auth cookie.
|
||||||
|
const hasOidcCookie =
|
||||||
|
setCookieHeader !== null && setCookieHeader.includes(COOKIE_NAME)
|
||||||
|
expect(hasOidcCookie).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('emits NO Set-Cookie when oidcAuthJwt is explicitly set to empty string', async () => {
|
||||||
|
const app = new Hono()
|
||||||
|
|
||||||
|
app.use('/api/*', async (c, next) => {
|
||||||
|
c.set('oidcAuthJwt' as never, '')
|
||||||
|
await next()
|
||||||
|
})
|
||||||
|
|
||||||
|
app.use('/api/*', persistSessionCookie())
|
||||||
|
app.get('/api/test', (c) => c.json({ ok: true }))
|
||||||
|
|
||||||
|
const res = await app.request('/api/test')
|
||||||
|
const setCookieHeader = res.headers.get('set-cookie')
|
||||||
|
|
||||||
|
const hasOidcCookie =
|
||||||
|
setCookieHeader !== null && setCookieHeader.includes(COOKIE_NAME)
|
||||||
|
expect(hasOidcCookie).toBe(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user