mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2024-11-25 11:16:31 -05:00
enhancement(api-tokens): add prefix and more strict validation
Signed-off-by: Erik Michelson <github@erik.michelson.eu>
This commit is contained in:
parent
0db5a0856b
commit
92bde4d281
3 changed files with 28 additions and 9 deletions
|
@ -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
|
||||||
|
|
|
@ -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}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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 () => {
|
||||||
|
|
Loading…
Reference in a new issue