From 2e98b056d92b93f3fdd3e3588c3f996e715a179d Mon Sep 17 00:00:00 2001 From: Wang Sijie Date: Mon, 18 Oct 2021 18:09:48 +0800 Subject: [PATCH] feat: token-set (#48) * feat: token-set * refactor: nowRoundToSec --- packages/client/src/token-set.test.ts | 64 +++++++++++++++++++++++++++ packages/client/src/token-set.ts | 35 +++++++++++++++ packages/client/src/utils.ts | 2 + 3 files changed, 101 insertions(+) create mode 100644 packages/client/src/token-set.test.ts create mode 100644 packages/client/src/token-set.ts diff --git a/packages/client/src/token-set.test.ts b/packages/client/src/token-set.test.ts new file mode 100644 index 000000000..b0a2dad7c --- /dev/null +++ b/packages/client/src/token-set.test.ts @@ -0,0 +1,64 @@ +import { SignJWT } from 'jose/jwt/sign'; +import { generateKeyPair } from 'jose/util/generate_key_pair'; + +import TokenSet from './token-set'; +import { nowRoundToSec } from './utils'; + +describe('TokenSet', () => { + test('sets the expire_at automatically from expires_in', () => { + const ts = new TokenSet({ + access_token: 'at', + expires_in: 300, + refresh_token: 'rt', + id_token: 'it', + }); + + expect(ts).toHaveProperty('expiresAt', nowRoundToSec() + 300); + expect(ts).toHaveProperty('expiresIn', 300); + expect(ts.expired()).toBeFalsy(); + }); + + test('expired token sets expires_in to -30', () => { + const ts = new TokenSet({ + access_token: 'at', + expires_in: -30, + refresh_token: 'rt', + id_token: 'it', + }); + + expect(ts).toHaveProperty('expiresAt', nowRoundToSec() - 30); + expect(ts).toHaveProperty('expiresIn', 0); + expect(ts.expired()).toBeTruthy(); + }); + + test('provides a #claims getter', async () => { + const ts = new TokenSet({ + access_token: 'at', + expires_in: -30, + refresh_token: 'rt', + id_token: await new SignJWT({}) + .setProtectedHeader({ alg: 'RS256' }) + .setAudience('foo') + .setSubject('foz') + .setIssuer('logto') + .setIssuedAt() + .setExpirationTime('2h') + .sign((await generateKeyPair('RS256')).privateKey), + }); + + expect(ts.claims().aud).toEqual('foo'); + expect(ts.claims().sub).toEqual('foz'); + expect(ts.claims().iss).toEqual('logto'); + }); + + test('#claims throws if no id_token is present', () => { + const ts = new TokenSet({ + access_token: 'at', + expires_in: 300, + refresh_token: 'rt', + id_token: '', + }); + + expect(() => ts.claims()).toThrowError('id_token not present in TokenSet'); + }); +}); diff --git a/packages/client/src/token-set.ts b/packages/client/src/token-set.ts new file mode 100644 index 000000000..d9c37e4d2 --- /dev/null +++ b/packages/client/src/token-set.ts @@ -0,0 +1,35 @@ +import { TokenSetParameters } from './grant-token'; +import { decodeToken, IDToken, nowRoundToSec } from './utils'; + +export default class TokenSet { + public accessToken: string; + public idToken: string; + public refreshToken: string; + public expiresAt = 0; + constructor(tokenSet: TokenSetParameters) { + this.accessToken = tokenSet.access_token; + this.expiresIn = tokenSet.expires_in; + this.idToken = tokenSet.id_token; + this.refreshToken = tokenSet.refresh_token; + } + + get expiresIn(): number { + return Math.max(this.expiresAt - nowRoundToSec(), 0); + } + + set expiresIn(value: number) { + this.expiresAt = nowRoundToSec() + value; + } + + public expired(): boolean { + return this.expiresIn === 0; + } + + public claims(): IDToken { + if (!this.idToken) { + throw new TypeError('id_token not present in TokenSet'); + } + + return decodeToken(this.idToken); + } +} diff --git a/packages/client/src/utils.ts b/packages/client/src/utils.ts index ec711f583..c15f0f104 100644 --- a/packages/client/src/utils.ts +++ b/packages/client/src/utils.ts @@ -52,3 +52,5 @@ export const decodeToken = (token: string): IDToken => { throw new Error('invalid token: JSON parse failed'); } }; + +export const nowRoundToSec = () => Math.floor(Date.now() / 1000);