Files

251 lines
7.8 KiB
TypeScript

import express from 'express'
import cookieParser from 'cookie-parser'
import jwt from 'jsonwebtoken'
import request from 'supertest'
import { afterEach, describe, expect, it, vi } from 'vitest'
import { requireLabAccess } from '../src/labAuth.js'
const secret = 'test-lab-secret'
function testApp(config: Parameters<typeof requireLabAccess>[0]) {
const app = express()
app.use(cookieParser())
app.use(express.urlencoded({ extended: false }))
app.use(requireLabAccess(config))
app.get('/admin', (_request, response) => response.send('ok'))
app.get('/api/ca/decks', (_request, response) => response.json([{ id: 'deck' }]))
return app
}
function authCookie(claims: object = {}) {
return `auth_token=${jwt.sign({ sub: '42', email: 'student@glitch.university', ...claims }, secret)}`
}
describe('lab auth middleware', () => {
afterEach(() => {
vi.restoreAllMocks()
})
it('allows requests when lab auth is disabled', async () => {
const app = testApp({
enabled: false,
gnommowebUrl: 'https://glitch.university',
jwtSecret: '',
keycardMeritSlug: 'lab-keycard',
keycardRequired: true,
signInUrl: 'https://glitch.university/auth/google',
userProfileUrl: 'https://glitch.university/api/user/profile'
})
await request(app).get('/admin').expect(200, 'ok')
})
it('shows the local lab login form for browser requests without a shared auth cookie', async () => {
const app = testApp({
enabled: true,
gnommowebUrl: 'https://glitch.university',
jwtSecret: secret,
keycardMeritSlug: 'lab-keycard',
keycardRequired: true,
signInUrl: 'https://glitch.university/auth/google?returnTo={returnTo}',
userProfileUrl: 'https://glitch.university/api/user/profile'
})
const response = await request(app)
.get('/admin')
.set('accept', 'text/html')
.set('host', 'lab.glitch.university')
.set('x-forwarded-proto', 'https')
.expect(401)
expect(response.text).toContain('Insert lab keycard')
expect(response.text).toContain('Temporary secret key')
})
it('allows browser requests with the temporary lab secret key', async () => {
const app = testApp({
enabled: true,
gnommowebUrl: 'https://glitch.university',
jwtSecret: '',
keycardMeritSlug: 'lab-keycard',
keycardRequired: true,
signInUrl: 'https://glitch.university/auth/google',
temporarySecretKey: 'supersecret',
userProfileUrl: 'https://glitch.university/api/user/profile'
})
const unlock = await request(app)
.post('/admin/lab-login')
.type('form')
.send({ secret_key: 'supersecret' })
.expect(302)
const cookie = unlock.headers['set-cookie']
expect(String(cookie)).toContain('lab_temp_key=supersecret')
await request(app)
.get('/admin')
.set('cookie', cookie)
.expect(200, 'ok')
})
it('rejects the temporary lab secret key when it is wrong', async () => {
const app = testApp({
enabled: true,
gnommowebUrl: 'https://glitch.university',
jwtSecret: secret,
keycardMeritSlug: 'lab-keycard',
keycardRequired: true,
signInUrl: 'https://glitch.university/auth/google',
temporarySecretKey: 'supersecret',
userProfileUrl: 'https://glitch.university/api/user/profile'
})
await request(app)
.post('/admin/lab-login')
.type('form')
.send({ secret_key: 'not-it' })
.expect(401)
.expect((response) => {
expect(response.text).toContain('Enter a JWT token or the temporary secret key')
})
})
it('rejects API requests with an invalid shared auth cookie', async () => {
const app = testApp({
enabled: true,
gnommowebUrl: 'https://glitch.university',
jwtSecret: secret,
keycardMeritSlug: 'lab-keycard',
keycardRequired: true,
signInUrl: 'https://glitch.university/auth/google',
userProfileUrl: 'https://glitch.university/api/user/profile'
})
await request(app)
.get('/api/ca/decks')
.set('cookie', 'auth_token=not-a-real-jwt')
.set('accept', 'application/json')
.expect(401)
.expect((response) => {
expect(response.body.error).toBe('Invalid or expired token')
})
})
it('allows valid JWTs when keycard checks are disabled', async () => {
const app = testApp({
enabled: true,
gnommowebUrl: 'https://glitch.university',
jwtSecret: secret,
keycardMeritSlug: 'lab-keycard',
keycardRequired: false,
signInUrl: 'https://glitch.university/auth/google',
userProfileUrl: 'https://glitch.university/api/user/profile'
})
await request(app).get('/api/ca/decks').set('cookie', authCookie()).expect(200)
})
it('allows valid JWTs that include a lab keycard claim', async () => {
const fetchSpy = vi.fn()
vi.stubGlobal('fetch', fetchSpy)
const app = testApp({
enabled: true,
gnommowebUrl: 'https://glitch.university',
jwtSecret: secret,
keycardMeritSlug: 'lab-keycard',
keycardRequired: true,
signInUrl: 'https://glitch.university/auth/google',
userProfileUrl: 'https://glitch.university/api/user/profile'
})
await request(app)
.get('/api/ca/decks')
.set('cookie', authCookie({ merit_slug: 'lab-keycard' }))
.expect(200)
expect(fetchSpy).not.toHaveBeenCalled()
})
it('allows valid JWTs when the gnommoweb user profile includes the merit slug', async () => {
vi.stubGlobal('fetch', vi.fn(async () => new Response(JSON.stringify({
merits: [{ slug: 'lab-keycard' }]
}), {
headers: { 'content-type': 'application/json' },
status: 200
})))
const app = testApp({
enabled: true,
gnommowebUrl: 'https://glitch.university',
jwtSecret: secret,
keycardMeritSlug: 'lab-keycard',
keycardRequired: true,
signInUrl: 'https://glitch.university/auth/google',
userProfileUrl: 'https://glitch.university/api/user/profile'
})
await request(app)
.get('/api/ca/decks')
.set('cookie', authCookie())
.set('accept', 'application/json')
.expect(200)
})
it('returns a locked response when the gnommoweb user profile lacks the merit slug', async () => {
vi.stubGlobal('fetch', vi.fn(async () => new Response(JSON.stringify({
merits: [{ slug: 'other_merit' }]
}), {
headers: { 'content-type': 'application/json' },
status: 200
})))
const app = testApp({
enabled: true,
gnommowebUrl: 'https://glitch.university',
jwtSecret: secret,
keycardMeritSlug: 'lab-keycard',
keycardRequired: true,
signInUrl: 'https://glitch.university/auth/google',
userProfileUrl: 'https://glitch.university/api/user/profile'
})
await request(app)
.get('/admin')
.set('cookie', authCookie())
.set('accept', 'text/html')
.expect(403)
.expect((response) => {
expect(response.text).toContain('Lab keycard required')
expect(response.text).toContain('does not have the CA Lab Keycard yet')
})
})
it('reports a missing gnommoweb user profile endpoint clearly', async () => {
vi.stubGlobal('fetch', vi.fn(async () => new Response(JSON.stringify({ error: 'Not found' }), {
headers: { 'content-type': 'application/json' },
status: 404
})))
const app = testApp({
enabled: true,
gnommowebUrl: 'https://glitch.university',
jwtSecret: secret,
keycardMeritSlug: 'lab-keycard',
keycardRequired: true,
signInUrl: 'https://glitch.university/auth/google',
userProfileUrl: 'https://glitch.university/api/user/profile'
})
await request(app)
.get('/api/ca/decks')
.set('cookie', authCookie())
.set('accept', 'application/json')
.expect(403)
.expect((response) => {
expect(response.body.reason).toContain('HTTP 404')
})
})
})