@lenne.tech/nest-server 11.10.1 → 11.10.3
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 +16 -133
- package/dist/config.env.js.map +1 -1
- package/dist/core/common/interfaces/server-options.interface.d.ts +4 -0
- package/dist/core/modules/auth/guards/auth.guard.d.ts +2 -2
- package/dist/core/modules/auth/guards/auth.guard.js +68 -8
- package/dist/core/modules/auth/guards/auth.guard.js.map +1 -1
- package/dist/core/modules/auth/guards/roles.guard.d.ts +3 -4
- package/dist/core/modules/auth/guards/roles.guard.js +64 -159
- package/dist/core/modules/auth/guards/roles.guard.js.map +1 -1
- package/dist/core/modules/better-auth/better-auth-token.service.d.ts +21 -0
- package/dist/core/modules/better-auth/better-auth-token.service.js +153 -0
- package/dist/core/modules/better-auth/better-auth-token.service.js.map +1 -0
- package/dist/core/modules/better-auth/better-auth.config.d.ts +3 -0
- package/dist/core/modules/better-auth/better-auth.config.js +176 -47
- package/dist/core/modules/better-auth/better-auth.config.js.map +1 -1
- package/dist/core/modules/better-auth/better-auth.types.d.ts +13 -0
- package/dist/core/modules/better-auth/better-auth.types.js.map +1 -1
- package/dist/core/modules/better-auth/core-better-auth-api.middleware.d.ts +5 -1
- package/dist/core/modules/better-auth/core-better-auth-api.middleware.js +101 -8
- package/dist/core/modules/better-auth/core-better-auth-api.middleware.js.map +1 -1
- package/dist/core/modules/better-auth/core-better-auth-challenge.service.d.ts +20 -0
- package/dist/core/modules/better-auth/core-better-auth-challenge.service.js +142 -0
- package/dist/core/modules/better-auth/core-better-auth-challenge.service.js.map +1 -0
- package/dist/core/modules/better-auth/core-better-auth-user.mapper.js +1 -1
- package/dist/core/modules/better-auth/core-better-auth-user.mapper.js.map +1 -1
- package/dist/core/modules/better-auth/core-better-auth-web.helper.d.ts +2 -0
- package/dist/core/modules/better-auth/core-better-auth-web.helper.js +29 -1
- package/dist/core/modules/better-auth/core-better-auth-web.helper.js.map +1 -1
- package/dist/core/modules/better-auth/core-better-auth.controller.js +5 -13
- package/dist/core/modules/better-auth/core-better-auth.controller.js.map +1 -1
- package/dist/core/modules/better-auth/core-better-auth.middleware.d.ts +0 -1
- package/dist/core/modules/better-auth/core-better-auth.middleware.js +6 -19
- package/dist/core/modules/better-auth/core-better-auth.middleware.js.map +1 -1
- package/dist/core/modules/better-auth/core-better-auth.module.d.ts +6 -1
- package/dist/core/modules/better-auth/core-better-auth.module.js +82 -19
- package/dist/core/modules/better-auth/core-better-auth.module.js.map +1 -1
- package/dist/core/modules/better-auth/core-better-auth.resolver.js +7 -6
- package/dist/core/modules/better-auth/core-better-auth.resolver.js.map +1 -1
- package/dist/core/modules/better-auth/core-better-auth.service.d.ts +1 -2
- package/dist/core/modules/better-auth/core-better-auth.service.js +27 -37
- package/dist/core/modules/better-auth/core-better-auth.service.js.map +1 -1
- package/dist/core/modules/better-auth/index.d.ts +1 -0
- package/dist/core/modules/better-auth/index.js +1 -0
- package/dist/core/modules/better-auth/index.js.map +1 -1
- package/dist/core.module.js +4 -0
- package/dist/core.module.js.map +1 -1
- package/dist/server/modules/better-auth/better-auth.module.d.ts +4 -1
- package/dist/server/modules/better-auth/better-auth.module.js +4 -1
- package/dist/server/modules/better-auth/better-auth.module.js.map +1 -1
- package/dist/server/server.module.js +1 -4
- package/dist/server/server.module.js.map +1 -1
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/src/config.env.ts +24 -174
- package/src/core/common/interfaces/server-options.interface.ts +288 -35
- package/src/core/modules/auth/guards/auth.guard.ts +136 -23
- package/src/core/modules/auth/guards/roles.guard.ts +119 -239
- package/src/core/modules/better-auth/INTEGRATION-CHECKLIST.md +82 -56
- package/src/core/modules/better-auth/README.md +132 -35
- package/src/core/modules/better-auth/better-auth-token.service.ts +241 -0
- package/src/core/modules/better-auth/better-auth.config.ts +402 -70
- package/src/core/modules/better-auth/better-auth.types.ts +37 -0
- package/src/core/modules/better-auth/core-better-auth-api.middleware.ts +158 -18
- package/src/core/modules/better-auth/core-better-auth-challenge.service.ts +254 -0
- package/src/core/modules/better-auth/core-better-auth-user.mapper.ts +1 -1
- package/src/core/modules/better-auth/core-better-auth-web.helper.ts +64 -1
- package/src/core/modules/better-auth/core-better-auth.controller.ts +7 -15
- package/src/core/modules/better-auth/core-better-auth.middleware.ts +7 -20
- package/src/core/modules/better-auth/core-better-auth.module.ts +182 -25
- package/src/core/modules/better-auth/core-better-auth.resolver.ts +8 -7
- package/src/core/modules/better-auth/core-better-auth.service.ts +40 -48
- package/src/core/modules/better-auth/index.ts +1 -0
- package/src/core.module.ts +8 -0
- package/src/server/modules/better-auth/better-auth.module.ts +40 -10
- package/src/server/server.module.ts +2 -4
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { ExecutionContext, ForbiddenException, Injectable, Logger, Optional, UnauthorizedException } from '@nestjs/common';
|
|
2
2
|
import { ModuleRef, Reflector } from '@nestjs/core';
|
|
3
3
|
import { GqlExecutionContext } from '@nestjs/graphql';
|
|
4
|
-
import { getConnectionToken } from '@nestjs/mongoose';
|
|
5
|
-
import { Connection, Types } from 'mongoose';
|
|
6
4
|
import { firstValueFrom, isObservable } from 'rxjs';
|
|
7
5
|
|
|
8
6
|
import { RoleEnum } from '../../../common/enums/role.enum';
|
|
7
|
+
import { BetterAuthTokenService } from '../../better-auth/better-auth-token.service';
|
|
8
|
+
import { BetterAuthenticatedUser } from '../../better-auth/better-auth.types';
|
|
9
9
|
import { CoreBetterAuthService } from '../../better-auth/core-better-auth.service';
|
|
10
10
|
import { ErrorCode } from '../../error-code';
|
|
11
11
|
import { AuthGuardStrategy } from '../auth-guard-strategy.enum';
|
|
@@ -23,7 +23,7 @@ import { AuthGuard } from './auth.guard';
|
|
|
23
23
|
* MULTI-TOKEN SUPPORT:
|
|
24
24
|
* This guard supports multiple authentication token types:
|
|
25
25
|
* 1. Legacy JWT tokens (Passport JWT strategy)
|
|
26
|
-
* 2. BetterAuth JWT tokens (verified via
|
|
26
|
+
* 2. BetterAuth JWT tokens (verified via BetterAuthTokenService)
|
|
27
27
|
* 3. BetterAuth session tokens (verified via database lookup)
|
|
28
28
|
*
|
|
29
29
|
* When Passport JWT validation fails, the guard falls back to BetterAuth verification:
|
|
@@ -37,7 +37,7 @@ import { AuthGuard } from './auth.guard';
|
|
|
37
37
|
export class RolesGuard extends AuthGuard(AuthGuardStrategy.JWT) {
|
|
38
38
|
private readonly logger = new Logger(RolesGuard.name);
|
|
39
39
|
private betterAuthService: CoreBetterAuthService | null = null;
|
|
40
|
-
private
|
|
40
|
+
private tokenService: BetterAuthTokenService | null = null;
|
|
41
41
|
private servicesResolved = false;
|
|
42
42
|
|
|
43
43
|
/**
|
|
@@ -51,7 +51,7 @@ export class RolesGuard extends AuthGuard(AuthGuardStrategy.JWT) {
|
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
/**
|
|
54
|
-
* Lazily resolve BetterAuth
|
|
54
|
+
* Lazily resolve BetterAuth services
|
|
55
55
|
*/
|
|
56
56
|
private resolveServices(): void {
|
|
57
57
|
if (this.servicesResolved || !this.moduleRef) {
|
|
@@ -65,10 +65,9 @@ export class RolesGuard extends AuthGuard(AuthGuardStrategy.JWT) {
|
|
|
65
65
|
}
|
|
66
66
|
|
|
67
67
|
try {
|
|
68
|
-
|
|
69
|
-
this.mongoConnection = this.moduleRef.get(getConnectionToken(), { strict: false });
|
|
68
|
+
this.tokenService = this.moduleRef.get(BetterAuthTokenService, { strict: false });
|
|
70
69
|
} catch {
|
|
71
|
-
//
|
|
70
|
+
// BetterAuthTokenService not available
|
|
72
71
|
}
|
|
73
72
|
|
|
74
73
|
this.servicesResolved = true;
|
|
@@ -78,291 +77,172 @@ export class RolesGuard extends AuthGuard(AuthGuardStrategy.JWT) {
|
|
|
78
77
|
* Override canActivate to add BetterAuth JWT fallback
|
|
79
78
|
*
|
|
80
79
|
* Flow:
|
|
81
|
-
* 1.
|
|
82
|
-
* 2.
|
|
83
|
-
* 3. If BetterAuth
|
|
80
|
+
* 1. Check if roles are required - if not, skip authentication entirely
|
|
81
|
+
* 2. Check if user is already authenticated via BetterAuth middleware
|
|
82
|
+
* 3. If BetterAuth is enabled, try BetterAuth token verification first
|
|
83
|
+
* 4. Otherwise, try Passport JWT authentication (Legacy JWT)
|
|
84
|
+
* 5. If Passport fails and BetterAuth is enabled, try BetterAuth as fallback
|
|
85
|
+
*
|
|
86
|
+
* This order ensures IAM-only setups work without requiring JWT strategy registration.
|
|
84
87
|
*/
|
|
85
88
|
override async canActivate(context: ExecutionContext): Promise<boolean> {
|
|
86
|
-
//
|
|
87
|
-
this.
|
|
89
|
+
// Get roles FIRST to check if authentication is even needed
|
|
90
|
+
const reflectorRoles = this.reflector.getAll<string[][]>('roles', [context.getHandler(), context.getClass()]);
|
|
91
|
+
const roles: string[] = reflectorRoles[0]
|
|
92
|
+
? reflectorRoles[1]
|
|
93
|
+
? [...reflectorRoles[0], ...reflectorRoles[1]]
|
|
94
|
+
: reflectorRoles[0]
|
|
95
|
+
: reflectorRoles[1];
|
|
88
96
|
|
|
89
|
-
//
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
} catch (passportError) {
|
|
94
|
-
// Passport JWT validation failed - try BetterAuth token fallback (JWT or session)
|
|
95
|
-
if (!this.betterAuthService?.isEnabled()) {
|
|
96
|
-
// BetterAuth not available - rethrow original error
|
|
97
|
-
throw passportError;
|
|
98
|
-
}
|
|
97
|
+
// Check if locked - always deny
|
|
98
|
+
if (roles && roles.includes(RoleEnum.S_NO_ONE)) {
|
|
99
|
+
throw new UnauthorizedException(ErrorCode.UNAUTHORIZED);
|
|
100
|
+
}
|
|
99
101
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
}
|
|
102
|
+
// If no roles required, or S_EVERYONE is set, allow access without authentication
|
|
103
|
+
// This allows public endpoints (without @Roles decorator or with S_EVERYONE) to work
|
|
104
|
+
if (!roles || !roles.some((value) => !!value) || roles.includes(RoleEnum.S_EVERYONE)) {
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
106
107
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
if (request) {
|
|
110
|
-
request.user = user;
|
|
111
|
-
}
|
|
108
|
+
// Resolve services lazily (only needed if authentication is required)
|
|
109
|
+
this.resolveServices();
|
|
112
110
|
|
|
113
|
-
|
|
114
|
-
|
|
111
|
+
// Get request and check for existing user (from BetterAuth middleware)
|
|
112
|
+
const request = this.getRequest(context);
|
|
113
|
+
const existingUser = request?.user;
|
|
115
114
|
|
|
115
|
+
// If user is already authenticated via BetterAuth middleware, validate roles directly
|
|
116
|
+
if (existingUser && existingUser._authenticatedViaBetterAuth === true) {
|
|
117
|
+
this.handleRequest(null, existingUser, null, context);
|
|
116
118
|
return true;
|
|
117
119
|
}
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
/**
|
|
121
|
-
* Verify BetterAuth token (JWT or session) and load the corresponding user
|
|
122
|
-
*
|
|
123
|
-
* This method tries multiple verification strategies:
|
|
124
|
-
* 1. BetterAuth JWT verification (if JWT plugin is enabled)
|
|
125
|
-
* 2. BetterAuth session token lookup (database lookup)
|
|
126
|
-
*
|
|
127
|
-
* @param context - ExecutionContext to extract request from
|
|
128
|
-
* @returns User object if verification succeeds, null otherwise
|
|
129
|
-
*/
|
|
130
|
-
private async verifyBetterAuthTokenFromContext(context: ExecutionContext): Promise<any> {
|
|
131
|
-
if (!this.betterAuthService || !this.mongoConnection) {
|
|
132
|
-
return null;
|
|
133
|
-
}
|
|
134
120
|
|
|
135
|
-
try
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
if (ctx?.req?.headers) {
|
|
144
|
-
authHeader = ctx.req.headers.authorization || ctx.req.headers.Authorization;
|
|
145
|
-
}
|
|
146
|
-
} catch {
|
|
147
|
-
// GraphQL context not available
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
// Fallback to HTTP context
|
|
151
|
-
if (!authHeader) {
|
|
152
|
-
try {
|
|
153
|
-
const httpRequest = context.switchToHttp().getRequest();
|
|
154
|
-
if (httpRequest?.headers) {
|
|
155
|
-
authHeader = httpRequest.headers.authorization || httpRequest.headers.Authorization;
|
|
156
|
-
}
|
|
157
|
-
} catch {
|
|
158
|
-
// HTTP context not available
|
|
121
|
+
// If BetterAuth is enabled, try BetterAuth verification FIRST
|
|
122
|
+
// This allows IAM-only setups to work without JWT strategy
|
|
123
|
+
if (this.betterAuthService?.isEnabled()) {
|
|
124
|
+
const user = await this.verifyBetterAuthTokenFromContext(context);
|
|
125
|
+
if (user) {
|
|
126
|
+
// BetterAuth token is valid - set the user on the request
|
|
127
|
+
if (request) {
|
|
128
|
+
request.user = user;
|
|
159
129
|
}
|
|
130
|
+
// Validate roles
|
|
131
|
+
this.handleRequest(null, user, null, context);
|
|
132
|
+
return true;
|
|
160
133
|
}
|
|
134
|
+
}
|
|
161
135
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
if
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
//
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
const ctx = gqlContext.getContext();
|
|
179
|
-
if (ctx?.req?.cookies) {
|
|
180
|
-
cookies = ctx.req.cookies;
|
|
181
|
-
}
|
|
182
|
-
} catch {
|
|
183
|
-
// GraphQL context not available
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
// Fallback to HTTP context
|
|
187
|
-
if (!cookies) {
|
|
188
|
-
try {
|
|
189
|
-
const httpRequest = context.switchToHttp().getRequest();
|
|
190
|
-
if (httpRequest?.cookies) {
|
|
191
|
-
cookies = httpRequest.cookies;
|
|
192
|
-
}
|
|
193
|
-
} catch {
|
|
194
|
-
// HTTP context not available
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
// Extract session token from cookies (try multiple cookie names)
|
|
199
|
-
if (cookies) {
|
|
200
|
-
// Get the basePath for cookie name (e.g., 'iam' -> 'iam.session_token')
|
|
201
|
-
const basePath = this.betterAuthService.getBasePath?.()?.replace(/^\//, '').replace(/\//g, '.') || 'iam';
|
|
202
|
-
const basePathCookie = `${basePath}.session_token`;
|
|
203
|
-
|
|
204
|
-
token =
|
|
205
|
-
cookies[basePathCookie] ||
|
|
206
|
-
cookies['better-auth.session_token'] ||
|
|
207
|
-
cookies['token'] ||
|
|
208
|
-
undefined;
|
|
136
|
+
// Try Passport JWT authentication (Legacy JWT)
|
|
137
|
+
try {
|
|
138
|
+
const result = super.canActivate(context);
|
|
139
|
+
return isObservable(result) ? await firstValueFrom(result) : await result;
|
|
140
|
+
} catch (passportError) {
|
|
141
|
+
// Check if this is an "Unknown authentication strategy" error
|
|
142
|
+
// This happens in IAM-only setups where JWT strategy is not registered
|
|
143
|
+
const errorMessage = passportError instanceof Error ? passportError.message : String(passportError);
|
|
144
|
+
const isStrategyError = errorMessage.includes('Unknown authentication strategy');
|
|
145
|
+
|
|
146
|
+
// If BetterAuth is enabled but verification failed earlier, or if this is a strategy error
|
|
147
|
+
if (this.betterAuthService?.isEnabled()) {
|
|
148
|
+
// For strategy errors, BetterAuth verification already failed above
|
|
149
|
+
// Rethrow with a more descriptive error
|
|
150
|
+
if (isStrategyError) {
|
|
151
|
+
throw new InvalidTokenException();
|
|
209
152
|
}
|
|
210
|
-
}
|
|
211
153
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
if (this.betterAuthService.isJwtEnabled()) {
|
|
218
|
-
try {
|
|
219
|
-
const payload = await this.betterAuthService.verifyJwtToken(token);
|
|
220
|
-
if (payload?.sub) {
|
|
221
|
-
const user = await this.loadUserFromPayload(payload);
|
|
222
|
-
if (user) {
|
|
223
|
-
return user;
|
|
224
|
-
}
|
|
154
|
+
// For other errors (e.g., invalid JWT), try BetterAuth as fallback one more time
|
|
155
|
+
const user = await this.verifyBetterAuthTokenFromContext(context);
|
|
156
|
+
if (user) {
|
|
157
|
+
if (request) {
|
|
158
|
+
request.user = user;
|
|
225
159
|
}
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
// Strategy 2: Try session token lookup (database lookup)
|
|
232
|
-
try {
|
|
233
|
-
const sessionResult = await this.betterAuthService.getSessionByToken(token);
|
|
234
|
-
if (sessionResult?.user) {
|
|
235
|
-
return this.loadUserFromSessionResult(sessionResult.user);
|
|
160
|
+
this.handleRequest(null, user, null, context);
|
|
161
|
+
return true;
|
|
236
162
|
}
|
|
237
|
-
} catch {
|
|
238
|
-
// Session lookup failed
|
|
239
163
|
}
|
|
240
164
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
this.logger.debug(
|
|
244
|
-
`BetterAuth token fallback failed: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
245
|
-
);
|
|
246
|
-
return null;
|
|
165
|
+
// BetterAuth verification also failed - rethrow original error
|
|
166
|
+
throw passportError;
|
|
247
167
|
}
|
|
248
168
|
}
|
|
249
169
|
|
|
250
170
|
/**
|
|
251
|
-
*
|
|
171
|
+
* Verify BetterAuth token (JWT or session) and load the corresponding user.
|
|
172
|
+
*
|
|
173
|
+
* Delegates to BetterAuthTokenService for token extraction and verification.
|
|
174
|
+
* Handles both GraphQL and HTTP contexts.
|
|
252
175
|
*
|
|
253
|
-
* @param
|
|
254
|
-
* @returns User object
|
|
176
|
+
* @param context - ExecutionContext to extract request from
|
|
177
|
+
* @returns User object if verification succeeds, null otherwise
|
|
255
178
|
*/
|
|
256
|
-
private async
|
|
257
|
-
if (!this.
|
|
179
|
+
private async verifyBetterAuthTokenFromContext(context: ExecutionContext): Promise<BetterAuthenticatedUser | null> {
|
|
180
|
+
if (!this.tokenService) {
|
|
258
181
|
return null;
|
|
259
182
|
}
|
|
260
183
|
|
|
261
184
|
try {
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
if (Types.ObjectId.isValid(payload.sub)) {
|
|
267
|
-
user = await usersCollection.findOne({ _id: new Types.ObjectId(payload.sub) });
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
// If not found, try by iamId
|
|
271
|
-
if (!user) {
|
|
272
|
-
user = await usersCollection.findOne({ iamId: payload.sub });
|
|
185
|
+
// Extract request from context (supports both GraphQL and HTTP)
|
|
186
|
+
const request = this.extractRequestFromContext(context);
|
|
187
|
+
if (!request) {
|
|
188
|
+
return null;
|
|
273
189
|
}
|
|
274
190
|
|
|
275
|
-
|
|
191
|
+
// Extract token from request
|
|
192
|
+
const { token } = this.tokenService.extractTokenFromRequest(request);
|
|
193
|
+
if (!token) {
|
|
276
194
|
return null;
|
|
277
195
|
}
|
|
278
196
|
|
|
279
|
-
//
|
|
280
|
-
|
|
281
|
-
...user,
|
|
282
|
-
_authenticatedViaBetterAuth: true,
|
|
283
|
-
// Add hasRole method for role checking
|
|
284
|
-
hasRole: (roles: string[]): boolean => {
|
|
285
|
-
if (!user.roles || !Array.isArray(user.roles)) {
|
|
286
|
-
return false;
|
|
287
|
-
}
|
|
288
|
-
return roles.some((role) => user.roles.includes(role));
|
|
289
|
-
},
|
|
290
|
-
id: user._id?.toString(),
|
|
291
|
-
};
|
|
292
|
-
|
|
293
|
-
return userObject;
|
|
197
|
+
// Verify token and load user
|
|
198
|
+
return await this.tokenService.verifyAndLoadUser(token);
|
|
294
199
|
} catch (error) {
|
|
295
200
|
this.logger.debug(
|
|
296
|
-
`
|
|
201
|
+
`BetterAuth token verification failed: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
297
202
|
);
|
|
298
203
|
return null;
|
|
299
204
|
}
|
|
300
205
|
}
|
|
301
206
|
|
|
302
207
|
/**
|
|
303
|
-
*
|
|
208
|
+
* Extracts the request object from ExecutionContext.
|
|
209
|
+
* Handles both GraphQL and HTTP contexts.
|
|
304
210
|
*
|
|
305
|
-
* @param
|
|
306
|
-
* @returns
|
|
211
|
+
* @param context - ExecutionContext
|
|
212
|
+
* @returns Request object with headers and cookies
|
|
307
213
|
*/
|
|
308
|
-
private
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
214
|
+
private extractRequestFromContext(context: ExecutionContext): null | {
|
|
215
|
+
cookies?: Record<string, string>;
|
|
216
|
+
headers?: Record<string, string | string[] | undefined>;
|
|
217
|
+
} {
|
|
218
|
+
// Try GraphQL context first
|
|
313
219
|
try {
|
|
314
|
-
const
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
let user: any = null;
|
|
319
|
-
|
|
320
|
-
// Try to find by email (most reliable)
|
|
321
|
-
if (sessionUser.email) {
|
|
322
|
-
user = await usersCollection.findOne({ email: sessionUser.email });
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
// If not found by email, try by iamId
|
|
326
|
-
if (!user && sessionUser.id) {
|
|
327
|
-
user = await usersCollection.findOne({ iamId: sessionUser.id });
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
// If still not found, try by _id (if the ID looks like a MongoDB ObjectId)
|
|
331
|
-
if (!user && sessionUser.id && Types.ObjectId.isValid(sessionUser.id)) {
|
|
332
|
-
user = await usersCollection.findOne({ _id: new Types.ObjectId(sessionUser.id) });
|
|
220
|
+
const gqlContext = GqlExecutionContext.create(context);
|
|
221
|
+
const ctx = gqlContext.getContext();
|
|
222
|
+
if (ctx?.req) {
|
|
223
|
+
return ctx.req;
|
|
333
224
|
}
|
|
225
|
+
} catch {
|
|
226
|
+
// GraphQL context not available
|
|
227
|
+
}
|
|
334
228
|
|
|
335
|
-
|
|
336
|
-
|
|
229
|
+
// Fallback to HTTP context
|
|
230
|
+
try {
|
|
231
|
+
const httpRequest = context.switchToHttp().getRequest();
|
|
232
|
+
if (httpRequest) {
|
|
233
|
+
return httpRequest;
|
|
337
234
|
}
|
|
338
|
-
|
|
339
|
-
//
|
|
340
|
-
const userObject = {
|
|
341
|
-
...user,
|
|
342
|
-
_authenticatedViaBetterAuth: true,
|
|
343
|
-
// Add hasRole method for role checking
|
|
344
|
-
hasRole: (roles: string[]): boolean => {
|
|
345
|
-
if (!user.roles || !Array.isArray(user.roles)) {
|
|
346
|
-
return false;
|
|
347
|
-
}
|
|
348
|
-
return roles.some((role) => user.roles.includes(role));
|
|
349
|
-
},
|
|
350
|
-
id: user._id?.toString(),
|
|
351
|
-
};
|
|
352
|
-
|
|
353
|
-
return userObject;
|
|
354
|
-
} catch (error) {
|
|
355
|
-
this.logger.debug(
|
|
356
|
-
`Failed to load user from session: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
357
|
-
);
|
|
358
|
-
return null;
|
|
235
|
+
} catch {
|
|
236
|
+
// HTTP context not available
|
|
359
237
|
}
|
|
238
|
+
|
|
239
|
+
return null;
|
|
360
240
|
}
|
|
361
241
|
|
|
362
242
|
/**
|
|
363
243
|
* Handle request
|
|
364
244
|
*/
|
|
365
|
-
override handleRequest(err, user, info, context) {
|
|
245
|
+
override handleRequest(err: Error | null, user: any, info: any, context: ExecutionContext) {
|
|
366
246
|
// Get roles
|
|
367
247
|
const reflectorRoles = this.reflector.getAll<string[][]>('roles', [context.getHandler(), context.getClass()]);
|
|
368
248
|
const roles: string[] = reflectorRoles[0]
|
|
@@ -129,38 +129,47 @@ export class ServerModule {}
|
|
|
129
129
|
**Modify:** `src/config.env.ts`
|
|
130
130
|
**Reference:** `node_modules/@lenne.tech/nest-server/src/config.env.ts`
|
|
131
131
|
|
|
132
|
-
####
|
|
132
|
+
#### Zero-Config (Default):
|
|
133
|
+
BetterAuth is **enabled by default** with JWT + 2FA. No configuration required!
|
|
134
|
+
|
|
133
135
|
```typescript
|
|
134
136
|
const config = {
|
|
135
|
-
//
|
|
137
|
+
// BetterAuth is automatically enabled with:
|
|
138
|
+
// - JWT tokens (for API clients)
|
|
139
|
+
// - 2FA/TOTP (users can enable it in settings)
|
|
140
|
+
// No betterAuth config needed!
|
|
141
|
+
|
|
142
|
+
// For new projects, disable Legacy Auth:
|
|
136
143
|
auth: {
|
|
137
144
|
legacyEndpoints: {
|
|
138
145
|
enabled: false,
|
|
139
146
|
},
|
|
140
147
|
},
|
|
141
|
-
|
|
142
|
-
|
|
148
|
+
};
|
|
149
|
+
```
|
|
143
150
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
151
|
+
#### With Passkey (auto-detected from baseUrl):
|
|
152
|
+
```typescript
|
|
153
|
+
const config = {
|
|
154
|
+
// Passkey is auto-activated when URLs can be resolved
|
|
155
|
+
// Option 1: Set root-level baseUrl (production)
|
|
156
|
+
baseUrl: 'https://api.example.com', // rpId, origin, trustedOrigins auto-detected
|
|
157
|
+
env: 'production',
|
|
158
|
+
|
|
159
|
+
// Option 2: Use env: 'local'/'ci'/'e2e' (development)
|
|
160
|
+
// env: 'local', // Uses localhost defaults: API=:3000, App=:3001
|
|
150
161
|
};
|
|
151
162
|
```
|
|
152
163
|
|
|
153
|
-
####
|
|
164
|
+
#### Disabling Features:
|
|
154
165
|
```typescript
|
|
155
166
|
const config = {
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
enabled: true, // Default - can disable after migration
|
|
160
|
-
},
|
|
167
|
+
betterAuth: {
|
|
168
|
+
jwt: false, // Disable JWT (use cookies only)
|
|
169
|
+
twoFactor: false, // Disable 2FA
|
|
161
170
|
},
|
|
162
|
-
//
|
|
163
|
-
betterAuth:
|
|
171
|
+
// OR disable BetterAuth completely:
|
|
172
|
+
betterAuth: false,
|
|
164
173
|
};
|
|
165
174
|
```
|
|
166
175
|
|
|
@@ -345,26 +354,31 @@ async function useBackupCode(code: string) {
|
|
|
345
354
|
|
|
346
355
|
### Passkey Login Flow (Client-Side)
|
|
347
356
|
|
|
348
|
-
Handle passkey authentication with session validation fallback
|
|
357
|
+
Handle passkey authentication with session validation fallback.
|
|
358
|
+
|
|
359
|
+
**IMPORTANT:** For JWT mode (`cookies: false`), you MUST use the `authenticateWithPasskey()` function from the composable instead of `authClient.signIn.passkey()` directly. This is because JWT mode requires sending a `challengeId` to the server for challenge verification.
|
|
349
360
|
|
|
350
361
|
```typescript
|
|
351
|
-
// login.vue - Passkey login
|
|
362
|
+
// login.vue - Passkey login (JWT-compatible)
|
|
363
|
+
// Use authenticateWithPasskey from useBetterAuth composable
|
|
364
|
+
const { authenticateWithPasskey, setUser, validateSession } = useBetterAuth();
|
|
365
|
+
|
|
352
366
|
async function onPasskeyLogin() {
|
|
353
367
|
try {
|
|
354
|
-
// Use
|
|
355
|
-
const result = await
|
|
368
|
+
// Use composable method which handles challengeId for JWT mode
|
|
369
|
+
const result = await authenticateWithPasskey();
|
|
356
370
|
|
|
357
|
-
|
|
358
|
-
|
|
371
|
+
// Check for error (returns { success, error?, user?, session? })
|
|
372
|
+
if (!result.success) {
|
|
373
|
+
showError(result.error || 'Passkey authentication failed');
|
|
359
374
|
return;
|
|
360
375
|
}
|
|
361
376
|
|
|
362
377
|
// Update auth state with user data if available
|
|
363
|
-
if (result.
|
|
364
|
-
setUser(result.
|
|
365
|
-
} else
|
|
366
|
-
//
|
|
367
|
-
// Fetch user data via session validation
|
|
378
|
+
if (result.user) {
|
|
379
|
+
setUser(result.user);
|
|
380
|
+
} else {
|
|
381
|
+
// Passkey auth may return success without user - fetch via session validation
|
|
368
382
|
await validateSession();
|
|
369
383
|
}
|
|
370
384
|
|
|
@@ -378,33 +392,36 @@ async function onPasskeyLogin() {
|
|
|
378
392
|
showError(err instanceof Error ? err.message : 'Passkey login failed');
|
|
379
393
|
}
|
|
380
394
|
}
|
|
381
|
-
|
|
382
|
-
// Helper: Validate session and fetch user data
|
|
383
|
-
async function validateSession() {
|
|
384
|
-
const sessionResult = await authClient.$fetch('/session');
|
|
385
|
-
if (sessionResult.user) {
|
|
386
|
-
setUser(sessionResult.user);
|
|
387
|
-
return true;
|
|
388
|
-
}
|
|
389
|
-
return false;
|
|
390
|
-
}
|
|
391
395
|
```
|
|
392
396
|
|
|
397
|
+
**Why not use `authClient.signIn.passkey()` directly?**
|
|
398
|
+
|
|
399
|
+
In JWT mode (`cookies: false`), the server stores WebAuthn challenges in the database instead of cookies. The server returns a `challengeId` in the generate-options response, which must be sent back in the verify request. The `authenticateWithPasskey()` composable function handles this automatically.
|
|
400
|
+
|
|
401
|
+
| Mode | Challenge Storage | Client Approach |
|
|
402
|
+
|------|------------------|-----------------|
|
|
403
|
+
| Cookie (`cookies: true` or not set) | Cookie (`better-auth.better-auth-passkey`) | Either approach works |
|
|
404
|
+
| JWT (`cookies: false`) | Database with `challengeId` mapping (auto-enabled) | **Must use composable** |
|
|
405
|
+
|
|
406
|
+
**Note:** Database challenge storage is automatically enabled when `options.cookies: false` is set. No additional configuration is required.
|
|
407
|
+
|
|
393
408
|
### Passkey Registration (Client-Side)
|
|
394
409
|
|
|
395
|
-
Register a new passkey for an authenticated user
|
|
410
|
+
Register a new passkey for an authenticated user.
|
|
411
|
+
|
|
412
|
+
**IMPORTANT:** For JWT mode (`cookies: false`), you MUST use the `registerPasskey()` function from the composable. This ensures the `challengeId` is correctly sent to the server.
|
|
396
413
|
|
|
397
414
|
```typescript
|
|
398
|
-
// settings.vue - Register new passkey
|
|
399
|
-
|
|
415
|
+
// settings.vue - Register new passkey (JWT-compatible)
|
|
416
|
+
const { registerPasskey, listPasskeys, deletePasskey } = useBetterAuth();
|
|
417
|
+
|
|
418
|
+
async function onRegisterPasskey() {
|
|
400
419
|
try {
|
|
401
|
-
//
|
|
402
|
-
const result = await
|
|
403
|
-
name: 'My Device', // Optional: passkey name
|
|
404
|
-
});
|
|
420
|
+
// Use composable method which handles challengeId for JWT mode
|
|
421
|
+
const result = await registerPasskey('My Device'); // Optional name
|
|
405
422
|
|
|
406
|
-
if (result.
|
|
407
|
-
showError(result.error
|
|
423
|
+
if (!result.success) {
|
|
424
|
+
showError(result.error || 'Passkey registration failed');
|
|
408
425
|
return;
|
|
409
426
|
}
|
|
410
427
|
|
|
@@ -420,23 +437,32 @@ async function registerPasskey() {
|
|
|
420
437
|
}
|
|
421
438
|
}
|
|
422
439
|
|
|
423
|
-
// List user's passkeys
|
|
440
|
+
// List user's passkeys (works in both modes)
|
|
424
441
|
async function loadPasskeys() {
|
|
425
|
-
const result = await
|
|
426
|
-
if (
|
|
427
|
-
passkeys.value = result.
|
|
442
|
+
const result = await listPasskeys();
|
|
443
|
+
if (result.success) {
|
|
444
|
+
passkeys.value = result.passkeys || [];
|
|
428
445
|
}
|
|
429
446
|
}
|
|
430
447
|
|
|
431
|
-
// Delete a passkey
|
|
432
|
-
async function
|
|
433
|
-
const result = await
|
|
434
|
-
if (
|
|
448
|
+
// Delete a passkey (works in both modes)
|
|
449
|
+
async function onDeletePasskey(passkeyId: string) {
|
|
450
|
+
const result = await deletePasskey(passkeyId);
|
|
451
|
+
if (result.success) {
|
|
435
452
|
await loadPasskeys();
|
|
436
453
|
}
|
|
437
454
|
}
|
|
438
455
|
```
|
|
439
456
|
|
|
457
|
+
**Alternative for Cookie mode only:**
|
|
458
|
+
|
|
459
|
+
If you're only using Cookie mode (`cookies: true`), you can use `authClient.passkey` directly:
|
|
460
|
+
|
|
461
|
+
```typescript
|
|
462
|
+
// Cookie mode ONLY - does NOT work with JWT mode
|
|
463
|
+
const result = await authClient.passkey.addPasskey({ name: 'My Device' });
|
|
464
|
+
```
|
|
465
|
+
|
|
440
466
|
---
|
|
441
467
|
|
|
442
468
|
## Better-Auth Hooks: Limitations & Warnings
|