mirror of
https://github.com/danbulant/jose
synced 2026-05-24 20:41:46 +00:00
As per https://tools.ietf.org/html/rfc7797 abstract This specification updates RFC 7519 by stating that JSON Web Tokens (JWTs) MUST NOT use the unencoded payload option defined by this specification.
394 lines
12 KiB
JavaScript
394 lines
12 KiB
JavaScript
import test from 'ava';
|
|
import timekeeper from 'timekeeper';
|
|
|
|
const root = !('WEBCRYPTO' in process.env) ? '#dist' : '#dist/webcrypto';
|
|
Promise.all([
|
|
import(`${root}/jwt/sign`),
|
|
import(`${root}/jwt/verify`),
|
|
import(`${root}/jws/compact/sign`),
|
|
]).then(
|
|
([{ default: SignJWT }, { default: jwtVerify }, { default: CompactSign }]) => {
|
|
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' },
|
|
);
|
|
});
|
|
},
|
|
(err) => {
|
|
test('failed to import', (t) => {
|
|
console.error(err);
|
|
t.fail();
|
|
});
|
|
},
|
|
);
|