diff --git a/src/commands/whoami.ts b/src/commands/whoami.ts index d6a32ef9..12105c2f 100644 --- a/src/commands/whoami.ts +++ b/src/commands/whoami.ts @@ -29,7 +29,7 @@ export default class Whoami extends Command { } else { if (validCreds.refreshRequired) { try { - await AuthService.instance.refreshUserToken(userCredentials); + await AuthService.instance.refreshAndStoreUserToken(userCredentials); } catch { /* noop */ } diff --git a/src/services/auth.service.ts b/src/services/auth.service.ts index dcc63bb2..30ef3e46 100644 --- a/src/services/auth.service.ts +++ b/src/services/auth.service.ts @@ -71,55 +71,32 @@ export class AuthService { throw new MissingCredentialsError(); } - const tokenDetails = ValidationService.instance.validateTokenAndCheckExpiration(loginCreds.token); + const { + isValid, + expiration: { expired, refreshRequired }, + } = ValidationService.instance.validateTokenAndCheckExpiration(loginCreds.token); const isValidMnemonic = ValidationService.instance.validateMnemonic(loginCreds.user.mnemonic); - if (!tokenDetails.isValid || !isValidMnemonic) { + if (!isValid || !isValidMnemonic) { throw new InvalidCredentialsError(); } - if (tokenDetails.expiration.expired) { + if (expired) { throw new ExpiredCredentialsError(); } - const refreshToken = tokenDetails.expiration.refreshRequired; - if (refreshToken) { - loginCreds = await this.refreshUserToken(loginCreds); + if (refreshRequired) { + loginCreds = await this.refreshAndStoreUserToken(loginCreds); } return loginCreds; }; /** - * Refreshes the user auth details + * Refreshes the user tokens and stores them in the credentials file * * @returns The user details and its renewed auth token */ - public refreshUserDetails = async (oldCreds: LoginCredentials): Promise => { - SdkManager.init({ token: oldCreds.token }); - const usersClient = SdkManager.instance.getUsers(); - const newCreds = await usersClient.getUserData({ userUuid: oldCreds.user.uuid }); - - const loginCreds: LoginCredentials = { - user: { - ...newCreds.user, - mnemonic: oldCreds.user.mnemonic, - createdAt: new Date(newCreds.user.createdAt).toISOString(), - }, - token: newCreds.newToken, - lastLoggedInAt: oldCreds.lastLoggedInAt, - lastTokenRefreshAt: new Date().toISOString(), - }; - SdkManager.init({ token: newCreds.newToken }); - await ConfigService.instance.saveUser(loginCreds); - return loginCreds; - }; - - /** - * Refreshes the user tokens - * - * @returns The user details and its renewed auth token - */ - public refreshUserToken = async (oldCreds: LoginCredentials): Promise => { + public refreshAndStoreUserToken = async (oldCreds: LoginCredentials): Promise => { SdkManager.init({ token: oldCreds.token }); const usersClient = SdkManager.instance.getUsers(); diff --git a/test/services/auth.service.test.ts b/test/services/auth.service.test.ts index abb9ad7e..4ac80af6 100644 --- a/test/services/auth.service.test.ts +++ b/test/services/auth.service.test.ts @@ -231,7 +231,7 @@ describe('Auth service', () => { .spyOn(ValidationService.instance, 'validateTokenAndCheckExpiration') .mockImplementationOnce(() => mockToken); const validateMnemonicStub = vi.spyOn(ValidationService.instance, 'validateMnemonic').mockReturnValue(true); - const refreshTokensStub = vi.spyOn(sut, 'refreshUserToken').mockResolvedValue(UserCredentialsFixture); + const refreshTokensStub = vi.spyOn(sut, 'refreshAndStoreUserToken').mockResolvedValue(UserCredentialsFixture); await sut.getAuthDetails(); expect(authStub).toHaveBeenCalledOnce(); diff --git a/test/services/validation.service.test.ts b/test/services/validation.service.test.ts index 269e4609..532d7a26 100644 --- a/test/services/validation.service.test.ts +++ b/test/services/validation.service.test.ts @@ -73,4 +73,83 @@ describe('Validation Service', () => { expect(ValidationService.instance.validateStringIsNotEmpty('\t')).to.be.equal(false); expect(ValidationService.instance.validateStringIsNotEmpty('\t\n')).to.be.equal(false); }); + + describe('Token validation', () => { + const createJWT = (payload: object): string => { + const header = { alg: 'HS256', typ: 'JWT' }; + const encodedHeader = btoa(JSON.stringify(header)); + const encodedPayload = btoa(JSON.stringify(payload)); + const signature = btoa('fake-signature'); + return `${encodedHeader}.${encodedPayload}.${signature}`; + }; + + const getCurrentTimeInSeconds = () => Math.floor(Date.now() / 1000); + const oneDayInSeconds = 24 * 60 * 60; + const threeDaysInSeconds = 3 * 24 * 60 * 60; + + it('should return result with isValid false when token is not a string', () => { + const result = ValidationService.instance.validateTokenAndCheckExpiration(undefined); + expect(result.isValid).to.be.equal(false); + expect(result.expiration.expired).to.be.equal(true); + expect(result.expiration.refreshRequired).to.be.equal(false); + }); + + it('should return result with isValid false when token does not have the proper jwt.token format', () => { + const invalidToken = 'not.a.valid.jwt.token'; + const result = ValidationService.instance.validateTokenAndCheckExpiration(invalidToken); + expect(result.isValid).to.be.equal(false); + expect(result.expiration.expired).to.be.equal(true); + expect(result.expiration.refreshRequired).to.be.equal(false); + }); + + it('should return result with isValid false when token payload expiration is not a number', () => { + const tokenWithInvalidExp = createJWT({ exp: 'not-a-number' }); + const result = ValidationService.instance.validateTokenAndCheckExpiration(tokenWithInvalidExp); + expect(result.isValid).to.be.equal(false); + expect(result.expiration.expired).to.be.equal(true); + expect(result.expiration.refreshRequired).to.be.equal(false); + }); + + it('should return result with isValid true when token is valid', () => { + const futureExp = getCurrentTimeInSeconds(); + const validToken = createJWT({ exp: futureExp }); + const result = ValidationService.instance.validateTokenAndCheckExpiration(validToken); + expect(result.isValid).to.be.equal(true); + }); + + it('should return result with expiration.expired true when token is expired', () => { + const pastExp = getCurrentTimeInSeconds(); + const expiredToken = createJWT({ exp: pastExp }); + const result = ValidationService.instance.validateTokenAndCheckExpiration(expiredToken); + expect(result.isValid).to.be.equal(true); + expect(result.expiration.expired).to.be.equal(true); + expect(result.expiration.refreshRequired).to.be.equal(false); + }); + + it('should return result with expiration.expired false when token is not expired', () => { + const futureExp = getCurrentTimeInSeconds() + threeDaysInSeconds; + const validToken = createJWT({ exp: futureExp }); + const result = ValidationService.instance.validateTokenAndCheckExpiration(validToken); + expect(result.isValid).to.be.equal(true); + expect(result.expiration.expired).to.be.equal(false); + }); + + it('should return result with refreshRequired true when token is about to expire in 2 days or less', () => { + const expiresInOneDayExp = getCurrentTimeInSeconds() + oneDayInSeconds; + const tokenExpiringSoon = createJWT({ exp: expiresInOneDayExp }); + const result = ValidationService.instance.validateTokenAndCheckExpiration(tokenExpiringSoon); + expect(result.isValid).to.be.equal(true); + expect(result.expiration.expired).to.be.equal(false); + expect(result.expiration.refreshRequired).to.be.equal(true); + }); + + it('should return object with refreshRequired false when token is not about to expire in 2 days', () => { + const expiresInThreeDaysExp = getCurrentTimeInSeconds() + threeDaysInSeconds; + const tokenNotExpiringSoon = createJWT({ exp: expiresInThreeDaysExp }); + const result = ValidationService.instance.validateTokenAndCheckExpiration(tokenNotExpiringSoon); + expect(result.isValid).to.be.equal(true); + expect(result.expiration.expired).to.be.equal(false); + expect(result.expiration.refreshRequired).to.be.equal(false); + }); + }); });