@villedemontreal/jwt-validator 5.7.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +313 -0
- package/dist/scripts/index.d.ts +6 -0
- package/dist/scripts/index.js +16 -0
- package/dist/scripts/index.js.map +1 -0
- package/dist/scripts/lint.d.ts +6 -0
- package/dist/scripts/lint.js +18 -0
- package/dist/scripts/lint.js.map +1 -0
- package/dist/scripts/lintFix.d.ts +6 -0
- package/dist/scripts/lintFix.js +21 -0
- package/dist/scripts/lintFix.js.map +1 -0
- package/dist/scripts/showCoverage.d.ts +13 -0
- package/dist/scripts/showCoverage.js +40 -0
- package/dist/scripts/showCoverage.js.map +1 -0
- package/dist/scripts/test.d.ts +13 -0
- package/dist/scripts/test.js +29 -0
- package/dist/scripts/test.js.map +1 -0
- package/dist/scripts/testUnits.d.ts +15 -0
- package/dist/scripts/testUnits.js +95 -0
- package/dist/scripts/testUnits.js.map +1 -0
- package/dist/scripts/watch.d.ts +14 -0
- package/dist/scripts/watch.js +96 -0
- package/dist/scripts/watch.js.map +1 -0
- package/dist/src/config/configs.d.ts +88 -0
- package/dist/src/config/configs.js +123 -0
- package/dist/src/config/configs.js.map +1 -0
- package/dist/src/config/constants.d.ts +56 -0
- package/dist/src/config/constants.js +66 -0
- package/dist/src/config/constants.js.map +1 -0
- package/dist/src/config/init.d.ts +15 -0
- package/dist/src/config/init.js +48 -0
- package/dist/src/config/init.js.map +1 -0
- package/dist/src/index.d.ts +10 -0
- package/dist/src/index.js +32 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/jwtValidator.d.ts +21 -0
- package/dist/src/jwtValidator.js +129 -0
- package/dist/src/jwtValidator.js.map +1 -0
- package/dist/src/jwtValidator.test.d.ts +1 -0
- package/dist/src/jwtValidator.test.js +500 -0
- package/dist/src/jwtValidator.test.js.map +1 -0
- package/dist/src/middleware/jwtMiddleware.d.ts +7 -0
- package/dist/src/middleware/jwtMiddleware.js +27 -0
- package/dist/src/middleware/jwtMiddleware.js.map +1 -0
- package/dist/src/models/customError.d.ts +11 -0
- package/dist/src/models/customError.js +38 -0
- package/dist/src/models/customError.js.map +1 -0
- package/dist/src/models/expressRequest.d.ts +15 -0
- package/dist/src/models/expressRequest.js +17 -0
- package/dist/src/models/expressRequest.js.map +1 -0
- package/dist/src/models/gluuUserType.d.ts +9 -0
- package/dist/src/models/gluuUserType.js +14 -0
- package/dist/src/models/gluuUserType.js.map +1 -0
- package/dist/src/models/jwtPayload.d.ts +30 -0
- package/dist/src/models/jwtPayload.js +19 -0
- package/dist/src/models/jwtPayload.js.map +1 -0
- package/dist/src/models/pagination.d.ts +16 -0
- package/dist/src/models/pagination.js +16 -0
- package/dist/src/models/pagination.js.map +1 -0
- package/dist/src/models/publicKey.d.ts +29 -0
- package/dist/src/models/publicKey.js +13 -0
- package/dist/src/models/publicKey.js.map +1 -0
- package/dist/src/repositories/cachedPublicKeyRepository.d.ts +53 -0
- package/dist/src/repositories/cachedPublicKeyRepository.js +102 -0
- package/dist/src/repositories/cachedPublicKeyRepository.js.map +1 -0
- package/dist/src/repositories/publicKeyRepository.d.ts +19 -0
- package/dist/src/repositories/publicKeyRepository.js +44 -0
- package/dist/src/repositories/publicKeyRepository.js.map +1 -0
- package/dist/src/userValidator.d.ts +30 -0
- package/dist/src/userValidator.js +35 -0
- package/dist/src/userValidator.js.map +1 -0
- package/dist/src/userValidator.test.d.ts +1 -0
- package/dist/src/userValidator.test.js +251 -0
- package/dist/src/userValidator.test.js.map +1 -0
- package/dist/src/utils/jwtMock.d.ts +31 -0
- package/dist/src/utils/jwtMock.js +221 -0
- package/dist/src/utils/jwtMock.js.map +1 -0
- package/dist/src/utils/logger.d.ts +11 -0
- package/dist/src/utils/logger.js +54 -0
- package/dist/src/utils/logger.js.map +1 -0
- package/dist/src/utils/testingConfigurations.d.ts +7 -0
- package/dist/src/utils/testingConfigurations.js +16 -0
- package/dist/src/utils/testingConfigurations.js.map +1 -0
- package/package.json +82 -0
- package/src/config/configs.ts +145 -0
- package/src/config/constants.ts +83 -0
- package/src/config/init.ts +58 -0
- package/src/index.ts +15 -0
- package/src/jwtValidator.test.ts +607 -0
- package/src/jwtValidator.ts +162 -0
- package/src/middleware/jwtMiddleware.ts +33 -0
- package/src/models/customError.ts +37 -0
- package/src/models/expressRequest.ts +27 -0
- package/src/models/gluuUserType.ts +9 -0
- package/src/models/jwtPayload.ts +58 -0
- package/src/models/pagination.ts +26 -0
- package/src/models/publicKey.ts +33 -0
- package/src/repositories/cachedPublicKeyRepository.ts +121 -0
- package/src/repositories/publicKeyRepository.ts +75 -0
- package/src/userValidator.test.ts +279 -0
- package/src/userValidator.ts +54 -0
- package/src/utils/jwtMock.ts +243 -0
- package/src/utils/logger.ts +60 -0
- package/src/utils/testingConfigurations.ts +12 -0
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { utils } from '@villedemontreal/general-utils';
|
|
2
|
+
import * as jwt from 'jsonwebtoken';
|
|
3
|
+
import * as moment from 'moment';
|
|
4
|
+
import { constants } from './config/constants';
|
|
5
|
+
import { createInvalidAuthHeaderError, createInvalidJwtError } from './models/customError';
|
|
6
|
+
import { IJWTPayload, isJWTPayload } from './models/jwtPayload';
|
|
7
|
+
import { IPublicKey, PublicKeyState } from './models/publicKey';
|
|
8
|
+
import { cachedPublicKeyRepository } from './repositories/cachedPublicKeyRepository';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* JWT validator
|
|
12
|
+
*/
|
|
13
|
+
export interface IJwtValidator {
|
|
14
|
+
/**
|
|
15
|
+
* Verifies an "Authorization" header containing a JWT, checks
|
|
16
|
+
* the JWT with the public key and returns the decoded payload.
|
|
17
|
+
*
|
|
18
|
+
* @param {string} header
|
|
19
|
+
* @return {Promise<IJWTPayload>}
|
|
20
|
+
*/
|
|
21
|
+
verifyAuthorizationHeader(header: string): Promise<IJWTPayload>;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Verifies a JWT with the public key and returns the decoded payload.
|
|
25
|
+
* @param {string} token
|
|
26
|
+
* @return {Promise<IJWTPayload>}
|
|
27
|
+
*/
|
|
28
|
+
verifyToken(token: string): Promise<IJWTPayload>;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* JWT Validator
|
|
33
|
+
*/
|
|
34
|
+
class JwtValidator implements IJwtValidator {
|
|
35
|
+
public async verifyAuthorizationHeader(header: string): Promise<IJWTPayload> {
|
|
36
|
+
if (utils.isBlank(header)) {
|
|
37
|
+
throw createInvalidAuthHeaderError({
|
|
38
|
+
code: constants.errors.codes.NULL_VALUE,
|
|
39
|
+
target: 'Authorization header',
|
|
40
|
+
message: 'Empty Authorization header',
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const parts: string[] = header.trim().split(' ');
|
|
45
|
+
if (parts[0] !== 'Bearer') {
|
|
46
|
+
throw createInvalidAuthHeaderError({
|
|
47
|
+
code: constants.errors.codes.INVALID_VALUE,
|
|
48
|
+
target: 'Authorization header',
|
|
49
|
+
message: 'Bad authentication scheme, "Bearer" required',
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return await this.verifyToken(parts[1]);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
public async verifyToken(token: string): Promise<IJWTPayload> {
|
|
57
|
+
const payload = this.parseJwt(token);
|
|
58
|
+
|
|
59
|
+
const key = await this.getJwtPublicKey(payload);
|
|
60
|
+
|
|
61
|
+
this.validateJwtCreationTimestamp(payload, key);
|
|
62
|
+
this.validateJwtExpirationTimestamp(payload, key);
|
|
63
|
+
|
|
64
|
+
return this.verifyJwt(token, key.publicKey);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
private parseJwt(token: string): IJWTPayload {
|
|
68
|
+
const payload: IJWTPayload = jwt.decode(token) as IJWTPayload;
|
|
69
|
+
if (!payload) {
|
|
70
|
+
throw createInvalidJwtError({
|
|
71
|
+
code: constants.errors.codes.INVALID_VALUE,
|
|
72
|
+
target: 'jwt',
|
|
73
|
+
message: 'jwt malformed',
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
return payload;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
private verifyJwt(token: string, publicKey: string): IJWTPayload {
|
|
80
|
+
let payload: any;
|
|
81
|
+
try {
|
|
82
|
+
payload = jwt.verify(token, publicKey);
|
|
83
|
+
} catch (err) {
|
|
84
|
+
throw createInvalidJwtError({
|
|
85
|
+
code: constants.errors.codes.INVALID_VALUE,
|
|
86
|
+
target: 'jwt',
|
|
87
|
+
message: err.message,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (isJWTPayload(payload)) {
|
|
92
|
+
return payload;
|
|
93
|
+
}
|
|
94
|
+
throw createInvalidJwtError({
|
|
95
|
+
code: constants.errors.codes.INVALID_VALUE,
|
|
96
|
+
target: 'jwt',
|
|
97
|
+
message: 'expected a valid JWT payload',
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
private async getJwtPublicKey(payload: IJWTPayload): Promise<IPublicKey> {
|
|
102
|
+
const keyId = payload.keyId;
|
|
103
|
+
if (!keyId || keyId <= 0) {
|
|
104
|
+
throw createInvalidJwtError({
|
|
105
|
+
code: constants.errors.codes.INVALID_VALUE,
|
|
106
|
+
target: 'jwt',
|
|
107
|
+
message: 'missing public key ID',
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const key: IPublicKey = await cachedPublicKeyRepository.getOne(keyId);
|
|
112
|
+
|
|
113
|
+
// Check key state
|
|
114
|
+
if (!key || key.state !== PublicKeyState.ACTIVE) {
|
|
115
|
+
throw createInvalidJwtError({
|
|
116
|
+
code: constants.errors.codes.INVALID_VALUE,
|
|
117
|
+
target: 'jwt',
|
|
118
|
+
message: 'this keyId is no longer active',
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
return key;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
private validateJwtCreationTimestamp(payload: IJWTPayload, key: IPublicKey) {
|
|
125
|
+
// Check the jwt was not created before the creation date of the key
|
|
126
|
+
const payloadIat: moment.Moment = moment.utc(payload.iat * 1000);
|
|
127
|
+
const keyCreatedAt: moment.Moment = moment.utc(key.createdAt);
|
|
128
|
+
if (payloadIat.diff(keyCreatedAt) < 0) {
|
|
129
|
+
throw createInvalidJwtError({
|
|
130
|
+
code: constants.errors.codes.INVALID_VALUE,
|
|
131
|
+
target: 'jwt',
|
|
132
|
+
message: "this jwt can't be created before the public key",
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
private validateJwtExpirationTimestamp(payload: IJWTPayload, key: IPublicKey) {
|
|
138
|
+
// Check expiration date
|
|
139
|
+
if (key.expiresAt) {
|
|
140
|
+
const keyexpiresAt: moment.Moment = moment.utc(key.expiresAt);
|
|
141
|
+
if (moment.utc().diff(keyexpiresAt) > 0) {
|
|
142
|
+
throw createInvalidJwtError({
|
|
143
|
+
code: constants.errors.codes.INVALID_VALUE,
|
|
144
|
+
target: 'jwt',
|
|
145
|
+
message: 'this keyId is expired',
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Check the jwt was not created after the expiration date of the key
|
|
150
|
+
const payloadIat: moment.Moment = moment.utc(payload.iat * 1000);
|
|
151
|
+
if (payloadIat.diff(keyexpiresAt) > 0) {
|
|
152
|
+
throw createInvalidJwtError({
|
|
153
|
+
code: constants.errors.codes.INVALID_VALUE,
|
|
154
|
+
target: 'jwt',
|
|
155
|
+
message: "this jwt can't be created after the expiration of the public key",
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export const jwtValidator: IJwtValidator = new JwtValidator();
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import * as express from 'express';
|
|
2
|
+
import httpHeaderFieldsTyped from 'http-header-fields-typed';
|
|
3
|
+
import { constants } from '../config/constants';
|
|
4
|
+
import { jwtValidator } from '../jwtValidator';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* JWT validation Middleware
|
|
8
|
+
*
|
|
9
|
+
* @param {boolean} mandatoryValidation Defines if the JWT is mandatory. Defaults to true.
|
|
10
|
+
*/
|
|
11
|
+
export const jwtValidationMiddleware: (
|
|
12
|
+
mandatoryValidation?: boolean
|
|
13
|
+
) => (req: express.Request, res: express.Response, next: express.NextFunction) => Promise<void> = (
|
|
14
|
+
mandatoryValidation = true
|
|
15
|
+
) => {
|
|
16
|
+
return async (
|
|
17
|
+
req: express.Request,
|
|
18
|
+
res: express.Response,
|
|
19
|
+
next: express.NextFunction
|
|
20
|
+
): Promise<void> => {
|
|
21
|
+
try {
|
|
22
|
+
const authHeader: string = req.get(httpHeaderFieldsTyped.AUTHORIZATION);
|
|
23
|
+
if (mandatoryValidation || authHeader) {
|
|
24
|
+
req[constants.requestExtraVariables.JWT] = await jwtValidator.verifyAuthorizationHeader(
|
|
25
|
+
authHeader
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
next();
|
|
29
|
+
} catch (err) {
|
|
30
|
+
next(err);
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { createError, IApiError, IApiErrorAndInfo } from '@villedemontreal/general-utils';
|
|
2
|
+
import { LogLevel } from '@villedemontreal/logger';
|
|
3
|
+
import * as HttpStatusCodes from 'http-status-codes';
|
|
4
|
+
import { constants } from '../config/constants';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Easily creates an "InvalidJWT" error (401). To throw when
|
|
8
|
+
* the user has a bad/expired JWT
|
|
9
|
+
*/
|
|
10
|
+
export function createInvalidJwtError(detail: IApiError): IApiErrorAndInfo {
|
|
11
|
+
return createError(constants.errors.codes.INVALID_JWT, 'Invalid JWT')
|
|
12
|
+
.httpStatus(HttpStatusCodes.UNAUTHORIZED)
|
|
13
|
+
.target('Authorization header')
|
|
14
|
+
.publicMessage('Invalid JWT')
|
|
15
|
+
.logLevel(LogLevel.ERROR)
|
|
16
|
+
.logStackTrace(false)
|
|
17
|
+
.addDetail(detail)
|
|
18
|
+
.build();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Easily creates an "invalidAuthHedaer" error (401). To throw when
|
|
23
|
+
* the user has a bad authorization header
|
|
24
|
+
*/
|
|
25
|
+
export function createInvalidAuthHeaderError(detail: IApiError): IApiErrorAndInfo {
|
|
26
|
+
return createError(
|
|
27
|
+
constants.errors.codes.INVALID_AUTHORIZATION_HEADER,
|
|
28
|
+
'Invalid Authorization header'
|
|
29
|
+
)
|
|
30
|
+
.httpStatus(HttpStatusCodes.UNAUTHORIZED)
|
|
31
|
+
.target('Authorization header')
|
|
32
|
+
.publicMessage('Invalid Authorization header')
|
|
33
|
+
.logLevel(LogLevel.ERROR)
|
|
34
|
+
.logStackTrace(false)
|
|
35
|
+
.addDetail(detail)
|
|
36
|
+
.build();
|
|
37
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extend Express Request object
|
|
3
|
+
*/
|
|
4
|
+
import * as express from 'express';
|
|
5
|
+
import * as _ from 'lodash';
|
|
6
|
+
import { constants } from '../config/constants';
|
|
7
|
+
import { IJWTPayload } from '../models/jwtPayload';
|
|
8
|
+
|
|
9
|
+
export interface IRequestWithJwt extends express.Request {
|
|
10
|
+
/**
|
|
11
|
+
* The JSON Web Token of the request.
|
|
12
|
+
*/
|
|
13
|
+
jwt: IJWTPayload;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Request with JWT Type Guard
|
|
18
|
+
*/
|
|
19
|
+
export const isRequestWithJwt = (obj: any): obj is IRequestWithJwt => {
|
|
20
|
+
return (
|
|
21
|
+
obj &&
|
|
22
|
+
_.isObject(obj) &&
|
|
23
|
+
'get' in obj &&
|
|
24
|
+
'headers' in obj &&
|
|
25
|
+
constants.requestExtraVariables.JWT in obj
|
|
26
|
+
);
|
|
27
|
+
};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import * as _ from 'lodash';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A JWT payload
|
|
5
|
+
*/
|
|
6
|
+
export interface IJWTPayload {
|
|
7
|
+
// TODO more comments on those properties!
|
|
8
|
+
|
|
9
|
+
accessToken: string;
|
|
10
|
+
iss: string;
|
|
11
|
+
|
|
12
|
+
// JWT information
|
|
13
|
+
// From Introspect
|
|
14
|
+
exp: number;
|
|
15
|
+
iat: number;
|
|
16
|
+
keyId: number;
|
|
17
|
+
|
|
18
|
+
// From ClientInfo
|
|
19
|
+
displayName: string;
|
|
20
|
+
aud: string;
|
|
21
|
+
|
|
22
|
+
// From UserInfo
|
|
23
|
+
name: string;
|
|
24
|
+
sub: string;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* @deprecated Please use mtlIdentityId or userName instead.
|
|
28
|
+
*/
|
|
29
|
+
inum: string;
|
|
30
|
+
|
|
31
|
+
userName: string;
|
|
32
|
+
givenName: string;
|
|
33
|
+
familyName: string;
|
|
34
|
+
userType: string;
|
|
35
|
+
email?: string;
|
|
36
|
+
|
|
37
|
+
// Corresponds to oro identity-id or inum
|
|
38
|
+
mtlIdentityId?: string;
|
|
39
|
+
// available only for employees
|
|
40
|
+
employeeNumber?: string;
|
|
41
|
+
customData?: any;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* IJWTPayload Type Guard
|
|
46
|
+
*/
|
|
47
|
+
export const isJWTPayload = (obj: any): obj is IJWTPayload => {
|
|
48
|
+
return (
|
|
49
|
+
obj &&
|
|
50
|
+
_.isObject(obj) &&
|
|
51
|
+
'accessToken' in obj &&
|
|
52
|
+
'iss' in obj &&
|
|
53
|
+
'exp' in obj &&
|
|
54
|
+
'iat' in obj &&
|
|
55
|
+
'sub' in obj &&
|
|
56
|
+
'keyId' in obj
|
|
57
|
+
);
|
|
58
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Represents a paginated result set. This follows the doc :
|
|
3
|
+
* https://villemontreal.atlassian.net/wiki/display/AES/REST+API#RESTAPI-Pagination.1
|
|
4
|
+
*/
|
|
5
|
+
export interface IPaginatedResult<T> {
|
|
6
|
+
paging: {
|
|
7
|
+
offset: number;
|
|
8
|
+
limit: number;
|
|
9
|
+
totalCount: number;
|
|
10
|
+
};
|
|
11
|
+
items: T[];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* IPaginatedResult Type Guard
|
|
16
|
+
*/
|
|
17
|
+
export const isPaginatedResult = (obj: any): obj is IPaginatedResult<any> => {
|
|
18
|
+
return (
|
|
19
|
+
obj &&
|
|
20
|
+
'paging' in obj &&
|
|
21
|
+
'offset' in obj.paging &&
|
|
22
|
+
'limit' in obj.paging &&
|
|
23
|
+
'totalCount' in obj.paging &&
|
|
24
|
+
'items' in obj
|
|
25
|
+
);
|
|
26
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The content of public key
|
|
3
|
+
*/
|
|
4
|
+
export interface IPublicKey {
|
|
5
|
+
id: number;
|
|
6
|
+
algorithm: string;
|
|
7
|
+
publicKey: string;
|
|
8
|
+
state: PublicKeyState;
|
|
9
|
+
createdAt?: string;
|
|
10
|
+
expiresAt?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* List of public key with keyId as key
|
|
15
|
+
*/
|
|
16
|
+
export interface IPublicKeys {
|
|
17
|
+
[keyId: number]: IPublicKey;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Array of public key
|
|
22
|
+
*/
|
|
23
|
+
// tslint:disable-next-line:no-empty-interface
|
|
24
|
+
export type IPublicKeysList = Array<IPublicKey>;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Public key state
|
|
28
|
+
*/
|
|
29
|
+
export enum PublicKeyState {
|
|
30
|
+
ACTIVE = 'active',
|
|
31
|
+
EXPIRED = 'expired',
|
|
32
|
+
REVOKED = 'revoked',
|
|
33
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { ApiErrorAndInfo } from '@villedemontreal/general-utils/dist/src';
|
|
2
|
+
import { extend } from 'lodash';
|
|
3
|
+
import * as moment from 'moment';
|
|
4
|
+
import { configs } from '../config/configs';
|
|
5
|
+
import { IPublicKey, IPublicKeys } from '../models/publicKey';
|
|
6
|
+
import { createLogger } from '../utils/logger';
|
|
7
|
+
import { IPublicKeyRepository, publicKeyRepository } from './publicKeyRepository';
|
|
8
|
+
const logger = createLogger('CachedPublicKeyRepository');
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Cached Public Key repository
|
|
12
|
+
*/
|
|
13
|
+
export interface ICachedPublicKeyRepository extends IPublicKeyRepository {
|
|
14
|
+
/**
|
|
15
|
+
* Clears the public keys in cache
|
|
16
|
+
*/
|
|
17
|
+
clearCache(): void;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export class CachedPublicKeyRepository implements ICachedPublicKeyRepository {
|
|
21
|
+
/**
|
|
22
|
+
* Next update
|
|
23
|
+
*/
|
|
24
|
+
private _nextUpdate: moment.Moment;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Cached public keys
|
|
28
|
+
*/
|
|
29
|
+
private _cachedKeys: IPublicKeys = {};
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Clears the public keys in cache
|
|
33
|
+
*/
|
|
34
|
+
public clearCache(): void {
|
|
35
|
+
this._cachedKeys = {};
|
|
36
|
+
this._nextUpdate = moment.utc();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Return the last public keys
|
|
41
|
+
* @return {Promise<IPublicKeys>}
|
|
42
|
+
*/
|
|
43
|
+
public async getAll(): Promise<IPublicKeys> {
|
|
44
|
+
if (!this.isValidCache()) {
|
|
45
|
+
const keysList: IPublicKeys = await publicKeyRepository.getAll();
|
|
46
|
+
|
|
47
|
+
if (keysList) {
|
|
48
|
+
this.updateNextCacheUpdate();
|
|
49
|
+
this._cachedKeys = extend(this._cachedKeys, keysList);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return this._cachedKeys;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Return one public key
|
|
57
|
+
* @param {number} keyId
|
|
58
|
+
* @return {Promise<IPublicKey>}
|
|
59
|
+
*/
|
|
60
|
+
public async getOne(keyId: number): Promise<IPublicKey> {
|
|
61
|
+
if (!this._cachedKeys[keyId] || !this.isValidCache()) {
|
|
62
|
+
// In case of network error while getting key, return the cached one
|
|
63
|
+
try {
|
|
64
|
+
const key: IPublicKey = await publicKeyRepository.getOne(keyId);
|
|
65
|
+
if (key) {
|
|
66
|
+
this.updateNextCacheUpdate();
|
|
67
|
+
this._cachedKeys[keyId] = key;
|
|
68
|
+
}
|
|
69
|
+
} catch (err) {
|
|
70
|
+
if (!this._cachedKeys[keyId] || !this.isTransientError(err)) {
|
|
71
|
+
throw err;
|
|
72
|
+
}
|
|
73
|
+
logger.error(
|
|
74
|
+
JSON.stringify(err),
|
|
75
|
+
`[getOne] There was an error getting key ${keyId}, cached value was sent as result`
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return this._cachedKeys[keyId];
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Check if the error is a network error, server error or
|
|
84
|
+
* If true, do not throw error
|
|
85
|
+
* @return {boolean}
|
|
86
|
+
*/
|
|
87
|
+
private isTransientError(err: any): boolean {
|
|
88
|
+
if (err instanceof ApiErrorAndInfo) {
|
|
89
|
+
if (err.httpStatus >= 500 || err.httpStatus === 429) return true;
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
// No status on a superagent network error
|
|
93
|
+
if (!err.status) return true;
|
|
94
|
+
const errStatus: number = +err.status;
|
|
95
|
+
if (errStatus >= 500 || errStatus === 429) return true;
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Check if the cache is stil valid
|
|
101
|
+
* @return {boolean}
|
|
102
|
+
*/
|
|
103
|
+
private isValidCache(): boolean {
|
|
104
|
+
if (!this._nextUpdate || moment.utc().diff(this._nextUpdate) >= 0) {
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return true;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Update the date of the next update
|
|
113
|
+
* @return
|
|
114
|
+
*/
|
|
115
|
+
private updateNextCacheUpdate(): void {
|
|
116
|
+
this._nextUpdate = moment.utc().add(configs.getCacheDuration(), 'seconds');
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export const cachedPublicKeyRepository: ICachedPublicKeyRepository =
|
|
121
|
+
new CachedPublicKeyRepository();
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { httpUtils } from '@villedemontreal/http-request';
|
|
2
|
+
import { isArray, keyBy } from 'lodash';
|
|
3
|
+
import * as superagent from 'superagent';
|
|
4
|
+
|
|
5
|
+
import { createError } from '@villedemontreal/general-utils';
|
|
6
|
+
import { configs } from '../config/configs';
|
|
7
|
+
import { constants } from '../config/constants';
|
|
8
|
+
import { IPaginatedResult } from '../models/pagination';
|
|
9
|
+
import { IPublicKey, IPublicKeys } from '../models/publicKey';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Public Key repository
|
|
13
|
+
*/
|
|
14
|
+
export interface IPublicKeyRepository {
|
|
15
|
+
/**
|
|
16
|
+
* Return the last public keys
|
|
17
|
+
* @return {Promise<IPublicKeys>}
|
|
18
|
+
*/
|
|
19
|
+
getAll(): Promise<IPublicKeys>;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Returns one public key.
|
|
23
|
+
* Returns null if not found.
|
|
24
|
+
* @param {number} keyId
|
|
25
|
+
* @return {Promise<IPublicKey>}
|
|
26
|
+
*/
|
|
27
|
+
getOne(keyId: number): Promise<IPublicKey>;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
class PublicKeyRepository implements IPublicKeyRepository {
|
|
31
|
+
public async getAll(): Promise<IPublicKeys> {
|
|
32
|
+
const url = `${httpUtils.urlJoin(
|
|
33
|
+
configs.getHost(),
|
|
34
|
+
configs.getEndpoint()
|
|
35
|
+
)}?${configs.getFetchKeysParameters()}`;
|
|
36
|
+
const request = superagent.get(url);
|
|
37
|
+
|
|
38
|
+
const response = await httpUtils.send(request);
|
|
39
|
+
if (!response.ok) {
|
|
40
|
+
throw new Error(`An error occured calling ${url} : ${response.error}`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const data: IPaginatedResult<IPublicKey> = response.body;
|
|
44
|
+
if (data && data.items && isArray(data.items)) {
|
|
45
|
+
const newKeys: IPublicKeys = keyBy(data.items, (item) => item.id);
|
|
46
|
+
return newKeys;
|
|
47
|
+
}
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
public async getOne(keyId: number): Promise<IPublicKey> {
|
|
52
|
+
const url = httpUtils.urlJoin(configs.getHost(), configs.getEndpoint(), keyId.toString());
|
|
53
|
+
const request = superagent.get(url);
|
|
54
|
+
|
|
55
|
+
const response = await httpUtils.send(request);
|
|
56
|
+
if (!response.ok) {
|
|
57
|
+
// ==========================================
|
|
58
|
+
// Not found: we return null
|
|
59
|
+
// ==========================================
|
|
60
|
+
if (response.status === 404) {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
throw createError(
|
|
65
|
+
constants.errors.codes.UNABLE_TO_GET_PUBLIC_KEY,
|
|
66
|
+
`An error occured calling ${url} : ${response.error}`
|
|
67
|
+
)
|
|
68
|
+
.httpStatus(response.status)
|
|
69
|
+
.build();
|
|
70
|
+
}
|
|
71
|
+
return response.body;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export const publicKeyRepository: IPublicKeyRepository = new PublicKeyRepository();
|