@lenne.tech/nest-server 11.6.2 → 11.7.1
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/config.env.js +19 -12
- package/dist/config.env.js.map +1 -1
- package/dist/core/common/helpers/filter.helper.d.ts +9 -9
- package/dist/core/common/helpers/filter.helper.js +2 -4
- package/dist/core/common/helpers/filter.helper.js.map +1 -1
- package/dist/core/common/helpers/gridfs.helper.js +3 -3
- package/dist/core/common/helpers/gridfs.helper.js.map +1 -1
- package/dist/core/common/interfaces/server-options.interface.d.ts +21 -3
- package/dist/core/common/services/crud.service.d.ts +16 -16
- package/dist/core/common/services/crud.service.js +1 -1
- package/dist/core/common/services/crud.service.js.map +1 -1
- package/dist/core/modules/auth/core-auth.controller.d.ts +1 -0
- package/dist/core/modules/auth/core-auth.controller.js +28 -2
- package/dist/core/modules/auth/core-auth.controller.js.map +1 -1
- package/dist/core/modules/auth/core-auth.module.js +14 -1
- package/dist/core/modules/auth/core-auth.module.js.map +1 -1
- package/dist/core/modules/auth/core-auth.resolver.d.ts +1 -0
- package/dist/core/modules/auth/core-auth.resolver.js +20 -2
- package/dist/core/modules/auth/core-auth.resolver.js.map +1 -1
- package/dist/core/modules/auth/exceptions/legacy-auth-disabled.exception.d.ts +4 -0
- package/dist/core/modules/auth/exceptions/legacy-auth-disabled.exception.js +17 -0
- package/dist/core/modules/auth/exceptions/legacy-auth-disabled.exception.js.map +1 -0
- package/dist/core/modules/auth/guards/legacy-auth-rate-limit.guard.d.ts +9 -0
- package/dist/core/modules/auth/guards/legacy-auth-rate-limit.guard.js +74 -0
- package/dist/core/modules/auth/guards/legacy-auth-rate-limit.guard.js.map +1 -0
- package/dist/core/modules/auth/interfaces/auth-provider.interface.d.ts +7 -0
- package/dist/core/modules/auth/interfaces/auth-provider.interface.js +5 -0
- package/dist/core/modules/auth/interfaces/auth-provider.interface.js.map +1 -0
- package/dist/core/modules/auth/interfaces/core-auth-user.interface.d.ts +1 -0
- package/dist/core/modules/auth/services/core-auth.service.d.ts +10 -1
- package/dist/core/modules/auth/services/core-auth.service.js +141 -9
- package/dist/core/modules/auth/services/core-auth.service.js.map +1 -1
- package/dist/core/modules/auth/services/legacy-auth-rate-limiter.service.d.ts +31 -0
- package/dist/core/modules/auth/services/legacy-auth-rate-limiter.service.js +153 -0
- package/dist/core/modules/auth/services/legacy-auth-rate-limiter.service.js.map +1 -0
- package/dist/core/modules/better-auth/better-auth-migration-status.model.d.ts +10 -0
- package/dist/core/modules/better-auth/better-auth-migration-status.model.js +57 -0
- package/dist/core/modules/better-auth/better-auth-migration-status.model.js.map +1 -0
- package/dist/core/modules/better-auth/better-auth-models.d.ts +0 -1
- package/dist/core/modules/better-auth/better-auth-models.js +0 -4
- package/dist/core/modules/better-auth/better-auth-models.js.map +1 -1
- package/dist/core/modules/better-auth/better-auth-user.mapper.d.ts +33 -0
- package/dist/core/modules/better-auth/better-auth-user.mapper.js +443 -0
- package/dist/core/modules/better-auth/better-auth-user.mapper.js.map +1 -1
- package/dist/core/modules/better-auth/better-auth.config.js +3 -0
- package/dist/core/modules/better-auth/better-auth.config.js.map +1 -1
- package/dist/core/modules/better-auth/better-auth.module.d.ts +10 -2
- package/dist/core/modules/better-auth/better-auth.module.js +40 -52
- package/dist/core/modules/better-auth/better-auth.module.js.map +1 -1
- package/dist/core/modules/better-auth/better-auth.resolver.d.ts +8 -12
- package/dist/core/modules/better-auth/better-auth.resolver.js +33 -351
- package/dist/core/modules/better-auth/better-auth.resolver.js.map +1 -1
- package/dist/core/modules/better-auth/better-auth.service.d.ts +0 -1
- package/dist/core/modules/better-auth/better-auth.service.js +0 -3
- package/dist/core/modules/better-auth/better-auth.service.js.map +1 -1
- package/dist/core/modules/better-auth/better-auth.types.d.ts +9 -8
- package/dist/core/modules/better-auth/better-auth.types.js +14 -3
- package/dist/core/modules/better-auth/better-auth.types.js.map +1 -1
- package/dist/core/modules/better-auth/core-better-auth.controller.d.ts +67 -0
- package/dist/core/modules/better-auth/core-better-auth.controller.js +504 -0
- package/dist/core/modules/better-auth/core-better-auth.controller.js.map +1 -0
- package/dist/core/modules/better-auth/core-better-auth.resolver.d.ts +61 -0
- package/dist/core/modules/better-auth/core-better-auth.resolver.js +552 -0
- package/dist/core/modules/better-auth/core-better-auth.resolver.js.map +1 -0
- package/dist/core/modules/better-auth/index.d.ts +3 -0
- package/dist/core/modules/better-auth/index.js +3 -0
- package/dist/core/modules/better-auth/index.js.map +1 -1
- package/dist/core/modules/user/core-user.service.d.ts +7 -1
- package/dist/core/modules/user/core-user.service.js +57 -3
- package/dist/core/modules/user/core-user.service.js.map +1 -1
- package/dist/core/modules/user/interfaces/core-user-service-options.interface.d.ts +4 -0
- package/dist/core/modules/user/interfaces/core-user-service-options.interface.js +3 -0
- package/dist/core/modules/user/interfaces/core-user-service-options.interface.js.map +1 -0
- package/dist/core.module.d.ts +3 -0
- package/dist/core.module.js +132 -54
- package/dist/core.module.js.map +1 -1
- package/dist/index.d.ts +5 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -1
- package/dist/server/modules/auth/auth.resolver.js +2 -0
- package/dist/server/modules/auth/auth.resolver.js.map +1 -1
- package/dist/server/modules/better-auth/better-auth.controller.d.ts +10 -0
- package/dist/server/modules/better-auth/better-auth.controller.js +36 -0
- package/dist/server/modules/better-auth/better-auth.controller.js.map +1 -0
- package/dist/server/modules/better-auth/better-auth.module.d.ts +9 -0
- package/dist/server/modules/better-auth/better-auth.module.js +44 -0
- package/dist/server/modules/better-auth/better-auth.module.js.map +1 -0
- package/dist/server/modules/better-auth/better-auth.resolver.d.ts +47 -0
- package/dist/server/modules/better-auth/better-auth.resolver.js +234 -0
- package/dist/server/modules/better-auth/better-auth.resolver.js.map +1 -0
- package/dist/server/modules/file/file-info.model.d.ts +71 -3
- package/dist/server/modules/user/user.model.d.ts +169 -3
- package/dist/server/modules/user/user.service.d.ts +3 -1
- package/dist/server/modules/user/user.service.js +7 -3
- package/dist/server/modules/user/user.service.js.map +1 -1
- package/dist/server/server.module.js +6 -1
- package/dist/server/server.module.js.map +1 -1
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +20 -29
- package/src/config.env.ts +34 -13
- package/src/core/common/helpers/filter.helper.ts +15 -17
- package/src/core/common/helpers/gridfs.helper.ts +5 -5
- package/src/core/common/interfaces/server-options.interface.ts +222 -14
- package/src/core/common/services/crud.service.ts +22 -22
- package/src/core/modules/auth/core-auth.controller.ts +93 -5
- package/src/core/modules/auth/core-auth.module.ts +15 -1
- package/src/core/modules/auth/core-auth.resolver.ts +70 -2
- package/src/core/modules/auth/exceptions/legacy-auth-disabled.exception.ts +35 -0
- package/src/core/modules/auth/guards/legacy-auth-rate-limit.guard.ts +109 -0
- package/src/core/modules/auth/interfaces/auth-provider.interface.ts +86 -0
- package/src/core/modules/auth/interfaces/core-auth-user.interface.ts +6 -0
- package/src/core/modules/auth/services/core-auth.service.ts +245 -6
- package/src/core/modules/auth/services/legacy-auth-rate-limiter.service.ts +283 -0
- package/src/core/modules/better-auth/INTEGRATION-CHECKLIST.md +254 -0
- package/src/core/modules/better-auth/README.md +698 -54
- package/src/core/modules/better-auth/better-auth-migration-status.model.ts +73 -0
- package/src/core/modules/better-auth/better-auth-models.ts +0 -3
- package/src/core/modules/better-auth/better-auth-user.mapper.ts +805 -0
- package/src/core/modules/better-auth/better-auth.config.ts +5 -0
- package/src/core/modules/better-auth/better-auth.module.ts +107 -66
- package/src/core/modules/better-auth/better-auth.resolver.ts +88 -553
- package/src/core/modules/better-auth/better-auth.service.ts +0 -9
- package/src/core/modules/better-auth/better-auth.types.ts +25 -10
- package/src/core/modules/better-auth/core-better-auth.controller.ts +646 -0
- package/src/core/modules/better-auth/core-better-auth.resolver.ts +730 -0
- package/src/core/modules/better-auth/index.ts +9 -1
- package/src/core/modules/user/core-user.service.ts +131 -4
- package/src/core/modules/user/interfaces/core-user-service-options.interface.ts +15 -0
- package/src/core.module.ts +257 -74
- package/src/index.ts +5 -0
- package/src/server/modules/auth/auth.resolver.ts +8 -0
- package/src/server/modules/better-auth/better-auth.controller.ts +41 -0
- package/src/server/modules/better-auth/better-auth.module.ts +88 -0
- package/src/server/modules/better-auth/better-auth.resolver.ts +210 -0
- package/src/server/modules/user/user.service.ts +4 -2
- package/src/server/server.module.ts +10 -1
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { CanActivate, ExecutionContext, HttpException, HttpStatus, Injectable } from '@nestjs/common';
|
|
2
|
+
import { GqlExecutionContext } from '@nestjs/graphql';
|
|
3
|
+
|
|
4
|
+
import { LegacyAuthRateLimiter } from '../services/legacy-auth-rate-limiter.service';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Guard for rate limiting Legacy Auth endpoints
|
|
8
|
+
*
|
|
9
|
+
* This guard applies rate limiting to protect against brute-force attacks.
|
|
10
|
+
* It works with both REST and GraphQL endpoints.
|
|
11
|
+
*
|
|
12
|
+
* Rate limiting must be enabled via configuration:
|
|
13
|
+
* ```typescript
|
|
14
|
+
* auth: {
|
|
15
|
+
* rateLimit: {
|
|
16
|
+
* enabled: true,
|
|
17
|
+
* max: 10,
|
|
18
|
+
* windowSeconds: 60,
|
|
19
|
+
* }
|
|
20
|
+
* }
|
|
21
|
+
* ```
|
|
22
|
+
*
|
|
23
|
+
* @since 11.7.x
|
|
24
|
+
*/
|
|
25
|
+
@Injectable()
|
|
26
|
+
export class LegacyAuthRateLimitGuard implements CanActivate {
|
|
27
|
+
constructor(private readonly rateLimiter: LegacyAuthRateLimiter) {}
|
|
28
|
+
|
|
29
|
+
canActivate(context: ExecutionContext): boolean {
|
|
30
|
+
// If rate limiting is disabled, always allow
|
|
31
|
+
if (!this.rateLimiter.isEnabled()) {
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const { endpoint, ip } = this.extractRequestInfo(context);
|
|
36
|
+
const result = this.rateLimiter.check(ip, endpoint);
|
|
37
|
+
|
|
38
|
+
if (!result.allowed) {
|
|
39
|
+
throw new HttpException(
|
|
40
|
+
{
|
|
41
|
+
error: 'Too Many Requests',
|
|
42
|
+
message: this.rateLimiter.getMessage(),
|
|
43
|
+
remaining: result.remaining,
|
|
44
|
+
retryAfter: result.resetIn,
|
|
45
|
+
statusCode: HttpStatus.TOO_MANY_REQUESTS,
|
|
46
|
+
},
|
|
47
|
+
HttpStatus.TOO_MANY_REQUESTS,
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Extract IP and endpoint from the execution context
|
|
56
|
+
*/
|
|
57
|
+
private extractRequestInfo(context: ExecutionContext): { endpoint: string; ip: string } {
|
|
58
|
+
const contextType = context.getType<'graphql' | 'http'>();
|
|
59
|
+
|
|
60
|
+
if (contextType === 'graphql') {
|
|
61
|
+
const gqlContext = GqlExecutionContext.create(context);
|
|
62
|
+
const info = gqlContext.getInfo();
|
|
63
|
+
const ctx = gqlContext.getContext();
|
|
64
|
+
|
|
65
|
+
// Get IP from request
|
|
66
|
+
const req = ctx.req;
|
|
67
|
+
const ip = this.getClientIp(req);
|
|
68
|
+
|
|
69
|
+
// Get endpoint from GraphQL field name
|
|
70
|
+
const endpoint = info?.fieldName || 'unknown';
|
|
71
|
+
|
|
72
|
+
return { endpoint, ip };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// HTTP context
|
|
76
|
+
const request = context.switchToHttp().getRequest();
|
|
77
|
+
const ip = this.getClientIp(request);
|
|
78
|
+
|
|
79
|
+
// Get endpoint from URL path
|
|
80
|
+
const url = request.url || request.path || '';
|
|
81
|
+
const endpoint = url.split('/').pop() || 'unknown';
|
|
82
|
+
|
|
83
|
+
return { endpoint, ip };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Get client IP from request, handling proxies
|
|
88
|
+
*/
|
|
89
|
+
private getClientIp(request: any): string {
|
|
90
|
+
if (!request) {
|
|
91
|
+
return 'unknown';
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Check common proxy headers
|
|
95
|
+
const forwardedFor = request.headers?.['x-forwarded-for'];
|
|
96
|
+
if (forwardedFor) {
|
|
97
|
+
// Take the first IP in the chain (original client)
|
|
98
|
+
return forwardedFor.split(',')[0].trim();
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const realIp = request.headers?.['x-real-ip'];
|
|
102
|
+
if (realIp) {
|
|
103
|
+
return realIp;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Fall back to direct connection IP
|
|
107
|
+
return request.ip || request.connection?.remoteAddress || 'unknown';
|
|
108
|
+
}
|
|
109
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { JwtPayload } from './jwt-payload.interface';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Auth Provider Interface
|
|
5
|
+
*
|
|
6
|
+
* This interface defines the contract for authentication providers.
|
|
7
|
+
* Both Legacy Auth (CoreAuthService) and BetterAuth can implement this interface,
|
|
8
|
+
* allowing CoreModule to work with either system transparently.
|
|
9
|
+
*
|
|
10
|
+
* @since 11.8.0
|
|
11
|
+
* @see https://github.com/lenneTech/nest-server/blob/develop/src/core/modules/auth/interfaces/auth-provider.interface.ts
|
|
12
|
+
*
|
|
13
|
+
* ## Roadmap
|
|
14
|
+
*
|
|
15
|
+
* ### v11.x (Current)
|
|
16
|
+
* - Interface introduced for future flexibility
|
|
17
|
+
* - Legacy Auth (CoreAuthService) is the default implementation
|
|
18
|
+
* - BetterAuth can be used alongside Legacy Auth
|
|
19
|
+
*
|
|
20
|
+
* ### Future Version (Planned)
|
|
21
|
+
* - CoreModule.forRoot will use IAuthProvider instead of concrete AuthService
|
|
22
|
+
* - Legacy Auth becomes optional (must be explicitly enabled)
|
|
23
|
+
* - BetterAuth becomes the recommended default
|
|
24
|
+
*
|
|
25
|
+
* ## Implementation Example
|
|
26
|
+
*
|
|
27
|
+
* ```typescript
|
|
28
|
+
* @Injectable()
|
|
29
|
+
* export class BetterAuthProvider implements IAuthProvider {
|
|
30
|
+
* constructor(private readonly betterAuthService: BetterAuthService) {}
|
|
31
|
+
*
|
|
32
|
+
* decodeJwt(token: string): JwtPayload {
|
|
33
|
+
* return this.betterAuthService.decodeJwt(token);
|
|
34
|
+
* }
|
|
35
|
+
*
|
|
36
|
+
* async validateUser(payload: JwtPayload): Promise<any> {
|
|
37
|
+
* return this.betterAuthService.validateUser(payload);
|
|
38
|
+
* }
|
|
39
|
+
*
|
|
40
|
+
* signToken(user: any, expiresIn?: string): string {
|
|
41
|
+
* return this.betterAuthService.signToken(user, expiresIn);
|
|
42
|
+
* }
|
|
43
|
+
* }
|
|
44
|
+
* ```
|
|
45
|
+
*/
|
|
46
|
+
export interface IAuthProvider {
|
|
47
|
+
/**
|
|
48
|
+
* Decode a JWT token without verification
|
|
49
|
+
* Used for extracting payload information
|
|
50
|
+
*
|
|
51
|
+
* @param token - The JWT token to decode
|
|
52
|
+
* @returns The decoded JWT payload
|
|
53
|
+
*/
|
|
54
|
+
decodeJwt(token: string): JwtPayload;
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Sign a new JWT token for a user
|
|
58
|
+
*
|
|
59
|
+
* @param user - The user to create a token for
|
|
60
|
+
* @param expiresIn - Optional expiration time (e.g., '15m', '7d')
|
|
61
|
+
* @returns The signed JWT token
|
|
62
|
+
*/
|
|
63
|
+
signToken(user: any, expiresIn?: string): string;
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Validate a user based on JWT payload
|
|
67
|
+
* Called during authentication to verify the user exists and is valid
|
|
68
|
+
*
|
|
69
|
+
* @param payload - The JWT payload containing user information
|
|
70
|
+
* @returns The validated user object, or null if invalid
|
|
71
|
+
*/
|
|
72
|
+
validateUser(payload: JwtPayload): Promise<any>;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Auth Provider Token for dependency injection
|
|
77
|
+
*
|
|
78
|
+
* Use this token to inject the auth provider in your services:
|
|
79
|
+
*
|
|
80
|
+
* ```typescript
|
|
81
|
+
* constructor(
|
|
82
|
+
* @Inject(AUTH_PROVIDER) private readonly authProvider: IAuthProvider,
|
|
83
|
+
* ) {}
|
|
84
|
+
* ```
|
|
85
|
+
*/
|
|
86
|
+
export const AUTH_PROVIDER = 'AUTH_PROVIDER';
|
|
@@ -1,4 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
BadRequestException,
|
|
3
|
+
Injectable,
|
|
4
|
+
Logger,
|
|
5
|
+
NotFoundException,
|
|
6
|
+
Optional,
|
|
7
|
+
UnauthorizedException,
|
|
8
|
+
} from '@nestjs/common';
|
|
2
9
|
import { JwtService } from '@nestjs/jwt';
|
|
3
10
|
import bcrypt = require('bcrypt');
|
|
4
11
|
import { randomUUID } from 'crypto';
|
|
@@ -8,6 +15,8 @@ import { getStringIds } from '../../../common/helpers/db.helper';
|
|
|
8
15
|
import { prepareServiceOptions } from '../../../common/helpers/service.helper';
|
|
9
16
|
import { ServiceOptions } from '../../../common/interfaces/service-options.interface';
|
|
10
17
|
import { ConfigService } from '../../../common/services/config.service';
|
|
18
|
+
import { BetterAuthUserMapper } from '../../better-auth/better-auth-user.mapper';
|
|
19
|
+
import { BetterAuthService } from '../../better-auth/better-auth.service';
|
|
11
20
|
import { CoreAuthModel } from '../core-auth.model';
|
|
12
21
|
import { CoreAuthSignInInput } from '../inputs/core-auth-sign-in.input';
|
|
13
22
|
import { CoreAuthSignUpInput } from '../inputs/core-auth-sign-up.input';
|
|
@@ -29,9 +38,23 @@ export interface GetResultOptions {
|
|
|
29
38
|
|
|
30
39
|
/**
|
|
31
40
|
* CoreAuthService to handle user authentication
|
|
41
|
+
*
|
|
42
|
+
* When Better-Auth (IAM) is enabled, this service delegates authentication to IAM
|
|
43
|
+
* while maintaining backwards compatibility by returning Legacy JWT format tokens.
|
|
44
|
+
*
|
|
45
|
+
* Migration strategy:
|
|
46
|
+
* - New users: Created directly in IAM with scrypt password hash
|
|
47
|
+
* - Existing Legacy users: Lazily migrated on first sign-in
|
|
48
|
+
* (Legacy bcrypt password verified, then IAM account created with scrypt hash)
|
|
49
|
+
*
|
|
50
|
+
* @deprecated The signIn and signUp methods are deprecated when IAM is enabled.
|
|
51
|
+
* Use the IAM REST endpoints (/iam/sign-in/email, /iam/sign-up/email) directly
|
|
52
|
+
* for new implementations. Legacy endpoints remain for backwards compatibility.
|
|
32
53
|
*/
|
|
33
54
|
@Injectable()
|
|
34
55
|
export class CoreAuthService {
|
|
56
|
+
private readonly logger = new Logger(CoreAuthService.name);
|
|
57
|
+
|
|
35
58
|
/**
|
|
36
59
|
* Integrate services
|
|
37
60
|
*/
|
|
@@ -39,6 +62,8 @@ export class CoreAuthService {
|
|
|
39
62
|
protected readonly userService: CoreAuthUserService,
|
|
40
63
|
protected readonly jwtService: JwtService,
|
|
41
64
|
protected readonly configService: ConfigService,
|
|
65
|
+
@Optional() protected readonly betterAuthService?: BetterAuthService,
|
|
66
|
+
@Optional() protected readonly betterAuthUserMapper?: BetterAuthUserMapper,
|
|
42
67
|
) {}
|
|
43
68
|
|
|
44
69
|
/**
|
|
@@ -99,6 +124,14 @@ export class CoreAuthService {
|
|
|
99
124
|
|
|
100
125
|
/**
|
|
101
126
|
* User sign in via email
|
|
127
|
+
*
|
|
128
|
+
* When IAM is enabled, this method:
|
|
129
|
+
* 1. For migrated users (have iamId): Verifies password via IAM
|
|
130
|
+
* 2. For non-migrated users: Verifies via Legacy, then migrates to IAM
|
|
131
|
+
*
|
|
132
|
+
* Always returns Legacy JWT format for backwards compatibility.
|
|
133
|
+
*
|
|
134
|
+
* @deprecated When IAM is enabled, prefer using /iam/sign-in/email REST endpoint directly.
|
|
102
135
|
*/
|
|
103
136
|
async signIn(input: CoreAuthSignInInput, serviceOptions?: ServiceOptions): Promise<CoreAuthModel> {
|
|
104
137
|
// Check input
|
|
@@ -106,6 +139,9 @@ export class CoreAuthService {
|
|
|
106
139
|
throw new BadRequestException('Missing input');
|
|
107
140
|
}
|
|
108
141
|
|
|
142
|
+
// Check if user enumeration prevention is enabled
|
|
143
|
+
const preventUserEnumeration = this.configService.getFastButReadOnly('auth.preventUserEnumeration', false);
|
|
144
|
+
|
|
109
145
|
// Prepare service options
|
|
110
146
|
const serviceOptionsForUserService = prepareServiceOptions(serviceOptions, {
|
|
111
147
|
// We need password, so we can't use prepare output handling and have to deactivate it
|
|
@@ -119,12 +155,47 @@ export class CoreAuthService {
|
|
|
119
155
|
const { deviceDescription, deviceId, email, password } = input;
|
|
120
156
|
|
|
121
157
|
// Get user
|
|
122
|
-
|
|
158
|
+
let user: ICoreAuthUser;
|
|
159
|
+
try {
|
|
160
|
+
user = await this.userService.getViaEmail(email, serviceOptionsForUserService);
|
|
161
|
+
} catch (error) {
|
|
162
|
+
if (error instanceof NotFoundException) {
|
|
163
|
+
throw new UnauthorizedException(preventUserEnumeration ? 'Invalid credentials' : 'Unknown email');
|
|
164
|
+
}
|
|
165
|
+
throw error;
|
|
166
|
+
}
|
|
123
167
|
if (!user) {
|
|
124
|
-
throw new UnauthorizedException('Unknown email');
|
|
168
|
+
throw new UnauthorizedException(preventUserEnumeration ? 'Invalid credentials' : 'Unknown email');
|
|
125
169
|
}
|
|
126
|
-
|
|
127
|
-
|
|
170
|
+
|
|
171
|
+
// Determine if IAM delegation is available
|
|
172
|
+
const iamEnabled = this.isIamEnabled();
|
|
173
|
+
|
|
174
|
+
if (iamEnabled && user.iamId) {
|
|
175
|
+
// User is already migrated to IAM - verify via IAM
|
|
176
|
+
const iamVerified = await this.verifyPasswordViaIam(email, password);
|
|
177
|
+
if (!iamVerified) {
|
|
178
|
+
throw new UnauthorizedException(preventUserEnumeration ? 'Invalid credentials' : 'Wrong password');
|
|
179
|
+
}
|
|
180
|
+
this.logger.debug(`User ${email} authenticated via IAM (already migrated)`);
|
|
181
|
+
} else {
|
|
182
|
+
// Verify via Legacy (bcrypt)
|
|
183
|
+
// Check if user has a password (social login only users don't have one)
|
|
184
|
+
if (!user.password) {
|
|
185
|
+
throw new UnauthorizedException(
|
|
186
|
+
preventUserEnumeration ? 'Invalid credentials' : 'No password set for this account',
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
if (
|
|
190
|
+
!((await bcrypt.compare(password, user.password)) || (await bcrypt.compare(sha256(password), user.password)))
|
|
191
|
+
) {
|
|
192
|
+
throw new UnauthorizedException(preventUserEnumeration ? 'Invalid credentials' : 'Wrong password');
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// If IAM is enabled but user not migrated, migrate them now
|
|
196
|
+
if (iamEnabled && !user.iamId) {
|
|
197
|
+
await this.migrateUserToIam(user, email, password);
|
|
198
|
+
}
|
|
128
199
|
}
|
|
129
200
|
|
|
130
201
|
// Return tokens and user with currentUser set so securityCheck knows user is requesting own data
|
|
@@ -136,6 +207,13 @@ export class CoreAuthService {
|
|
|
136
207
|
|
|
137
208
|
/**
|
|
138
209
|
* Register a new user account
|
|
210
|
+
*
|
|
211
|
+
* When IAM is enabled, this method:
|
|
212
|
+
* 1. Creates the user in IAM first (with scrypt password hash)
|
|
213
|
+
* 2. Creates/links the Legacy user with iamId
|
|
214
|
+
* 3. Returns Legacy JWT format for backwards compatibility
|
|
215
|
+
*
|
|
216
|
+
* @deprecated When IAM is enabled, prefer using /iam/sign-up/email REST endpoint directly.
|
|
139
217
|
*/
|
|
140
218
|
async signUp(input: CoreAuthSignUpInput, serviceOptions?: ServiceOptions): Promise<CoreAuthModel> {
|
|
141
219
|
// Prepare service options
|
|
@@ -146,7 +224,19 @@ export class CoreAuthService {
|
|
|
146
224
|
|
|
147
225
|
// Get and check user
|
|
148
226
|
try {
|
|
149
|
-
|
|
227
|
+
// Determine if IAM delegation is available
|
|
228
|
+
const iamEnabled = this.isIamEnabled();
|
|
229
|
+
|
|
230
|
+
let user: ICoreAuthUser;
|
|
231
|
+
|
|
232
|
+
if (iamEnabled) {
|
|
233
|
+
// Create via IAM first, then create/link Legacy user
|
|
234
|
+
user = await this.createUserViaIam(input, serviceOptionsForUserService);
|
|
235
|
+
} else {
|
|
236
|
+
// Create via Legacy
|
|
237
|
+
user = await this.userService.create(input, serviceOptionsForUserService);
|
|
238
|
+
}
|
|
239
|
+
|
|
150
240
|
if (!user) {
|
|
151
241
|
throw new BadRequestException('Email address already in use');
|
|
152
242
|
}
|
|
@@ -332,4 +422,153 @@ export class CoreAuthService {
|
|
|
332
422
|
// Return new token
|
|
333
423
|
return newRefreshToken;
|
|
334
424
|
}
|
|
425
|
+
|
|
426
|
+
// ===================================================================================================================
|
|
427
|
+
// IAM Delegation Helper Methods
|
|
428
|
+
// ===================================================================================================================
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* Checks if IAM (Better-Auth) delegation is available and enabled
|
|
432
|
+
*/
|
|
433
|
+
protected isIamEnabled(): boolean {
|
|
434
|
+
return !!(this.betterAuthService?.isEnabled() && this.betterAuthUserMapper);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* Verifies password via IAM for already-migrated users
|
|
439
|
+
*
|
|
440
|
+
* @param email - User email
|
|
441
|
+
* @param password - Plain password to verify
|
|
442
|
+
* @returns true if password is valid, false otherwise
|
|
443
|
+
*/
|
|
444
|
+
protected async verifyPasswordViaIam(email: string, password: string): Promise<boolean> {
|
|
445
|
+
if (!this.betterAuthService) {
|
|
446
|
+
return false;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
const api = this.betterAuthService.getApi();
|
|
450
|
+
if (!api) {
|
|
451
|
+
return false;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
try {
|
|
455
|
+
const response = await api.signInEmail({
|
|
456
|
+
body: { email, password },
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
// Check if response indicates successful authentication
|
|
460
|
+
return !!(response && 'user' in response && response.user);
|
|
461
|
+
} catch (error) {
|
|
462
|
+
this.logger.debug(
|
|
463
|
+
`IAM password verification failed for ${email}: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
464
|
+
);
|
|
465
|
+
return false;
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/**
|
|
470
|
+
* Migrates a Legacy user to IAM
|
|
471
|
+
*
|
|
472
|
+
* This creates the IAM user and account with a scrypt password hash,
|
|
473
|
+
* then links the Legacy user via iamId.
|
|
474
|
+
*
|
|
475
|
+
* @param user - The Legacy user to migrate
|
|
476
|
+
* @param email - User email
|
|
477
|
+
* @param plainPassword - Plain password (needed to create scrypt hash)
|
|
478
|
+
*/
|
|
479
|
+
protected async migrateUserToIam(user: ICoreAuthUser, email: string, plainPassword: string): Promise<void> {
|
|
480
|
+
if (!this.betterAuthUserMapper) {
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
try {
|
|
485
|
+
// Create IAM account with the plain password (creates scrypt hash)
|
|
486
|
+
const migrated = await this.betterAuthUserMapper.migrateAccountToIam(email, plainPassword);
|
|
487
|
+
|
|
488
|
+
if (migrated) {
|
|
489
|
+
this.logger.log(`Migrated Legacy user ${email} to IAM`);
|
|
490
|
+
|
|
491
|
+
// Refresh user to get updated iamId
|
|
492
|
+
const updatedUser = await this.userService.getViaEmail(email, { force: true });
|
|
493
|
+
if (updatedUser?.iamId) {
|
|
494
|
+
// Update the user object in place so subsequent operations see the iamId
|
|
495
|
+
(user as any).iamId = updatedUser.iamId;
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
} catch (error) {
|
|
499
|
+
// Log but don't throw - migration failure shouldn't block login
|
|
500
|
+
this.logger.warn(
|
|
501
|
+
`Failed to migrate user ${email} to IAM: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
502
|
+
);
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
/**
|
|
507
|
+
* Creates a user via IAM and links to Legacy user
|
|
508
|
+
*
|
|
509
|
+
* @param input - Sign-up input data
|
|
510
|
+
* @param serviceOptions - Service options for user service
|
|
511
|
+
* @returns The created Legacy user (linked to IAM)
|
|
512
|
+
*/
|
|
513
|
+
protected async createUserViaIam(input: CoreAuthSignUpInput, serviceOptions: ServiceOptions): Promise<ICoreAuthUser> {
|
|
514
|
+
if (!this.betterAuthService || !this.betterAuthUserMapper) {
|
|
515
|
+
throw new BadRequestException('IAM service not available');
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
const api = this.betterAuthService.getApi();
|
|
519
|
+
if (!api) {
|
|
520
|
+
throw new BadRequestException('IAM API not available');
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
try {
|
|
524
|
+
// Create user in IAM first
|
|
525
|
+
// Note: firstName and lastName are project-specific fields, may not exist on CoreAuthSignUpInput
|
|
526
|
+
const inputAny = input as any;
|
|
527
|
+
const name = [inputAny.firstName, inputAny.lastName].filter(Boolean).join(' ') || input.email.split('@')[0];
|
|
528
|
+
const response = await api.signUpEmail({
|
|
529
|
+
body: {
|
|
530
|
+
email: input.email,
|
|
531
|
+
name,
|
|
532
|
+
password: input.password,
|
|
533
|
+
},
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
if (!response || !('user' in response) || !response.user) {
|
|
537
|
+
throw new BadRequestException('Email address already in use');
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
// Link or create Legacy user with iamId
|
|
541
|
+
const iamUser = response.user as { email: string; id: string; name?: string };
|
|
542
|
+
const syncedUser = await this.betterAuthUserMapper.linkOrCreateUser(iamUser as any, {
|
|
543
|
+
firstName: inputAny.firstName,
|
|
544
|
+
lastName: inputAny.lastName,
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
if (!syncedUser) {
|
|
548
|
+
throw new BadRequestException('Failed to create user');
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
// Sync password to Legacy (enables backwards compatibility)
|
|
552
|
+
// Pass plain password so bcrypt hash can be created for Legacy Auth
|
|
553
|
+
await this.betterAuthUserMapper.syncPasswordToLegacy(iamUser.id, input.email, input.password);
|
|
554
|
+
|
|
555
|
+
this.logger.log(`Created user ${input.email} via IAM`);
|
|
556
|
+
|
|
557
|
+
// Get the full user from our database
|
|
558
|
+
const user = await this.userService.getViaEmail(input.email, serviceOptions);
|
|
559
|
+
if (!user) {
|
|
560
|
+
throw new BadRequestException('Failed to retrieve created user');
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
return user;
|
|
564
|
+
} catch (error) {
|
|
565
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
566
|
+
this.logger.debug(`IAM sign-up error for ${input.email}: ${errorMessage}`);
|
|
567
|
+
|
|
568
|
+
if (errorMessage.includes('already exists') || errorMessage.includes('already in use')) {
|
|
569
|
+
throw new BadRequestException('Email address already in use');
|
|
570
|
+
}
|
|
571
|
+
throw error;
|
|
572
|
+
}
|
|
573
|
+
}
|
|
335
574
|
}
|