@wabot-dev/framework 0.1.0 → 0.2.0-beta.10
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/dist/src/addon/auth/api-key/@apiKeyHandshakeGuard.js +16 -0
- package/dist/src/addon/auth/api-key/ApiKey.js +29 -34
- package/dist/src/addon/auth/api-key/ApiKeyGuardMiddleware.js +3 -6
- package/dist/src/addon/auth/api-key/{ApiKeyConnectionGuardMiddleware.js → ApiKeyHandshakeGuardMiddleware.js} +5 -5
- package/dist/src/addon/auth/api-key/ApiKeyRepository.js +7 -17
- package/dist/src/addon/auth/api-key/PgApiKeyRepository.js +39 -6
- package/dist/src/addon/auth/api-key/RemoteApiKeyRepository.js +22 -14
- package/dist/src/addon/auth/jwt/@jwtHandshakeGuard.js +16 -0
- package/dist/src/addon/auth/jwt/Jwt.js +7 -9
- package/dist/src/addon/auth/jwt/{JwtConnectionGuardMiddleware.js → JwtHandshakeGuardMiddleware.js} +4 -4
- package/dist/src/addon/auth/jwt/JwtRefreshToken.js +40 -35
- package/dist/src/addon/auth/jwt/JwtRefreshTokenRepository.js +6 -0
- package/dist/src/addon/auth/jwt/JwtSigner.js +1 -1
- package/dist/src/addon/auth/jwt/PgJwtRefreshTokenRepository.js +27 -0
- package/dist/src/addon/chat-controller/socket/@socket.js +1 -1
- package/dist/src/addon/chat-controller/socket/SocketChannel.js +64 -29
- package/dist/src/addon/chat-controller/socket/SocketChannelConfig.js +6 -4
- package/dist/src/core/auth/Auth.js +6 -0
- package/dist/src/core/mapper/Mapper.js +22 -6
- package/dist/src/core/validation/core/validateModel.js +37 -4
- package/dist/src/core/validation/{validate.js → validateAndTransform.js} +2 -2
- package/dist/src/feature/chat-controller/runChatControllers.js +3 -1
- package/dist/src/feature/rest-controller/runRestControllers.js +2 -2
- package/dist/src/feature/socket-controller/metadata/@handshakeMiddlewares.js +16 -0
- package/dist/src/feature/socket-controller/metadata/{@socketEvent.js → @onSocketEvent.js} +2 -2
- package/dist/src/feature/socket-controller/metadata/SocketControllerMetadataStore.js +12 -34
- package/dist/src/feature/socket-controller/runSocketControllers.js +96 -78
- package/dist/src/index.d.ts +131 -129
- package/dist/src/index.js +8 -9
- package/package.json +4 -5
- package/dist/src/addon/auth/api-key/@apiKeyConnectionGuard.js +0 -16
- package/dist/src/addon/auth/jwt/@jwtConnectionGuard.js +0 -16
- package/dist/src/feature/socket-controller/metadata/@connectionMiddleware.js +0 -16
- package/dist/src/feature/socket-controller/metadata/@socketConnection.js +0 -18
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { handshakeMiddlewares } from '../../../feature/socket-controller/metadata/@handshakeMiddlewares.js';
|
|
2
|
+
import '../../../feature/socket-controller/metadata/SocketControllerMetadataStore.js';
|
|
3
|
+
import '../../../core/injection/index.js';
|
|
4
|
+
import 'debug';
|
|
5
|
+
import '../../../core/validation/metadata/ValidationMetadataStore.js';
|
|
6
|
+
import 'socket.io';
|
|
7
|
+
import '../../../feature/socket/SocketServerProvider.js';
|
|
8
|
+
import { ApiKeyHandshakeGuardMiddleware } from './ApiKeyHandshakeGuardMiddleware.js';
|
|
9
|
+
|
|
10
|
+
function apiKeyHandshakeGuard() {
|
|
11
|
+
return function (target) {
|
|
12
|
+
handshakeMiddlewares([ApiKeyHandshakeGuardMiddleware])(target);
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export { apiKeyHandshakeGuard };
|
|
@@ -1,52 +1,47 @@
|
|
|
1
1
|
import { Entity } from '../../../core/entity/Entity.js';
|
|
2
2
|
import { CustomError } from '../../../core/error/CustomError.js';
|
|
3
|
-
import
|
|
3
|
+
import crypto from 'node:crypto';
|
|
4
4
|
|
|
5
5
|
class ApiKey extends Entity {
|
|
6
|
+
static PREFIX = 'sk_';
|
|
7
|
+
static hashSecret(secret) {
|
|
8
|
+
return crypto.createHash('sha256').update(secret).digest('hex');
|
|
9
|
+
}
|
|
6
10
|
get authInfo() {
|
|
7
11
|
return this.data.authInfo;
|
|
8
12
|
}
|
|
13
|
+
get metadata() {
|
|
14
|
+
return this.data.metadata ?? {};
|
|
15
|
+
}
|
|
16
|
+
get name() {
|
|
17
|
+
return this.data.name;
|
|
18
|
+
}
|
|
9
19
|
setAuthInfo(authInfo) {
|
|
10
20
|
this.data.authInfo = authInfo;
|
|
11
21
|
}
|
|
12
|
-
|
|
13
|
-
if (this.data.
|
|
14
|
-
throw new Error('This
|
|
22
|
+
generateSecret() {
|
|
23
|
+
if (this.data.secretHash) {
|
|
24
|
+
throw new Error('This API key already has a secret');
|
|
15
25
|
}
|
|
16
|
-
const
|
|
17
|
-
this.data.
|
|
18
|
-
return
|
|
26
|
+
const secret = `${ApiKey.PREFIX}${crypto.randomBytes(32).toString('hex')}`;
|
|
27
|
+
this.data.secretHash = ApiKey.hashSecret(secret);
|
|
28
|
+
return secret;
|
|
19
29
|
}
|
|
20
|
-
|
|
21
|
-
if (!
|
|
30
|
+
isValidSecret(secret) {
|
|
31
|
+
if (!secret.startsWith(ApiKey.PREFIX))
|
|
22
32
|
return false;
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
static inflate(secret) {
|
|
31
|
-
try {
|
|
32
|
-
const json = Buffer.from(secret, 'base64').toString('utf-8');
|
|
33
|
-
const data = JSON.parse(json);
|
|
34
|
-
if (!data.id || !data.pass) {
|
|
35
|
-
throw new Error('invalid secret structure');
|
|
36
|
-
}
|
|
37
|
-
return data;
|
|
38
|
-
}
|
|
39
|
-
catch (err) {
|
|
40
|
-
throw new Error('fail to inflate secret: ' + err.message);
|
|
41
|
-
}
|
|
33
|
+
if (!this.data.secretHash)
|
|
34
|
+
return false;
|
|
35
|
+
const hashed = ApiKey.hashSecret(secret);
|
|
36
|
+
const stored = this.data.secretHash;
|
|
37
|
+
const hashedBuf = Buffer.from(hashed, 'hex');
|
|
38
|
+
const storedBuf = Buffer.from(stored, 'hex');
|
|
39
|
+
return hashedBuf.length === storedBuf.length && crypto.timingSafeEqual(hashedBuf, storedBuf);
|
|
42
40
|
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
throw new Error('id and pass required');
|
|
41
|
+
validateSecret(secret) {
|
|
42
|
+
if (!this.isValidSecret(secret)) {
|
|
43
|
+
throw new CustomError({ message: 'Invalid API key', httpCode: 401 });
|
|
47
44
|
}
|
|
48
|
-
const json = JSON.stringify({ id, pass });
|
|
49
|
-
return Buffer.from(json, 'utf-8').toString('base64');
|
|
50
45
|
}
|
|
51
46
|
}
|
|
52
47
|
|
|
@@ -3,7 +3,6 @@ import { injectable } from '../../../core/injection/index.js';
|
|
|
3
3
|
import { Auth } from '../../../core/auth/Auth.js';
|
|
4
4
|
import { CustomError } from '../../../core/error/CustomError.js';
|
|
5
5
|
import { ApiKeyRepository } from './ApiKeyRepository.js';
|
|
6
|
-
import { ApiKey } from './ApiKey.js';
|
|
7
6
|
|
|
8
7
|
let ApiKeyGuardMiddleware = class ApiKeyGuardMiddleware {
|
|
9
8
|
apiKeyRepository;
|
|
@@ -22,15 +21,13 @@ let ApiKeyGuardMiddleware = class ApiKeyGuardMiddleware {
|
|
|
22
21
|
throw new CustomError({ httpCode: 401, message: 'Authorization should be an Api-Key' });
|
|
23
22
|
}
|
|
24
23
|
try {
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
apiKey.validatePassword(keyData.pass);
|
|
28
|
-
this.auth.assign(apiKey.authInfo);
|
|
24
|
+
const authInfo = await this.apiKeyRepository.findAndValidate(keySecret);
|
|
25
|
+
this.auth.assign(authInfo);
|
|
29
26
|
}
|
|
30
27
|
catch (err) {
|
|
31
28
|
throw new CustomError({
|
|
32
29
|
httpCode: 401,
|
|
33
|
-
message: err instanceof Error ? `Invalid
|
|
30
|
+
message: err instanceof Error ? `Invalid key: ${err.message}` : 'Invalid key',
|
|
34
31
|
cause: err instanceof Error ? err : undefined,
|
|
35
32
|
});
|
|
36
33
|
}
|
|
@@ -4,7 +4,7 @@ import { CustomError } from '../../../core/error/CustomError.js';
|
|
|
4
4
|
import { injectable } from '../../../core/injection/index.js';
|
|
5
5
|
import { ApiKeyRepository } from './ApiKeyRepository.js';
|
|
6
6
|
|
|
7
|
-
let
|
|
7
|
+
let ApiKeyHandshakeGuardMiddleware = class ApiKeyHandshakeGuardMiddleware {
|
|
8
8
|
apiKeyRepository;
|
|
9
9
|
auth;
|
|
10
10
|
constructor(apiKeyRepository, auth) {
|
|
@@ -33,7 +33,7 @@ let ApiKeyConnectionGuardMiddleware = class ApiKeyConnectionGuardMiddleware {
|
|
|
33
33
|
throw new CustomError({ httpCode: 401, message: 'Token not available' });
|
|
34
34
|
}
|
|
35
35
|
try {
|
|
36
|
-
const authInfo = await this.apiKeyRepository.
|
|
36
|
+
const authInfo = await this.apiKeyRepository.findAndValidate(keySecret);
|
|
37
37
|
this.auth.assign(authInfo);
|
|
38
38
|
}
|
|
39
39
|
catch (err) {
|
|
@@ -45,10 +45,10 @@ let ApiKeyConnectionGuardMiddleware = class ApiKeyConnectionGuardMiddleware {
|
|
|
45
45
|
}
|
|
46
46
|
}
|
|
47
47
|
};
|
|
48
|
-
|
|
48
|
+
ApiKeyHandshakeGuardMiddleware = __decorate([
|
|
49
49
|
injectable(),
|
|
50
50
|
__metadata("design:paramtypes", [ApiKeyRepository,
|
|
51
51
|
Auth])
|
|
52
|
-
],
|
|
52
|
+
], ApiKeyHandshakeGuardMiddleware);
|
|
53
53
|
|
|
54
|
-
export {
|
|
54
|
+
export { ApiKeyHandshakeGuardMiddleware };
|
|
@@ -1,34 +1,24 @@
|
|
|
1
|
-
import { ApiKey } from './ApiKey.js';
|
|
2
|
-
|
|
3
1
|
class ApiKeyRepository {
|
|
4
|
-
findAuthInfo(secret) {
|
|
5
|
-
throw new Error('Method not implemented.');
|
|
6
|
-
}
|
|
7
2
|
find(id) {
|
|
8
3
|
throw new Error('Method not implemented.');
|
|
9
4
|
}
|
|
10
5
|
findOrThrow(id) {
|
|
11
6
|
throw new Error('Method not implemented.');
|
|
12
7
|
}
|
|
8
|
+
findByMetadata(metadata) {
|
|
9
|
+
throw new Error('Method not implemented.');
|
|
10
|
+
}
|
|
13
11
|
create(item) {
|
|
14
12
|
throw new Error('Method not implemented.');
|
|
15
13
|
}
|
|
16
14
|
generate(req) {
|
|
17
15
|
throw new Error('Method not implemented.');
|
|
18
16
|
}
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
const pass = apiKey.generatePassword();
|
|
22
|
-
await repository.create(apiKey);
|
|
23
|
-
const id = apiKey.id;
|
|
24
|
-
const secret = ApiKey.deflate({ id, pass });
|
|
25
|
-
return { id, secret };
|
|
17
|
+
findBySecret(secret) {
|
|
18
|
+
throw new Error('Method not implemented.');
|
|
26
19
|
}
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
const apiKey = await repository.findOrThrow(id);
|
|
30
|
-
apiKey.validatePassword(pass);
|
|
31
|
-
return apiKey.authInfo;
|
|
20
|
+
findAndValidate(secret) {
|
|
21
|
+
throw new Error('Method not implemented.');
|
|
32
22
|
}
|
|
33
23
|
}
|
|
34
24
|
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { __decorate, __metadata } from 'tslib';
|
|
2
|
-
import { ApiKey } from './ApiKey.js';
|
|
3
|
-
import { Pool } from 'pg';
|
|
4
2
|
import { PgCrudRepository } from '../../../feature/pg/PgCrudRepository.js';
|
|
3
|
+
import { Pool } from 'pg';
|
|
5
4
|
import { singleton } from 'tsyringe';
|
|
6
|
-
import {
|
|
5
|
+
import { ApiKey } from './ApiKey.js';
|
|
6
|
+
import { CustomError } from '../../../core/error/CustomError.js';
|
|
7
7
|
|
|
8
8
|
let PgApiKeyRepository = class PgApiKeyRepository extends PgCrudRepository {
|
|
9
9
|
constructor(pool) {
|
|
@@ -13,11 +13,44 @@ let PgApiKeyRepository = class PgApiKeyRepository extends PgCrudRepository {
|
|
|
13
13
|
constructor: ApiKey,
|
|
14
14
|
});
|
|
15
15
|
}
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
async findAndValidate(secret) {
|
|
17
|
+
const apiKey = await this.findBySecret(secret);
|
|
18
|
+
if (!apiKey) {
|
|
19
|
+
throw new CustomError({ message: 'Invalid Api Key', httpCode: 401 });
|
|
20
|
+
}
|
|
21
|
+
return apiKey.authInfo;
|
|
22
|
+
}
|
|
23
|
+
async findBySecret(secret) {
|
|
24
|
+
const secretHash = ApiKey.hashSecret(secret);
|
|
25
|
+
const query = `
|
|
26
|
+
SELECT ${this.columns}
|
|
27
|
+
FROM ${this.table}
|
|
28
|
+
WHERE data @> $1::jsonb
|
|
29
|
+
LIMIT 1
|
|
30
|
+
`;
|
|
31
|
+
const items = await this.query(query, [JSON.stringify({ secretHash })]);
|
|
32
|
+
return items[0] ?? null;
|
|
33
|
+
}
|
|
34
|
+
async findByMetadata(metadata) {
|
|
35
|
+
const query = `
|
|
36
|
+
SELECT ${this.columns}
|
|
37
|
+
FROM ${this.table}
|
|
38
|
+
WHERE data @> $1::jsonb
|
|
39
|
+
`;
|
|
40
|
+
return await this.query(query, [JSON.stringify({ metadata })]);
|
|
18
41
|
}
|
|
19
42
|
async generate(req) {
|
|
20
|
-
|
|
43
|
+
const apiKey = new ApiKey({
|
|
44
|
+
name: req.name,
|
|
45
|
+
metadata: req.metadata,
|
|
46
|
+
authInfo: req.authInfo,
|
|
47
|
+
});
|
|
48
|
+
const secret = apiKey.generateSecret();
|
|
49
|
+
await this.create(apiKey);
|
|
50
|
+
return {
|
|
51
|
+
apiKey,
|
|
52
|
+
secret,
|
|
53
|
+
};
|
|
21
54
|
}
|
|
22
55
|
};
|
|
23
56
|
PgApiKeyRepository = __decorate([
|
|
@@ -1,40 +1,48 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { CustomError } from '../../../core/error/CustomError.js';
|
|
2
2
|
|
|
3
3
|
class RemoteApiKeyRepository {
|
|
4
4
|
fetcher;
|
|
5
5
|
cacheSeconds;
|
|
6
|
-
|
|
6
|
+
cacheBySecret = new Map();
|
|
7
7
|
constructor(fetcher, cacheSeconds) {
|
|
8
8
|
this.fetcher = fetcher;
|
|
9
9
|
this.cacheSeconds = cacheSeconds;
|
|
10
10
|
}
|
|
11
|
-
async
|
|
11
|
+
async findAndValidate(secret) {
|
|
12
12
|
const now = Date.now();
|
|
13
|
-
const cached = this.
|
|
13
|
+
const cached = this.cacheBySecret.get(secret);
|
|
14
14
|
if (cached && cached.expiresAt > now) {
|
|
15
|
+
if (!cached.value) {
|
|
16
|
+
throw new CustomError({ message: 'Invalid Api Key', httpCode: 401 });
|
|
17
|
+
}
|
|
15
18
|
return cached.value;
|
|
16
19
|
}
|
|
17
|
-
const result = await this.fetcher(
|
|
18
|
-
this.
|
|
20
|
+
const result = await this.fetcher.fetchAuthInfoBySecret(secret);
|
|
21
|
+
this.cacheBySecret.set(secret, {
|
|
19
22
|
value: result,
|
|
20
23
|
expiresAt: now + this.cacheSeconds * 1000,
|
|
21
24
|
});
|
|
22
|
-
return result;
|
|
23
|
-
}
|
|
24
|
-
async findOrThrow(id) {
|
|
25
|
-
const result = await this.find(id);
|
|
26
25
|
if (!result) {
|
|
27
|
-
throw new
|
|
26
|
+
throw new CustomError({ message: 'Invalid Api Key', httpCode: 401 });
|
|
28
27
|
}
|
|
29
28
|
return result;
|
|
30
29
|
}
|
|
31
|
-
|
|
32
|
-
|
|
30
|
+
find(id) {
|
|
31
|
+
throw new Error('Method not implemented.');
|
|
32
|
+
}
|
|
33
|
+
findOrThrow(id) {
|
|
34
|
+
throw new Error('Method not implemented.');
|
|
35
|
+
}
|
|
36
|
+
findBySecret(secret) {
|
|
37
|
+
throw new Error('Method not implemented.');
|
|
38
|
+
}
|
|
39
|
+
findByMetadata(metadata) {
|
|
40
|
+
throw new Error('Method not implemented.');
|
|
33
41
|
}
|
|
34
42
|
create(item) {
|
|
35
43
|
throw new Error('Method not implemented.');
|
|
36
44
|
}
|
|
37
|
-
|
|
45
|
+
generate(req) {
|
|
38
46
|
throw new Error('Method not implemented.');
|
|
39
47
|
}
|
|
40
48
|
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { handshakeMiddlewares } from '../../../feature/socket-controller/metadata/@handshakeMiddlewares.js';
|
|
2
|
+
import '../../../feature/socket-controller/metadata/SocketControllerMetadataStore.js';
|
|
3
|
+
import '../../../core/injection/index.js';
|
|
4
|
+
import 'debug';
|
|
5
|
+
import '../../../core/validation/metadata/ValidationMetadataStore.js';
|
|
6
|
+
import 'socket.io';
|
|
7
|
+
import '../../../feature/socket/SocketServerProvider.js';
|
|
8
|
+
import { JwtHandshakeGuardMiddleware } from './JwtHandshakeGuardMiddleware.js';
|
|
9
|
+
|
|
10
|
+
function jwtHandshakeGuard() {
|
|
11
|
+
return function (target) {
|
|
12
|
+
handshakeMiddlewares([JwtHandshakeGuardMiddleware])(target);
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export { jwtHandshakeGuard };
|
|
@@ -17,17 +17,18 @@ let Jwt = class Jwt {
|
|
|
17
17
|
this.jwtRefreshTokenRepository = jwtRefreshTokenRepository;
|
|
18
18
|
this.config = config;
|
|
19
19
|
}
|
|
20
|
-
async createToken() {
|
|
20
|
+
async createToken(metadata) {
|
|
21
21
|
const authInfo = this.auth.require();
|
|
22
22
|
const refreshToken = new JwtRefreshToken({
|
|
23
|
+
metadata,
|
|
23
24
|
authInfo,
|
|
24
|
-
expirationTime:
|
|
25
|
+
expirationTime: Date.now() + this.config.refreshExpirationSeconds * 1000,
|
|
25
26
|
});
|
|
26
|
-
const
|
|
27
|
+
const refreshSecret = refreshToken.generateSecret();
|
|
27
28
|
await this.jwtRefreshTokenRepository.create(refreshToken);
|
|
28
29
|
const access = await this.jwtSigner.signAccessToken(refreshToken);
|
|
29
30
|
const refresh = {
|
|
30
|
-
token:
|
|
31
|
+
token: refreshSecret,
|
|
31
32
|
expiration: new Date(refreshToken.expirationTime),
|
|
32
33
|
};
|
|
33
34
|
return {
|
|
@@ -35,11 +36,8 @@ let Jwt = class Jwt {
|
|
|
35
36
|
refresh,
|
|
36
37
|
};
|
|
37
38
|
}
|
|
38
|
-
async
|
|
39
|
-
|
|
40
|
-
const refreshToken = await this.jwtRefreshTokenRepository.findOrThrow(id);
|
|
41
|
-
refreshToken.validatePassword(pass);
|
|
42
|
-
return this.jwtSigner.signAccessToken(refreshToken);
|
|
39
|
+
async findRefreshTokenAuthInfo(secret) {
|
|
40
|
+
return await this.jwtRefreshTokenRepository.findAndValidate(secret);
|
|
43
41
|
}
|
|
44
42
|
};
|
|
45
43
|
Jwt = __decorate([
|
package/dist/src/addon/auth/jwt/{JwtConnectionGuardMiddleware.js → JwtHandshakeGuardMiddleware.js}
RENAMED
|
@@ -5,7 +5,7 @@ import { Auth } from '../../../core/auth/Auth.js';
|
|
|
5
5
|
import { CustomError } from '../../../core/error/CustomError.js';
|
|
6
6
|
import { JwtConfig } from './JwtConfig.js';
|
|
7
7
|
|
|
8
|
-
let
|
|
8
|
+
let JwtHandshakeGuardMiddleware = class JwtHandshakeGuardMiddleware {
|
|
9
9
|
config;
|
|
10
10
|
auth;
|
|
11
11
|
constructor(config, auth) {
|
|
@@ -48,10 +48,10 @@ let JwtConnectionGuardMiddleware = class JwtConnectionGuardMiddleware {
|
|
|
48
48
|
}
|
|
49
49
|
}
|
|
50
50
|
};
|
|
51
|
-
|
|
51
|
+
JwtHandshakeGuardMiddleware = __decorate([
|
|
52
52
|
injectable(),
|
|
53
53
|
__metadata("design:paramtypes", [JwtConfig,
|
|
54
54
|
Auth])
|
|
55
|
-
],
|
|
55
|
+
], JwtHandshakeGuardMiddleware);
|
|
56
56
|
|
|
57
|
-
export {
|
|
57
|
+
export { JwtHandshakeGuardMiddleware };
|
|
@@ -1,55 +1,60 @@
|
|
|
1
1
|
import { Entity } from '../../../core/entity/Entity.js';
|
|
2
2
|
import { CustomError } from '../../../core/error/CustomError.js';
|
|
3
|
-
import
|
|
3
|
+
import crypto from 'node:crypto';
|
|
4
4
|
|
|
5
5
|
class JwtRefreshToken extends Entity {
|
|
6
|
+
static PREFIX = 'rt_';
|
|
7
|
+
static hashSecret(secret) {
|
|
8
|
+
return crypto.createHash('sha256').update(secret).digest('hex');
|
|
9
|
+
}
|
|
6
10
|
get authInfo() {
|
|
7
11
|
return this.data.authInfo;
|
|
8
12
|
}
|
|
13
|
+
get metadata() {
|
|
14
|
+
return this.data.metadata ?? {};
|
|
15
|
+
}
|
|
9
16
|
get expirationTime() {
|
|
10
17
|
return new Date(this.data.expirationTime);
|
|
11
18
|
}
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
19
|
+
isExpired() {
|
|
20
|
+
return Date.now() > this.data.expirationTime;
|
|
21
|
+
}
|
|
22
|
+
revoke() {
|
|
23
|
+
this.data.revokedAt = Date.now();
|
|
24
|
+
}
|
|
25
|
+
isRevoked() {
|
|
26
|
+
return this.data.revokedAt != null;
|
|
27
|
+
}
|
|
28
|
+
generateSecret() {
|
|
29
|
+
if (this.data.secretHash) {
|
|
30
|
+
throw new Error('This Token key already has a secret');
|
|
15
31
|
}
|
|
16
|
-
const
|
|
17
|
-
this.data.
|
|
18
|
-
return
|
|
32
|
+
const secret = `${JwtRefreshToken.PREFIX}${crypto.randomBytes(32).toString('hex')}`;
|
|
33
|
+
this.data.secretHash = JwtRefreshToken.hashSecret(secret);
|
|
34
|
+
return secret;
|
|
19
35
|
}
|
|
20
|
-
|
|
21
|
-
if (
|
|
36
|
+
isValidSecret(secret) {
|
|
37
|
+
if (!secret.startsWith(JwtRefreshToken.PREFIX))
|
|
22
38
|
return false;
|
|
23
|
-
|
|
24
|
-
if (!this.data.passwordHash)
|
|
39
|
+
if (!this.data.secretHash)
|
|
25
40
|
return false;
|
|
26
|
-
|
|
41
|
+
const hashed = JwtRefreshToken.hashSecret(secret);
|
|
42
|
+
const stored = this.data.secretHash;
|
|
43
|
+
const hashedBuf = Buffer.from(hashed, 'hex');
|
|
44
|
+
const storedBuf = Buffer.from(stored, 'hex');
|
|
45
|
+
return hashedBuf.length === storedBuf.length && crypto.timingSafeEqual(hashedBuf, storedBuf);
|
|
27
46
|
}
|
|
28
|
-
|
|
29
|
-
if (
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
try {
|
|
35
|
-
const json = Buffer.from(secret, 'base64').toString('utf-8');
|
|
36
|
-
const data = JSON.parse(json);
|
|
37
|
-
if (!data.id || !data.pass) {
|
|
38
|
-
throw new Error('invalid secret structure');
|
|
39
|
-
}
|
|
40
|
-
return data;
|
|
41
|
-
}
|
|
42
|
-
catch (err) {
|
|
43
|
-
throw new Error('fail to inflate secret: ' + err.message);
|
|
44
|
-
}
|
|
47
|
+
isValidToken(secret) {
|
|
48
|
+
if (this.isExpired())
|
|
49
|
+
return false;
|
|
50
|
+
if (this.isRevoked())
|
|
51
|
+
return false;
|
|
52
|
+
return this.isValidSecret(secret);
|
|
45
53
|
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
throw new Error('id and pass required');
|
|
54
|
+
validateToken(secret) {
|
|
55
|
+
if (!this.isValidToken(secret)) {
|
|
56
|
+
throw new CustomError({ message: 'Invalid Token', httpCode: 401 });
|
|
50
57
|
}
|
|
51
|
-
const json = JSON.stringify({ id, pass });
|
|
52
|
-
return Buffer.from(json, 'utf-8').toString('base64');
|
|
53
58
|
}
|
|
54
59
|
}
|
|
55
60
|
|
|
@@ -23,7 +23,7 @@ let JwtSigner = class JwtSigner {
|
|
|
23
23
|
const token = jwt.sign(_authInfo, this.config.secretOrPrivateKey, {
|
|
24
24
|
expiresIn: this.config.accessExpirationSeconds,
|
|
25
25
|
});
|
|
26
|
-
const expiration = new Date().getTime() + this.config.accessExpirationSeconds * 1000;
|
|
26
|
+
const expiration = new Date(new Date().getTime() + this.config.accessExpirationSeconds * 1000);
|
|
27
27
|
return this.mapper.map({ token, expiration }, JwtTokenDto);
|
|
28
28
|
}
|
|
29
29
|
};
|
|
@@ -3,6 +3,7 @@ import { singleton } from '../../../core/injection/index.js';
|
|
|
3
3
|
import { Pool } from 'pg';
|
|
4
4
|
import { PgCrudRepository } from '../../../feature/pg/PgCrudRepository.js';
|
|
5
5
|
import { JwtRefreshToken } from './JwtRefreshToken.js';
|
|
6
|
+
import { CustomError } from '../../../core/error/CustomError.js';
|
|
6
7
|
|
|
7
8
|
let PgJwtRefreshTokenRepository = class PgJwtRefreshTokenRepository extends PgCrudRepository {
|
|
8
9
|
constructor(pool) {
|
|
@@ -12,6 +13,32 @@ let PgJwtRefreshTokenRepository = class PgJwtRefreshTokenRepository extends PgCr
|
|
|
12
13
|
constructor: JwtRefreshToken,
|
|
13
14
|
});
|
|
14
15
|
}
|
|
16
|
+
async findAndValidate(secret) {
|
|
17
|
+
const apiKey = await this.findBySecret(secret);
|
|
18
|
+
if (!apiKey) {
|
|
19
|
+
throw new CustomError({ message: 'Invalid Token', httpCode: 401 });
|
|
20
|
+
}
|
|
21
|
+
return apiKey.authInfo;
|
|
22
|
+
}
|
|
23
|
+
async findBySecret(secret) {
|
|
24
|
+
const secretHash = JwtRefreshToken.hashSecret(secret);
|
|
25
|
+
const query = `
|
|
26
|
+
SELECT ${this.columns}
|
|
27
|
+
FROM ${this.table}
|
|
28
|
+
WHERE data @> $1::jsonb
|
|
29
|
+
LIMIT 1
|
|
30
|
+
`;
|
|
31
|
+
const items = await this.query(query, [JSON.stringify({ secretHash })]);
|
|
32
|
+
return items[0] ?? null;
|
|
33
|
+
}
|
|
34
|
+
async findByMetadata(metadata) {
|
|
35
|
+
const query = `
|
|
36
|
+
SELECT ${this.columns}
|
|
37
|
+
FROM ${this.table}
|
|
38
|
+
WHERE data @> $1::jsonb
|
|
39
|
+
`;
|
|
40
|
+
return await this.query(query, [JSON.stringify({ metadata })]);
|
|
41
|
+
}
|
|
15
42
|
};
|
|
16
43
|
PgJwtRefreshTokenRepository = __decorate([
|
|
17
44
|
singleton(),
|
|
@@ -18,7 +18,7 @@ function socket(config) {
|
|
|
18
18
|
channelConstructor: SocketChannel,
|
|
19
19
|
functionName: propertyKey.toString(),
|
|
20
20
|
controllerConstructor: target.constructor,
|
|
21
|
-
channelConfig: new SocketChannelConfig(config.
|
|
21
|
+
channelConfig: new SocketChannelConfig(config.namespace, config.handshakeMidlewares),
|
|
22
22
|
});
|
|
23
23
|
};
|
|
24
24
|
}
|