Skip to content
Closed
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
2 changes: 1 addition & 1 deletion src/commands/whoami.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
}
Expand Down
43 changes: 10 additions & 33 deletions src/services/auth.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<LoginCredentials> => {
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<LoginCredentials> => {
public refreshAndStoreUserToken = async (oldCreds: LoginCredentials): Promise<LoginCredentials> => {
SdkManager.init({ token: oldCreds.token });

const usersClient = SdkManager.instance.getUsers();
Expand Down
2 changes: 1 addition & 1 deletion test/services/auth.service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
79 changes: 79 additions & 0 deletions test/services/validation.service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
});
});