Skip to content

Commit

Permalink
Improved OIDC compliance (#248)
Browse files Browse the repository at this point in the history
* idtv-1

* max_age

* finish idtv

* Update src/jwt.ts

Co-Authored-By: Luciano Balmaceda <balmacedaluciano@gmail.com>

* fix1

* nonce

* azp clientid

* idclient

* client_id

* nonce

* date types

* fixed auth_time test

* auth_time date
  • Loading branch information
Luís Rudge authored Oct 24, 2019
1 parent c70f64d commit 65a1424
Show file tree
Hide file tree
Showing 7 changed files with 339 additions and 76 deletions.
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 () => {
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

0 comments on commit 65a1424

Please sign in to comment.