@lenne.tech/nest-server 9.2.1 → 9.2.2
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/README.md +1 -2
- package/dist/config.env.js +3 -0
- package/dist/config.env.js.map +1 -1
- package/dist/core/common/helpers/context.helper.d.ts +5 -2
- package/dist/core/common/helpers/context.helper.js +14 -8
- package/dist/core/common/helpers/context.helper.js.map +1 -1
- package/dist/core/common/interfaces/server-options.interface.d.ts +3 -1
- package/dist/core/common/pipes/check-input.pipe.js +2 -2
- package/dist/core/common/pipes/check-input.pipe.js.map +1 -1
- package/dist/core/modules/auth/core-auth.module.js +5 -1
- package/dist/core/modules/auth/core-auth.module.js.map +1 -1
- package/dist/core/modules/auth/core-auth.resolver.d.ts +6 -6
- package/dist/core/modules/auth/core-auth.resolver.js +25 -22
- package/dist/core/modules/auth/core-auth.resolver.js.map +1 -1
- package/dist/core/modules/auth/guards/auth.guard.js +9 -2
- package/dist/core/modules/auth/guards/auth.guard.js.map +1 -1
- package/dist/core/modules/auth/inputs/core-auth-sign-in.input.d.ts +1 -0
- package/dist/core/modules/auth/inputs/core-auth-sign-in.input.js +6 -1
- package/dist/core/modules/auth/inputs/core-auth-sign-in.input.js.map +1 -1
- package/dist/core/modules/auth/inputs/core-auth-sign-up.input.d.ts +2 -5
- package/dist/core/modules/auth/inputs/core-auth-sign-up.input.js +2 -23
- package/dist/core/modules/auth/inputs/core-auth-sign-up.input.js.map +1 -1
- package/dist/core/modules/auth/interfaces/core-auth-user.interface.d.ts +2 -2
- package/dist/core/modules/auth/interfaces/core-token-data.interface.d.ts +5 -0
- package/dist/core/modules/auth/interfaces/core-token-data.interface.js +3 -0
- package/dist/core/modules/auth/interfaces/core-token-data.interface.js.map +1 -0
- package/dist/core/modules/auth/interfaces/jwt-payload.interface.d.ts +3 -0
- package/dist/core/modules/auth/services/core-auth.service.d.ts +11 -9
- package/dist/core/modules/auth/services/core-auth.service.js +62 -50
- package/dist/core/modules/auth/services/core-auth.service.js.map +1 -1
- package/dist/core/modules/auth/strategies/jwt-refresh.strategy.js +1 -10
- package/dist/core/modules/auth/strategies/jwt-refresh.strategy.js.map +1 -1
- package/dist/core/modules/auth/strategies/jwt.strategy.js +1 -1
- package/dist/core/modules/auth/strategies/jwt.strategy.js.map +1 -1
- package/dist/core/modules/auth/tokens.decorator.d.ts +1 -0
- package/dist/core/modules/auth/tokens.decorator.js +20 -0
- package/dist/core/modules/auth/tokens.decorator.js.map +1 -0
- package/dist/core/modules/user/core-user.model.d.ts +2 -2
- package/dist/core/modules/user/core-user.model.js +0 -6
- package/dist/core/modules/user/core-user.model.js.map +1 -1
- package/dist/core.module.js +1 -1
- package/dist/core.module.js.map +1 -1
- package/dist/index.d.ts +4 -2
- package/dist/index.js +4 -2
- package/dist/index.js.map +1 -1
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/src/config.env.ts +3 -0
- package/src/core/common/helpers/context.helper.ts +25 -10
- package/src/core/common/interfaces/server-options.interface.ts +11 -1
- package/src/core/common/pipes/check-input.pipe.ts +2 -2
- package/src/core/modules/auth/core-auth.module.ts +5 -1
- package/src/core/modules/auth/core-auth.resolver.ts +22 -19
- package/src/core/modules/auth/guards/auth.guard.ts +9 -2
- package/src/core/modules/auth/inputs/core-auth-sign-in.input.ts +4 -1
- package/src/core/modules/auth/inputs/core-auth-sign-up.input.ts +3 -16
- package/src/core/modules/auth/interfaces/core-auth-user.interface.ts +3 -6
- package/src/core/modules/auth/interfaces/core-token-data.interface.ts +19 -0
- package/src/core/modules/auth/interfaces/jwt-payload.interface.ts +3 -0
- package/src/core/modules/auth/services/core-auth.service.ts +93 -81
- package/src/core/modules/auth/strategies/jwt-refresh.strategy.ts +1 -11
- package/src/core/modules/auth/strategies/jwt.strategy.ts +1 -1
- package/src/core/modules/auth/tokens.decorator.ts +36 -0
- package/src/core/modules/user/core-user.model.ts +5 -11
- package/src/core.module.ts +1 -2
- package/src/index.ts +4 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lenne.tech/nest-server",
|
|
3
|
-
"version": "9.2.
|
|
3
|
+
"version": "9.2.2",
|
|
4
4
|
"description": "Modern, fast, powerful Node.js web framework in TypeScript based on Nest with a GraphQL API and a connection to MongoDB (or other databases).",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"node",
|
package/src/config.env.ts
CHANGED
|
@@ -62,6 +62,7 @@ const config: { [env: string]: IServerOptions } = {
|
|
|
62
62
|
expiresIn: '15m',
|
|
63
63
|
},
|
|
64
64
|
refresh: {
|
|
65
|
+
renewal: true,
|
|
65
66
|
secret: 'SECRET_OR_PRIVATE_KEY_LOCAL_REFRESH',
|
|
66
67
|
signInOptions: {
|
|
67
68
|
expiresIn: '7d',
|
|
@@ -134,6 +135,7 @@ const config: { [env: string]: IServerOptions } = {
|
|
|
134
135
|
expiresIn: '15m',
|
|
135
136
|
},
|
|
136
137
|
refresh: {
|
|
138
|
+
renewal: true,
|
|
137
139
|
secret: 'SECRET_OR_PRIVATE_KEY_DEV_REFRESH',
|
|
138
140
|
signInOptions: {
|
|
139
141
|
expiresIn: '7d',
|
|
@@ -206,6 +208,7 @@ const config: { [env: string]: IServerOptions } = {
|
|
|
206
208
|
expiresIn: '15m',
|
|
207
209
|
},
|
|
208
210
|
refresh: {
|
|
211
|
+
renewal: true,
|
|
209
212
|
secret: 'SECRET_OR_PRIVATE_KEY_PROD_REFRESH',
|
|
210
213
|
signInOptions: {
|
|
211
214
|
expiresIn: '7d',
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { ExecutionContext } from '@nestjs/common';
|
|
2
2
|
import { GqlExecutionContext } from '@nestjs/graphql';
|
|
3
|
+
import { Request as RequestType } from 'express';
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Helper for context processing
|
|
@@ -10,7 +11,11 @@ export default class Context {
|
|
|
10
11
|
* Get data from Context
|
|
11
12
|
* @deprecated use getContextData function
|
|
12
13
|
*/
|
|
13
|
-
public static getData(context: ExecutionContext): {
|
|
14
|
+
public static getData(context: ExecutionContext): {
|
|
15
|
+
args: any;
|
|
16
|
+
currentUser: { [key: string]: any };
|
|
17
|
+
request: RequestType;
|
|
18
|
+
} {
|
|
14
19
|
return getContextData(context);
|
|
15
20
|
}
|
|
16
21
|
}
|
|
@@ -18,26 +23,36 @@ export default class Context {
|
|
|
18
23
|
/**
|
|
19
24
|
* Get data from Context
|
|
20
25
|
*/
|
|
21
|
-
export function getContextData(context: ExecutionContext): {
|
|
26
|
+
export function getContextData(context: ExecutionContext): {
|
|
27
|
+
args: any;
|
|
28
|
+
currentUser: { [key: string]: any };
|
|
29
|
+
request: RequestType;
|
|
30
|
+
} {
|
|
22
31
|
// Check context
|
|
23
32
|
if (!context) {
|
|
24
|
-
return { currentUser: null, args: null };
|
|
33
|
+
return { currentUser: null, args: null, request: null };
|
|
25
34
|
}
|
|
26
35
|
|
|
27
36
|
// Init data
|
|
28
37
|
let user: { [key: string]: any };
|
|
38
|
+
let rawContext: any = null;
|
|
29
39
|
let ctx: any = null;
|
|
40
|
+
let request: any;
|
|
30
41
|
try {
|
|
31
|
-
|
|
42
|
+
rawContext = GqlExecutionContext.create(context);
|
|
43
|
+
ctx = rawContext?.getContext();
|
|
44
|
+
request = ctx.req;
|
|
32
45
|
} catch (e) {
|
|
33
46
|
// console.info(e);
|
|
34
47
|
}
|
|
35
48
|
|
|
36
49
|
let args: any;
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
50
|
+
if (rawContext) {
|
|
51
|
+
try {
|
|
52
|
+
args = rawContext.getArgs();
|
|
53
|
+
} catch (e) {
|
|
54
|
+
// console.info(e);
|
|
55
|
+
}
|
|
41
56
|
}
|
|
42
57
|
|
|
43
58
|
// Get data
|
|
@@ -45,7 +60,7 @@ export function getContextData(context: ExecutionContext): { currentUser: { [key
|
|
|
45
60
|
// User from GraphQL context
|
|
46
61
|
user = ctx?.user || ctx?.req?.user;
|
|
47
62
|
} else {
|
|
48
|
-
|
|
63
|
+
request = context?.switchToHttp ? context.switchToHttp()?.getRequest() : null;
|
|
49
64
|
if (request) {
|
|
50
65
|
args = request.body;
|
|
51
66
|
|
|
@@ -55,5 +70,5 @@ export function getContextData(context: ExecutionContext): { currentUser: { [key
|
|
|
55
70
|
}
|
|
56
71
|
|
|
57
72
|
// Return data
|
|
58
|
-
return { currentUser: user,
|
|
73
|
+
return { args, currentUser: user, request };
|
|
59
74
|
}
|
|
@@ -182,7 +182,17 @@ export interface IServerOptions {
|
|
|
182
182
|
* Configuration of JavaScript Web Token (JWT) module
|
|
183
183
|
*/
|
|
184
184
|
jwt?: {
|
|
185
|
-
|
|
185
|
+
/**
|
|
186
|
+
* Configuration for refresh Token (JWT)
|
|
187
|
+
*/
|
|
188
|
+
refresh?: {
|
|
189
|
+
/**
|
|
190
|
+
* Whether renewal of the refresh token is permitted
|
|
191
|
+
* If falsy (default): during refresh only a new token, the refresh token retains its original term
|
|
192
|
+
* If true: during refresh not only a new token but also a new refresh token is created
|
|
193
|
+
*/
|
|
194
|
+
renewal?: boolean;
|
|
195
|
+
} & IJwt;
|
|
186
196
|
} & IJwt &
|
|
187
197
|
JwtModuleOptions;
|
|
188
198
|
|
|
@@ -25,9 +25,9 @@ export class CheckInputPipe implements PipeTransform {
|
|
|
25
25
|
const metatype = metadata?.metatype;
|
|
26
26
|
|
|
27
27
|
// Get user
|
|
28
|
-
const {
|
|
28
|
+
const { currentUser }: any = getContextData(this.context);
|
|
29
29
|
|
|
30
30
|
// Check and return
|
|
31
|
-
return check(value,
|
|
31
|
+
return check(value, currentUser, { metatype });
|
|
32
32
|
}
|
|
33
33
|
}
|
|
@@ -30,7 +30,11 @@ export class CoreAuthModule {
|
|
|
30
30
|
}
|
|
31
31
|
): DynamicModule {
|
|
32
32
|
// Process imports
|
|
33
|
-
let imports: any[] = [
|
|
33
|
+
let imports: any[] = [
|
|
34
|
+
UserModule,
|
|
35
|
+
PassportModule.register({ defaultStrategy: ['jwt', 'jwt-refresh'] }),
|
|
36
|
+
JwtModule.register(options),
|
|
37
|
+
];
|
|
34
38
|
if (Array.isArray(options?.imports)) {
|
|
35
39
|
imports = imports.concat(options.imports);
|
|
36
40
|
}
|
|
@@ -10,6 +10,7 @@ import { CoreAuthSignInInput } from './inputs/core-auth-sign-in.input';
|
|
|
10
10
|
import { CoreAuthSignUpInput } from './inputs/core-auth-sign-up.input';
|
|
11
11
|
import { ICoreAuthUser } from './interfaces/core-auth-user.interface';
|
|
12
12
|
import { CoreAuthService } from './services/core-auth.service';
|
|
13
|
+
import { Tokens } from './tokens.decorator';
|
|
13
14
|
|
|
14
15
|
/**
|
|
15
16
|
* Authentication resolver for the sign in
|
|
@@ -25,31 +26,18 @@ export class CoreAuthResolver {
|
|
|
25
26
|
// Mutations
|
|
26
27
|
// ===========================================================================
|
|
27
28
|
|
|
28
|
-
/**
|
|
29
|
-
* Sign in user via email and password (on specific device)
|
|
30
|
-
*/
|
|
31
|
-
@Mutation((returns) => CoreAuthModel, {
|
|
32
|
-
description: 'Sign in user via email and password and get JWT tokens (for specific device)',
|
|
33
|
-
})
|
|
34
|
-
async signIn(
|
|
35
|
-
@Info() info: GraphQLResolveInfo,
|
|
36
|
-
@Context() ctx: { res: ResponseType },
|
|
37
|
-
@Args('input') input: CoreAuthSignInInput
|
|
38
|
-
): Promise<CoreAuthModel> {
|
|
39
|
-
const result = await this.authService.signIn(input, { fieldSelection: { info, select: 'signIn' } });
|
|
40
|
-
return this.processCookies(ctx, result);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
29
|
/**
|
|
44
30
|
* Logout user (from specific device)
|
|
45
31
|
*/
|
|
32
|
+
@UseGuards(AuthGuard('jwt'))
|
|
46
33
|
@Mutation((returns) => CoreAuthModel, { description: 'Logout user (from specific device)' })
|
|
47
34
|
async logout(
|
|
48
35
|
@GraphQLUser() currentUser: ICoreAuthUser,
|
|
49
36
|
@Context() ctx: { res: ResponseType },
|
|
50
|
-
@
|
|
37
|
+
@Tokens('token') token: string,
|
|
38
|
+
@Args('allDevices', { nullable: true }) allDevices?: boolean
|
|
51
39
|
): Promise<boolean> {
|
|
52
|
-
const result = await this.authService.logout({ currentUser,
|
|
40
|
+
const result = await this.authService.logout(token, { currentUser, allDevices });
|
|
53
41
|
return this.processCookies(ctx, result);
|
|
54
42
|
}
|
|
55
43
|
|
|
@@ -60,10 +48,25 @@ export class CoreAuthResolver {
|
|
|
60
48
|
@Mutation((returns) => CoreAuthModel, { description: 'Refresh tokens (for specific device)' })
|
|
61
49
|
async refreshToken(
|
|
62
50
|
@GraphQLUser() user: ICoreAuthUser,
|
|
51
|
+
@Tokens('refreshToken') refreshToken: string,
|
|
52
|
+
@Context() ctx: { res: ResponseType }
|
|
53
|
+
): Promise<CoreAuthModel> {
|
|
54
|
+
const result = await this.authService.refreshTokens(user, refreshToken);
|
|
55
|
+
return this.processCookies(ctx, result);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Sign in user via email and password (on specific device)
|
|
60
|
+
*/
|
|
61
|
+
@Mutation((returns) => CoreAuthModel, {
|
|
62
|
+
description: 'Sign in user via email and password and get JWT tokens (for specific device)',
|
|
63
|
+
})
|
|
64
|
+
async signIn(
|
|
65
|
+
@Info() info: GraphQLResolveInfo,
|
|
63
66
|
@Context() ctx: { res: ResponseType },
|
|
64
|
-
@Args('
|
|
67
|
+
@Args('input') input: CoreAuthSignInInput
|
|
65
68
|
): Promise<CoreAuthModel> {
|
|
66
|
-
const result = await this.authService.
|
|
69
|
+
const result = await this.authService.signIn(input, { fieldSelection: { info, select: 'signIn' } });
|
|
67
70
|
return this.processCookies(ctx, result);
|
|
68
71
|
}
|
|
69
72
|
|
|
@@ -3,6 +3,7 @@ import { GqlExecutionContext } from '@nestjs/graphql';
|
|
|
3
3
|
import { AuthModuleOptions, Type } from '@nestjs/passport';
|
|
4
4
|
import { defaultOptions } from '@nestjs/passport/dist/options';
|
|
5
5
|
import { memoize } from '@nestjs/passport/dist/utils/memoize.util';
|
|
6
|
+
import * as jwt from 'jsonwebtoken';
|
|
6
7
|
import * as passport from 'passport';
|
|
7
8
|
|
|
8
9
|
/**
|
|
@@ -103,8 +104,14 @@ function createAuthGuard(type?: string): Type<CanActivate> {
|
|
|
103
104
|
* Process request
|
|
104
105
|
*/
|
|
105
106
|
handleRequest(err, user, info, context): TUser {
|
|
106
|
-
if (err
|
|
107
|
-
|
|
107
|
+
if (err) {
|
|
108
|
+
if (err instanceof jwt.JsonWebTokenError) {
|
|
109
|
+
throw new UnauthorizedException('Invalid token');
|
|
110
|
+
}
|
|
111
|
+
throw err;
|
|
112
|
+
}
|
|
113
|
+
if (!user) {
|
|
114
|
+
throw new UnauthorizedException('Invalid token');
|
|
108
115
|
}
|
|
109
116
|
return user;
|
|
110
117
|
}
|
|
@@ -10,9 +10,12 @@ export class CoreAuthSignInInput extends CoreInput {
|
|
|
10
10
|
// Properties
|
|
11
11
|
// ===================================================================================================================
|
|
12
12
|
|
|
13
|
-
@Field({ description: 'Device ID', nullable: true })
|
|
13
|
+
@Field({ description: 'Device ID (is created automatically if it is not set)', nullable: true })
|
|
14
14
|
deviceId?: string = undefined;
|
|
15
15
|
|
|
16
|
+
@Field({ description: 'Device description', nullable: true })
|
|
17
|
+
deviceDescription?: string = undefined;
|
|
18
|
+
|
|
16
19
|
@Field({ description: 'Email', nullable: false })
|
|
17
20
|
email: string = undefined;
|
|
18
21
|
|
|
@@ -1,21 +1,8 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { InputType } from '@nestjs/graphql';
|
|
2
|
+
import { CoreAuthSignInInput } from './core-auth-sign-in.input';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* SignUp input
|
|
6
6
|
*/
|
|
7
7
|
@InputType({ description: 'Sign-up input' })
|
|
8
|
-
export class CoreAuthSignUpInput extends
|
|
9
|
-
// ===================================================================================================================
|
|
10
|
-
// Properties
|
|
11
|
-
// ===================================================================================================================
|
|
12
|
-
|
|
13
|
-
@Field({ description: 'Device ID', nullable: true })
|
|
14
|
-
deviceId?: string = undefined;
|
|
15
|
-
|
|
16
|
-
@Field({ description: 'Email', nullable: false })
|
|
17
|
-
email: string = undefined;
|
|
18
|
-
|
|
19
|
-
@Field({ description: 'Password', nullable: false })
|
|
20
|
-
password: string = undefined;
|
|
21
|
-
}
|
|
8
|
+
export class CoreAuthSignUpInput extends CoreAuthSignInInput {}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { CoreTokenData } from './core-token-data.interface';
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* Interface for user used in authorization module
|
|
3
5
|
*/
|
|
@@ -17,13 +19,8 @@ export interface ICoreAuthUser {
|
|
|
17
19
|
*/
|
|
18
20
|
password: string;
|
|
19
21
|
|
|
20
|
-
/**
|
|
21
|
-
* Refresh token
|
|
22
|
-
*/
|
|
23
|
-
refreshToken?: string;
|
|
24
|
-
|
|
25
22
|
/**
|
|
26
23
|
* Refresh tokens for different devices
|
|
27
24
|
*/
|
|
28
|
-
refreshTokens?: Record<string,
|
|
25
|
+
refreshTokens?: Record<string, CoreTokenData>;
|
|
29
26
|
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Data of the token
|
|
3
|
+
*/
|
|
4
|
+
export interface CoreTokenData {
|
|
5
|
+
/**
|
|
6
|
+
* ID of the device from which the token was generated
|
|
7
|
+
*/
|
|
8
|
+
deviceId?: string;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Description of the device from which the token was generated
|
|
12
|
+
*/
|
|
13
|
+
deviceDescription?: string;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Token ID to make sure that there is only one RefreshToken for each device
|
|
17
|
+
*/
|
|
18
|
+
tokenId: string;
|
|
19
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { Injectable, UnauthorizedException } from '@nestjs/common';
|
|
1
|
+
import { BadRequestException, Injectable, UnauthorizedException } from '@nestjs/common';
|
|
2
2
|
import { JwtService } from '@nestjs/jwt';
|
|
3
3
|
import * as bcrypt from 'bcrypt';
|
|
4
|
+
import { randomUUID } from 'crypto';
|
|
4
5
|
import { sha256 } from 'js-sha256';
|
|
5
6
|
import { getStringIds } from '../../../common/helpers/db.helper';
|
|
6
7
|
import { prepareServiceOptions } from '../../../common/helpers/service.helper';
|
|
@@ -37,40 +38,43 @@ export class CoreAuthService {
|
|
|
37
38
|
/**
|
|
38
39
|
* Logout user (from device)
|
|
39
40
|
*/
|
|
40
|
-
async logout(
|
|
41
|
+
async logout(
|
|
42
|
+
tokenOrRefreshToken: string,
|
|
43
|
+
serviceOptions: ServiceOptions & { allDevices?: boolean }
|
|
44
|
+
): Promise<boolean> {
|
|
45
|
+
// Check authentication
|
|
41
46
|
const user = serviceOptions.currentUser;
|
|
42
|
-
if (!
|
|
47
|
+
if (!user || !tokenOrRefreshToken) {
|
|
43
48
|
throw new UnauthorizedException();
|
|
44
49
|
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
50
|
+
|
|
51
|
+
// Check authorization
|
|
52
|
+
const deviceId = this.decodeJwt(tokenOrRefreshToken)?.deviceId;
|
|
53
|
+
if (!deviceId || !user.refreshTokens[deviceId]) {
|
|
54
|
+
throw new UnauthorizedException('Invalid refresh token');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Logout from all devices
|
|
58
|
+
if (serviceOptions.allDevices) {
|
|
59
|
+
user.refreshTokens = {};
|
|
60
|
+
await this.userService.update(user.id, { refreshTokens: {} }, serviceOptions);
|
|
52
61
|
return true;
|
|
53
62
|
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
{
|
|
59
|
-
refreshToken: user.refreshToken,
|
|
60
|
-
refreshTokens: user.refreshTokens,
|
|
61
|
-
},
|
|
62
|
-
serviceOptions
|
|
63
|
-
);
|
|
63
|
+
|
|
64
|
+
// Logout from specific devices
|
|
65
|
+
delete user.refreshTokens[deviceId];
|
|
66
|
+
await this.userService.update(user.id, { refreshTokens: user.refreshTokens }, serviceOptions);
|
|
64
67
|
return true;
|
|
65
68
|
}
|
|
66
69
|
|
|
67
70
|
/**
|
|
68
71
|
* Refresh tokens
|
|
69
72
|
*/
|
|
70
|
-
async refreshTokens(user: ICoreAuthUser,
|
|
73
|
+
async refreshTokens(user: ICoreAuthUser, currentRefreshToken: string) {
|
|
71
74
|
// Create new tokens
|
|
72
|
-
const
|
|
73
|
-
await this.
|
|
75
|
+
const { deviceId, deviceDescription } = this.decodeJwt(currentRefreshToken);
|
|
76
|
+
const tokens = await this.createTokens(user.id, { deviceId, deviceDescription });
|
|
77
|
+
tokens.refreshToken = await this.updateRefreshToken(user, currentRefreshToken, tokens.refreshToken);
|
|
74
78
|
|
|
75
79
|
// Return
|
|
76
80
|
return CoreAuthModel.map({
|
|
@@ -93,10 +97,10 @@ export class CoreAuthService {
|
|
|
93
97
|
});
|
|
94
98
|
|
|
95
99
|
// Inputs
|
|
96
|
-
const { email, password, deviceId } = input;
|
|
100
|
+
const { email, password, deviceId, deviceDescription } = input;
|
|
97
101
|
|
|
98
102
|
// Get user
|
|
99
|
-
const user = await this.userService.getViaEmail(email,
|
|
103
|
+
const user = await this.userService.getViaEmail(email, serviceOptionsForUserService);
|
|
100
104
|
if (
|
|
101
105
|
!user ||
|
|
102
106
|
!((await bcrypt.compare(password, user.password)) || (await bcrypt.compare(sha256(password), user.password)))
|
|
@@ -104,11 +108,8 @@ export class CoreAuthService {
|
|
|
104
108
|
throw new UnauthorizedException();
|
|
105
109
|
}
|
|
106
110
|
|
|
107
|
-
// Set device ID
|
|
108
|
-
serviceOptionsForUserService.deviceId = input.deviceId;
|
|
109
|
-
|
|
110
111
|
// Return tokens and user
|
|
111
|
-
return this.getResult(user,
|
|
112
|
+
return this.getResult(user, { deviceId, deviceDescription });
|
|
112
113
|
}
|
|
113
114
|
|
|
114
115
|
/**
|
|
@@ -124,14 +125,14 @@ export class CoreAuthService {
|
|
|
124
125
|
// Get and check user
|
|
125
126
|
const user = await this.userService.create(input, serviceOptionsForUserService);
|
|
126
127
|
if (!user) {
|
|
127
|
-
throw
|
|
128
|
+
throw new BadRequestException('Email Address already in use');
|
|
128
129
|
}
|
|
129
130
|
|
|
130
131
|
// Set device ID
|
|
131
|
-
|
|
132
|
+
const { deviceId, deviceDescription } = input;
|
|
132
133
|
|
|
133
134
|
// Return tokens and user
|
|
134
|
-
return this.getResult(user,
|
|
135
|
+
return this.getResult(user, { deviceId, deviceDescription });
|
|
135
136
|
}
|
|
136
137
|
|
|
137
138
|
/**
|
|
@@ -142,7 +143,8 @@ export class CoreAuthService {
|
|
|
142
143
|
const user = await this.userService.get(payload.id);
|
|
143
144
|
|
|
144
145
|
// Check if user exists and is logged in
|
|
145
|
-
|
|
146
|
+
const device = user?.refreshTokens?.[payload.deviceId];
|
|
147
|
+
if (!device || !payload.tokenId || device.tokenId !== payload.tokenId) {
|
|
146
148
|
return null;
|
|
147
149
|
}
|
|
148
150
|
|
|
@@ -157,12 +159,16 @@ export class CoreAuthService {
|
|
|
157
159
|
/**
|
|
158
160
|
* Rest result with user and tokens
|
|
159
161
|
*/
|
|
160
|
-
protected async getResult(
|
|
162
|
+
protected async getResult(
|
|
163
|
+
user: ICoreAuthUser,
|
|
164
|
+
data?: { [key: string]: any; deviceId?: string },
|
|
165
|
+
currentRefreshToken?: string
|
|
166
|
+
) {
|
|
161
167
|
// Create new tokens
|
|
162
|
-
const tokens = await this.
|
|
168
|
+
const tokens = await this.createTokens(user.id, data);
|
|
163
169
|
|
|
164
170
|
// Set refresh token
|
|
165
|
-
await this.updateRefreshToken(user, tokens.refreshToken,
|
|
171
|
+
tokens.refreshToken = await this.updateRefreshToken(user, currentRefreshToken, tokens.refreshToken, data);
|
|
166
172
|
|
|
167
173
|
// Return tokens and user
|
|
168
174
|
return CoreAuthModel.map({
|
|
@@ -190,22 +196,22 @@ export class CoreAuthService {
|
|
|
190
196
|
/**
|
|
191
197
|
* Get JWT and refresh token
|
|
192
198
|
*/
|
|
193
|
-
protected async
|
|
199
|
+
protected async createTokens(userId: string, data?: { [key: string]: any; deviceId?: string }) {
|
|
200
|
+
const payload: { [key: string]: any; id: string; deviceId: string } = {
|
|
201
|
+
...data,
|
|
202
|
+
id: userId,
|
|
203
|
+
deviceId: data?.deviceId || randomUUID(),
|
|
204
|
+
tokenId: randomUUID(),
|
|
205
|
+
};
|
|
194
206
|
const [token, refreshToken] = await Promise.all([
|
|
195
|
-
this.jwtService.signAsync(
|
|
196
|
-
|
|
197
|
-
{
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
{ id: userId },
|
|
204
|
-
{
|
|
205
|
-
secret: this.getSecretFromConfig(true),
|
|
206
|
-
...this.configService.getFastButReadOnly('jwt.refresh.signInOptions', {}),
|
|
207
|
-
}
|
|
208
|
-
),
|
|
207
|
+
this.jwtService.signAsync(payload, {
|
|
208
|
+
secret: this.getSecretFromConfig(false),
|
|
209
|
+
...this.configService.getFastButReadOnly('jwt.signInOptions', {}),
|
|
210
|
+
}),
|
|
211
|
+
this.jwtService.signAsync(payload, {
|
|
212
|
+
secret: this.getSecretFromConfig(true),
|
|
213
|
+
...this.configService.getFastButReadOnly('jwt.refresh.signInOptions', {}),
|
|
214
|
+
}),
|
|
209
215
|
]);
|
|
210
216
|
return {
|
|
211
217
|
token,
|
|
@@ -218,39 +224,45 @@ export class CoreAuthService {
|
|
|
218
224
|
*/
|
|
219
225
|
protected async updateRefreshToken(
|
|
220
226
|
user: ICoreAuthUser,
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
227
|
+
currentRefreshToken: string,
|
|
228
|
+
newRefreshToken: string,
|
|
229
|
+
data?: Record<string, any>
|
|
230
|
+
): Promise<string> {
|
|
231
|
+
// Check if the update of the update token is allowed
|
|
232
|
+
let deviceId: string;
|
|
233
|
+
if (currentRefreshToken) {
|
|
234
|
+
deviceId = this.decodeJwt(currentRefreshToken)?.deviceId;
|
|
235
|
+
if (!deviceId || !user.refreshTokens?.[deviceId]) {
|
|
236
|
+
throw new UnauthorizedException('Invalid refresh token');
|
|
229
237
|
}
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
if (!user.refreshToken) {
|
|
234
|
-
user.refreshToken = hashedRefreshToken;
|
|
238
|
+
if (!this.configService.getFastButReadOnly('jwt.refresh.renewal')) {
|
|
239
|
+
// Return currentToken
|
|
240
|
+
return currentRefreshToken;
|
|
235
241
|
}
|
|
242
|
+
}
|
|
236
243
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
...serviceOptions,
|
|
242
|
-
force: true,
|
|
243
|
-
}
|
|
244
|
-
);
|
|
244
|
+
// Prepare data
|
|
245
|
+
data = data || {};
|
|
246
|
+
if (!user.refreshTokens) {
|
|
247
|
+
user.refreshTokens = {};
|
|
245
248
|
}
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
249
|
+
if (deviceId) {
|
|
250
|
+
const oldData = user.refreshTokens[deviceId] || {};
|
|
251
|
+
data = Object.assign(oldData, data);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Set new token
|
|
255
|
+
const payload = this.decodeJwt(newRefreshToken);
|
|
256
|
+
if (!payload) {
|
|
257
|
+
throw new UnauthorizedException();
|
|
258
|
+
}
|
|
259
|
+
if (!deviceId) {
|
|
260
|
+
deviceId = payload.deviceId;
|
|
261
|
+
}
|
|
262
|
+
user.refreshTokens[deviceId] = { ...data, deviceId, tokenId: payload.tokenId };
|
|
263
|
+
await this.userService.update(getStringIds(user), { refreshTokens: user.refreshTokens }, { force: true });
|
|
264
|
+
|
|
265
|
+
// Return new token
|
|
266
|
+
return newRefreshToken;
|
|
255
267
|
}
|
|
256
268
|
}
|
|
@@ -37,17 +37,7 @@ export class JwtRefreshStrategy extends PassportStrategy(Strategy, 'jwt-refresh'
|
|
|
37
37
|
// Check user
|
|
38
38
|
const user = await this.authService.validateUser(payload);
|
|
39
39
|
if (!user) {
|
|
40
|
-
throw new UnauthorizedException();
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
// Check refresh token
|
|
44
|
-
const refreshToken = req
|
|
45
|
-
.get('Authorization')
|
|
46
|
-
.replace(/bearer/i, '')
|
|
47
|
-
.trim();
|
|
48
|
-
const refreshTokenMatches = await bcrypt.compare(refreshToken, user.refreshToken);
|
|
49
|
-
if (!refreshTokenMatches) {
|
|
50
|
-
throw new ForbiddenException('Access Denied');
|
|
40
|
+
throw new UnauthorizedException('Unknown user');
|
|
51
41
|
}
|
|
52
42
|
|
|
53
43
|
// Return user
|
|
@@ -41,7 +41,7 @@ export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') {
|
|
|
41
41
|
async validate(payload: JwtPayload) {
|
|
42
42
|
const user = await this.authService.validateUser(payload);
|
|
43
43
|
if (!user) {
|
|
44
|
-
throw new UnauthorizedException();
|
|
44
|
+
throw new UnauthorizedException('Unknown user');
|
|
45
45
|
}
|
|
46
46
|
return user;
|
|
47
47
|
}
|