Skip to content
Merged
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 .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ module.exports = {
'sonarjs/no-useless-intersection': 'warn',
'sonarjs/prefer-read-only-props': 'off',
'sonarjs/pseudo-random': 'warn',
'sonarjs/public-static-readonly': 'warn',
'sonarjs/public-static-readonly': 'off',
'sonarjs/redundant-type-aliases': 'off',
'sonarjs/slow-regex': 'off',
'sonarjs/todo-tag': 'off',
Expand Down
60 changes: 60 additions & 0 deletions src/apps/main/auth/handlers.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { partialSpyOn } from '@/tests/vitest/utils.helper.test';
import { checkIfUserIsLoggedIn } from './handlers';
import * as getUser from './service';
import { TokenScheduler } from '../token-scheduler/TokenScheduler';

describe('handlers', () => {
const getUserMock = partialSpyOn(getUser, 'getUser');
const getMillisecondsToRenewMock = partialSpyOn(TokenScheduler, 'getMillisecondsToRenew');

describe('checkUserIsLoggedIn', () => {
beforeEach(() => {
getUserMock.mockReturnValue({ needLogout: false });
});

it('should return false if user does not exist', () => {
// Given
getUserMock.mockReturnValue(null);
// When
const res = checkIfUserIsLoggedIn();
// Then
expect(res).toBe(false);
});

it('should return false if user needs logout', () => {
// Given
getUserMock.mockReturnValue({ needLogout: undefined });
// When
const res = checkIfUserIsLoggedIn();
// Then
expect(res).toBe(false);
});

it('should return false if token is expired', () => {
// Given
getMillisecondsToRenewMock.mockReturnValue(-1);
// When
const res = checkIfUserIsLoggedIn();
// Then
expect(res).toBe(false);
});

it('should return false if cannot get token', () => {
// Given
getMillisecondsToRenewMock.mockReturnValue(null);
// When
const res = checkIfUserIsLoggedIn();
// Then
expect(res).toBe(false);
});

it('should return true if token is not expired', () => {
// Given
getMillisecondsToRenewMock.mockReturnValue(100);
// When
const res = checkIfUserIsLoggedIn();
// Then
expect(res).toBe(true);
});
});
});
16 changes: 10 additions & 6 deletions src/apps/main/auth/handlers.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { ipcMain } from 'electron';
import eventBus from '../event-bus';
import { getWidget } from '../windows/widget';
import { refreshToken } from './refresh-token';
import { getUser } from './service';
import { logger } from '@/apps/shared/logger/logger';
import { cleanAndStartRemoteNotifications } from '../realtime';
Expand Down Expand Up @@ -32,7 +31,7 @@ export function onUserUnauthorized() {
eventBus.emit('USER_LOGGED_OUT');
}

export async function checkIfUserIsLoggedIn() {
export function checkIfUserIsLoggedIn() {
const user = getUser();

if (!user) {
Expand All @@ -45,7 +44,13 @@ export async function checkIfUserIsLoggedIn() {
return false;
}

return await refreshToken();
const msToRenew = TokenScheduler.getMillisecondsToRenew();
if (msToRenew === null || msToRenew <= 0) {
logger.debug({ tag: 'AUTH', msg: 'User token is expired' });
return false;
}

return true;
}

export function setupAuthIpcHandlers() {
Expand All @@ -59,8 +64,7 @@ export function setupAuthIpcHandlers() {
export async function emitUserLoggedIn() {
logger.debug({ tag: 'AUTH', msg: 'User logged in' });

const scheduler = new TokenScheduler();
scheduler.schedule();
TokenScheduler.schedule();

const abortController = new AbortController();
setMaxListeners(0, abortController.signal);
Expand All @@ -79,7 +83,7 @@ export async function emitUserLoggedIn() {
eventBus.once('USER_LOGGED_OUT', () => {
logger.debug({ tag: 'AUTH', msg: 'Received logout event' });
clearLoggedPreloadIpc();
scheduler.stop();
TokenScheduler.stop();
BackupScheduler.stop();
logout({ ctx });
});
Expand Down
13 changes: 0 additions & 13 deletions src/apps/main/auth/refresh-token.ts

This file was deleted.

14 changes: 1 addition & 13 deletions src/apps/main/auth/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,6 @@ const TOKEN_ENCODING = 'latin1';

export function obtainToken(): string {
const token = ConfigStore.get('newToken');
const isEncrypted = ConfigStore.get('newTokenEncrypted');

if (!isEncrypted) {
return token;
}

if (!safeStorage.isEncryptionAvailable()) {
throw new Error('[AUTH] Safe Storage was not available when decrypting encrypted token');
}

const buffer = Buffer.from(token, TOKEN_ENCODING);

Expand All @@ -35,12 +26,9 @@ export function setUser(userData: User) {
}

export function updateCredentials({ newToken }: { newToken: string }) {
const isSafeStorageAvailable = safeStorage.isEncryptionAvailable();

const token = isSafeStorageAvailable ? ecnryptToken(newToken) : newToken;
const token = ecnryptToken(newToken);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the encryption is not available, wouldn't this break?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On windonws the encryption is always available. (electronjs.org/docs/latest/api/safe-storage#safestorageisencryptionavailable)


ConfigStore.set('newToken', token);
ConfigStore.set('newTokenEncrypted', isSafeStorageAvailable);
}

export function getUser(): User | null {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,4 +110,9 @@ export async function launchBackupProcesses({ ctx }: Props) {
ipcMain.removeAllListeners('stop-backups-process');

powerSaveBlocker.stop(suspensionBlockId);

logger.debug({
tag: 'BACKUPS',
msg: 'Backup finished',
});
}
1 change: 0 additions & 1 deletion src/apps/main/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ const schema: Schema<AppStore> = {
backupList: { type: 'object' },

newToken: { type: 'string' },
newTokenEncrypted: { type: 'boolean' },
userData: { type: 'object' },
mnemonic: { type: 'string' },

Expand Down
2 changes: 1 addition & 1 deletion src/apps/main/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ app

setUpBackups();

const isLoggedIn = await checkIfUserIsLoggedIn();
const isLoggedIn = checkIfUserIsLoggedIn();

if (isLoggedIn) {
setIsLoggedIn(true);
Expand Down
60 changes: 32 additions & 28 deletions src/apps/main/token-scheduler/TokenScheduler.ts
Original file line number Diff line number Diff line change
@@ -1,49 +1,53 @@
import { logger } from '@/apps/shared/logger/logger';
import jwtDecode, { JwtPayload } from 'jwt-decode';
import { refreshToken } from '../auth/refresh-token';
import { obtainToken } from '../auth/service';
import { obtainToken, updateCredentials } from '../auth/service';
import { driveServerWip } from '@/infra/drive-server-wip/drive-server-wip.module';

const DAYS_BEFORE = 1;

export class TokenScheduler {
timeout: NodeJS.Timeout | undefined;
static timeout: NodeJS.Timeout | undefined;

getExpiration() {
const token = obtainToken();
const decoded = jwtDecode<JwtPayload>(token);
if (!decoded.exp) throw new Error('Token does not have expiration time');
return decoded.exp * 1000;
}

schedule() {
static getMillisecondsToRenew() {
try {
const expirationDate = this.getExpiration();
const renewDate = expirationDate - DAYS_BEFORE * 24 * 60 * 60 * 1000;
const delayInMs = renewDate - Date.now();
const token = obtainToken();
const decoded = jwtDecode<JwtPayload>(token);
if (!decoded.exp) throw new Error('Token does not have expiration time');

const expiresAt = decoded.exp * 1000;
const renewAt = expiresAt - DAYS_BEFORE * 24 * 60 * 60 * 1000;
const msToRenew = renewAt - Date.now();

logger.debug({
tag: 'AUTH',
msg: 'Token renew date',
expiresAt: new Date(expirationDate),
renewAt: new Date(renewDate),
expiresAt: new Date(expiresAt),
renewAt: new Date(renewAt),
msToRenew,
});

this.timeout = setTimeout(async () => {
const isRefreshed = await refreshToken();
if (isRefreshed) {
this.schedule();
}
}, delayInMs);
return msToRenew;
} catch (error) {
logger.error({
tag: 'AUTH',
msg: 'Error scheduling refresh token',
error,
});
logger.error({ tag: 'AUTH', msg: 'Error getting token', error });
return null;
}
}

stop() {
static schedule() {
const msToRenew = this.getMillisecondsToRenew();
if (msToRenew === null) return;

this.timeout = setTimeout(async () => {
const { data } = await driveServerWip.auth.refresh();

if (data) {
updateCredentials({ newToken: data.newToken });
this.schedule();
}
}, msToRenew);
}

static stop() {
clearTimeout(this.timeout);
}
}
63 changes: 38 additions & 25 deletions src/apps/main/token-scheduler/token-scheduler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,21 @@ import jwt from 'jsonwebtoken';
import { StringValue } from 'ms';

import { TokenScheduler } from './TokenScheduler';
import { call, partialSpyOn } from '@/tests/vitest/utils.helper.test';
import { call, calls, partialSpyOn } from '@/tests/vitest/utils.helper.test';
import * as obtainToken from '../auth/service';
import { loggerMock } from '@/tests/vitest/mocks.helper.test';
import { driveServerWip } from '@/infra/drive-server-wip/drive-server-wip.module';

function createToken(expiresIn: StringValue) {
const email = 'test@internxt.com';

return jwt.sign({ email }, 'JWT_SECRET', { expiresIn });
}

describe('Token Scheduler', () => {
describe('token-scheduler', () => {
const obtainTokenMock = partialSpyOn(obtainToken, 'obtainToken');

const scheduler = new TokenScheduler();

const jwtWithoutExpiration =
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJwYXlsb2FkIjp7InV1aWQiOiIzMjE2YzUzNi1kZDJjLTVhNjEtOGM3Ni0yMmU0ZDQ4ZjY4OWUiLCJlbWFpbCI6InRlc3RAaW50ZXJueHQuY29tIiwibmFtZSI6InRlc3QiLCJsYXN0bmFtZSI6InRlc3QiLCJ1c2VybmFtZSI6InRlc3RAaW50ZXJueHQuY29tIiwic2hhcmVkV29ya3NwYWNlIjp0cnVlLCJuZXR3b3JrQ3JlZGVudGlhbHMiOnsidXNlciI6InRlc3RAaW50ZXJueHQuY29tIiwicGFzcyI6IiQyYSQwOCQ2QmhjZkRxaDE4c0kwN25kb2x0N29PNEtaTkpVQmpXSzYvZTRxMWppclR2SzdOTWE4dmZpLiJ9fSwiaWF0IjoxNjY3ODI4MDA2fQ.ckwjRsdNu9UUKUtdO3G32SwUUoMj7FAAOuBqVsIemo0';
const updateCredentialsMock = partialSpyOn(obtainToken, 'updateCredentials');
const refreshMock = partialSpyOn(driveServerWip.auth, 'refresh');

beforeEach(() => {
vi.useFakeTimers();
Expand All @@ -29,40 +27,55 @@ describe('Token Scheduler', () => {
vi.useRealTimers();
});

it('should not schedule if token does not have expiration time', () => {
it('should return Infinity if token does not have expiration time', () => {
// Given
obtainTokenMock.mockReturnValue(jwtWithoutExpiration);
obtainTokenMock.mockReturnValue(createToken('0 day'));
// When
scheduler.schedule();
TokenScheduler.getMillisecondsToRenew();
// Then
expect(scheduler.timeout).toBe(undefined);
expect(TokenScheduler.timeout).toBeUndefined();
call(loggerMock.error).toMatchObject({
error: new Error('Token does not have expiration time'),
msg: 'Error scheduling refresh token',
msg: 'Error getting token',
});
});

it('should not schedule if token is invalid', () => {
it('should return Infinity if token is invalid', () => {
// Given
obtainTokenMock.mockReturnValue('invalid');
// When
scheduler.schedule();
TokenScheduler.getMillisecondsToRenew();
// Then
expect(scheduler.timeout).toBe(undefined);
call(loggerMock.error).toMatchObject({ msg: 'Error scheduling refresh token' });
expect(TokenScheduler.timeout).toBeUndefined();
call(loggerMock.error).toMatchObject({
msg: 'Error getting token',
error: expect.objectContaining({
message: expect.stringContaining('Invalid token specified'),
}),
});
});

it('should schedule if token is valid', () => {
it('should refresh if token is expired', async () => {
// Given
obtainTokenMock.mockReturnValue(createToken('31 day'));
obtainTokenMock.mockReturnValueOnce(createToken('1 day')).mockReturnValueOnce(createToken('31 day'));
refreshMock.mockResolvedValue({ data: { newToken: 'token' } });
// When
scheduler.schedule();
TokenScheduler.schedule();
await vi.runOnlyPendingTimersAsync();
// Then
expect(scheduler.timeout).not.toBe(undefined);
call(loggerMock.debug).toMatchObject({
msg: 'Token renew date',
expiresAt: new Date('1970-02-01'),
renewAt: new Date('1970-01-31'),
});
expect(TokenScheduler.timeout).not.toBe(undefined);
call(updateCredentialsMock).toStrictEqual({ newToken: 'token' });
calls(loggerMock.debug).toMatchObject([
{
msg: 'Token renew date',
expiresAt: new Date('1970-01-02'),
renewAt: new Date('1970-01-01'),
},
{
msg: 'Token renew date',
expiresAt: new Date('1970-02-01'),
renewAt: new Date('1970-01-31'),
},
]);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ describe('resetConfig', () => {
['deviceUuid', ''],
['backupList', {}],
['newToken', ''],
['newTokenEncrypted', false],
['userData', {}],
['mnemonic', ''],
]);
Expand Down
1 change: 0 additions & 1 deletion src/core/electron/store/app-store.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ export type AppStore = {
backupList: BackupList;

newToken: string;
newTokenEncrypted: boolean;
userData: User;
mnemonic: string;

Expand Down
Loading
Loading