mirror of
https://github.com/directus/directus.git
synced 2026-02-12 11:35:03 -05:00
Users: Verify JWT on accept invitation (#16711)
* Adds ability to verify JWT with meaningful errors * Fix tests * Apply verify JWT to accept invitation * Update per review * Add joselcvarela to contributors He's a core team member; already signed the CLA outside of GH --------- Co-authored-by: Brainslug <br41nslug@users.noreply.github.com> Co-authored-by: rijkvanzanten <rijkvanzanten@me.com>
This commit is contained in:
@@ -6,7 +6,7 @@ import {
|
||||
TokenExpiredException,
|
||||
} from '../../src/exceptions/index.js';
|
||||
import type { DirectusTokenPayload } from '../../src/types/index.js';
|
||||
import { verifyAccessJWT } from '../../src/utils/jwt.js';
|
||||
import { verifyAccessJWT, verifyJWT } from '../../src/utils/jwt.js';
|
||||
|
||||
const payload: DirectusTokenPayload = { role: null, app_access: false, admin_access: false };
|
||||
const secret = 'test-secret';
|
||||
@@ -14,33 +14,62 @@ const options = { issuer: 'directus' };
|
||||
|
||||
test('Returns the payload of a correctly signed token', () => {
|
||||
const token = jwt.sign(payload, secret, options);
|
||||
const result = verifyAccessJWT(token, secret);
|
||||
expect(result).toEqual(payload);
|
||||
const result = verifyJWT(token, secret);
|
||||
|
||||
expect(result['admin_access']).toEqual(payload.admin_access);
|
||||
expect(result['app_access']).toEqual(payload.app_access);
|
||||
expect(result['role']).toEqual(payload.role);
|
||||
expect(result['iss']).toBe('directus');
|
||||
expect(result['iat']).toBeTypeOf('number');
|
||||
});
|
||||
|
||||
test('Throws TokenExpiredException when token used has expired', () => {
|
||||
const token = jwt.sign({ ...payload, exp: new Date().getTime() / 1000 - 500 }, secret, options);
|
||||
expect(() => verifyAccessJWT(token, secret)).toThrow(TokenExpiredException);
|
||||
expect(() => verifyJWT(token, secret)).toThrow(TokenExpiredException);
|
||||
});
|
||||
|
||||
const InvalidTokenCases = {
|
||||
'wrong issuer': jwt.sign(payload, secret, { issuer: 'wrong' }),
|
||||
'wrong secret': jwt.sign(payload, 'wrong-secret', options),
|
||||
'string payload': jwt.sign('illegal payload', secret),
|
||||
'missing properties in token payload': jwt.sign({ role: null }, secret, options),
|
||||
};
|
||||
|
||||
Object.entries(InvalidTokenCases).forEach(([title, token]) =>
|
||||
test(`Throws InvalidTokenError - ${title}`, () => {
|
||||
expect(() => verifyAccessJWT(token, secret)).toThrow(InvalidTokenException);
|
||||
expect(() => verifyJWT(token, secret)).toThrow(InvalidTokenException);
|
||||
})
|
||||
);
|
||||
|
||||
test(`Throws ServiceUnavailableException for unexpected error from jsonwebtoken`, () => {
|
||||
vi.spyOn(jwt, 'verify').mockImplementation(() => {
|
||||
const mock = vi.spyOn(jwt, 'verify').mockImplementation(() => {
|
||||
throw new Error();
|
||||
});
|
||||
|
||||
const token = jwt.sign(payload, secret, options);
|
||||
expect(() => verifyAccessJWT(token, secret)).toThrow(ServiceUnavailableException);
|
||||
expect(() => verifyJWT(token, secret)).toThrow(ServiceUnavailableException);
|
||||
mock.mockRestore();
|
||||
});
|
||||
|
||||
const RequiredEntries: Array<keyof DirectusTokenPayload> = ['role', 'app_access', 'admin_access'];
|
||||
|
||||
RequiredEntries.forEach((entry) => {
|
||||
test(`Throws InvalidTokenException if ${entry} not defined`, () => {
|
||||
const { [entry]: _entryName, ...rest } = payload;
|
||||
const token = jwt.sign(rest, secret, options);
|
||||
expect(() => verifyAccessJWT(token, secret)).toThrow(InvalidTokenException);
|
||||
});
|
||||
});
|
||||
|
||||
test('Returns the payload of an access token', () => {
|
||||
const payload = { id: 1, role: 1, app_access: true, admin_access: true };
|
||||
const token = jwt.sign(payload, secret, options);
|
||||
const result = verifyAccessJWT(token, secret);
|
||||
expect(result).toEqual({
|
||||
id: 1,
|
||||
role: 1,
|
||||
app_access: true,
|
||||
admin_access: true,
|
||||
share: undefined,
|
||||
share_scope: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,7 +2,7 @@ import jwt from 'jsonwebtoken';
|
||||
import { InvalidTokenException, ServiceUnavailableException, TokenExpiredException } from '../exceptions/index.js';
|
||||
import type { DirectusTokenPayload } from '../types/index.js';
|
||||
|
||||
export function verifyAccessJWT(token: string, secret: string): DirectusTokenPayload {
|
||||
export function verifyJWT(token: string, secret: string): Record<string, any> {
|
||||
let payload;
|
||||
|
||||
try {
|
||||
@@ -19,7 +19,11 @@ export function verifyAccessJWT(token: string, secret: string): DirectusTokenPay
|
||||
}
|
||||
}
|
||||
|
||||
const { id, role, app_access, admin_access, share, share_scope } = payload;
|
||||
return payload;
|
||||
}
|
||||
|
||||
export function verifyAccessJWT(token: string, secret: string): DirectusTokenPayload {
|
||||
const { id, role, app_access, admin_access, share, share_scope } = verifyJWT(token, secret);
|
||||
|
||||
if (role === undefined || app_access === undefined || admin_access === undefined) {
|
||||
throw new InvalidTokenException('Invalid token payload.');
|
||||
|
||||
Reference in New Issue
Block a user