127 lines
4.3 KiB
TypeScript
127 lines
4.3 KiB
TypeScript
/**
|
|
* MWT decode tests — round-trip + cross-language pin against Python create_mwt.
|
|
*/
|
|
|
|
import { describe, test, expect } from 'bun:test'
|
|
import { createHmac } from 'crypto'
|
|
import { decodeMwt, decodeJwtBearer, identityFromMwt } from '../src'
|
|
|
|
function b64url(buf: Buffer | string): string {
|
|
return Buffer.from(buf).toString('base64url')
|
|
}
|
|
|
|
/** Mint an HS256 MWT with node crypto, mirroring Python create_mwt. */
|
|
function mint(payload: Record<string, any>, secret: string, kid = 'v1'): string {
|
|
const header = b64url(JSON.stringify({ alg: 'HS256', kid, typ: 'JWT' }))
|
|
const body = b64url(JSON.stringify(payload))
|
|
const sig = createHmac('sha256', secret).update(`${header}.${body}`).digest('base64url')
|
|
return `${header}.${body}.${sig}`
|
|
}
|
|
|
|
const SECRET = 'round-trip-secret'
|
|
const now = Math.floor(Date.now() / 1000)
|
|
|
|
function basePayload(overrides: Record<string, any> = {}) {
|
|
return {
|
|
sub: '7',
|
|
staff: true,
|
|
super: false,
|
|
pkey: 'abc123',
|
|
aud: 'mizan',
|
|
iat: now,
|
|
nbf: now,
|
|
exp: now + 300,
|
|
...overrides,
|
|
}
|
|
}
|
|
|
|
describe('MWT round-trip', () => {
|
|
test('valid token decodes', () => {
|
|
const token = mint(basePayload(), SECRET)
|
|
const p = decodeMwt(token, SECRET)
|
|
expect(p).not.toBeNull()
|
|
expect(p!.sub).toBe('7')
|
|
expect(p!.staff).toBe(true)
|
|
expect(p!.super).toBe(false)
|
|
expect(p!.pkey).toBe('abc123')
|
|
expect(p!.kid).toBe('v1')
|
|
expect(p!.aud).toBe('mizan')
|
|
})
|
|
|
|
test('identityFromMwt maps claims', () => {
|
|
const token = mint(basePayload({ sub: '99', staff: false, super: true }), SECRET)
|
|
const p = decodeMwt(token, SECRET)!
|
|
expect(identityFromMwt(p)).toEqual({
|
|
isAuthenticated: true,
|
|
isStaff: false,
|
|
isSuperuser: true,
|
|
id: 99,
|
|
})
|
|
})
|
|
|
|
test('decodeJwtBearer strips Bearer prefix', () => {
|
|
const token = mint(basePayload(), SECRET)
|
|
const p = decodeJwtBearer(`Bearer ${token}`, SECRET)
|
|
expect(p).not.toBeNull()
|
|
expect(p!.sub).toBe('7')
|
|
})
|
|
|
|
test('null on tampered signature', () => {
|
|
const token = mint(basePayload(), SECRET)
|
|
const tampered = token.slice(0, -2) + (token.endsWith('AA') ? 'BB' : 'AA')
|
|
expect(decodeMwt(tampered, SECRET)).toBeNull()
|
|
})
|
|
|
|
test('null on wrong secret', () => {
|
|
const token = mint(basePayload(), SECRET)
|
|
expect(decodeMwt(token, 'other-secret')).toBeNull()
|
|
})
|
|
|
|
test('null on expired exp', () => {
|
|
const token = mint(basePayload({ exp: now - 10 }), SECRET)
|
|
expect(decodeMwt(token, SECRET)).toBeNull()
|
|
})
|
|
|
|
test('null on future nbf', () => {
|
|
const token = mint(basePayload({ nbf: now + 1000 }), SECRET)
|
|
expect(decodeMwt(token, SECRET)).toBeNull()
|
|
})
|
|
|
|
test('null on wrong aud', () => {
|
|
const token = mint(basePayload({ aud: 'other' }), SECRET)
|
|
expect(decodeMwt(token, SECRET)).toBeNull()
|
|
})
|
|
|
|
test('null on malformed token', () => {
|
|
expect(decodeMwt('not.a.jwt', SECRET)).toBeNull()
|
|
expect(decodeMwt('onlyonepart', SECRET)).toBeNull()
|
|
expect(decodeMwt('', SECRET)).toBeNull()
|
|
})
|
|
})
|
|
|
|
describe('MWT cross-language pin (Python create_mwt)', () => {
|
|
const TOKEN = 'eyJhbGciOiJIUzI1NiIsImtpZCI6InYxIiwidHlwIjoiSldUIn0.eyJzdWIiOiI0MiIsInN0YWZmIjp0cnVlLCJzdXBlciI6ZmFsc2UsInBrZXkiOiIwZTk5OGE5ZmYxNjkwNDYzN2EwM2QyZWEwZmJkYmY5NzQyOTdhOWQxYTVkMjViOGQ0Mjk0ZmE4ODIxMTVlNDU3IiwiYXVkIjoibWl6YW4iLCJpYXQiOjE3MDAwMDAwMDAsIm5iZiI6MTcwMDAwMDAwMCwiZXhwIjo0MTAyNDQ0ODAwfQ._V92JXiLSLXoyuSwbNvvJjwzgmczmC7dvX34kVSLIa8'
|
|
const PIN_SECRET = 'pin-test-secret-mwt'
|
|
|
|
test('decodes the Python-minted token', () => {
|
|
const p = decodeMwt(TOKEN, PIN_SECRET)
|
|
expect(p).not.toBeNull()
|
|
expect(p!.sub).toBe('42')
|
|
expect(p!.staff).toBe(true)
|
|
expect(p!.super).toBe(false)
|
|
expect(p!.pkey).toBe('0e998a9ff16904637a03d2ea0fbdbf974297a9d1a5d25b8d4294fa882115e457')
|
|
expect(p!.kid).toBe('v1')
|
|
expect(p!.aud).toBe('mizan')
|
|
})
|
|
|
|
test('identity from Python-minted token', () => {
|
|
const p = decodeMwt(TOKEN, PIN_SECRET)!
|
|
expect(identityFromMwt(p)).toEqual({
|
|
isAuthenticated: true,
|
|
isStaff: true,
|
|
isSuperuser: false,
|
|
id: 42,
|
|
})
|
|
})
|
|
})
|