@ambushsoftworks/nestjs-auth-graphql 0.2.0 → 0.3.0
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 +759 -0
- package/dist/auth.module.d.ts +4 -1
- package/dist/auth.module.d.ts.map +1 -1
- package/dist/auth.module.js +20 -1
- package/dist/auth.module.js.map +1 -1
- package/dist/constants.d.ts +1 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +2 -1
- package/dist/constants.js.map +1 -1
- package/dist/decorators/current-user.decorator.js +1 -1
- package/dist/decorators/current-user.decorator.js.map +1 -1
- package/dist/dto/biometric-challenge.dto.d.ts.map +1 -1
- package/dist/dto/biometric-challenge.dto.js.map +1 -1
- package/dist/dto/biometric-credential.dto.d.ts.map +1 -1
- package/dist/dto/biometric-credential.dto.js.map +1 -1
- package/dist/dto/biometric-login.input.d.ts.map +1 -1
- package/dist/dto/biometric-login.input.js.map +1 -1
- package/dist/dto/biometric-status.dto.d.ts.map +1 -1
- package/dist/dto/biometric-status.dto.js.map +1 -1
- package/dist/dto/complete-facebook-signup.input.d.ts +6 -1
- package/dist/dto/complete-facebook-signup.input.d.ts.map +1 -1
- package/dist/dto/complete-facebook-signup.input.js.map +1 -1
- package/dist/dto/enable-biometric.input.d.ts.map +1 -1
- package/dist/dto/enable-biometric.input.js.map +1 -1
- package/dist/dto/enroll-biometric.input.d.ts.map +1 -1
- package/dist/dto/enroll-biometric.input.js.map +1 -1
- package/dist/dto/link-google-account.input.d.ts.map +1 -1
- package/dist/dto/link-google-account.input.js.map +1 -1
- package/dist/dto/login.input.d.ts.map +1 -1
- package/dist/dto/login.input.js +27 -2
- package/dist/dto/login.input.js.map +1 -1
- package/dist/dto/logout.input.d.ts.map +1 -1
- package/dist/dto/logout.input.js +3 -0
- package/dist/dto/logout.input.js.map +1 -1
- package/dist/dto/password-reset-response.dto.d.ts +11 -0
- package/dist/dto/password-reset-response.dto.d.ts.map +1 -0
- package/dist/dto/password-reset-response.dto.js +7 -0
- package/dist/dto/password-reset-response.dto.js.map +1 -0
- package/dist/dto/refresh-token.input.d.ts.map +1 -1
- package/dist/dto/refresh-token.input.js +22 -2
- package/dist/dto/refresh-token.input.js.map +1 -1
- package/dist/dto/remove-biometric-device-response.dto.d.ts.map +1 -1
- package/dist/dto/remove-biometric-device-response.dto.js.map +1 -1
- package/dist/dto/request-password-reset.input.d.ts +7 -0
- package/dist/dto/request-password-reset.input.d.ts.map +1 -0
- package/dist/dto/request-password-reset.input.js +21 -0
- package/dist/dto/request-password-reset.input.js.map +1 -0
- package/dist/dto/reset-password.input.d.ts +11 -0
- package/dist/dto/reset-password.input.d.ts.map +1 -0
- package/dist/dto/reset-password.input.js +31 -0
- package/dist/dto/reset-password.input.js.map +1 -0
- package/dist/dto/send-phone-verification.input.d.ts.map +1 -1
- package/dist/dto/send-phone-verification.input.js +10 -0
- package/dist/dto/send-phone-verification.input.js.map +1 -1
- package/dist/dto/signup.input.d.ts.map +1 -1
- package/dist/dto/signup.input.js +31 -2
- package/dist/dto/signup.input.js.map +1 -1
- package/dist/dto/unlink-social-account-response.dto.d.ts.map +1 -1
- package/dist/dto/unlink-social-account-response.dto.js.map +1 -1
- package/dist/dto/unlink-social-account.input.d.ts.map +1 -1
- package/dist/dto/unlink-social-account.input.js.map +1 -1
- package/dist/dto/verify-biometric-signature.input.d.ts.map +1 -1
- package/dist/dto/verify-biometric-signature.input.js.map +1 -1
- package/dist/dto/verify-email.input.d.ts.map +1 -1
- package/dist/dto/verify-email.input.js +4 -0
- package/dist/dto/verify-email.input.js.map +1 -1
- package/dist/dto/verify-phone.input.d.ts.map +1 -1
- package/dist/dto/verify-phone.input.js +7 -0
- package/dist/dto/verify-phone.input.js.map +1 -1
- package/dist/entities/auth-user.entity.d.ts.map +1 -1
- package/dist/enums/verification-type.enum.d.ts +9 -0
- package/dist/enums/verification-type.enum.d.ts.map +1 -0
- package/dist/enums/verification-type.enum.js +11 -0
- package/dist/enums/verification-type.enum.js.map +1 -0
- package/dist/exceptions/password-reset.exceptions.d.ts +8 -0
- package/dist/exceptions/password-reset.exceptions.d.ts.map +1 -0
- package/dist/exceptions/password-reset.exceptions.js +25 -0
- package/dist/exceptions/password-reset.exceptions.js.map +1 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +15 -0
- package/dist/index.js.map +1 -1
- package/dist/interfaces/auth-lifecycle-hooks.interface.d.ts +1 -1
- package/dist/interfaces/auth-lifecycle-hooks.interface.d.ts.map +1 -1
- package/dist/interfaces/auth-user.interface.d.ts +1 -0
- package/dist/interfaces/auth-user.interface.d.ts.map +1 -1
- package/dist/interfaces/magic-link-repository.interface.d.ts +6 -0
- package/dist/interfaces/magic-link-repository.interface.d.ts.map +1 -0
- package/dist/interfaces/magic-link-repository.interface.js +3 -0
- package/dist/interfaces/magic-link-repository.interface.js.map +1 -0
- package/dist/interfaces/password-policy-config.interface.d.ts +16 -0
- package/dist/interfaces/password-policy-config.interface.d.ts.map +1 -0
- package/dist/interfaces/password-policy-config.interface.js +3 -0
- package/dist/interfaces/password-policy-config.interface.js.map +1 -0
- package/dist/interfaces/password-reset-strategy.interface.d.ts +7 -0
- package/dist/interfaces/password-reset-strategy.interface.d.ts.map +1 -0
- package/dist/interfaces/password-reset-strategy.interface.js +3 -0
- package/dist/interfaces/password-reset-strategy.interface.js.map +1 -0
- package/dist/interfaces/rate-limiter.interface.d.ts +9 -0
- package/dist/interfaces/rate-limiter.interface.d.ts.map +1 -0
- package/dist/interfaces/rate-limiter.interface.js +3 -0
- package/dist/interfaces/rate-limiter.interface.js.map +1 -0
- package/dist/interfaces/user-repository.interface.d.ts +14 -10
- package/dist/interfaces/user-repository.interface.d.ts.map +1 -1
- package/dist/interfaces/verification-repository.interface.d.ts +5 -4
- package/dist/interfaces/verification-repository.interface.d.ts.map +1 -1
- package/dist/repositories/noop-biometric.repository.d.ts +7 -4
- package/dist/repositories/noop-biometric.repository.d.ts.map +1 -1
- package/dist/repositories/noop-biometric.repository.js +22 -8
- package/dist/repositories/noop-biometric.repository.js.map +1 -1
- package/dist/repositories/noop-brute-force.repository.d.ts +3 -0
- package/dist/repositories/noop-brute-force.repository.d.ts.map +1 -1
- package/dist/repositories/noop-brute-force.repository.js +16 -2
- package/dist/repositories/noop-brute-force.repository.js.map +1 -1
- package/dist/repositories/noop-magic-link.repository.d.ts +9 -0
- package/dist/repositories/noop-magic-link.repository.d.ts.map +1 -0
- package/dist/repositories/noop-magic-link.repository.js +37 -0
- package/dist/repositories/noop-magic-link.repository.js.map +1 -0
- package/dist/repositories/noop-rate-limiter.d.ts +12 -0
- package/dist/repositories/noop-rate-limiter.d.ts.map +1 -0
- package/dist/repositories/noop-rate-limiter.js +38 -0
- package/dist/repositories/noop-rate-limiter.js.map +1 -0
- package/dist/repositories/noop-verification.repository.d.ts +8 -4
- package/dist/repositories/noop-verification.repository.d.ts.map +1 -1
- package/dist/repositories/noop-verification.repository.js +18 -3
- package/dist/repositories/noop-verification.repository.js.map +1 -1
- package/dist/resolvers/base-auth.resolver.d.ts +7 -0
- package/dist/resolvers/base-auth.resolver.d.ts.map +1 -1
- package/dist/resolvers/base-auth.resolver.js +20 -0
- package/dist/resolvers/base-auth.resolver.js.map +1 -1
- package/dist/resolvers/oauth.controller.d.ts.map +1 -1
- package/dist/resolvers/oauth.controller.js +16 -6
- package/dist/resolvers/oauth.controller.js.map +1 -1
- package/dist/services/auth.service.d.ts +15 -2
- package/dist/services/auth.service.d.ts.map +1 -1
- package/dist/services/auth.service.js +155 -17
- package/dist/services/auth.service.js.map +1 -1
- package/dist/services/biometric-auth.service.d.ts +4 -2
- package/dist/services/biometric-auth.service.d.ts.map +1 -1
- package/dist/services/biometric-auth.service.js +16 -6
- package/dist/services/biometric-auth.service.js.map +1 -1
- package/dist/services/biometric-verification.service.d.ts.map +1 -1
- package/dist/services/biometric-verification.service.js +9 -7
- package/dist/services/biometric-verification.service.js.map +1 -1
- package/dist/services/brute-force-protection.service.d.ts +0 -5
- package/dist/services/brute-force-protection.service.d.ts.map +1 -1
- package/dist/services/brute-force-protection.service.js +0 -5
- package/dist/services/brute-force-protection.service.js.map +1 -1
- package/dist/services/in-memory-rate-limiter.service.d.ts +15 -0
- package/dist/services/in-memory-rate-limiter.service.d.ts.map +1 -0
- package/dist/services/in-memory-rate-limiter.service.js +75 -0
- package/dist/services/in-memory-rate-limiter.service.js.map +1 -0
- package/dist/services/oauth-linking-token.service.d.ts.map +1 -1
- package/dist/services/oauth-linking-token.service.js +8 -2
- package/dist/services/oauth-linking-token.service.js.map +1 -1
- package/dist/services/oauth-state.service.d.ts.map +1 -1
- package/dist/services/oauth-state.service.js +3 -2
- package/dist/services/oauth-state.service.js.map +1 -1
- package/dist/services/password-validation.service.d.ts +11 -0
- package/dist/services/password-validation.service.d.ts.map +1 -0
- package/dist/services/password-validation.service.js +75 -0
- package/dist/services/password-validation.service.js.map +1 -0
- package/dist/services/refresh-token.service.d.ts +4 -1
- package/dist/services/refresh-token.service.d.ts.map +1 -1
- package/dist/services/refresh-token.service.js +7 -1
- package/dist/services/refresh-token.service.js.map +1 -1
- package/dist/services/sendgrid-email.service.d.ts +1 -1
- package/dist/services/sendgrid-email.service.d.ts.map +1 -1
- package/dist/services/sendgrid-email.service.js +90 -107
- package/dist/services/sendgrid-email.service.js.map +1 -1
- package/dist/services/twilio-sms.service.d.ts.map +1 -1
- package/dist/services/twilio-sms.service.js +6 -3
- package/dist/services/twilio-sms.service.js.map +1 -1
- package/dist/services/verification.service.d.ts.map +1 -1
- package/dist/services/verification.service.js +44 -30
- package/dist/services/verification.service.js.map +1 -1
- package/dist/strategies/facebook.strategy.d.ts.map +1 -1
- package/dist/strategies/facebook.strategy.js +4 -2
- package/dist/strategies/facebook.strategy.js.map +1 -1
- package/dist/strategies/google.strategy.d.ts.map +1 -1
- package/dist/strategies/google.strategy.js +2 -1
- package/dist/strategies/google.strategy.js.map +1 -1
- package/dist/strategies/jwt.strategy.d.ts +0 -1
- package/dist/strategies/jwt.strategy.d.ts.map +1 -1
- package/dist/strategies/jwt.strategy.js +0 -1
- package/dist/strategies/jwt.strategy.js.map +1 -1
- package/dist/strategies/magic-link.strategy.d.ts +16 -0
- package/dist/strategies/magic-link.strategy.d.ts.map +1 -0
- package/dist/strategies/magic-link.strategy.js +80 -0
- package/dist/strategies/magic-link.strategy.js.map +1 -0
- package/dist/strategies/noop-facebook.strategy.d.ts +1 -1
- package/dist/strategies/noop-facebook.strategy.d.ts.map +1 -1
- package/dist/strategies/noop-facebook.strategy.js +1 -1
- package/dist/strategies/noop-facebook.strategy.js.map +1 -1
- package/dist/strategies/noop-google.strategy.d.ts +1 -1
- package/dist/strategies/noop-google.strategy.d.ts.map +1 -1
- package/dist/strategies/noop-google.strategy.js +1 -1
- package/dist/strategies/noop-google.strategy.js.map +1 -1
- package/dist/strategies/verification-code.strategy.d.ts +11 -0
- package/dist/strategies/verification-code.strategy.d.ts.map +1 -0
- package/dist/strategies/verification-code.strategy.js +44 -0
- package/dist/strategies/verification-code.strategy.js.map +1 -0
- package/dist/test-utils/index.d.ts +5 -0
- package/dist/test-utils/index.d.ts.map +1 -0
- package/dist/test-utils/index.js +40 -0
- package/dist/test-utils/index.js.map +1 -0
- package/dist/test-utils/mock-repositories.d.ts +7 -0
- package/dist/test-utils/mock-repositories.d.ts.map +1 -0
- package/dist/test-utils/mock-repositories.js +84 -0
- package/dist/test-utils/mock-repositories.js.map +1 -0
- package/dist/test-utils/mock-services.d.ts +6 -0
- package/dist/test-utils/mock-services.d.ts.map +1 -0
- package/dist/test-utils/mock-services.js +47 -0
- package/dist/test-utils/mock-services.js.map +1 -0
- package/dist/test-utils/test-factories.d.ts +51 -0
- package/dist/test-utils/test-factories.d.ts.map +1 -0
- package/dist/test-utils/test-factories.js +201 -0
- package/dist/test-utils/test-factories.js.map +1 -0
- package/dist/test-utils/test-helpers.d.ts +24 -0
- package/dist/test-utils/test-helpers.d.ts.map +1 -0
- package/dist/test-utils/test-helpers.js +137 -0
- package/dist/test-utils/test-helpers.js.map +1 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -25,6 +25,7 @@ Production-grade authentication package for NestJS with GraphQL, extracted from
|
|
|
25
25
|
- **OAuth 2.0**: Google and Facebook social login
|
|
26
26
|
- **Email Verification**: 6-digit PIN codes via SendGrid with rate limiting
|
|
27
27
|
- **SMS Verification**: Phone number verification via Twilio
|
|
28
|
+
- **Password Reset**: 6-digit verification codes with email enumeration protection
|
|
28
29
|
- **Biometric Authentication**: Face ID, Touch ID, fingerprint support
|
|
29
30
|
- **Brute Force Protection**: Account lockout after failed login attempts
|
|
30
31
|
- **Account Linking**: Link/unlink social accounts to existing accounts
|
|
@@ -253,6 +254,93 @@ export class AuthResolver extends BaseAuthResolver<User> {
|
|
|
253
254
|
export class AppModule {}
|
|
254
255
|
```
|
|
255
256
|
|
|
257
|
+
### 4. Configure Input Validation
|
|
258
|
+
|
|
259
|
+
**IMPORTANT**: This package includes input validation using `class-validator` decorators on all input DTOs. You **must** enable NestJS's `ValidationPipe` globally for automatic validation.
|
|
260
|
+
|
|
261
|
+
Add this to your `main.ts`:
|
|
262
|
+
|
|
263
|
+
```typescript
|
|
264
|
+
import { NestFactory } from '@nestjs/core';
|
|
265
|
+
import { ValidationPipe } from '@nestjs/common';
|
|
266
|
+
import { AppModule } from './app.module';
|
|
267
|
+
|
|
268
|
+
async function bootstrap() {
|
|
269
|
+
const app = await NestFactory.create(AppModule);
|
|
270
|
+
|
|
271
|
+
// Enable validation globally
|
|
272
|
+
app.useGlobalPipes(
|
|
273
|
+
new ValidationPipe({
|
|
274
|
+
whitelist: true, // Strip properties not in DTO
|
|
275
|
+
forbidNonWhitelisted: true, // Throw error if extra properties
|
|
276
|
+
transform: true, // Auto-transform payloads to DTO instances
|
|
277
|
+
transformOptions: {
|
|
278
|
+
enableImplicitConversion: true, // Auto-convert primitive types
|
|
279
|
+
},
|
|
280
|
+
}),
|
|
281
|
+
);
|
|
282
|
+
|
|
283
|
+
await app.listen(3000);
|
|
284
|
+
}
|
|
285
|
+
bootstrap();
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
**What gets validated:**
|
|
289
|
+
|
|
290
|
+
The package validates all input fields with appropriate decorators:
|
|
291
|
+
|
|
292
|
+
- **Email fields**: `@IsEmail()` - Validates proper email format
|
|
293
|
+
- **Password fields** (signup): `@MinLength(8)`, `@MaxLength(100)`, `@Matches()` - Enforces strong passwords with uppercase, lowercase, number, and special character
|
|
294
|
+
- **Password fields** (login): `@IsNotEmpty()` - Validates presence only (no strength check for existing passwords)
|
|
295
|
+
- **Verification codes**: `@Matches(/^\d{6}$/)` - Validates 6-digit PIN codes
|
|
296
|
+
- **Phone numbers**: `@Matches(/^\+[1-9]\d{1,14}$/)` - Validates E.164 international format (e.g., +14155552671)
|
|
297
|
+
- **Tokens**: `@IsString()`, `@IsNotEmpty()` - Validates refresh tokens and OAuth tokens
|
|
298
|
+
- **Device IDs**: `@IsUUID()` or `@IsString()` - Validates biometric device identifiers
|
|
299
|
+
- **Provider names**: `@IsIn(['google', 'facebook', 'apple'])` - Validates OAuth provider names
|
|
300
|
+
|
|
301
|
+
**Validation error responses:**
|
|
302
|
+
|
|
303
|
+
When validation fails, NestJS automatically returns a `400 Bad Request` with detailed error messages:
|
|
304
|
+
|
|
305
|
+
```json
|
|
306
|
+
{
|
|
307
|
+
"statusCode": 400,
|
|
308
|
+
"message": [
|
|
309
|
+
"Please provide a valid email address",
|
|
310
|
+
"Password must be at least 8 characters",
|
|
311
|
+
"Password must contain uppercase, lowercase, number, and special character"
|
|
312
|
+
],
|
|
313
|
+
"error": "Bad Request"
|
|
314
|
+
}
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
**Custom validation for your DTOs:**
|
|
318
|
+
|
|
319
|
+
If you create your own input DTOs implementing the package interfaces (recommended for full type control), add `class-validator` decorators:
|
|
320
|
+
|
|
321
|
+
```typescript
|
|
322
|
+
import { InputType, Field } from '@nestjs/graphql';
|
|
323
|
+
import { IsEmail, IsString, MinLength, Matches } from 'class-validator';
|
|
324
|
+
import { IAuthSignupInput } from '@ambushsoftworks/nestjs-auth-graphql';
|
|
325
|
+
|
|
326
|
+
@InputType()
|
|
327
|
+
export class SignupInput implements IAuthSignupInput {
|
|
328
|
+
@Field()
|
|
329
|
+
@IsEmail({}, { message: 'Please provide a valid email address' })
|
|
330
|
+
email: string;
|
|
331
|
+
|
|
332
|
+
@Field()
|
|
333
|
+
@IsString()
|
|
334
|
+
@MinLength(12, { message: 'Password must be at least 12 characters' })
|
|
335
|
+
@Matches(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])/, {
|
|
336
|
+
message: 'Password must contain uppercase, lowercase, number, and special character',
|
|
337
|
+
})
|
|
338
|
+
password: string;
|
|
339
|
+
}
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
**Note**: The package's default input classes (marked as `@deprecated`) already include validation decorators for backward compatibility. If using these, validation works automatically with `ValidationPipe` enabled.
|
|
343
|
+
|
|
256
344
|
## Configuration Options
|
|
257
345
|
|
|
258
346
|
### Required Options
|
|
@@ -370,6 +458,308 @@ import { NoOpEmailService, NoOpSmsService } from '@yourorg/nestjs-auth-graphql';
|
|
|
370
458
|
})
|
|
371
459
|
```
|
|
372
460
|
|
|
461
|
+
## Password Reset
|
|
462
|
+
|
|
463
|
+
Secure password reset flow with 6-digit verification codes, rate limiting, and email enumeration protection.
|
|
464
|
+
|
|
465
|
+
### Features
|
|
466
|
+
|
|
467
|
+
- **6-Digit Verification Codes** - SMS/email verification pattern (not magic links)
|
|
468
|
+
- **Email Enumeration Protection** - Generic success messages for all requests
|
|
469
|
+
- **Rate Limiting** - 60-second cooldown between requests per user
|
|
470
|
+
- **Password Strength Validation** - Configurable requirements (default: 8+ chars, uppercase, lowercase, number)
|
|
471
|
+
- **Token Revocation** - All refresh tokens invalidated on password change
|
|
472
|
+
- **Brute Force Protection** - Integration with account locking system
|
|
473
|
+
- **OAuth User Protection** - Users authenticated via social login cannot reset passwords
|
|
474
|
+
- **Security Logging** - Audit trail for all password reset activities
|
|
475
|
+
|
|
476
|
+
### Consumer Setup
|
|
477
|
+
|
|
478
|
+
#### Step 1: Database Migration
|
|
479
|
+
|
|
480
|
+
Add `passwordResetSentAt` field to your User model:
|
|
481
|
+
|
|
482
|
+
**Prisma Example:**
|
|
483
|
+
```prisma
|
|
484
|
+
model User {
|
|
485
|
+
id String @id @default(cuid())
|
|
486
|
+
email String @unique
|
|
487
|
+
passwordHash String?
|
|
488
|
+
|
|
489
|
+
// Password reset rate limiting
|
|
490
|
+
passwordResetSentAt DateTime? // 60-second cooldown
|
|
491
|
+
|
|
492
|
+
// ... other fields
|
|
493
|
+
}
|
|
494
|
+
```
|
|
495
|
+
|
|
496
|
+
**TypeORM Example:**
|
|
497
|
+
```typescript
|
|
498
|
+
@Entity()
|
|
499
|
+
export class User {
|
|
500
|
+
@PrimaryGeneratedColumn('uuid')
|
|
501
|
+
id: string;
|
|
502
|
+
|
|
503
|
+
@Column({ nullable: true })
|
|
504
|
+
passwordResetSentAt: Date;
|
|
505
|
+
|
|
506
|
+
// ... other fields
|
|
507
|
+
}
|
|
508
|
+
```
|
|
509
|
+
|
|
510
|
+
#### Step 2: Configure Email Service
|
|
511
|
+
|
|
512
|
+
Ensure `SendGridEmailService` (or your custom email service) is configured:
|
|
513
|
+
|
|
514
|
+
```typescript
|
|
515
|
+
AuthModule.forRootAsync({
|
|
516
|
+
imports: [ConfigModule],
|
|
517
|
+
inject: [ConfigService, UsersRepository, /* ... */],
|
|
518
|
+
useFactory: (config: ConfigService, usersRepo, /* ... */) => ({
|
|
519
|
+
// ... other options
|
|
520
|
+
|
|
521
|
+
emailServiceInstance: new SendGridEmailService(
|
|
522
|
+
config.get('SENDGRID_API_KEY'),
|
|
523
|
+
),
|
|
524
|
+
|
|
525
|
+
// IMPORTANT: Set frontend URL for email templates
|
|
526
|
+
// (Not used in 6-digit code flow, but required by email service)
|
|
527
|
+
features: {
|
|
528
|
+
emailVerification: true,
|
|
529
|
+
// ... other features
|
|
530
|
+
},
|
|
531
|
+
}),
|
|
532
|
+
}),
|
|
533
|
+
```
|
|
534
|
+
|
|
535
|
+
**Environment Variable:**
|
|
536
|
+
```bash
|
|
537
|
+
FRONTEND_URL=https://yourapp.com # Used in email branding
|
|
538
|
+
```
|
|
539
|
+
|
|
540
|
+
#### Step 3: Create GraphQL DTOs
|
|
541
|
+
|
|
542
|
+
Create consumer-specific DTOs with GraphQL decorators:
|
|
543
|
+
|
|
544
|
+
**`request-password-reset.input.ts`:**
|
|
545
|
+
```typescript
|
|
546
|
+
import { InputType, Field } from '@nestjs/graphql';
|
|
547
|
+
import { IAuthRequestPasswordResetInput } from '@ambushsoftworks/nestjs-auth-graphql';
|
|
548
|
+
|
|
549
|
+
@InputType()
|
|
550
|
+
export class RequestPasswordResetInput implements IAuthRequestPasswordResetInput {
|
|
551
|
+
@Field(() => String, {
|
|
552
|
+
description: 'User email address. Code sent if account exists.',
|
|
553
|
+
})
|
|
554
|
+
email: string;
|
|
555
|
+
}
|
|
556
|
+
```
|
|
557
|
+
|
|
558
|
+
**`reset-password.input.ts`:**
|
|
559
|
+
```typescript
|
|
560
|
+
import { InputType, Field } from '@nestjs/graphql';
|
|
561
|
+
import { IAuthResetPasswordInput } from '@ambushsoftworks/nestjs-auth-graphql';
|
|
562
|
+
|
|
563
|
+
@InputType()
|
|
564
|
+
export class ResetPasswordInput implements IAuthResetPasswordInput {
|
|
565
|
+
@Field(() => String)
|
|
566
|
+
email: string;
|
|
567
|
+
|
|
568
|
+
@Field(() => String, { description: '6-digit verification code' })
|
|
569
|
+
code: string;
|
|
570
|
+
|
|
571
|
+
@Field(() => String, { description: 'New password (8+ chars, uppercase, lowercase, number)' })
|
|
572
|
+
newPassword: string;
|
|
573
|
+
}
|
|
574
|
+
```
|
|
575
|
+
|
|
576
|
+
**`password-reset-response.dto.ts`:**
|
|
577
|
+
```typescript
|
|
578
|
+
import { ObjectType, Field, Int } from '@nestjs/graphql';
|
|
579
|
+
import { IAuthPasswordResetResponse } from '@ambushsoftworks/nestjs-auth-graphql';
|
|
580
|
+
|
|
581
|
+
@ObjectType()
|
|
582
|
+
export class PasswordResetResponse implements IAuthPasswordResetResponse {
|
|
583
|
+
@Field(() => Boolean)
|
|
584
|
+
success: boolean;
|
|
585
|
+
|
|
586
|
+
@Field(() => String)
|
|
587
|
+
message: string;
|
|
588
|
+
|
|
589
|
+
@Field(() => Int, { nullable: true })
|
|
590
|
+
retryAfterSeconds?: number;
|
|
591
|
+
}
|
|
592
|
+
```
|
|
593
|
+
|
|
594
|
+
#### Step 4: Add Resolver Mutations
|
|
595
|
+
|
|
596
|
+
Extend your custom resolver with password reset mutations:
|
|
597
|
+
|
|
598
|
+
```typescript
|
|
599
|
+
import { Resolver, Mutation, Args, Context } from '@nestjs/graphql';
|
|
600
|
+
import { Throttle } from '@nestjs/throttler';
|
|
601
|
+
import { BaseAuthResolver } from '@ambushsoftworks/nestjs-auth-graphql';
|
|
602
|
+
import { RequestPasswordResetInput } from './dto/request-password-reset.input';
|
|
603
|
+
import { ResetPasswordInput } from './dto/reset-password.input';
|
|
604
|
+
import { PasswordResetResponse } from './dto/password-reset-response.dto';
|
|
605
|
+
import { User } from './entities/user.entity';
|
|
606
|
+
|
|
607
|
+
@Resolver()
|
|
608
|
+
export class AppAuthResolver extends BaseAuthResolver<User> {
|
|
609
|
+
// ... other mutations (signup, login, etc.)
|
|
610
|
+
|
|
611
|
+
@Mutation(() => PasswordResetResponse, {
|
|
612
|
+
name: 'requestPasswordReset',
|
|
613
|
+
description: 'Request password reset code via email',
|
|
614
|
+
})
|
|
615
|
+
@Throttle({ default: { limit: 3, ttl: 60000 } }) // 3 requests per minute
|
|
616
|
+
async requestPasswordReset(
|
|
617
|
+
@Args('input') input: RequestPasswordResetInput,
|
|
618
|
+
@Context() context: any,
|
|
619
|
+
): Promise<PasswordResetResponse> {
|
|
620
|
+
return this.performRequestPasswordReset(input, context) as Promise<PasswordResetResponse>;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
@Mutation(() => PasswordResetResponse, {
|
|
624
|
+
name: 'resetPassword',
|
|
625
|
+
description: 'Reset password using verification code',
|
|
626
|
+
})
|
|
627
|
+
@Throttle({ default: { limit: 5, ttl: 900000 } }) // 5 attempts per 15 minutes
|
|
628
|
+
async resetPassword(
|
|
629
|
+
@Args('input') input: ResetPasswordInput,
|
|
630
|
+
@Context() context: any,
|
|
631
|
+
): Promise<PasswordResetResponse> {
|
|
632
|
+
return this.performResetPassword(input, context) as Promise<PasswordResetResponse>;
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
```
|
|
636
|
+
|
|
637
|
+
#### Step 5: Deploy & Test
|
|
638
|
+
|
|
639
|
+
```bash
|
|
640
|
+
# Run migration
|
|
641
|
+
npx prisma migrate deploy
|
|
642
|
+
|
|
643
|
+
# Start dev server
|
|
644
|
+
npm run start:dev
|
|
645
|
+
|
|
646
|
+
# Test GraphQL API
|
|
647
|
+
curl -X POST http://localhost:3000/graphql \
|
|
648
|
+
-H "Content-Type: application/json" \
|
|
649
|
+
-d '{"query":"mutation { requestPasswordReset(input: {email: \"test@example.com\"}) { success message } }"}'
|
|
650
|
+
```
|
|
651
|
+
|
|
652
|
+
### GraphQL Schema
|
|
653
|
+
|
|
654
|
+
After setup, your schema will include:
|
|
655
|
+
|
|
656
|
+
```graphql
|
|
657
|
+
type Mutation {
|
|
658
|
+
requestPasswordReset(input: RequestPasswordResetInput!): PasswordResetResponse!
|
|
659
|
+
resetPassword(input: ResetPasswordInput!): PasswordResetResponse!
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
input RequestPasswordResetInput {
|
|
663
|
+
email: String!
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
input ResetPasswordInput {
|
|
667
|
+
email: String!
|
|
668
|
+
code: String!
|
|
669
|
+
newPassword: String!
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
type PasswordResetResponse {
|
|
673
|
+
success: Boolean!
|
|
674
|
+
message: String!
|
|
675
|
+
retryAfterSeconds: Int
|
|
676
|
+
}
|
|
677
|
+
```
|
|
678
|
+
|
|
679
|
+
### Error Handling
|
|
680
|
+
|
|
681
|
+
**Expected Exceptions:**
|
|
682
|
+
|
|
683
|
+
| Exception | HTTP Status | When Thrown | Client Action |
|
|
684
|
+
|-----------|-------------|-------------|---------------|
|
|
685
|
+
| `PasswordResetRateLimitException` | 429 | < 60 seconds since last request | Display countdown: "Try again in X seconds" |
|
|
686
|
+
| `WeakPasswordException` | 400 | Password doesn't meet requirements | Show validation errors to user |
|
|
687
|
+
| `AccountLockedException` | 403 | Account locked due to brute force | Show "Account locked" message |
|
|
688
|
+
| `UnauthorizedException` | 401 | Invalid/expired code | "Code is invalid or expired" |
|
|
689
|
+
|
|
690
|
+
**Example Client-Side Error Handling (GraphQL):**
|
|
691
|
+
|
|
692
|
+
```typescript
|
|
693
|
+
try {
|
|
694
|
+
const result = await client.mutate({
|
|
695
|
+
mutation: RESET_PASSWORD_MUTATION,
|
|
696
|
+
variables: { input: { email, code, newPassword } },
|
|
697
|
+
});
|
|
698
|
+
|
|
699
|
+
if (result.data?.resetPassword?.success) {
|
|
700
|
+
// Redirect to login
|
|
701
|
+
router.push('/login');
|
|
702
|
+
}
|
|
703
|
+
} catch (error) {
|
|
704
|
+
if (error.extensions?.code === 'BAD_REQUEST') {
|
|
705
|
+
// WeakPasswordException
|
|
706
|
+
showErrors(error.extensions.errors); // ["Password must contain uppercase", ...]
|
|
707
|
+
} else if (error.extensions?.code === 'TOO_MANY_REQUESTS') {
|
|
708
|
+
// PasswordResetRateLimitException
|
|
709
|
+
const retryAfter = error.extensions.retryAfterSeconds;
|
|
710
|
+
showCountdown(retryAfter);
|
|
711
|
+
} else if (error.message.includes('invalid or expired')) {
|
|
712
|
+
// UnauthorizedException
|
|
713
|
+
showError('Code is invalid or expired');
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
```
|
|
717
|
+
|
|
718
|
+
### Security Considerations
|
|
719
|
+
|
|
720
|
+
1. **Never reveal whether email exists**
|
|
721
|
+
- Always return success message, even for non-existent emails
|
|
722
|
+
- Client cannot enumerate valid email addresses
|
|
723
|
+
|
|
724
|
+
2. **Rate limiting is essential**
|
|
725
|
+
- Implement both per-user (60s) AND per-IP (via @Throttle) limits
|
|
726
|
+
- Prevents abuse and spam
|
|
727
|
+
|
|
728
|
+
3. **Code security**
|
|
729
|
+
- Codes are HMAC-SHA256 hashed in database (never plain text)
|
|
730
|
+
- Constant-time comparison prevents timing attacks
|
|
731
|
+
- 15-minute expiry limits exposure window
|
|
732
|
+
- Single-use enforcement prevents replay attacks
|
|
733
|
+
|
|
734
|
+
4. **Token revocation**
|
|
735
|
+
- All refresh tokens are invalidated on password change
|
|
736
|
+
- Forces re-authentication on all devices
|
|
737
|
+
- Prevents attacker from maintaining access
|
|
738
|
+
|
|
739
|
+
5. **Email template security**
|
|
740
|
+
- Do NOT include personalized reset URLs with embedded tokens
|
|
741
|
+
- Use 6-digit codes displayed in email (user manually enters in app)
|
|
742
|
+
- Prevents phishing attacks via link manipulation
|
|
743
|
+
|
|
744
|
+
### Lifecycle Hooks
|
|
745
|
+
|
|
746
|
+
Optionally track password reset events:
|
|
747
|
+
|
|
748
|
+
```typescript
|
|
749
|
+
export class AppAuthHooks implements IAuthLifecycleHooks<User> {
|
|
750
|
+
async onPasswordReset(user: User): Promise<void> {
|
|
751
|
+
// Send security alert to user's phone
|
|
752
|
+
await this.smsService.send(user.phoneNumber, 'Your password was just changed');
|
|
753
|
+
|
|
754
|
+
// Log to analytics
|
|
755
|
+
await this.analytics.track(user.id, 'password_reset_completed');
|
|
756
|
+
|
|
757
|
+
// Revoke API keys (if your app has them)
|
|
758
|
+
await this.apiKeyService.revokeAllKeys(user.id);
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
```
|
|
762
|
+
|
|
373
763
|
## GraphQL API
|
|
374
764
|
|
|
375
765
|
The package provides a complete GraphQL API:
|
|
@@ -426,6 +816,23 @@ mutation VerifyPhone($input: VerifyPhoneInput!) {
|
|
|
426
816
|
}
|
|
427
817
|
}
|
|
428
818
|
|
|
819
|
+
# Password Reset Request
|
|
820
|
+
mutation RequestPasswordReset($input: RequestPasswordResetInput!) {
|
|
821
|
+
requestPasswordReset(input: $input) {
|
|
822
|
+
success
|
|
823
|
+
message
|
|
824
|
+
retryAfterSeconds
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
# Password Reset Confirmation
|
|
829
|
+
mutation ResetPassword($input: ResetPasswordInput!) {
|
|
830
|
+
resetPassword(input: $input) {
|
|
831
|
+
success
|
|
832
|
+
message
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
|
|
429
836
|
# Google OAuth Account Linking
|
|
430
837
|
mutation LinkGoogleAccount($input: LinkGoogleAccountInput!) {
|
|
431
838
|
linkGoogleAccount(linkGoogleAccountInput: $input) {
|
|
@@ -561,6 +968,358 @@ export class MyAuthHooks implements IAuthLifecycleHooks {
|
|
|
561
968
|
- **Device enrollment**: Multiple device support
|
|
562
969
|
- **Challenge-response**: Prevents replay attacks
|
|
563
970
|
|
|
971
|
+
## Security Best Practices
|
|
972
|
+
|
|
973
|
+
### 🔐 Secret Management
|
|
974
|
+
|
|
975
|
+
**CRITICAL**: Never commit secrets to version control. Use environment variables and secret management systems.
|
|
976
|
+
|
|
977
|
+
**Required Secrets:**
|
|
978
|
+
|
|
979
|
+
1. **JWT_SECRET**:
|
|
980
|
+
- Generate: `openssl rand -base64 64`
|
|
981
|
+
- Minimum: 32 bytes (256 bits)
|
|
982
|
+
- Rotate: Every 90 days or on suspected compromise
|
|
983
|
+
- Store: Environment variables, AWS Secrets Manager, HashiCorp Vault, etc.
|
|
984
|
+
|
|
985
|
+
2. **ENCRYPTION_KEY** (for OAuth):
|
|
986
|
+
- Generate: `openssl rand -hex 32` (produces 64-character hex string)
|
|
987
|
+
- Required length: Exactly 32 bytes (64 hex characters)
|
|
988
|
+
- Purpose: AES-256-GCM encryption for OAuth access tokens at rest
|
|
989
|
+
- Auto-generated if not provided, but **provide explicitly in production** for consistency across instances
|
|
990
|
+
|
|
991
|
+
3. **OAuth Secrets**:
|
|
992
|
+
- Never expose in client-side code
|
|
993
|
+
- Use environment-specific secrets (dev/staging/prod)
|
|
994
|
+
- Rotate on suspected compromise
|
|
995
|
+
|
|
996
|
+
**Example `.env` file** (never commit this file):
|
|
997
|
+
```bash
|
|
998
|
+
# Generate with: openssl rand -base64 64
|
|
999
|
+
JWT_SECRET=your_random_64_byte_base64_secret
|
|
1000
|
+
|
|
1001
|
+
# Generate with: openssl rand -hex 32
|
|
1002
|
+
ENCRYPTION_KEY=your_64_character_hex_string
|
|
1003
|
+
|
|
1004
|
+
# OAuth secrets from provider dashboards
|
|
1005
|
+
GOOGLE_CLIENT_SECRET=...
|
|
1006
|
+
FACEBOOK_CLIENT_SECRET=...
|
|
1007
|
+
|
|
1008
|
+
# Third-party API keys
|
|
1009
|
+
SENDGRID_API_KEY=...
|
|
1010
|
+
TWILIO_AUTH_TOKEN=...
|
|
1011
|
+
```
|
|
1012
|
+
|
|
1013
|
+
**Production Secret Management:**
|
|
1014
|
+
|
|
1015
|
+
```typescript
|
|
1016
|
+
// ❌ BAD: Hard-coded secrets
|
|
1017
|
+
AuthModule.forRootAsync({
|
|
1018
|
+
useFactory: () => ({
|
|
1019
|
+
jwtSecret: 'my-secret-key', // NEVER DO THIS
|
|
1020
|
+
}),
|
|
1021
|
+
});
|
|
1022
|
+
|
|
1023
|
+
// ✅ GOOD: Environment variables
|
|
1024
|
+
AuthModule.forRootAsync({
|
|
1025
|
+
inject: [ConfigService],
|
|
1026
|
+
useFactory: (config: ConfigService) => ({
|
|
1027
|
+
jwtSecret: config.get<string>('JWT_SECRET'), // Read from env
|
|
1028
|
+
encryptionKey: config.get<string>('ENCRYPTION_KEY'),
|
|
1029
|
+
}),
|
|
1030
|
+
});
|
|
1031
|
+
|
|
1032
|
+
// ✅ BETTER: Validation with config module
|
|
1033
|
+
import { ConfigModule } from '@nestjs/config';
|
|
1034
|
+
import * as Joi from 'joi';
|
|
1035
|
+
|
|
1036
|
+
ConfigModule.forRoot({
|
|
1037
|
+
validationSchema: Joi.object({
|
|
1038
|
+
JWT_SECRET: Joi.string().min(32).required(),
|
|
1039
|
+
ENCRYPTION_KEY: Joi.string().length(64).pattern(/^[0-9a-f]{64}$/).required(),
|
|
1040
|
+
GOOGLE_CLIENT_SECRET: Joi.string().when('GOOGLE_CLIENT_ID', {
|
|
1041
|
+
is: Joi.exist(),
|
|
1042
|
+
then: Joi.required(),
|
|
1043
|
+
}),
|
|
1044
|
+
}),
|
|
1045
|
+
});
|
|
1046
|
+
```
|
|
1047
|
+
|
|
1048
|
+
### 🛡️ Token Configuration
|
|
1049
|
+
|
|
1050
|
+
**Access Token Best Practices:**
|
|
1051
|
+
|
|
1052
|
+
```typescript
|
|
1053
|
+
AuthModule.forRootAsync({
|
|
1054
|
+
useFactory: (config) => ({
|
|
1055
|
+
// Short-lived access tokens (minimize damage if compromised)
|
|
1056
|
+
jwtExpiresIn: '15m', // Default: 15 minutes
|
|
1057
|
+
|
|
1058
|
+
// For mobile apps with poor connectivity, consider 1h max
|
|
1059
|
+
// jwtExpiresIn: '1h', // Longer for mobile, but less secure
|
|
1060
|
+
|
|
1061
|
+
// NEVER use long-lived access tokens
|
|
1062
|
+
// jwtExpiresIn: '30d', // ❌ INSECURE
|
|
1063
|
+
}),
|
|
1064
|
+
});
|
|
1065
|
+
```
|
|
1066
|
+
|
|
1067
|
+
**Refresh Token Best Practices:**
|
|
1068
|
+
|
|
1069
|
+
```typescript
|
|
1070
|
+
AuthModule.forRootAsync({
|
|
1071
|
+
useFactory: (config) => ({
|
|
1072
|
+
// Balance between security and user experience
|
|
1073
|
+
refreshTokenExpiresIn: '30d', // Default: 30 days
|
|
1074
|
+
|
|
1075
|
+
// For high-security applications, use shorter expiration
|
|
1076
|
+
// refreshTokenExpiresIn: '7d', // Re-authenticate weekly
|
|
1077
|
+
|
|
1078
|
+
// For consumer apps, longer is acceptable
|
|
1079
|
+
// refreshTokenExpiresIn: '90d', // Re-authenticate quarterly
|
|
1080
|
+
}),
|
|
1081
|
+
});
|
|
1082
|
+
```
|
|
1083
|
+
|
|
1084
|
+
**Token Rotation**: This package automatically rotates refresh tokens on each use. Old tokens are invalidated to prevent replay attacks.
|
|
1085
|
+
|
|
1086
|
+
### 🔒 Password Policy
|
|
1087
|
+
|
|
1088
|
+
**Enforce Strong Passwords:**
|
|
1089
|
+
|
|
1090
|
+
The package includes default password validation (8+ characters, uppercase, lowercase, number, special character). For stronger policies:
|
|
1091
|
+
|
|
1092
|
+
```typescript
|
|
1093
|
+
import { InputType, Field } from '@nestjs/graphql';
|
|
1094
|
+
import { IsEmail, IsString, MinLength, MaxLength, Matches } from 'class-validator';
|
|
1095
|
+
|
|
1096
|
+
@InputType()
|
|
1097
|
+
export class SignupInput {
|
|
1098
|
+
@Field()
|
|
1099
|
+
@IsEmail({}, { message: 'Please provide a valid email address' })
|
|
1100
|
+
email: string;
|
|
1101
|
+
|
|
1102
|
+
@Field()
|
|
1103
|
+
@IsString()
|
|
1104
|
+
@MinLength(12, { message: 'Password must be at least 12 characters' })
|
|
1105
|
+
@MaxLength(128, { message: 'Password must not exceed 128 characters' })
|
|
1106
|
+
@Matches(
|
|
1107
|
+
/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]+$/,
|
|
1108
|
+
{ message: 'Password must contain uppercase, lowercase, number, and special character' }
|
|
1109
|
+
)
|
|
1110
|
+
password: string;
|
|
1111
|
+
}
|
|
1112
|
+
```
|
|
1113
|
+
|
|
1114
|
+
**Additional Recommendations:**
|
|
1115
|
+
- ✅ Check against common password lists (e.g., `have-i-been-pwned` API)
|
|
1116
|
+
- ✅ Implement password history (prevent reuse of last 5 passwords)
|
|
1117
|
+
- ✅ Require password change on first login (for admin-created accounts)
|
|
1118
|
+
- ✅ Implement password expiration (60-90 days for high-security apps)
|
|
1119
|
+
|
|
1120
|
+
### ⏱️ Rate Limiting
|
|
1121
|
+
|
|
1122
|
+
**Configure Throttling:**
|
|
1123
|
+
|
|
1124
|
+
```typescript
|
|
1125
|
+
import { ThrottlerModule } from '@nestjs/throttler';
|
|
1126
|
+
|
|
1127
|
+
@Module({
|
|
1128
|
+
imports: [
|
|
1129
|
+
// Global rate limiting
|
|
1130
|
+
ThrottlerModule.forRoot([{
|
|
1131
|
+
ttl: 60000, // 60 seconds
|
|
1132
|
+
limit: 100, // 100 requests per minute
|
|
1133
|
+
}]),
|
|
1134
|
+
|
|
1135
|
+
AuthModule.forRootAsync({
|
|
1136
|
+
// Auth-specific throttling is handled per-mutation
|
|
1137
|
+
// See AuthResolver for @Throttle() decorators
|
|
1138
|
+
}),
|
|
1139
|
+
],
|
|
1140
|
+
})
|
|
1141
|
+
```
|
|
1142
|
+
|
|
1143
|
+
**Per-Endpoint Throttling** (already implemented in BaseAuthResolver):
|
|
1144
|
+
- Login: 5 requests/minute (prevent brute force)
|
|
1145
|
+
- Signup: 5 requests/minute (prevent spam)
|
|
1146
|
+
- Refresh: 10 requests/minute (allow frequent refreshes)
|
|
1147
|
+
- Verification codes: 3 requests/minute (prevent SMS/email bombing)
|
|
1148
|
+
|
|
1149
|
+
**Brute Force Protection**: Enabled by default - 5 failed login attempts = 15-minute account lockout.
|
|
1150
|
+
|
|
1151
|
+
### 🌐 HTTPS/TLS Requirements
|
|
1152
|
+
|
|
1153
|
+
**PRODUCTION REQUIREMENT**: All authentication endpoints MUST use HTTPS.
|
|
1154
|
+
|
|
1155
|
+
```typescript
|
|
1156
|
+
// Production enforcement example
|
|
1157
|
+
import { NestFactory } from '@nestjs/core';
|
|
1158
|
+
import { AppModule } from './app.module';
|
|
1159
|
+
|
|
1160
|
+
async function bootstrap() {
|
|
1161
|
+
const app = await NestFactory.create(AppModule);
|
|
1162
|
+
|
|
1163
|
+
// Enforce HTTPS in production
|
|
1164
|
+
if (process.env.NODE_ENV === 'production') {
|
|
1165
|
+
app.use((req, res, next) => {
|
|
1166
|
+
if (!req.secure && req.headers['x-forwarded-proto'] !== 'https') {
|
|
1167
|
+
return res.redirect(301, `https://${req.headers.host}${req.url}`);
|
|
1168
|
+
}
|
|
1169
|
+
next();
|
|
1170
|
+
});
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
await app.listen(3000);
|
|
1174
|
+
}
|
|
1175
|
+
bootstrap();
|
|
1176
|
+
```
|
|
1177
|
+
|
|
1178
|
+
**OAuth Callback URLs**: Must be HTTPS in production (required by Google/Facebook).
|
|
1179
|
+
|
|
1180
|
+
### 📊 Security Logging
|
|
1181
|
+
|
|
1182
|
+
**Implement Custom Security Logger** for production monitoring:
|
|
1183
|
+
|
|
1184
|
+
```typescript
|
|
1185
|
+
import { Injectable } from '@nestjs/common';
|
|
1186
|
+
import { IAuthLogger, SecurityEvent } from '@ambushsoftworks/nestjs-auth-graphql';
|
|
1187
|
+
|
|
1188
|
+
@Injectable()
|
|
1189
|
+
export class ProductionAuthLogger implements IAuthLogger {
|
|
1190
|
+
constructor(
|
|
1191
|
+
private readonly datadogClient: DatadogClient,
|
|
1192
|
+
private readonly slackNotifier: SlackNotifier,
|
|
1193
|
+
) {}
|
|
1194
|
+
|
|
1195
|
+
log(event: SecurityEvent, metadata: Record<string, any>) {
|
|
1196
|
+
// Log to centralized logging service
|
|
1197
|
+
this.datadogClient.log({
|
|
1198
|
+
level: 'info',
|
|
1199
|
+
message: event,
|
|
1200
|
+
tags: ['auth', 'security'],
|
|
1201
|
+
...metadata,
|
|
1202
|
+
});
|
|
1203
|
+
|
|
1204
|
+
// Alert on suspicious events
|
|
1205
|
+
if (event === SecurityEvent.TOKEN_REUSE_DETECTED) {
|
|
1206
|
+
this.slackNotifier.send(`⚠️ Security Alert: Token reuse detected for user ${metadata.userId}`);
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
error(message: string, trace?: string, context?: string) {
|
|
1211
|
+
this.datadogClient.error({ message, trace, context });
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
warn(message: string, context?: string) {
|
|
1215
|
+
this.datadogClient.warn({ message, context });
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
debug(message: string, context?: string) {
|
|
1219
|
+
// Only in development
|
|
1220
|
+
if (process.env.NODE_ENV === 'development') {
|
|
1221
|
+
this.datadogClient.debug({ message, context });
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
verbose(message: string, context?: string) {
|
|
1226
|
+
// Only in development
|
|
1227
|
+
if (process.env.NODE_ENV === 'development') {
|
|
1228
|
+
this.datadogClient.log({ level: 'verbose', message, context });
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
```
|
|
1233
|
+
|
|
1234
|
+
**Critical Events to Monitor:**
|
|
1235
|
+
- `TOKEN_REUSE_DETECTED`: Possible security breach
|
|
1236
|
+
- `ACCOUNT_LOCKED`: High failed login attempts
|
|
1237
|
+
- `LOGIN_FAILURE`: Pattern analysis for attacks
|
|
1238
|
+
- `VERIFICATION_CODE_FAILED`: Potential brute force on codes
|
|
1239
|
+
|
|
1240
|
+
### 🚀 Production Deployment
|
|
1241
|
+
|
|
1242
|
+
**Environment Separation:**
|
|
1243
|
+
|
|
1244
|
+
```bash
|
|
1245
|
+
# Development (.env.development)
|
|
1246
|
+
NODE_ENV=development
|
|
1247
|
+
JWT_SECRET=dev_secret_key
|
|
1248
|
+
ENCRYPTION_KEY=dev_encryption_key
|
|
1249
|
+
|
|
1250
|
+
# Staging (.env.staging)
|
|
1251
|
+
NODE_ENV=staging
|
|
1252
|
+
JWT_SECRET=staging_secret_key_different_from_dev
|
|
1253
|
+
ENCRYPTION_KEY=staging_encryption_key_different_from_dev
|
|
1254
|
+
|
|
1255
|
+
# Production (.env.production)
|
|
1256
|
+
NODE_ENV=production
|
|
1257
|
+
JWT_SECRET=production_secret_key_from_secrets_manager
|
|
1258
|
+
ENCRYPTION_KEY=production_encryption_key_from_secrets_manager
|
|
1259
|
+
```
|
|
1260
|
+
|
|
1261
|
+
**Multi-Instance Deployment** (Load Balanced):
|
|
1262
|
+
- See "Production Deployment" section in CLAUDE.md for cache limitations
|
|
1263
|
+
- Use Redis for shared refresh token cache across instances
|
|
1264
|
+
- OR configure sticky sessions on load balancer
|
|
1265
|
+
- OR accept 10-second grace period limitation for most apps
|
|
1266
|
+
|
|
1267
|
+
**Security Checklist:**
|
|
1268
|
+
- ✅ HTTPS enforced (no HTTP in production)
|
|
1269
|
+
- ✅ Secrets managed via environment variables or secret manager
|
|
1270
|
+
- ✅ CORS configured to allow only trusted domains
|
|
1271
|
+
- ✅ Rate limiting enabled globally
|
|
1272
|
+
- ✅ Security logging to centralized service
|
|
1273
|
+
- ✅ Database connections use TLS
|
|
1274
|
+
- ✅ OAuth callback URLs whitelisted
|
|
1275
|
+
- ✅ ValidationPipe enabled globally
|
|
1276
|
+
- ✅ Helmet middleware for HTTP security headers
|
|
1277
|
+
- ✅ CSRF protection enabled (built-in for OAuth)
|
|
1278
|
+
|
|
1279
|
+
**Security Headers Example:**
|
|
1280
|
+
|
|
1281
|
+
```typescript
|
|
1282
|
+
import helmet from 'helmet';
|
|
1283
|
+
|
|
1284
|
+
async function bootstrap() {
|
|
1285
|
+
const app = await NestFactory.create(AppModule);
|
|
1286
|
+
|
|
1287
|
+
// Security headers
|
|
1288
|
+
app.use(helmet({
|
|
1289
|
+
contentSecurityPolicy: {
|
|
1290
|
+
directives: {
|
|
1291
|
+
defaultSrc: ["'self'"],
|
|
1292
|
+
styleSrc: ["'self'", "'unsafe-inline'"],
|
|
1293
|
+
scriptSrc: ["'self'"],
|
|
1294
|
+
},
|
|
1295
|
+
},
|
|
1296
|
+
hsts: {
|
|
1297
|
+
maxAge: 31536000,
|
|
1298
|
+
includeSubDomains: true,
|
|
1299
|
+
preload: true,
|
|
1300
|
+
},
|
|
1301
|
+
}));
|
|
1302
|
+
|
|
1303
|
+
await app.listen(3000);
|
|
1304
|
+
}
|
|
1305
|
+
```
|
|
1306
|
+
|
|
1307
|
+
### 🔍 Security Audits
|
|
1308
|
+
|
|
1309
|
+
**Regular Security Practices:**
|
|
1310
|
+
1. **Dependency Scanning**: `npm audit` (weekly in CI/CD)
|
|
1311
|
+
2. **Secret Scanning**: Use tools like GitGuardian, TruffleHog
|
|
1312
|
+
3. **Penetration Testing**: Quarterly security assessments
|
|
1313
|
+
4. **Log Review**: Weekly review of security event logs
|
|
1314
|
+
5. **Incident Response**: Document and practice breach response procedures
|
|
1315
|
+
|
|
1316
|
+
**Monitoring Alerts:**
|
|
1317
|
+
- Failed login spike (>100/hour)
|
|
1318
|
+
- Account lockout spike (>10/hour)
|
|
1319
|
+
- Token reuse detection (any occurrence)
|
|
1320
|
+
- Unusual geographic login patterns
|
|
1321
|
+
- Multiple verification code failures
|
|
1322
|
+
|
|
564
1323
|
## Database Schema Requirements
|
|
565
1324
|
|
|
566
1325
|
This package expects specific database tables to implement the repository interfaces. You have two options:
|