mirror of
https://github.com/danbulant/jose
synced 2026-05-19 20:38:42 +00:00
382 lines
10 KiB
JavaScript
382 lines
10 KiB
JavaScript
import test from 'ava'
|
|
import timekeeper from 'timekeeper'
|
|
import { root } from '../dist.mjs'
|
|
|
|
const { SignJWT, jwtVerify, CompactSign } = 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 SignJWT(t.context.payload)
|
|
.setProtectedHeader({ alg: 'HS256', typ })
|
|
.setIssuer(issuer)
|
|
.setSubject(subject)
|
|
.setAudience(audience)
|
|
.setJti(jti)
|
|
.setNotBefore(nbf)
|
|
.setExpirationTime(exp)
|
|
.setIssuedAt(iat)
|
|
.sign(t.context.secret)
|
|
|
|
await t.notThrowsAsync(
|
|
jwtVerify(jwt, t.context.secret, {
|
|
issuer,
|
|
subject,
|
|
audience,
|
|
jti,
|
|
typ,
|
|
maxTokenAge: '30s',
|
|
}),
|
|
)
|
|
await t.notThrowsAsync(jwtVerify(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 CompactSign(encode(JSON.stringify(value)))
|
|
.setProtectedHeader({ alg: 'HS256' })
|
|
.sign(t.context.secret)
|
|
await t.throwsAsync(jwtVerify(token, t.context.secret), {
|
|
code: 'ERR_JWT_INVALID',
|
|
message: 'JWT Claims Set must be a top-level JSON object',
|
|
})
|
|
}
|
|
})
|
|
|
|
test('incorrect hmac signature lengths', async (t) => {
|
|
const jwt = await new SignJWT(t.context.payload)
|
|
.setProtectedHeader({ alg: 'HS256' })
|
|
.sign(t.context.secret)
|
|
|
|
await t.throwsAsync(jwtVerify(jwt.slice(0, -3), t.context.secret), {
|
|
code: 'ERR_JWS_SIGNATURE_VERIFICATION_FAILED',
|
|
message: 'signature verification failed',
|
|
})
|
|
})
|
|
|
|
test('Payload must JSON parseable', async (t) => {
|
|
const encode = TextEncoder.prototype.encode.bind(new TextEncoder())
|
|
const token = await new CompactSign(encode('{'))
|
|
.setProtectedHeader({ alg: 'HS256' })
|
|
.sign(t.context.secret)
|
|
await t.throwsAsync(jwtVerify(token, t.context.secret), {
|
|
code: 'ERR_JWT_INVALID',
|
|
message: 'JWT Claims Set must be a top-level JSON object',
|
|
})
|
|
})
|
|
|
|
test('algorithms options', async (t) => {
|
|
const jwt = await new SignJWT(t.context.payload)
|
|
.setProtectedHeader({ alg: 'HS256' })
|
|
.sign(t.context.secret)
|
|
|
|
await t.throwsAsync(
|
|
jwtVerify(jwt, t.context.secret, {
|
|
algorithms: ['PS256'],
|
|
}),
|
|
{
|
|
code: 'ERR_JOSE_ALG_NOT_ALLOWED',
|
|
message: '"alg" (Algorithm) Header Parameter not allowed',
|
|
},
|
|
)
|
|
await t.throwsAsync(
|
|
jwtVerify(jwt, t.context.secret, {
|
|
algorithms: [null],
|
|
}),
|
|
{
|
|
instanceOf: TypeError,
|
|
message: '"algorithms" option must be an array of strings',
|
|
},
|
|
)
|
|
})
|
|
|
|
test('typ verification', async (t) => {
|
|
{
|
|
const typ = 'urn:example:typ'
|
|
const jwt = await new SignJWT(t.context.payload)
|
|
.setProtectedHeader({ alg: 'HS256', typ })
|
|
.sign(t.context.secret)
|
|
|
|
await t.notThrowsAsync(
|
|
jwtVerify(jwt, t.context.secret, {
|
|
typ: 'application/urn:example:typ',
|
|
}),
|
|
)
|
|
|
|
await t.throwsAsync(
|
|
jwtVerify(jwt, t.context.secret, {
|
|
typ: 'urn:example:typ:2',
|
|
}),
|
|
{ code: 'ERR_JWT_CLAIM_VALIDATION_FAILED', message: 'unexpected "typ" JWT header value' },
|
|
)
|
|
|
|
await t.throwsAsync(
|
|
jwtVerify(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 SignJWT(t.context.payload)
|
|
.setProtectedHeader({ alg: 'HS256', typ })
|
|
.sign(t.context.secret)
|
|
|
|
await t.notThrowsAsync(
|
|
jwtVerify(jwt, t.context.secret, {
|
|
typ: 'urn:example:typ',
|
|
}),
|
|
)
|
|
|
|
await t.throwsAsync(
|
|
jwtVerify(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(
|
|
jwtVerify(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 SignJWT(t.context.payload)
|
|
.setProtectedHeader({ alg: 'HS256' })
|
|
.setIssuer(issuer)
|
|
.sign(t.context.secret)
|
|
|
|
await t.notThrowsAsync(
|
|
jwtVerify(jwt, t.context.secret, {
|
|
issuer: [issuer],
|
|
}),
|
|
)
|
|
})
|
|
|
|
test('Issuer[] verification failed', async (t) => {
|
|
const issuer = 'urn:example:issuer'
|
|
const jwt = await new SignJWT(t.context.payload)
|
|
.setProtectedHeader({ alg: 'HS256' })
|
|
.setIssuer(issuer)
|
|
.sign(t.context.secret)
|
|
|
|
await t.throwsAsync(
|
|
jwtVerify(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 SignJWT(t.context.payload)
|
|
.setProtectedHeader({ alg: 'HS256' })
|
|
.setIssuer([issuer])
|
|
.sign(t.context.secret)
|
|
|
|
await t.throwsAsync(
|
|
jwtVerify(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 SignJWT(t.context.payload)
|
|
.setProtectedHeader({ alg: 'HS256' })
|
|
.setAudience(audience)
|
|
.sign(t.context.secret)
|
|
|
|
await t.notThrowsAsync(
|
|
jwtVerify(jwt, t.context.secret, {
|
|
audience: [audience],
|
|
}),
|
|
)
|
|
})
|
|
|
|
test('Audience[] verification failed', async (t) => {
|
|
const audience = 'urn:example:audience'
|
|
const jwt = await new SignJWT(t.context.payload)
|
|
.setProtectedHeader({ alg: 'HS256' })
|
|
.setAudience(audience)
|
|
.sign(t.context.secret)
|
|
|
|
await t.throwsAsync(
|
|
jwtVerify(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 SignJWT(t.context.payload)
|
|
.setProtectedHeader({ alg: 'HS256' })
|
|
.setAudience([audience])
|
|
.sign(t.context.secret)
|
|
|
|
await t.throwsAsync(
|
|
jwtVerify(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 SignJWT(t.context.payload)
|
|
.setProtectedHeader({ alg: 'HS256' })
|
|
.setSubject(subject)
|
|
.sign(t.context.secret)
|
|
|
|
await t.throwsAsync(
|
|
jwtVerify(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 SignJWT({ [claim]: null })
|
|
.setProtectedHeader({ alg: 'HS256' })
|
|
.sign(t.context.secret)
|
|
|
|
await t.throwsAsync(jwtVerify(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 SignJWT({ exp: now })
|
|
.setProtectedHeader({ alg: 'HS256' })
|
|
.sign(t.context.secret)
|
|
|
|
await t.notThrowsAsync(jwtVerify(jwt, t.context.secret, { clockTolerance: 1 }))
|
|
await t.notThrowsAsync(jwtVerify(jwt, t.context.secret, { clockTolerance: '1s' }))
|
|
})
|
|
|
|
async function failingNumericDate(t, claims, assertion, verifyOptions) {
|
|
const jwt = await new SignJWT({ ...claims })
|
|
.setProtectedHeader({ alg: 'HS256' })
|
|
.sign(t.context.secret)
|
|
|
|
await t.throwsAsync(jwtVerify(jwt, t.context.secret, { ...verifyOptions }), 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)
|
|
}
|
|
|
|
test('Signed JWTs cannot use unencoded payload', async (t) => {
|
|
await t.throwsAsync(
|
|
jwtVerify(
|
|
'eyJhbGciOiJIUzI1NiIsImI2NCI6ZmFsc2UsImNyaXQiOlsiYjY0Il19.foo.VklKdp4tVYD61VNPDBTqxqdEQcUL3JK-D4dGXu9NvWs',
|
|
t.context.secret,
|
|
),
|
|
{ code: 'ERR_JWT_INVALID', message: 'JWTs MUST NOT use unencoded payload' },
|
|
)
|
|
})
|