enhancement(api-tokens): add prefix and more strict validation

Signed-off-by: Erik Michelson <github@erik.michelson.eu>
This commit is contained in:
Erik Michelson 2024-03-23 01:14:41 +01:00
parent 0db5a0856b
commit 92bde4d281
3 changed files with 28 additions and 9 deletions

View file

@ -230,7 +230,7 @@ describe('AuthService', () => {
return authToken; return authToken;
}); });
const userByToken = await service.validateToken( const userByToken = await service.validateToken(
`${authToken.keyId}.${testSecret}`, `hd2.${authToken.keyId}.${testSecret}`,
); );
expect(userByToken).toEqual({ expect(userByToken).toEqual({
...user, ...user,
@ -238,14 +238,31 @@ describe('AuthService', () => {
}); });
}); });
describe('fails:', () => { describe('fails:', () => {
it('the prefix is missing', async () => {
await expect(
service.validateToken(`${authToken.keyId}.${'a'.repeat(73)}`),
).rejects.toThrow(TokenNotValidError);
});
it('the prefix is wrong', async () => {
await expect(
service.validateToken(`hd1.${authToken.keyId}.${'a'.repeat(73)}`),
).rejects.toThrow(TokenNotValidError);
});
it('the secret is missing', async () => { it('the secret is missing', async () => {
await expect( await expect(
service.validateToken(`${authToken.keyId}`), service.validateToken(`hd2.${authToken.keyId}`),
).rejects.toThrow(TokenNotValidError); ).rejects.toThrow(TokenNotValidError);
}); });
it('the secret is too long', async () => { it('the secret is too long', async () => {
await expect( await expect(
service.validateToken(`${authToken.keyId}.${'a'.repeat(73)}`), service.validateToken(`hd2.${authToken.keyId}.${'a'.repeat(73)}`),
).rejects.toThrow(TokenNotValidError);
});
it('the token contains sections after the secret', async () => {
await expect(
service.validateToken(
`hd2.${authToken.keyId}.${'a'.repeat(73)}.extra`,
),
).rejects.toThrow(TokenNotValidError); ).rejects.toThrow(TokenNotValidError);
}); });
}); });
@ -296,7 +313,7 @@ describe('AuthService', () => {
(new Date().getTime() + 2 * 365 * 24 * 60 * 60 * 1000), (new Date().getTime() + 2 * 365 * 24 * 60 * 60 * 1000),
).toBeLessThanOrEqual(10000); ).toBeLessThanOrEqual(10000);
expect(token.lastUsedAt).toBeNull(); expect(token.lastUsedAt).toBeNull();
expect(token.secret.startsWith(token.keyId)).toBeTruthy(); expect(token.secret.startsWith('hd2.' + token.keyId)).toBeTruthy();
}); });
it('with validUntil not 0', async () => { it('with validUntil not 0', async () => {
jest.spyOn(authTokenRepo, 'find').mockResolvedValueOnce([authToken]); jest.spyOn(authTokenRepo, 'find').mockResolvedValueOnce([authToken]);
@ -313,7 +330,7 @@ describe('AuthService', () => {
expect(token.label).toEqual(identifier); expect(token.label).toEqual(identifier);
expect(token.validUntil.getTime()).toEqual(validUntil); expect(token.validUntil.getTime()).toEqual(validUntil);
expect(token.lastUsedAt).toBeNull(); expect(token.lastUsedAt).toBeNull();
expect(token.secret.startsWith(token.keyId)).toBeTruthy(); expect(token.secret.startsWith('hd2.' + token.keyId)).toBeTruthy();
}); });
it('should throw TooManyTokensError when number of tokens >= 200', async () => { it('should throw TooManyTokensError when number of tokens >= 200', async () => {
jest jest

View file

@ -21,6 +21,8 @@ import { TimestampMillis } from '../utils/timestamp';
import { AuthTokenDto, AuthTokenWithSecretDto } from './auth-token.dto'; import { AuthTokenDto, AuthTokenWithSecretDto } from './auth-token.dto';
import { AuthToken } from './auth-token.entity'; import { AuthToken } from './auth-token.entity';
export const AUTH_TOKEN_PREFIX = 'hd2';
@Injectable() @Injectable()
export class AuthService { export class AuthService {
constructor( constructor(
@ -32,8 +34,8 @@ export class AuthService {
} }
async validateToken(tokenString: string): Promise<User> { async validateToken(tokenString: string): Promise<User> {
const [keyId, secret] = tokenString.split('.'); const [prefix, keyId, secret, ...rest] = tokenString.split('.');
if (!secret) { if (!keyId || !secret || prefix !== AUTH_TOKEN_PREFIX || rest.length > 0) {
throw new TokenNotValidError('Invalid AuthToken format'); throw new TokenNotValidError('Invalid AuthToken format');
} }
if (secret.length != 86) { if (secret.length != 86) {
@ -105,7 +107,7 @@ export class AuthService {
)) as AuthToken; )) as AuthToken;
return this.toAuthTokenWithSecretDto( return this.toAuthTokenWithSecretDto(
createdToken, createdToken,
`${createdToken.keyId}.${secret}`, `${AUTH_TOKEN_PREFIX}.${createdToken.keyId}.${secret}`,
); );
} }

View file

@ -51,7 +51,7 @@ describe('Tokens', () => {
Date.now(), Date.now(),
); );
expect(response.body.lastUsedAt).toBe(null); expect(response.body.lastUsedAt).toBe(null);
expect(response.body.secret.length).toBe(98); expect(response.body.secret.length).toBe(102);
}); });
it(`GET /tokens`, async () => { it(`GET /tokens`, async () => {