Skip to content

Commit

Permalink
[SDK-2793] Ability to define a custom now provider (#802)
Browse files Browse the repository at this point in the history
* Introduce nowProvider to configure the current time

* Add tests

* Add tests

* Update tests

* Update docs for nowProvider property

* Support a now provider that returns a number

* Fix build

* Use mockReturnValue when testing now provider
  • Loading branch information
frederikprijck authored Sep 27, 2021
1 parent dc23a48 commit 4c0c755
Show file tree
Hide file tree
Showing 7 changed files with 279 additions and 15 deletions.
204 changes: 203 additions & 1 deletion __tests__/cache/cache-manager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ cacheFactories.forEach(cacheFactory => {
await manager.get(
new CacheKey({
client_id: TEST_CLIENT_ID,
audience: 'the_audience',
audience: TEST_AUDIENCE,
scope: TEST_SCOPES
}),
60
Expand Down Expand Up @@ -233,6 +233,54 @@ cacheFactories.forEach(cacheFactory => {
});
});

it('reads from the cache when expires_in > date.now', async () => {
const data = {
...defaultData,
expires_in: 70
};

await manager.set(data);

const cacheKey = CacheKey.fromCacheEntry(data);

// Test that the cache state is normal before we expire the data
expect(await manager.get(cacheKey)).toStrictEqual(data);

const result = await manager.get(cacheKey, 60);

// And test that the cache has been emptied
expect(result).toBeTruthy();
});

it('reads from the cache when expires_in > date.now using custom now provider', async () => {
const now = Date.now();
const data = {
...defaultData,
expires_in: 50
};
const expiryAdjustmentSeconds = 60;

const provider = jest.fn().mockResolvedValue(Date.now());
const manager = new CacheManager(cache, keyManifest, provider);

await manager.set(data);

const cacheKey = CacheKey.fromCacheEntry(data);

// Test that the cache state is normal before we expire the data
expect(await manager.get(cacheKey)).toStrictEqual(data);

// Move back in time to ensure the token is valid
provider.mockResolvedValue(
now - (expiryAdjustmentSeconds - data.expires_in) * 1000
);

const result = await manager.get(cacheKey, expiryAdjustmentSeconds);

// And test that the cache has been emptied
expect(result).toBeTruthy();
});

it('expires the cache on read when the date.now > expires_in', async () => {
const now = Date.now();
const realDateNow = Date.now.bind(global.Date);
Expand Down Expand Up @@ -277,6 +325,90 @@ cacheFactories.forEach(cacheFactory => {
}
});

it('expires the cache on read when the date.now > expires_in when using custom now provider with a promise', async () => {
const now = Date.now();
const cacheRemoveSpy = jest.spyOn(cache, 'remove');

const data = {
...defaultData,
decodedToken: {
claims: {
__raw: TEST_ID_TOKEN,
name: 'Test',
exp: nowSeconds() + dayInSeconds * 2
},
user: { name: 'Test' }
}
};

const provider = jest.fn().mockResolvedValue(now);
const manager = new CacheManager(cache, keyManifest, provider);

await manager.set(data);

const cacheKey = CacheKey.fromCacheEntry(data);

// Test that the cache state is normal before we expire the data
expect(await manager.get(cacheKey)).toStrictEqual(data);

// Advance the time to just past the expiry..
provider.mockResolvedValue((now + dayInSeconds + 100) * 1000);

const result = await manager.get(cacheKey);

// And test that the cache has been emptied
expect(result).toBeFalsy();

// And that the data has been removed from the key manifest
if (keyManifest) {
expect(cacheRemoveSpy).toHaveBeenCalledWith(
`@@auth0spajs@@::${data.client_id}`
);
}
});

it('expires the cache on read when the date.now > expires_in when using custom now provider', async () => {
const now = Date.now();
const cacheRemoveSpy = jest.spyOn(cache, 'remove');

const data = {
...defaultData,
decodedToken: {
claims: {
__raw: TEST_ID_TOKEN,
name: 'Test',
exp: nowSeconds() + dayInSeconds * 2
},
user: { name: 'Test' }
}
};

const provider = jest.fn().mockReturnValue(now);
const manager = new CacheManager(cache, keyManifest, provider);

await manager.set(data);

const cacheKey = CacheKey.fromCacheEntry(data);

// Test that the cache state is normal before we expire the data
expect(await manager.get(cacheKey)).toStrictEqual(data);

// Advance the time to just past the expiry..
provider.mockReturnValue((now + dayInSeconds + 100) * 1000);

const result = await manager.get(cacheKey);

// And test that the cache has been emptied
expect(result).toBeFalsy();

// And that the data has been removed from the key manifest
if (keyManifest) {
expect(cacheRemoveSpy).toHaveBeenCalledWith(
`@@auth0spajs@@::${data.client_id}`
);
}
});

it('expires the cache on read when the date.now > token.exp', async () => {
const now = Date.now();
const realDateNow = Date.now.bind(global.Date);
Expand Down Expand Up @@ -313,6 +445,76 @@ cacheFactories.forEach(cacheFactory => {
}
});

it('expires the cache on read when the date.now > token.exp when using custom now provider with a promise', async () => {
const now = Date.now();
const cacheRemoveSpy = jest.spyOn(cache, 'remove');

const data = {
...defaultData,
expires_in: dayInSeconds * 120
};

const provider = jest.fn().mockResolvedValue(now);
const manager = new CacheManager(cache, keyManifest, provider);

await manager.set(data);

const cacheKey = CacheKey.fromCacheEntry(data);

// Test that the cache state is normal before we expire the data
expect(await manager.get(cacheKey)).toStrictEqual(data);

// Advance the time to just past the expiry..
provider.mockResolvedValue((now + dayInSeconds + 100) * 1000);

const result = await manager.get(cacheKey);

// And test that the cache has been emptied
expect(result).toBeFalsy();

// And that the data has been removed from the key manifest
if (keyManifest) {
expect(cacheRemoveSpy).toHaveBeenCalledWith(
`@@auth0spajs@@::${data.client_id}`
);
}
});

it('expires the cache on read when the date.now > token.exp when using custom now provider', async () => {
const now = Date.now();
const cacheRemoveSpy = jest.spyOn(cache, 'remove');

const data = {
...defaultData,
expires_in: dayInSeconds * 120
};

const provider = jest.fn().mockReturnValue(now);
const manager = new CacheManager(cache, keyManifest, provider);

await manager.set(data);

const cacheKey = CacheKey.fromCacheEntry(data);

// Test that the cache state is normal before we expire the data
expect(await manager.get(cacheKey)).toStrictEqual(data);

// Advance the time to just past the expiry..
provider.mockReturnValue((now + dayInSeconds + 100) * 1000);

const result = await manager.get(cacheKey);

// And test that the cache has been emptied
expect(result).toBeFalsy();

// And that the data has been removed from the key manifest
if (keyManifest) {
expect(cacheRemoveSpy).toHaveBeenCalledWith(
`@@auth0spajs@@::${data.client_id}`
);
}
});

it('clears all keys from the cache', async () => {
const entry1 = { ...defaultData };
const entry2 = { ...defaultData, scope: 'scope-1' };
Expand Down
32 changes: 32 additions & 0 deletions __tests__/jwt.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,14 @@ describe('jwt', () => {
`Expiration Time (exp) claim error in the ID token`
);
});
it('validates exp using custom now', async () => {
const id_token = await createJWT(DEFAULT_PAYLOAD, {
expiresIn: '-1h'
});
expect(() =>
verify({ ...verifyOptions, id_token, now: Date.now() - 3600 * 1000 })
).not.toThrow();
});
it('validates nbf', async () => {
const id_token = await createJWT(DEFAULT_PAYLOAD, {
notBefore: '1h'
Expand Down Expand Up @@ -336,6 +344,30 @@ describe('jwt', () => {
);
});

it('validate auth_time + max_age is in the future using custom now', async () => {
const yesterday = new Date();
yesterday.setDate(yesterday.getDate() - 1);

const maxAge = 1;
const leeway = 60;
const authTime = Math.floor(yesterday.getTime() / 1000);

const id_token = await createJWT({
...DEFAULT_PAYLOAD,
auth_time: authTime
});

expect(() =>
verify({
...verifyOptions,
id_token,
max_age: maxAge,
leeway,
now: Math.floor(yesterday.getTime())
})
).not.toThrow();
});

it('validate org_id is present when organizationId is provided', async () => {
const id_token = await createJWT({ ...DEFAULT_PAYLOAD });

Expand Down
27 changes: 19 additions & 8 deletions src/Auth0Client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,8 @@ import {
RECOVERABLE_ERRORS,
DEFAULT_SESSION_CHECK_EXPIRY_DAYS,
DEFAULT_AUTH0_CLIENT,
INVALID_REFRESH_TOKEN_ERROR_MESSAGE
INVALID_REFRESH_TOKEN_ERROR_MESSAGE,
DEFAULT_NOW_PROVIDER
} from './constants';

import {
Expand Down Expand Up @@ -186,6 +187,7 @@ export default class Auth0Client {
private sessionCheckExpiryDays: number;
private orgHintCookieName: string;
private isAuthenticatedCookieName: string;
private nowProvider: () => number | Promise<number>;

cacheLocation: CacheLocation;
private worker: Worker;
Expand Down Expand Up @@ -240,11 +242,14 @@ export default class Auth0Client {
this.options.client_id
);

this.nowProvider = this.options.nowProvider || DEFAULT_NOW_PROVIDER;

this.cacheManager = new CacheManager(
cache,
!cache.allKeys
? new CacheKeyManifest(cache, this.options.client_id)
: null
: null,
this.nowProvider
);

this.domainUrl = getDomain(this.options.domain);
Expand Down Expand Up @@ -325,19 +330,22 @@ export default class Auth0Client {
return this._url(`/authorize?${createQueryParams(authorizeOptions)}`);
}

private _verifyIdToken(
private async _verifyIdToken(
id_token: string,
nonce?: string,
organizationId?: string
) {
const now = await this.nowProvider();

return verifyIdToken({
iss: this.tokenIssuer,
aud: this.options.client_id,
id_token,
nonce,
organizationId,
leeway: this.options.leeway,
max_age: this._parseNumber(this.options.max_age)
max_age: this._parseNumber(this.options.max_age),
now
});
}

Expand Down Expand Up @@ -489,7 +497,7 @@ export default class Auth0Client {

const organizationId = options.organization || this.options.organization;

const decodedToken = this._verifyIdToken(
const decodedToken = await this._verifyIdToken(
authResult.id_token,
nonceIn,
organizationId
Expand Down Expand Up @@ -647,7 +655,7 @@ export default class Auth0Client {

const authResult = await oauthToken(tokenOptions, this.worker);

const decodedToken = this._verifyIdToken(
const decodedToken = await this._verifyIdToken(
authResult.id_token,
transaction.nonce,
transaction.organizationId
Expand Down Expand Up @@ -1033,7 +1041,10 @@ export default class Auth0Client {
this.worker
);

const decodedToken = this._verifyIdToken(tokenResult.id_token, nonceIn);
const decodedToken = await this._verifyIdToken(
tokenResult.id_token,
nonceIn
);

this._processOrgIdHint(decodedToken.claims.org_id);

Expand Down Expand Up @@ -1126,7 +1137,7 @@ export default class Auth0Client {
throw e;
}

const decodedToken = this._verifyIdToken(tokenResult.id_token);
const decodedToken = await this._verifyIdToken(tokenResult.id_token);

return {
...tokenResult,
Expand Down
Loading

0 comments on commit 4c0c755

Please sign in to comment.