Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improved OIDC compliance #248

Merged
merged 13 commits into from
Oct 24, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 30 additions & 6 deletions __tests__/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -283,7 +283,9 @@ describe('Auth0', () => {
id_token: TEST_ID_TOKEN,
nonce: TEST_RANDOM_STRING,
aud: 'test-client-id',
iss: 'https://test.auth0.com/'
iss: 'https://test.auth0.com/',
leeway: undefined,
max_age: undefined
});
});
it('calls `tokenVerifier.verify` with the `issuer` from in the oauth/token response', async () => {
Expand All @@ -296,7 +298,9 @@ describe('Auth0', () => {
aud: 'test-client-id',
id_token: TEST_ID_TOKEN,
nonce: TEST_RANDOM_STRING,
iss: 'https://test-123.auth0.com/'
iss: 'https://test-123.auth0.com/',
leeway: undefined,
max_age: undefined
});
});
it('calls `tokenVerifier.verify` with the `leeway` from constructor', async () => {
Expand All @@ -308,7 +312,21 @@ describe('Auth0', () => {
nonce: TEST_RANDOM_STRING,
aud: 'test-client-id',
iss: 'https://test.auth0.com/',
leeway: 10
leeway: 10,
max_age: undefined
});
});
it('calls `tokenVerifier.verify` with the `max_age` from constructor', async () => {
const { auth0, tokenVerifier } = await setup({ max_age: '10' });

await auth0.loginWithPopup({});
expect(tokenVerifier).toHaveBeenCalledWith({
id_token: TEST_ID_TOKEN,
nonce: TEST_RANDOM_STRING,
aud: 'test-client-id',
iss: 'https://test.auth0.com/',
leeway: undefined,
max_age: '10'
});
});
it('saves cache', async () => {
Expand Down Expand Up @@ -616,7 +634,9 @@ describe('Auth0', () => {
id_token: TEST_ID_TOKEN,
nonce: TEST_RANDOM_STRING,
aud: 'test-client-id',
iss: 'https://test.auth0.com/'
iss: 'https://test.auth0.com/',
leeway: undefined,
max_age: undefined
});
});
it('saves cache', async () => {
Expand Down Expand Up @@ -773,7 +793,9 @@ describe('Auth0', () => {
id_token: TEST_ID_TOKEN,
nonce: TEST_RANDOM_STRING,
aud: 'test-client-id',
iss: 'https://test.auth0.com/'
iss: 'https://test.auth0.com/',
leeway: undefined,
max_age: undefined
});
});
it('saves cache', async () => {
Expand Down Expand Up @@ -1118,7 +1140,9 @@ describe('Auth0', () => {
id_token: TEST_ID_TOKEN,
nonce: TEST_RANDOM_STRING,
aud: 'test-client-id',
iss: 'https://test.auth0.com/'
iss: 'https://test.auth0.com/',
leeway: undefined,
max_age: undefined
});
});
it('saves cache', async () => {
Expand Down
209 changes: 173 additions & 36 deletions __tests__/jwt.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ interface Certificate {
const verifyOptions = {
iss: 'https://brucke.auth0.com/',
aud: 'k5u3o2fiAA8XweXEEX604KCwCjzjtMU6',
nonce: 'omcw.ptjx3~.8VBm3OuMziLdn5PB0uXG'
nonce: 'omcw.ptjx3~.8VBm3OuMziLdn5PB0uXG',
client_id: 'the_client_id'
};

const createCertificate = (): Promise<Certificate> =>
Expand All @@ -33,7 +34,12 @@ const createCertificate = (): Promise<Certificate> =>
});
});

