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:
José Varela
2023-04-11 20:49:26 +01:00
committed by GitHub
parent dcc246e165
commit 54f5081e69
4 changed files with 47 additions and 12 deletions

View File

@@ -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,
});
});

View File

@@ -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.');