jose/test/jwt/decrypt.test.mjs

410 lines
11 KiB
JavaScript

import test from 'ava'
import timekeeper from 'timekeeper'
import { root } from '../dist.mjs'
const { EncryptJWT, jwtDecrypt, CompactEncrypt } = await import(root)
const now = 1604416038
test.before(async (t) => {
t.context.secret = new Uint8Array(32)
t.context.payload = { 'urn:example:claim': true }
timekeeper.freeze(now * 1000)
})
test.after(timekeeper.reset)
test('Basic JWT Claims Set verification', async (t) => {
const issuer = 'urn:example:issuer'
const subject = 'urn:example:subject'
const audience = 'urn:example:audience'
const jti = 'urn:example:jti'
const nbf = now - 10
const iat = now - 20
const exp = now + 10
const typ = 'urn:example:typ'
const jwt = await new EncryptJWT(t.context.payload)
.setProtectedHeader({ alg: 'dir', enc: 'A256GCM', typ })
.setIssuer(issuer)
.setSubject(subject)
.setAudience(audience)
.setJti(jti)
.setNotBefore(nbf)
.setExpirationTime(exp)
.setIssuedAt(iat)
.encrypt(t.context.secret)
await t.notThrowsAsync(
jwtDecrypt(jwt, t.context.secret, {
issuer,
subject,
audience,
jti,
typ,
maxTokenAge: '30s',
}),
)
await t.notThrowsAsync(jwtDecrypt(new TextEncoder().encode(jwt), t.context.secret))
})
test('Payload must be an object', async (t) => {
const encode = TextEncoder.prototype.encode.bind(new TextEncoder())
for (const value of [0, 1, -1, true, false, null, [], '']) {
const token = await new CompactEncrypt(encode(JSON.stringify(value)))
.setProtectedHeader({ alg: 'dir', enc: 'A256GCM' })
.encrypt(t.context.secret)
await t.throwsAsync(jwtDecrypt(token, t.context.secret), {
code: 'ERR_JWT_INVALID',
message: 'JWT Claims Set must be a top-level JSON object',
})
}
})
test('Payload must JSON parseable', async (t) => {
const encode = TextEncoder.prototype.encode.bind(new TextEncoder())
const token = await new CompactEncrypt(encode('{'))
.setProtectedHeader({ alg: 'dir', enc: 'A256GCM' })
.encrypt(t.context.secret)
await t.throwsAsync(jwtDecrypt(token, t.context.secret), {
code: 'ERR_JWT_INVALID',
message: 'JWT Claims Set must be a top-level JSON object',
})
})
test('contentEncryptionAlgorithms and keyManagementAlgorithms options', async (t) => {
const jwt = await new EncryptJWT(t.context.payload)
.setProtectedHeader({ alg: 'dir', enc: 'A256GCM' })
.encrypt(t.context.secret)
await t.throwsAsync(
jwtDecrypt(jwt, t.context.secret, {
keyManagementAlgorithms: ['RSA-OAEP'],
}),
{
code: 'ERR_JOSE_ALG_NOT_ALLOWED',
message: '"alg" (Algorithm) Header Parameter not allowed',
},
)
await t.throwsAsync(
jwtDecrypt(jwt, t.context.secret, {
keyManagementAlgorithms: [null],
}),
{
instanceOf: TypeError,
message: '"keyManagementAlgorithms" option must be an array of strings',
},
)
await t.throwsAsync(
jwtDecrypt(jwt, t.context.secret, {
contentEncryptionAlgorithms: ['A128GCM'],
}),
{
code: 'ERR_JOSE_ALG_NOT_ALLOWED',
message: '"enc" (Encryption Algorithm) Header Parameter not allowed',
},
)
await t.throwsAsync(
jwtDecrypt(jwt, t.context.secret, {
contentEncryptionAlgorithms: [null],
}),
{
instanceOf: TypeError,
message: '"contentEncryptionAlgorithms" option must be an array of strings',
},
)
})
test('typ verification', async (t) => {
{
const typ = 'urn:example:typ'
const jwt = await new EncryptJWT(t.context.payload)
.setProtectedHeader({ alg: 'dir', enc: 'A256GCM', typ })
.encrypt(t.context.secret)
await t.notThrowsAsync(
jwtDecrypt(jwt, t.context.secret, {
typ: 'application/urn:example:typ',
}),
)
await t.throwsAsync(
jwtDecrypt(jwt, t.context.secret, {
typ: 'urn:example:typ:2',
}),
{ code: 'ERR_JWT_CLAIM_VALIDATION_FAILED', message: 'unexpected "typ" JWT header value' },
)
await t.throwsAsync(
jwtDecrypt(jwt, t.context.secret, {
typ: 'application/urn:example:typ:2',
}),
{ code: 'ERR_JWT_CLAIM_VALIDATION_FAILED', message: 'unexpected "typ" JWT header value' },
)
}
{
const typ = 'application/urn:example:typ'
const jwt = await new EncryptJWT(t.context.payload)
.setProtectedHeader({ alg: 'dir', enc: 'A256GCM', typ })
.encrypt(t.context.secret)
await t.notThrowsAsync(
jwtDecrypt(jwt, t.context.secret, {
typ: 'urn:example:typ',
}),
)
await t.throwsAsync(
jwtDecrypt(jwt, t.context.secret, {
typ: 'application/urn:example:typ:2',
}),
{ code: 'ERR_JWT_CLAIM_VALIDATION_FAILED', message: 'unexpected "typ" JWT header value' },
)
await t.throwsAsync(
jwtDecrypt(jwt, t.context.secret, {
typ: 'urn:example:typ:2',
}),
{ code: 'ERR_JWT_CLAIM_VALIDATION_FAILED', message: 'unexpected "typ" JWT header value' },
)
}
})
test('Issuer[] verification', async (t) => {
const issuer = 'urn:example:issuer'
const jwt = await new EncryptJWT(t.context.payload)
.setProtectedHeader({ alg: 'dir', enc: 'A256GCM' })
.setIssuer(issuer)
.encrypt(t.context.secret)
await t.notThrowsAsync(
jwtDecrypt(jwt, t.context.secret, {
issuer: [issuer],
}),
)
})
test('Issuer[] verification failed', async (t) => {
const issuer = 'urn:example:issuer'
const jwt = await new EncryptJWT(t.context.payload)
.setProtectedHeader({ alg: 'dir', enc: 'A256GCM' })
.setIssuer(issuer)
.encrypt(t.context.secret)
await t.throwsAsync(
jwtDecrypt(jwt, t.context.secret, {
issuer: [],
}),
{ code: 'ERR_JWT_CLAIM_VALIDATION_FAILED', message: 'unexpected "iss" claim value' },
)
})
test('Issuer[] verification failed []', async (t) => {
const issuer = 'urn:example:issuer'
const jwt = await new EncryptJWT(t.context.payload)
.setProtectedHeader({ alg: 'dir', enc: 'A256GCM' })
.setIssuer([issuer])
.encrypt(t.context.secret)
await t.throwsAsync(
jwtDecrypt(jwt, t.context.secret, {
issuer: [],
}),
{ code: 'ERR_JWT_CLAIM_VALIDATION_FAILED', message: 'unexpected "iss" claim value' },
)
})
test('Audience[] verification', async (t) => {
const audience = 'urn:example:audience'
const jwt = await new EncryptJWT(t.context.payload)
.setProtectedHeader({ alg: 'dir', enc: 'A256GCM' })
.setAudience(audience)
.encrypt(t.context.secret)
await t.notThrowsAsync(
jwtDecrypt(jwt, t.context.secret, {
audience: [audience],
}),
)
})
test('Audience[] verification failed', async (t) => {
const audience = 'urn:example:audience'
const jwt = await new EncryptJWT(t.context.payload)
.setProtectedHeader({ alg: 'dir', enc: 'A256GCM' })
.setAudience(audience)
.encrypt(t.context.secret)
await t.throwsAsync(
jwtDecrypt(jwt, t.context.secret, {
audience: [],
}),
{ code: 'ERR_JWT_CLAIM_VALIDATION_FAILED', message: 'unexpected "aud" claim value' },
)
})
test('Audience[] verification failed []', async (t) => {
const audience = 'urn:example:audience'
const jwt = await new EncryptJWT(t.context.payload)
.setProtectedHeader({ alg: 'dir', enc: 'A256GCM' })
.setAudience([audience])
.encrypt(t.context.secret)
await t.throwsAsync(
jwtDecrypt(jwt, t.context.secret, {
audience: [],
}),
{ code: 'ERR_JWT_CLAIM_VALIDATION_FAILED', message: 'unexpected "aud" claim value' },
)
})
test('Subject verification failed', async (t) => {
const subject = 'urn:example:subject'
const jwt = await new EncryptJWT(t.context.payload)
.setProtectedHeader({ alg: 'dir', enc: 'A256GCM' })
.setSubject(subject)
.encrypt(t.context.secret)
await t.throwsAsync(
jwtDecrypt(jwt, t.context.secret, {
subject: 'urn:example:subject:2',
}),
{ code: 'ERR_JWT_CLAIM_VALIDATION_FAILED', message: 'unexpected "sub" claim value' },
)
})
async function numericDateNumber(t, claim) {
const jwt = await new EncryptJWT({ [claim]: null })
.setProtectedHeader({ alg: 'dir', enc: 'A256GCM' })
.encrypt(t.context.secret)
await t.throwsAsync(jwtDecrypt(jwt, t.context.secret), {
code: 'ERR_JWT_CLAIM_VALIDATION_FAILED',
message: `"${claim}" claim must be a number`,
})
}
numericDateNumber.title = (t, claim) => `${claim} must be a number`
test('clockTolerance num', async (t) => {
const jwt = await new EncryptJWT({ exp: now })
.setProtectedHeader({ alg: 'dir', enc: 'A256GCM' })
.encrypt(t.context.secret)
await t.notThrowsAsync(jwtDecrypt(jwt, t.context.secret, { clockTolerance: 1 }))
await t.notThrowsAsync(jwtDecrypt(jwt, t.context.secret, { clockTolerance: '1s' }))
await t.throwsAsync(jwtDecrypt(jwt, t.context.secret, { clockTolerance: null }), {
instanceOf: TypeError,
message: 'Invalid clockTolerance option type',
})
})
async function failingNumericDate(t, claims, assertion, decryptOptions) {
const jwt = await new EncryptJWT({ ...claims })
.setProtectedHeader({ alg: 'dir', enc: 'A256GCM' })
.encrypt(t.context.secret)
await t.throwsAsync(jwtDecrypt(jwt, t.context.secret, { ...decryptOptions }), assertion)
}
test(
'exp must be in the future',
failingNumericDate,
{ exp: now },
{
code: 'ERR_JWT_EXPIRED',
message: '"exp" claim timestamp check failed',
},
)
test(
'nbf must be at least now',
failingNumericDate,
{ nbf: now + 1 },
{
code: 'ERR_JWT_CLAIM_VALIDATION_FAILED',
message: '"nbf" claim timestamp check failed',
},
)
test(
'iat must be in the past (maxTokenAge, no exp)',
failingNumericDate,
{ iat: now + 1 },
{
code: 'ERR_JWT_CLAIM_VALIDATION_FAILED',
message: '"iat" claim timestamp check failed (it should be in the past)',
},
{
maxTokenAge: 5,
},
)
test(
'iat must be in the past (maxTokenAge, with exp)',
failingNumericDate,
{ iat: now + 1, exp: now + 10 },
{
code: 'ERR_JWT_CLAIM_VALIDATION_FAILED',
message: '"iat" claim timestamp check failed (it should be in the past)',
},
{
maxTokenAge: 5,
},
)
test(
'iat must be in the past (maxTokenAge, with exp, as a string)',
failingNumericDate,
{ iat: now + 1, exp: now + 10 },
{
code: 'ERR_JWT_CLAIM_VALIDATION_FAILED',
message: '"iat" claim timestamp check failed (it should be in the past)',
},
{
maxTokenAge: '5s',
},
)
test(
'maxTokenAge option',
failingNumericDate,
{ iat: now - 31 },
{
code: 'ERR_JWT_EXPIRED',
message: '"iat" claim timestamp check failed (too far in the past)',
},
{
maxTokenAge: '30s',
},
)
for (const claim of ['iat', 'nbf', 'exp']) {
test(numericDateNumber, claim)
}
async function replicatedClaimCheck(t, claim) {
{
const jwt = await new EncryptJWT({ [claim]: 'urn:example' })
.setProtectedHeader({ alg: 'dir', enc: 'A256GCM', [claim]: 'urn:example' })
.encrypt(t.context.secret)
await t.notThrowsAsync(jwtDecrypt(jwt, t.context.secret))
}
{
const jwt = await new EncryptJWT({ [claim]: 'urn:example:mismatched' })
.setProtectedHeader({ alg: 'dir', enc: 'A256GCM', [claim]: 'urn:example' })
.encrypt(t.context.secret)
await t.throwsAsync(
jwtDecrypt(jwt, t.context.secret, {
code: 'ERR_JWT_CLAIM_VALIDATION_FAILED',
message: `replicated "${claim}" claim header parameter mismatch`,
}),
)
}
}
replicatedClaimCheck.title = (t, claim) => `${claim} header claim must match the payload`
for (const claim of ['iss', 'sub', 'aud']) {
test(replicatedClaimCheck, claim)
}