const DEFAULT_PAYLOAD = <any>{ payload: true, nonce: verifyOptions.nonce };
const DEFAULT_PAYLOAD = <any>{
sub: 'id|123',
payload: true,
nonce: verifyOptions.nonce,
azp: verifyOptions.aud
};
const createJWT = async (payload = DEFAULT_PAYLOAD, options = {}) => {
const cert = await createCertificate();
return jwt.sign(payload, cert.serviceKey, {
Expand Down Expand Up @@ -82,6 +88,26 @@ describe('jwt', async () => {
}
});
});
describe('validates id_token', () => {
const IDTOKEN_ERROR_MESSAGE = 'ID token could not be decoded';
it('throws when there is less than 3 parts', () => {
expect(() => decode('test')).toThrow(IDTOKEN_ERROR_MESSAGE);
expect(() => decode('test.')).toThrow(IDTOKEN_ERROR_MESSAGE);
expect(() => decode('test.test')).toThrow(IDTOKEN_ERROR_MESSAGE);
expect(() => decode('test.test.test.test')).toThrow(
IDTOKEN_ERROR_MESSAGE
);
});
it('throws when there is no header', () => {
expect(() => decode('.test.test')).toThrow(IDTOKEN_ERROR_MESSAGE);
});
it('throws when there is no payload', () => {
expect(() => decode('test..test')).toThrow(IDTOKEN_ERROR_MESSAGE);
});
it('throws when there is no signature', () => {
expect(() => decode('test.test.')).toThrow(IDTOKEN_ERROR_MESSAGE);
});
});
it('verifies correctly', async done => {
const id_token = await createJWT();
const { encoded, header, claims } = verify({
Expand All @@ -97,72 +123,183 @@ describe('jwt', async () => {
done();
});
});
it('verifies correctly with multiple audiences and azp', async () => {
const id_token = await createJWT(DEFAULT_PAYLOAD, {
audience: ['item 1', verifyOptions.aud]
});

const { encoded, header, claims } = verify({
...verifyOptions,
id_token
});
expect({ encoded, header, payload: claims }).toMatchObject(
verifier.decode(id_token)
);
});
it('validates id_token is present', async () => {
expect(() => verify({ ...verifyOptions, id_token: '' })).toThrow(
'ID token is required but missing'
);
});

it('validates algorithm', async () => {
const id_token = await createJWT(DEFAULT_PAYLOAD, {
algorithm: 'HS256'
});
expect(() => verify({ ...verifyOptions, id_token })).toThrow(
`Signature algorithm of "HS256" is not supported. Expected the ID token to be signed with "RS256".`
);
});
it('validates issuer is present', async () => {
const id_token = await createJWT(DEFAULT_PAYLOAD, { issuer: '' });
expect(() => verify({ ...verifyOptions, id_token })).toThrow(
'Issuer (iss) claim must be a string present in the ID token'
);
});
it('validates issuer', async () => {
const id_token = await createJWT();
expect(() => verify({ ...verifyOptions, id_token, iss: 'wrong' })).toThrow(
'Invalid issuer'
`Issuer (iss) claim mismatch in the ID token; expected "wrong", found "${verifyOptions.iss}"`
);
});
it('validates `sub` is present', async () => {
const id_token = await createJWT({ ...DEFAULT_PAYLOAD, sub: undefined });
expect(() => verify({ ...verifyOptions, id_token })).toThrow(
'Subject (sub) claim must be a string present in the ID token'
);
});
it('validates aud is present', async () => {
const id_token = await createJWT(DEFAULT_PAYLOAD, { audience: '' });
expect(() => verify({ ...verifyOptions, id_token })).toThrow(
'Audience (aud) claim must be a string or array of strings present in the ID token'
);
});
it('validates audience', async () => {
it('validates audience with `aud` is an array', async () => {
const id_token = await createJWT(DEFAULT_PAYLOAD, {
audience: ['client_id']
});
expect(() => verify({ ...verifyOptions, id_token, aud: 'wrong' })).toThrow(
`Audience (aud) claim mismatch in the ID token; expected "wrong" but was not one of "client_id"`
);
});
it('validates audience with `aud` is a string', async () => {
const id_token = await createJWT();
expect(() => verify({ ...verifyOptions, id_token, aud: 'wrong' })).toThrow(
'Invalid audience'
`Audience (aud) claim mismatch in the ID token; expected "wrong" but found "${verifyOptions.aud}"`
);
});
it('validates algorithm', async () => {
const id_token = await createJWT(
{
nonce: verifyOptions.nonce
},
{
algorithm: 'HS256'
}
it('validates exp', async () => {
const id_token = await createJWT(DEFAULT_PAYLOAD, {
expiresIn: '-1h'
});
expect(() => verify({ ...verifyOptions, id_token })).toThrow(
`Expiration Time (exp) claim error in the ID token`
);
});
it('validates nbf', async () => {
const id_token = await createJWT(DEFAULT_PAYLOAD, {
notBefore: '1h'
});
expect(() => verify({ ...verifyOptions, id_token })).toThrow(
`Not Before time (nbf) claim in the ID token indicates that this token can't be used just yet.`
);
});
it('validates iat is present', async () => {
const id_token = await createJWT(DEFAULT_PAYLOAD, { noTimestamp: true });
expect(() => verify({ ...verifyOptions, id_token })).toThrow(
'Invalid algorithm'
'Issued At (iat) claim must be a number present in the ID token'
);
});
it('validates iat', async () => {
const tomorrow = new Date();
tomorrow.setDate(tomorrow.getDate() + 1);
const id_token = await createJWT({
...DEFAULT_PAYLOAD,
iat: tomorrow.getTime()
});
expect(() => verify({ ...verifyOptions, id_token })).toThrow(
'Issued At (iat) claim error in the ID token'
);
});
it('does not validate nonce is present when options.nonce is undefined', async () => {
const id_token = await createJWT({ ...DEFAULT_PAYLOAD, nonce: undefined });
expect(() =>
verify({ ...verifyOptions, nonce: undefined, id_token })
).not.toThrow();
});
it('validates nonce is present', async () => {
const id_token = await createJWT({ ...DEFAULT_PAYLOAD, nonce: undefined });
expect(() => verify({ ...verifyOptions, id_token })).toThrow(
'Nonce (nonce) claim must be a string present in the ID token'
);
});
it('validates nonce', async () => {
const id_token = await createJWT();
expect(() =>
verify({ ...verifyOptions, id_token, nonce: 'wrong' })
).toThrow('Invalid nonce');
).toThrow(
`Nonce (nonce) claim mismatch in the ID token; expected "wrong", found "${verifyOptions.nonce}"`
);
});
it('validates exp', async () => {
it('does not validate azp is present when `aud` is a string', async () => {
const id_token = await createJWT(DEFAULT_PAYLOAD, {
audience: 'aud'
});
expect(() =>
verify({ ...verifyOptions, id_token, aud: 'aud' })
).not.toThrow();
});
it('does not validate azp is present when `aud` is an array with a single item', async () => {
luisrudge marked this conversation as resolved.
Show resolved Hide resolved
const id_token = await createJWT(DEFAULT_PAYLOAD, {
audience: ['item 1']
});
expect(() =>
verify({ ...verifyOptions, id_token, aud: 'item 1' })
).not.toThrow();
});
it('validates azp is present when `aud` is an array with more than one item', async () => {
const id_token = await createJWT(
{ ...DEFAULT_PAYLOAD, azp: undefined },
{
nonce: verifyOptions.nonce
},
{
expiresIn: '-1h'
audience: ['item 1', 'other_value']
}
);
expect(() => verify({ ...verifyOptions, id_token })).toThrow(
'id_token expired'
expect(() =>
verify({ ...verifyOptions, id_token, aud: 'other_value' })
).toThrow(
'Authorized Party (azp) claim must be a string present in the ID token when Audience (aud) claim has multiple values'
);
});
it('validates nbf', async () => {
it('validates azp when `aud` is an array with more than one item', async () => {
const id_token = await createJWT(
{ ...DEFAULT_PAYLOAD, azp: 'not_the_client_id' },
{
nonce: verifyOptions.nonce
},
{
notBefore: '1h'
audience: ['item 1', 'other_value']
}
);
expect(() => verify({ ...verifyOptions, id_token })).toThrow(
'token is not yet valid (invalid notBefore)'
expect(() =>
verify({ ...verifyOptions, id_token, aud: 'other_value' })
).toThrow(
`Authorized Party (azp) claim mismatch in the ID token; expected "other_value", found "not_the_client_id"`
);
});
it('validates iat', async () => {
const tomorrow = new Date();
tomorrow.setDate(tomorrow.getDate() + 1);
it('validate auth_time is present when max_age is provided', async () => {
const id_token = await createJWT({ ...DEFAULT_PAYLOAD });
expect(() =>
verify({ ...verifyOptions, id_token, max_age: '123' })
).toThrow(
'Authentication Time (auth_time) claim must be a number present in the ID token when Max Age (max_age) is specified'
);
});
it('validate auth_time + max_age is in the future', async () => {
const yesterday = new Date();
yesterday.setDate(yesterday.getDate() - 1);
const id_token = await createJWT({
nonce: verifyOptions.nonce,
iat: tomorrow.getTime()
...DEFAULT_PAYLOAD,
auth_time: yesterday.getTime()
});
expect(() => verify({ ...verifyOptions, id_token })).toThrow(
'id_token was issued in the future (invalid iat)'
expect(() => verify({ ...verifyOptions, id_token, max_age: '1' })).toThrow(
'Authentication Time (auth_time) claim in the ID token indicates that too much time has passed since the last end-user authentication.'
);
});
});
Loading