mirror of
https://github.com/verdaccio/verdaccio.git
synced 2024-11-08 23:25:51 +01:00
fix: ui expired token (#4007)
* fix: login state when token expired If there token in the localstorage, the user was always displayed as authenticated, regardless of the token expiration * chore: added changeset for @verdaccio/ui-components * tests: JSON error for node versions older than 20
This commit is contained in:
parent
d2d3bad0d0
commit
92f1c34ae8
5
.changeset/angry-trees-tie.md
Normal file
5
.changeset/angry-trees-tie.md
Normal file
@ -0,0 +1,5 @@
|
||||
---
|
||||
'@verdaccio/ui-components': patch
|
||||
---
|
||||
|
||||
- fixed login state when token is expired (@ku3mi41 in #3980)
|
@ -0,0 +1,33 @@
|
||||
/**
|
||||
* Token Utility
|
||||
*/
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
function encodeBase64(string: string) {
|
||||
return Buffer.from(string).toString('base64');
|
||||
}
|
||||
|
||||
export function generateTokenWithTimeRange(amount = 0) {
|
||||
const payload = {
|
||||
username: 'verdaccio',
|
||||
exp: Number.parseInt(String(dayjs(new Date()).add(amount, 'hour').valueOf() / 1000), 10),
|
||||
};
|
||||
return `xxxxxx.${encodeBase64(JSON.stringify(payload))}.xxxxxx`;
|
||||
}
|
||||
|
||||
export function generateTokenWithExpirationAsString() {
|
||||
const payload = { username: 'verdaccio', exp: 'I am not a number' };
|
||||
return `xxxxxx.${encodeBase64(JSON.stringify(payload))}.xxxxxx`;
|
||||
}
|
||||
|
||||
export function generateInvalidToken() {
|
||||
const payload = `invalidtoken`;
|
||||
return `xxxxxx.${encodeBase64(payload)}.xxxxxx`;
|
||||
}
|
||||
|
||||
export function generateTokenWithOutExpiration() {
|
||||
const payload = {
|
||||
username: 'verdaccio',
|
||||
};
|
||||
return `xxxxxx.${encodeBase64(JSON.stringify(payload))}.xxxxxx`;
|
||||
}
|
@ -37,6 +37,7 @@
|
||||
"highlight.js": "11.7.0",
|
||||
"history": "4.10.1",
|
||||
"i18next": "20.6.1",
|
||||
"js-base64": "3.7.5",
|
||||
"localstorage-memory": "1.0.3",
|
||||
"lodash": "4.17.21",
|
||||
"marked": "4.3.0",
|
||||
|
38
packages/ui-components/src/store/models/login.test.ts
Normal file
38
packages/ui-components/src/store/models/login.test.ts
Normal file
@ -0,0 +1,38 @@
|
||||
// eslint-disable-next-line jest/no-mocks-import
|
||||
import { generateTokenWithTimeRange } from '../../../jest/unit/components/__mocks__/token';
|
||||
|
||||
describe('getDefaultUserState', (): void => {
|
||||
const username = 'xyz';
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetModules();
|
||||
});
|
||||
|
||||
test('should return state with empty user', (): void => {
|
||||
const token = 'token-xx-xx-xx';
|
||||
|
||||
jest.doMock('../storage', () => ({
|
||||
getItem: (key: string) => (key === 'token' ? token : username),
|
||||
}));
|
||||
const { getDefaultUserState } = require('./login');
|
||||
const result = {
|
||||
token: null,
|
||||
username: null,
|
||||
};
|
||||
expect(getDefaultUserState()).toEqual(result);
|
||||
});
|
||||
|
||||
test('should return state with user from storage', (): void => {
|
||||
const token = generateTokenWithTimeRange(24);
|
||||
|
||||
jest.doMock('../storage', () => ({
|
||||
getItem: (key: string) => (key === 'token' ? token : username),
|
||||
}));
|
||||
const { getDefaultUserState } = require('./login');
|
||||
const result = {
|
||||
token,
|
||||
username,
|
||||
};
|
||||
expect(getDefaultUserState()).toEqual(result);
|
||||
});
|
||||
});
|
@ -2,6 +2,7 @@ import { createModel } from '@rematch/core';
|
||||
import i18next from 'i18next';
|
||||
|
||||
import type { RootModel } from '.';
|
||||
import { isTokenExpire } from '../../utils';
|
||||
import API from '../api';
|
||||
import storage from '../storage';
|
||||
|
||||
@ -23,12 +24,17 @@ export type LoginBody = {
|
||||
error?: LoginError;
|
||||
} & LoginResponse;
|
||||
|
||||
export function getDefaultUserState(): LoginBody {
|
||||
const token = storage.getItem('token');
|
||||
const username = storage.getItem('username');
|
||||
const defaultUserState: LoginBody = {
|
||||
token,
|
||||
username,
|
||||
};
|
||||
const defaultUserState = isTokenExpire(token)
|
||||
? { token: null, username: null }
|
||||
: { token, username };
|
||||
|
||||
return defaultUserState;
|
||||
}
|
||||
|
||||
const defaultUserState: LoginBody = getDefaultUserState();
|
||||
|
||||
/**
|
||||
*
|
||||
|
@ -8,3 +8,4 @@ export {
|
||||
} from './cli-utils';
|
||||
export { default as loadable } from './loadable';
|
||||
export { Route } from './routes';
|
||||
export * from './token';
|
||||
|
54
packages/ui-components/src/utils/token.test.ts
Normal file
54
packages/ui-components/src/utils/token.test.ts
Normal file
@ -0,0 +1,54 @@
|
||||
// eslint-disable-next-line jest/no-mocks-import
|
||||
import {
|
||||
generateInvalidToken,
|
||||
generateTokenWithExpirationAsString,
|
||||
generateTokenWithOutExpiration,
|
||||
generateTokenWithTimeRange,
|
||||
} from '../../jest/unit/components/__mocks__/token';
|
||||
import { isTokenExpire } from './token';
|
||||
|
||||
/* eslint-disable no-console */
|
||||
console.error = jest.fn();
|
||||
|
||||
describe('isTokenExpire', (): void => {
|
||||
test('isTokenExpire - null is not a valid payload', (): void => {
|
||||
expect(isTokenExpire(null)).toBeTruthy();
|
||||
});
|
||||
|
||||
test('isTokenExpire - token is not a valid payload', (): void => {
|
||||
expect(isTokenExpire('not_a_valid_token')).toBeTruthy();
|
||||
});
|
||||
|
||||
test('isTokenExpire - token should not expire in 24 hrs range', (): void => {
|
||||
const token = generateTokenWithTimeRange(24);
|
||||
expect(isTokenExpire(token)).toBeFalsy();
|
||||
});
|
||||
|
||||
test('isTokenExpire - token should expire for current time', (): void => {
|
||||
const token = generateTokenWithTimeRange();
|
||||
expect(isTokenExpire(token)).toBeTruthy();
|
||||
});
|
||||
|
||||
test('isTokenExpire - token expiration is not available', (): void => {
|
||||
const token = generateTokenWithOutExpiration();
|
||||
expect(isTokenExpire(token)).toBeTruthy();
|
||||
});
|
||||
|
||||
test('isTokenExpire - token is not a valid json token', (): void => {
|
||||
const NODE_MAJOR_VERSION = +process.versions.node.split('.')[0];
|
||||
const errorToken = new SyntaxError(
|
||||
NODE_MAJOR_VERSION >= 20
|
||||
? 'Unexpected token \'i\', "invalidtoken" is not valid JSON'
|
||||
: 'Unexpected token i in JSON at position 0'
|
||||
);
|
||||
const token = generateInvalidToken();
|
||||
const result = ['Invalid token:', errorToken, 'xxxxxx.aW52YWxpZHRva2Vu.xxxxxx'];
|
||||
expect(isTokenExpire(token)).toBeTruthy();
|
||||
expect(console.error).toHaveBeenCalledWith(...result);
|
||||
});
|
||||
|
||||
test('isTokenExpire - token expiration is not a number', (): void => {
|
||||
const token = generateTokenWithExpirationAsString();
|
||||
expect(isTokenExpire(token)).toBeTruthy();
|
||||
});
|
||||
});
|
33
packages/ui-components/src/utils/token.ts
Normal file
33
packages/ui-components/src/utils/token.ts
Normal file
@ -0,0 +1,33 @@
|
||||
import { Base64 } from 'js-base64';
|
||||
import isNumber from 'lodash/isNumber';
|
||||
import isString from 'lodash/isString';
|
||||
|
||||
export function isTokenExpire(token: string | null): boolean {
|
||||
if (!isString(token)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const [, payload] = token.split('.');
|
||||
|
||||
if (!payload) {
|
||||
return true;
|
||||
}
|
||||
|
||||
let exp: number;
|
||||
try {
|
||||
exp = JSON.parse(Base64.decode(payload)).exp;
|
||||
} catch (error: any) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error('Invalid token:', error, token);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!exp || !isNumber(exp)) {
|
||||
return true;
|
||||
}
|
||||
// Report as expire before (real expire time - 30s)
|
||||
const jsTimestamp = exp * 1000 - 30000;
|
||||
const expired = Date.now() >= jsTimestamp;
|
||||
|
||||
return expired;
|
||||
}
|
815
pnpm-lock.yaml
generated
815
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user