Skip to content

feat: add authentication provider abstraction#4

Open
aabrius wants to merge 2 commits into
masterfrom
feature/auth-provider-abstraction
Open

feat: add authentication provider abstraction#4
aabrius wants to merge 2 commits into
masterfrom
feature/auth-provider-abstraction

Conversation

@aabrius

@aabrius aabrius commented Dec 7, 2025

Copy link
Copy Markdown
Contributor

Summary

  • Implements Google OAuth provider with ID token verification
  • Adds self-issued JWT service for internal token generation
  • Creates JWT strategy for Passport authentication
  • Adds 43 unit tests covering all auth components

Test plan

  • Run npm test -- --testPathPattern="google.provider|jwt.service|self-issued-jwt.strategy" to verify all tests pass
  • Manual testing with Google OAuth flow

🤖 Generated with Claude Code

Summary by CodeRabbit

Release Notes

  • New Features
    • Added Google OAuth authentication support for seamless user login
    • Introduced JWT-based token authentication with access and refresh token generation
    • Added new OAuth endpoint for third-party identity provider integration
    • Implemented secure token signing and verification mechanisms

✏️ Tip: You can customize this high-level summary in your review settings.

aabrius and others added 2 commits December 6, 2025 22:08
- Create IdentityProvider interface for OAuth provider abstraction
- Add GoogleProvider using google-auth-library
- Add JwtTokenService for self-managed RS256 JWT generation
- Add SelfIssuedJwtStrategy for Passport integration
- Add /users/oauth/google endpoint for Google sign-in
- Add key generation script (npm run generate:keys)
- Update auth module with optional Google provider
- Update UsersService with loginWithGoogle method

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
- Add GoogleProvider tests (12 tests):
  - Token verification
  - Error handling (missing email, null payload)
  - Provider ID formatting
  - Missing GOOGLE_CLIENT_ID validation

- Add JwtTokenService tests (24 tests):
  - Token generation (access and refresh)
  - Token verification and expiration
  - RS256 signing algorithm
  - Key loading and availability
  - ExpiresIn parsing (seconds, minutes, hours, days)

- Add SelfIssuedJwtStrategy tests (7 tests):
  - Payload validation
  - Role preservation
  - Email handling

Total: 43 tests for auth abstraction layer

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Dec 7, 2025

Copy link
Copy Markdown

Walkthrough

This PR introduces JWT token service with RSA-based signing, Google OAuth provider integration, and supporting infrastructure. Changes include environment variable configuration, key generation script, JWT strategies, Google provider implementation, and updates to the users module to support Google OAuth login.

Changes

Cohort / File(s) Summary
Configuration & Dependencies
apps/backend/.env.example, apps/backend/package.json
Added environment variables for Google OAuth (GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET) and JWT configuration (JWT_ISSUER, JWT_PRIVATE_KEY_PATH, JWT_PUBLIC_KEY_PATH, JWT_EXPIRES_IN). Added npm dependencies: google-auth-library, jsonwebtoken; added generate:keys script; updated @types/jsonwebtoken to ^9.0.10.
Key Generation Infrastructure
apps/backend/scripts/generate-keys.sh
New shell script to generate RSA 2048-bit key pairs for JWT signing with appropriate file permissions (600 for private, 644 for public) and configuration hints.
JWT Token Service
apps/backend/src/auth/jwt/jwt.service.ts, apps/backend/src/auth/jwt/jwt.service.spec.ts
New JwtTokenService providing RS256 token generation, verification, and parsing with lazy key loading from configured file paths. Includes comprehensive test coverage for token generation, verification, refresh tokens, and error handling.
Self-Issued JWT Strategy
apps/backend/src/auth/jwt/self-issued-jwt.strategy.ts, apps/backend/src/auth/jwt/self-issued-jwt.strategy.spec.ts
New Passport strategy for validating self-issued RS256 JWTs with configurable issuer and public key path. Maps JWT payload to ValidatedUser object. Includes full test suite.
JWT Module Barrel Export
apps/backend/src/auth/jwt/index.ts
New index file consolidating exports from jwt.service and self-issued-jwt.strategy.
Identity Provider Infrastructure
apps/backend/src/auth/providers/identity-provider.interface.ts, apps/backend/src/auth/providers/index.ts
New interfaces defining IdentityProvider abstraction and IdentityProviderUser data structure for OAuth providers. Barrel export file aggregates provider module exports.
Google OAuth Provider
apps/backend/src/auth/providers/google.provider.ts, apps/backend/src/auth/providers/google.provider.spec.ts
New GoogleProvider implementing IdentityProvider for Google ID token verification using google-auth-library. Extracts and standardizes user information. Comprehensive test coverage included.
Authentication Module Integration
apps/backend/src/auth/auth.module.ts
Extended AuthModule to include JwtTokenService, SelfIssuedJwtStrategy, and GoogleProvider (with factory-based conditional initialization). Exports JWT and Google services for module-wide access.
Users Module Google OAuth
apps/backend/src/modules/users/dto/google-oauth.dto.ts, apps/backend/src/modules/users/users.controller.ts, apps/backend/src/modules/users/users.service.ts
Added GoogleOAuthDto and OAuthTokenResponse DTOs; new POST /oauth/google endpoint in controller; new loginWithGoogle method in UsersService handling token verification, user lookup/creation, and JWT token pair generation. GoogleProvider and JwtTokenService injected as optional dependencies.

Sequence Diagram(s)

sequenceDiagram
    actor Client
    participant Controller as UsersController
    participant Service as UsersService
    participant GoogleProvider
    participant Database as DB
    participant JwtService as JwtTokenService

    Client->>Controller: POST /oauth/google<br/>(idToken)
    Controller->>Service: loginWithGoogle(idToken)
    
    Service->>GoogleProvider: verifyToken(idToken)
    GoogleProvider-->>Service: IdentityProviderUser<br/>(providerId, email, etc.)
    
    alt User Exists
        Service->>Database: findByGoogleProviderId
        Database-->>Service: User
        Service->>Database: Update user (status, provider IDs, image)
    else User Not Found
        Service->>Database: Create new user
        Database-->>Service: User
    end
    
    Service->>Database: findUserAccount(userId)
    Database-->>Service: UserAccount
    
    Service->>JwtService: generateTokenPair(sub, email,<br/>accountId, role)
    JwtService-->>Service: TokenPair<br/>(accessToken, refreshToken)
    
    Service-->>Controller: OAuthTokenResponse<br/>(tokens + user info)
    Controller-->>Client: 201 Created<br/>(tokens + user info)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45–60 minutes

Key areas requiring focused attention:

  • JWT Security: Verify RSA key handling, expiration parsing logic, and token verification against public key—especially the issuer validation in SelfIssuedJwtStrategy
  • Google OAuth Integration: Confirm OAuth2Client initialization, idToken verification flow, and IdentityProviderUser mapping
  • Optional Dependency Injection: Review GoogleProvider and JwtTokenService as optional dependencies in UsersService—verify null-check handling in loginWithGoogle
  • Token Generation & Expiration: Validate parseExpiresIn utility for human-friendly duration parsing (e.g., "30s", "5m", "2d") and ensure refresh token expiry exceeds access token expiry
  • User Lifecycle: Examine user creation, update, and account assignment flow in loginWithGoogle, including role determination and provider ID persistence
  • Test Coverage: All tests are present; verify they adequately cover edge cases (missing keys, malformed tokens, network errors, etc.)

Suggested labels

size/XXL, feature/authentication, feature/oauth, security/jwt

Suggested reviewers

  • filipecrosk

Poem

🐰 Token keys dance in pairs so fine,
Google's sign-in now does align,
RS256 whispers secure and true,
Fresh rabbits login straight on through! 🔐✨

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'feat: add authentication provider abstraction' accurately describes the main change: introducing authentication provider abstraction with Google OAuth implementation and JWT-based authentication infrastructure.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/auth-provider-abstraction

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (9)
apps/backend/scripts/generate-keys.sh (1)

24-24: Consider 4096-bit keys for enhanced future-proofing.

While 2048-bit RSA keys are currently secure and widely accepted, 4096-bit keys offer better long-term security and are recommended for new implementations.

Apply this diff to use 4096-bit keys:

-openssl genrsa -out "$KEYS_DIR/private.pem" 2048
+openssl genrsa -out "$KEYS_DIR/private.pem" 4096
apps/backend/src/auth/jwt/self-issued-jwt.strategy.ts (2)

31-42: Silent failure hides key loading issues.

The empty catch block silently swallows file read errors. Consider adding a warning log to help debugging when keys fail to load, similar to JwtTokenService.loadKeys() which logs a warning.

     if (publicKeyPath) {
       try {
         const basePath = process.cwd();
         publicKey = fs.readFileSync(
           path.resolve(basePath, publicKeyPath),
           'utf8',
         );
-      } catch {
+      } catch (error) {
         // Keys not available - strategy will fail validation
+        console.warn(
+          'RSA public key not found for self-jwt strategy.',
+          'Authentication with self-issued tokens will fail.',
+        );
       }
     }

25-50: Key loading logic is duplicated with JwtTokenService.

Both SelfIssuedJwtStrategy and JwtTokenService independently load the public key from the same path. Consider injecting JwtTokenService and using getPublicKey() to avoid duplication and ensure consistency.

This would centralize key management:

constructor(
  private readonly configService: ConfigService,
  private readonly jwtTokenService: JwtTokenService,
) {
  const issuer = configService.get<string>('JWT_ISSUER') || 'https://api.boilerplate.local';
  
  let publicKey: string | undefined;
  if (jwtTokenService.isAvailable()) {
    publicKey = jwtTokenService.getPublicKey();
  }
  
  super({
    jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
    secretOrKey: publicKey || 'placeholder-key-will-fail',
    algorithms: ['RS256'],
    issuer,
  });
}
apps/backend/src/auth/providers/google.provider.ts (1)

14-22: Avoid re-reading clientId from config on every token verification.

The GOOGLE_CLIENT_ID is already validated in the constructor but re-fetched in verifyToken. Store it as a class member for consistency and minor performance improvement.

 @Injectable()
 export class GoogleProvider implements IdentityProvider {
   readonly providerName = 'google';
   private client: OAuth2Client;
+  private readonly clientId: string;

   constructor(private config: ConfigService) {
     const clientId = config.get<string>('GOOGLE_CLIENT_ID');
     if (!clientId) {
       throw new Error(
         'GOOGLE_CLIENT_ID is required when using GoogleProvider',
       );
     }
+    this.clientId = clientId;
     this.client = new OAuth2Client(clientId);
   }

   async verifyToken(idToken: string): Promise<IdentityProviderUser> {
     const ticket = await this.client.verifyIdToken({
       idToken,
-      audience: this.config.get<string>('GOOGLE_CLIENT_ID'),
+      audience: this.clientId,
     });

Also applies to: 30-34

apps/backend/src/auth/jwt/jwt.service.spec.ts (1)

24-32: Consider typing mockUser more explicitly.

Using as any bypasses type checking. Consider using Partial<User> or a proper mock factory to maintain type safety in tests.

-  const mockUser = {
+  const mockUser: Partial<User> & { id: string; email: string } = {
     id: 'user-123',
     email: 'test@example.com',
     name: 'Test User',
     status: 'accepted',
     providerIds: [],
     isSuperAdmin: false,
     profileImage: null,
-  } as any;
+  };

Then cast only where needed in method calls.

apps/backend/src/auth/jwt/jwt.service.ts (3)

56-60: Use NestJS Logger instead of console.warn.

Using console.warn is inconsistent with NestJS patterns. The framework's Logger provides structured logging with context.

+import { Injectable, OnModuleInit, Logger } from '@nestjs/common';
-import { Injectable, OnModuleInit } from '@nestjs/common';

 @Injectable()
 export class JwtTokenService implements OnModuleInit {
+  private readonly logger = new Logger(JwtTokenService.name);
   private privateKey: string | null = null;
   // ...

   private loadKeys(): void {
     // ...
       } catch (error) {
-        console.warn(
+        this.logger.warn(
           'RSA keys not found. Self-issued JWT will not be available.',
-          'Run `npm run generate:keys` to create RSA key pair.',
         );
+        this.logger.warn('Run `npm run generate:keys` to create RSA key pair.');
       }
   }

102-106: Redundant non-null assertions after null checks.

Lines 102 and 125 use this.privateKey! but the null check on lines 91-93 and 116-118 already guarantees non-null. TypeScript should infer this, but if not, consider extracting to a local variable.

   generateAccessToken(user: User, accountId: string, role: Role): string {
     if (!this.privateKey) {
       throw new Error('Private key not loaded. Run `npm run generate:keys`.');
     }
+    const privateKey = this.privateKey;

     const payload = { /* ... */ };

-    return jwt.sign(payload, this.privateKey!, {
+    return jwt.sign(payload, privateKey, {
       algorithm: 'RS256',
       // ...
     });
   }

Also applies to: 125-129


125-129: Refresh token expiry is hardcoded.

The access token expiry is configurable via JWT_EXPIRES_IN, but refresh token expiry is hardcoded to '7d'. Consider making this configurable for flexibility.

 constructor(private config: ConfigService) {
   this.issuer = config.get<string>('JWT_ISSUER') || 'https://api.boilerplate.local';
   this.expiresIn = config.get<string>('JWT_EXPIRES_IN') || '1h';
+  this.refreshExpiresIn = config.get<string>('JWT_REFRESH_EXPIRES_IN') || '7d';
 }
apps/backend/src/auth/providers/identity-provider.interface.ts (1)

1-59: Identity provider abstractions look good; consider naming the invitation result type

The interface shapes and comments are clear and should work well across providers. One small refinement: the inline object return type of sendInvitation could be extracted to a named interface (e.g., IdentityProviderInvitationResult) to better follow the “prefer interfaces for object definitions” guideline and keep this contract reusable and self-documenting.

export interface IdentityProviderInvitationResult {
  userId: string;
  ticket?: string;
}

export interface IdentityProvider {
  // ...
  sendInvitation?(
    email: string,
    name: string,
  ): Promise<IdentityProviderInvitationResult>;
  // ...
}

This is optional and can be deferred if you prefer to keep the surface minimal for now.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 793cb77 and 1e851f7.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (16)
  • apps/backend/.env.example (1 hunks)
  • apps/backend/package.json (3 hunks)
  • apps/backend/scripts/generate-keys.sh (1 hunks)
  • apps/backend/src/auth/auth.module.ts (1 hunks)
  • apps/backend/src/auth/jwt/index.ts (1 hunks)
  • apps/backend/src/auth/jwt/jwt.service.spec.ts (1 hunks)
  • apps/backend/src/auth/jwt/jwt.service.ts (1 hunks)
  • apps/backend/src/auth/jwt/self-issued-jwt.strategy.spec.ts (1 hunks)
  • apps/backend/src/auth/jwt/self-issued-jwt.strategy.ts (1 hunks)
  • apps/backend/src/auth/providers/google.provider.spec.ts (1 hunks)
  • apps/backend/src/auth/providers/google.provider.ts (1 hunks)
  • apps/backend/src/auth/providers/identity-provider.interface.ts (1 hunks)
  • apps/backend/src/auth/providers/index.ts (1 hunks)
  • apps/backend/src/modules/users/dto/google-oauth.dto.ts (1 hunks)
  • apps/backend/src/modules/users/users.controller.ts (2 hunks)
  • apps/backend/src/modules/users/users.service.ts (4 hunks)
🧰 Additional context used
📓 Path-based instructions (13)
**/*.{js,ts,tsx,jsx,css,scss,json,yaml,yml,md}

📄 CodeRabbit inference engine (.cursor/rules/cursor.json)

**/*.{js,ts,tsx,jsx,css,scss,json,yaml,yml,md}: Maximum line length should not exceed 100 characters
Trim trailing whitespace from all lines
Insert a final newline at the end of every file

Files:

  • apps/backend/src/auth/providers/index.ts
  • apps/backend/src/modules/users/dto/google-oauth.dto.ts
  • apps/backend/src/auth/jwt/index.ts
  • apps/backend/src/auth/providers/identity-provider.interface.ts
  • apps/backend/src/auth/providers/google.provider.ts
  • apps/backend/src/modules/users/users.controller.ts
  • apps/backend/src/auth/providers/google.provider.spec.ts
  • apps/backend/src/auth/jwt/jwt.service.spec.ts
  • apps/backend/src/auth/jwt/self-issued-jwt.strategy.ts
  • apps/backend/src/auth/jwt/jwt.service.ts
  • apps/backend/package.json
  • apps/backend/src/auth/auth.module.ts
  • apps/backend/src/auth/jwt/self-issued-jwt.strategy.spec.ts
  • apps/backend/src/modules/users/users.service.ts
**/*.{js,ts,tsx,jsx,css,scss,json,yaml,yml}

📄 CodeRabbit inference engine (.cursor/rules/cursor.json)

Use 2-space indentation for all files

Files:

  • apps/backend/src/auth/providers/index.ts
  • apps/backend/src/modules/users/dto/google-oauth.dto.ts
  • apps/backend/src/auth/jwt/index.ts
  • apps/backend/src/auth/providers/identity-provider.interface.ts
  • apps/backend/src/auth/providers/google.provider.ts
  • apps/backend/src/modules/users/users.controller.ts
  • apps/backend/src/auth/providers/google.provider.spec.ts
  • apps/backend/src/auth/jwt/jwt.service.spec.ts
  • apps/backend/src/auth/jwt/self-issued-jwt.strategy.ts
  • apps/backend/src/auth/jwt/jwt.service.ts
  • apps/backend/package.json
  • apps/backend/src/auth/auth.module.ts
  • apps/backend/src/auth/jwt/self-issued-jwt.strategy.spec.ts
  • apps/backend/src/modules/users/users.service.ts
**/*

📄 CodeRabbit inference engine (.cursor/rules/cursor.json)

**/*: Use LF (line feed) as end-of-line character
Use UTF-8 character encoding for all files

Files:

  • apps/backend/src/auth/providers/index.ts
  • apps/backend/src/modules/users/dto/google-oauth.dto.ts
  • apps/backend/src/auth/jwt/index.ts
  • apps/backend/src/auth/providers/identity-provider.interface.ts
  • apps/backend/src/auth/providers/google.provider.ts
  • apps/backend/src/modules/users/users.controller.ts
  • apps/backend/src/auth/providers/google.provider.spec.ts
  • apps/backend/src/auth/jwt/jwt.service.spec.ts
  • apps/backend/src/auth/jwt/self-issued-jwt.strategy.ts
  • apps/backend/src/auth/jwt/jwt.service.ts
  • apps/backend/scripts/generate-keys.sh
  • apps/backend/package.json
  • apps/backend/src/auth/auth.module.ts
  • apps/backend/src/auth/jwt/self-issued-jwt.strategy.spec.ts
  • apps/backend/src/modules/users/users.service.ts
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (.cursor/rules/cursor.json)

**/*.{ts,tsx,js,jsx}: Use single quotes for string literals in TypeScript/JavaScript
Semicolons must be present at the end of statements in TypeScript/JavaScript
Use trailing commas in multiline constructs following ES5 convention
Enable bracket spacing in object literals (e.g., { foo: bar } instead of {foo: bar})
Omit parentheses around single arrow function parameters when possible

Files:

  • apps/backend/src/auth/providers/index.ts
  • apps/backend/src/modules/users/dto/google-oauth.dto.ts
  • apps/backend/src/auth/jwt/index.ts
  • apps/backend/src/auth/providers/identity-provider.interface.ts
  • apps/backend/src/auth/providers/google.provider.ts
  • apps/backend/src/modules/users/users.controller.ts
  • apps/backend/src/auth/providers/google.provider.spec.ts
  • apps/backend/src/auth/jwt/jwt.service.spec.ts
  • apps/backend/src/auth/jwt/self-issued-jwt.strategy.ts
  • apps/backend/src/auth/jwt/jwt.service.ts
  • apps/backend/src/auth/auth.module.ts
  • apps/backend/src/auth/jwt/self-issued-jwt.strategy.spec.ts
  • apps/backend/src/modules/users/users.service.ts
**/*.{ts,tsx,js,jsx,vue}

📄 CodeRabbit inference engine (.cursor/rules/etus-methodology.mdc)

**/*.{ts,tsx,js,jsx,vue}: All UI component imports must use @/components/ui/* from @ETUS Design System, fail with error if components are imported from outside this path
Do not create components outside @ETUS Design System

Files:

  • apps/backend/src/auth/providers/index.ts
  • apps/backend/src/modules/users/dto/google-oauth.dto.ts
  • apps/backend/src/auth/jwt/index.ts
  • apps/backend/src/auth/providers/identity-provider.interface.ts
  • apps/backend/src/auth/providers/google.provider.ts
  • apps/backend/src/modules/users/users.controller.ts
  • apps/backend/src/auth/providers/google.provider.spec.ts
  • apps/backend/src/auth/jwt/jwt.service.spec.ts
  • apps/backend/src/auth/jwt/self-issued-jwt.strategy.ts
  • apps/backend/src/auth/jwt/jwt.service.ts
  • apps/backend/src/auth/auth.module.ts
  • apps/backend/src/auth/jwt/self-issued-jwt.strategy.spec.ts
  • apps/backend/src/modules/users/users.service.ts
apps/backend/src/**/*.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/project-context.mdc)

All operations must include account context (accountId) for account isolation

Files:

  • apps/backend/src/auth/providers/index.ts
  • apps/backend/src/modules/users/dto/google-oauth.dto.ts
  • apps/backend/src/auth/jwt/index.ts
  • apps/backend/src/auth/providers/identity-provider.interface.ts
  • apps/backend/src/auth/providers/google.provider.ts
  • apps/backend/src/modules/users/users.controller.ts
  • apps/backend/src/auth/providers/google.provider.spec.ts
  • apps/backend/src/auth/jwt/jwt.service.spec.ts
  • apps/backend/src/auth/jwt/self-issued-jwt.strategy.ts
  • apps/backend/src/auth/jwt/jwt.service.ts
  • apps/backend/src/auth/auth.module.ts
  • apps/backend/src/auth/jwt/self-issued-jwt.strategy.spec.ts
  • apps/backend/src/modules/users/users.service.ts
apps/**/*.{vue,ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/project-context.mdc)

Implement comprehensive error handling scenarios in Vue components and backend services

Files:

  • apps/backend/src/auth/providers/index.ts
  • apps/backend/src/modules/users/dto/google-oauth.dto.ts
  • apps/backend/src/auth/jwt/index.ts
  • apps/backend/src/auth/providers/identity-provider.interface.ts
  • apps/backend/src/auth/providers/google.provider.ts
  • apps/backend/src/modules/users/users.controller.ts
  • apps/backend/src/auth/providers/google.provider.spec.ts
  • apps/backend/src/auth/jwt/jwt.service.spec.ts
  • apps/backend/src/auth/jwt/self-issued-jwt.strategy.ts
  • apps/backend/src/auth/jwt/jwt.service.ts
  • apps/backend/src/auth/auth.module.ts
  • apps/backend/src/auth/jwt/self-issued-jwt.strategy.spec.ts
  • apps/backend/src/modules/users/users.service.ts
**/*.{ts,tsx,d.ts}

📄 CodeRabbit inference engine (.cursor/rules/typescript.mdc)

**/*.{ts,tsx,d.ts}: Prefer interfaces over types for object definitions
Use type for unions, intersections, and mapped types
Avoid using any, prefer unknown for unknown types
Leverage TypeScript's built-in utility types
Use generics for reusable type patterns
Use PascalCase for type names and interfaces
Use camelCase for variables and functions
Use UPPER_CASE for constants
Use descriptive names with auxiliary verbs for boolean variables and functions (e.g., isLoading, hasError)
Prefix React component prop interfaces with 'Props' (e.g., ButtonProps)
Keep type definitions close to where they're used
Export types and interfaces from dedicated type files when shared
Use explicit return types for public functions
Use arrow functions for callbacks and methods
Implement proper error handling with custom error types
Use function overloads for complex type scenarios
Prefer async/await over Promises
Use readonly for immutable properties
Leverage discriminated unions for type safety
Use type guards for runtime type checking
Implement proper null checking
Avoid type assertions unless necessary
Create custom error types for domain-specific errors
Use Result types for operations that can fail
Use try-catch blocks with typed catch clauses
Handle Promise rejections properly

Files:

  • apps/backend/src/auth/providers/index.ts
  • apps/backend/src/modules/users/dto/google-oauth.dto.ts
  • apps/backend/src/auth/jwt/index.ts
  • apps/backend/src/auth/providers/identity-provider.interface.ts
  • apps/backend/src/auth/providers/google.provider.ts
  • apps/backend/src/modules/users/users.controller.ts
  • apps/backend/src/auth/providers/google.provider.spec.ts
  • apps/backend/src/auth/jwt/jwt.service.spec.ts
  • apps/backend/src/auth/jwt/self-issued-jwt.strategy.ts
  • apps/backend/src/auth/jwt/jwt.service.ts
  • apps/backend/src/auth/auth.module.ts
  • apps/backend/src/auth/jwt/self-issued-jwt.strategy.spec.ts
  • apps/backend/src/modules/users/users.service.ts
**/index.ts

📄 CodeRabbit inference engine (.cursor/rules/typescript.mdc)

Use barrel exports (index.ts) for organizing exports

Files:

  • apps/backend/src/auth/providers/index.ts
  • apps/backend/src/auth/jwt/index.ts
apps/backend/src/modules/**/dto/*.dto.ts

📄 CodeRabbit inference engine (CLAUDE.md)

Define DTOs with validation decorators in dto/ subdirectory

Files:

  • apps/backend/src/modules/users/dto/google-oauth.dto.ts
apps/backend/src/modules/**/*.controller.ts

📄 CodeRabbit inference engine (CLAUDE.md)

apps/backend/src/modules/**/*.controller.ts: Include API documentation with Swagger decorators on all controllers
Use JwtAuthGuard on all protected controller endpoints
Implement input validation with DTOs and class-validator in controllers

Files:

  • apps/backend/src/modules/users/users.controller.ts
apps/backend/src/**/*.spec.ts

📄 CodeRabbit inference engine (CLAUDE.md)

Include unit tests for all backend services using Jest

Files:

  • apps/backend/src/auth/providers/google.provider.spec.ts
  • apps/backend/src/auth/jwt/jwt.service.spec.ts
  • apps/backend/src/auth/jwt/self-issued-jwt.strategy.spec.ts
apps/backend/src/modules/**/*.service.ts

📄 CodeRabbit inference engine (CLAUDE.md)

apps/backend/src/modules/**/*.service.ts: All database operations must include account context filtering using accountId from CLS (Continuation Local Storage)
Always filter queries by accountId and deletedAt: IsNull() in where clauses for soft delete compatibility
Use save() instead of update() for audit compatibility in TypeORM operations
Implement proper error handling with custom exceptions in backend services

Files:

  • apps/backend/src/modules/users/users.service.ts
🧠 Learnings (10)
📚 Learning: 2025-11-28T18:14:14.256Z
Learnt from: CR
Repo: etusdigital/boilerplate PR: 0
File: .cursor/rules/typescript.mdc:0-0
Timestamp: 2025-11-28T18:14:14.256Z
Learning: Applies to **/index.ts : Use barrel exports (index.ts) for organizing exports

Applied to files:

  • apps/backend/src/auth/providers/index.ts
  • apps/backend/src/auth/jwt/index.ts
📚 Learning: 2025-11-28T18:14:14.256Z
Learnt from: CR
Repo: etusdigital/boilerplate PR: 0
File: .cursor/rules/typescript.mdc:0-0
Timestamp: 2025-11-28T18:14:14.256Z
Learning: Applies to **/*.{ts,tsx,d.ts} : Export types and interfaces from dedicated type files when shared

Applied to files:

  • apps/backend/src/auth/providers/index.ts
  • apps/backend/src/auth/jwt/index.ts
📚 Learning: 2025-11-28T18:13:02.946Z
Learnt from: CR
Repo: etusdigital/boilerplate PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-28T18:13:02.946Z
Learning: Applies to apps/backend/src/modules/**/*.controller.ts : Include API documentation with Swagger decorators on all controllers

Applied to files:

  • apps/backend/src/auth/providers/index.ts
  • apps/backend/src/modules/users/dto/google-oauth.dto.ts
  • apps/backend/src/auth/jwt/index.ts
  • apps/backend/src/modules/users/users.controller.ts
📚 Learning: 2025-11-28T18:13:02.946Z
Learnt from: CR
Repo: etusdigital/boilerplate PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-28T18:13:02.946Z
Learning: Applies to apps/frontend-{vue,react}/src/**/api/**/*.ts : Maintain account isolation with all API calls including account-id header via interceptors

Applied to files:

  • apps/backend/src/auth/providers/index.ts
  • apps/backend/src/auth/jwt/index.ts
📚 Learning: 2025-11-28T18:13:02.946Z
Learnt from: CR
Repo: etusdigital/boilerplate PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-28T18:13:02.946Z
Learning: Applies to apps/backend/src/modules/**/*.controller.ts : Use JwtAuthGuard on all protected controller endpoints

Applied to files:

  • apps/backend/src/auth/providers/index.ts
  • apps/backend/src/auth/jwt/index.ts
  • apps/backend/src/modules/users/users.controller.ts
  • apps/backend/src/auth/jwt/jwt.service.spec.ts
  • apps/backend/src/auth/jwt/self-issued-jwt.strategy.ts
  • apps/backend/src/auth/jwt/jwt.service.ts
  • apps/backend/src/auth/auth.module.ts
📚 Learning: 2025-11-28T18:13:02.946Z
Learnt from: CR
Repo: etusdigital/boilerplate PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-28T18:13:02.946Z
Learning: Applies to apps/backend/src/modules/**/dto/*.dto.ts : Define DTOs with validation decorators in dto/ subdirectory

Applied to files:

  • apps/backend/src/modules/users/dto/google-oauth.dto.ts
  • apps/backend/src/modules/users/users.controller.ts
  • apps/backend/src/modules/users/users.service.ts
📚 Learning: 2025-11-28T18:13:02.946Z
Learnt from: CR
Repo: etusdigital/boilerplate PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-28T18:13:02.946Z
Learning: Applies to apps/backend/src/modules/**/*.controller.ts : Implement input validation with DTOs and class-validator in controllers

Applied to files:

  • apps/backend/src/modules/users/dto/google-oauth.dto.ts
📚 Learning: 2025-11-28T18:13:02.946Z
Learnt from: CR
Repo: etusdigital/boilerplate PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-28T18:13:02.946Z
Learning: Applies to apps/backend/src/modules/**/ : Organize backend modules in feature structure: [entity].module.ts, [entity].controller.ts, [entity].service.ts, [entity].spec.ts, dto/

Applied to files:

  • apps/backend/src/modules/users/dto/google-oauth.dto.ts
  • apps/backend/src/auth/jwt/index.ts
📚 Learning: 2025-11-28T18:13:02.946Z
Learnt from: CR
Repo: etusdigital/boilerplate PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-28T18:13:02.946Z
Learning: Applies to apps/backend/src/**/*.spec.ts : Include unit tests for all backend services using Jest

Applied to files:

  • apps/backend/src/auth/jwt/index.ts
  • apps/backend/src/auth/providers/google.provider.spec.ts
  • apps/backend/src/auth/jwt/jwt.service.spec.ts
  • apps/backend/package.json
  • apps/backend/src/auth/jwt/self-issued-jwt.strategy.spec.ts
📚 Learning: 2025-11-28T18:13:58.953Z
Learnt from: CR
Repo: etusdigital/boilerplate PR: 0
File: .cursor/rules/project-context.mdc:0-0
Timestamp: 2025-11-28T18:13:58.953Z
Learning: Applies to apps/backend/src/**/{services,controllers}/**/*.ts : Validate account ownership before any CRUD operation

Applied to files:

  • apps/backend/src/modules/users/users.controller.ts
🧬 Code graph analysis (7)
apps/backend/src/auth/providers/google.provider.ts (1)
apps/backend/src/auth/providers/identity-provider.interface.ts (2)
  • IdentityProvider (18-59)
  • IdentityProviderUser (4-11)
apps/backend/src/modules/users/users.controller.ts (1)
apps/backend/src/modules/users/dto/google-oauth.dto.ts (2)
  • OAuthTokenResponse (14-30)
  • GoogleOAuthDto (4-12)
apps/backend/src/auth/jwt/self-issued-jwt.strategy.ts (1)
apps/backend/src/auth/jwt/jwt.service.ts (2)
  • Injectable (25-188)
  • JwtPayload (9-17)
apps/backend/src/auth/jwt/jwt.service.ts (1)
apps/backend/src/auth/jwt/self-issued-jwt.strategy.ts (1)
  • Injectable (20-64)
apps/backend/src/auth/auth.module.ts (2)
apps/backend/src/modules/users/users.module.ts (1)
  • Module (12-18)
apps/backend/src/app.module.ts (1)
  • Module (14-47)
apps/backend/src/auth/jwt/self-issued-jwt.strategy.spec.ts (1)
apps/backend/src/auth/jwt/jwt.service.ts (1)
  • JwtPayload (9-17)
apps/backend/src/modules/users/users.service.ts (1)
apps/backend/src/modules/users/dto/google-oauth.dto.ts (1)
  • OAuthTokenResponse (14-30)
🪛 dotenv-linter (4.0.0)
apps/backend/.env.example

[warning] 30-30: [UnorderedKey] The JWT_EXPIRES_IN key should go before the JWT_ISSUER key

(UnorderedKey)

🔇 Additional comments (15)
apps/backend/.env.example (1)

21-30: LGTM! Environment configuration is well-documented.

The new environment variables for Google OAuth and self-issued JWT are clearly documented with helpful comments. The generate:keys script reference on Line 26 aligns with the new script in package.json.

apps/backend/src/auth/jwt/index.ts (1)

1-2: LGTM! Proper barrel export pattern.

The barrel file cleanly exports the JWT-related functionality, following the established pattern for organizing exports.

Based on learnings, this aligns with the barrel export convention.

apps/backend/src/auth/providers/index.ts (1)

1-2: LGTM! Consistent barrel export pattern.

The providers barrel file follows the same clean export pattern as the JWT barrel, providing a consolidated public API for authentication providers.

Based on learnings, this aligns with the barrel export convention.

apps/backend/scripts/generate-keys.sh (1)

1-42: LGTM! Well-implemented key generation script.

The script properly handles key generation with appropriate security measures:

  • Error handling with set -e
  • Checks for existing keys before overwriting
  • Correct file permissions (600 for private, 644 for public)
  • Clear security warning about not committing the private key
apps/backend/package.json (2)

21-21: LGTM! Script addition aligns with key generation workflow.

The new generate:keys script properly references the shell script added in apps/backend/scripts/generate-keys.sh.


37-38: Dependency versions are secure.

Both google-auth-library@^10.5.0 and jsonwebtoken@^9.0.3 have no known security vulnerabilities. The jsonwebtoken version specifically resolves prior CVEs (CVE-2022-23540 and CVE-2022-23541) that affected earlier releases.

apps/backend/src/modules/users/users.controller.ts (1)

67-74: LGTM! Comprehensive API documentation.

The Swagger documentation for the Google OAuth endpoint is thorough, including operation summary, success response with type, and error scenarios (400, 403).

apps/backend/src/auth/jwt/self-issued-jwt.strategy.spec.ts (1)

1-162: LGTM! Comprehensive test coverage for SelfIssuedJwtStrategy.

The test suite thoroughly covers:

  • Key loading from configured paths with proper fallback behavior
  • Default issuer configuration
  • JWT payload validation and mapping
  • Role preservation across all enum values
  • Special character handling in email fields

The tests use appropriate mocking for external dependencies (fs, ConfigService) and cover both happy paths and edge cases.

Based on learnings, this follows the guideline to include unit tests for all backend services.

apps/backend/src/modules/users/dto/google-oauth.dto.ts (1)

4-12: LGTM! Proper DTO validation and documentation.

The GoogleOAuthDto correctly uses class-validator decorators (@IsString(), @IsNotEmpty()) and includes comprehensive Swagger documentation for the idToken field.

Based on learnings, this follows the guidelines for defining DTOs with validation decorators in the dto/ subdirectory.

apps/backend/src/auth/providers/google.provider.spec.ts (1)

69-201: Good test coverage for verifyToken scenarios.

The tests comprehensively cover valid tokens, missing name/email/picture fields, null payload, error propagation, and providerId formatting. This aligns well with the implementation.

apps/backend/src/auth/providers/google.provider.ts (1)

45-52: Clean implementation of IdentityProviderUser mapping.

The mapping correctly handles optional fields with sensible defaults: name falls back to email prefix, picture is passed through as optional, and emailVerified defaults to false. The providerId format follows the documented convention.

apps/backend/src/auth/jwt/jwt.service.spec.ts (1)

67-428: Excellent test coverage for JwtTokenService.

The tests thoroughly cover:

  • Key loading and availability checks
  • Token generation with correct claims and algorithm
  • Token verification including expiry, issuer validation, and tampering detection
  • Duration parsing for various time units

This provides good confidence in the JWT implementation.

apps/backend/src/auth/jwt/jwt.service.ts (1)

9-23: Well-defined interfaces for JWT payloads.

The JwtPayload and TokenPair interfaces are clearly defined with appropriate fields. The payload includes all necessary claims for authorization (accountId, role) which aligns with the account isolation requirement.

apps/backend/src/auth/auth.module.ts (1)

20-30: GoogleProvider null export is already properly handled with @optional().

The injection site in apps/backend/src/modules/users/users.service.ts (line 34) already uses @Optional() with explicit null typing: @Optional() private readonly googleProvider: GoogleProvider | null. The module's factory pattern correctly avoids throwing errors by returning null before instantiation, and consumers are properly prepared for the optional dependency. No changes needed.

apps/backend/src/modules/users/users.service.ts (1)

5-6: Import and DI wiring for Google/JWT auth looks consistent

The new imports (Optional, OAuthTokenResponse, GoogleProvider, JwtTokenService) and the optional constructor injections for googleProvider/jwtTokenService are wired in cleanly and align with the existing NestJS service pattern. No issues from a structure/coupling standpoint.

Also applies to: 15-19, 34-36

Comment on lines +41 to +54
it('should throw error when GOOGLE_CLIENT_ID is not set', async () => {
const configWithoutClientId = {
get: jest.fn().mockReturnValue(undefined),
};

await expect(
Test.createTestingModule({
providers: [
GoogleProvider,
{ provide: ConfigService, useValue: configWithoutClientId },
],
}).compile(),
).rejects.toThrow('GOOGLE_CLIENT_ID is required');
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Error message assertion mismatch with implementation.

The test expects 'GOOGLE_CLIENT_ID is required' but the actual implementation in google.provider.ts throws 'GOOGLE_CLIENT_ID is required when using GoogleProvider'. This test will fail.

       await expect(
         Test.createTestingModule({
           providers: [
             GoogleProvider,
             { provide: ConfigService, useValue: configWithoutClientId },
           ],
         }).compile(),
-      ).rejects.toThrow('GOOGLE_CLIENT_ID is required');
+      ).rejects.toThrow('GOOGLE_CLIENT_ID is required when using GoogleProvider');
🤖 Prompt for AI Agents
In apps/backend/src/auth/providers/google.provider.spec.ts around lines 41 to
54, the test asserts the error message 'GOOGLE_CLIENT_ID is required' but the
implementation throws 'GOOGLE_CLIENT_ID is required when using GoogleProvider';
update the test to expect the exact implemented message (or adjust to use a
partial match like expect(...).rejects.toThrow(/GOOGLE_CLIENT_ID is required
when using GoogleProvider/) so the assertion matches the thrown error string).

Comment on lines +18 to +19
@ApiProperty({ description: 'JWT refresh token' })
refreshToken: string;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for refresh token implementation

echo "=== Searching for refresh token generation ==="
rg -nP --type=ts -C3 'refreshToken.*=|generateRefresh|createRefresh' apps/backend/src/ 2>/dev/null || true

echo -e "\n=== Searching for token refresh endpoint ==="
rg -nP --type=ts -C3 '@Post.*refresh|@Put.*refresh' apps/backend/src/ 2>/dev/null || true

echo -e "\n=== Searching for refresh token validation ==="
rg -nP --type=ts -C3 'validateRefresh|verifyRefresh' apps/backend/src/ 2>/dev/null || true

echo -e "\n=== Searching for OAuth service files ==="
fd -e ts -e tsx 'oauth|token' apps/backend/src/ 2>/dev/null | head -20

echo -e "\n=== Searching for JWT/token service implementations ==="
rg -nP --type=ts -C2 'class.*Service' apps/backend/src/modules/users/ 2>/dev/null || true

Repository: etusdigital/boilerplate

Length of output: 3735


🏁 Script executed:

#!/bin/bash

echo "=== Checking jwt.service.ts for all methods ==="
rg -nP --type=ts 'public.*(' apps/backend/src/auth/jwt/jwt.service.ts | head -30

echo -e "\n=== Searching for refresh endpoint with various patterns ==="
rg -nP --type=ts 'refresh|token.*endpoint|auth.*controller' apps/backend/src/ -i | grep -E '(Controller|@(Get|Post|Put|Delete)|refresh)' | head -20

echo -e "\n=== Checking auth controller files ==="
fd -e ts auth controller apps/backend/src/ 2>/dev/null | head -10

echo -e "\n=== Checking for JwtStrategy/validation ==="
rg -nP --type=ts 'validate|verify.*token' apps/backend/src/auth/ -C2 | head -40

echo -e "\n=== Full content of jwt.service.ts ==="
wc -l apps/backend/src/auth/jwt/jwt.service.ts

Repository: etusdigital/boilerplate

Length of output: 5284


🏁 Script executed:

#!/bin/bash

echo "=== Reading jwt.service.ts ==="
cat -n apps/backend/src/auth/jwt/jwt.service.ts

echo -e "\n=== Searching for auth controller ==="
fd -e ts 'controller' apps/backend/src/ --path '*auth*' 2>/dev/null

echo -e "\n=== Searching for routes with 'refresh' ==="
rg -nP --type=ts 'refresh' apps/backend/src/ -B2 -A2 | grep -E '(refresh|@Post|@Get|Controller|Route)' | head -30

Repository: etusdigital/boilerplate

Length of output: 7992


🏁 Script executed:

#!/bin/bash

echo "=== Searching for all controller files ==="
fd -e ts 'controller' apps/backend/src/ 2>/dev/null | head -20

echo -e "\n=== Checking users controller ==="
fd -e ts -e tsx users.controller apps/backend/src/ 2>/dev/null

echo -e "\n=== Searching for @Controller decorator ==="
rg -nP --type=ts '@Controller' apps/backend/src/ -A5 | head -40

echo -e "\n=== Searching for endpoint definitions with @Post/@Get ==="
rg -nP --type=ts '@(Post|Get|Put|Patch)\(' apps/backend/src/ | grep -i 'refresh\|token' | head -20

echo -e "\n=== Searching for token rotation/invalidation/revocation ==="
rg -nP --type=ts 'revok|rotati|invalidat|blacklist' apps/backend/src/ -i -C2 | head -40

Repository: etusdigital/boilerplate

Length of output: 4344


🏁 Script executed:

#!/bin/bash

echo "=== Reading users.controller.ts ==="
cat -n apps/backend/src/modules/users/users.controller.ts

echo -e "\n=== Searching for refresh-related methods in users.service ==="
rg -nP --type=ts 'refresh|token' apps/backend/src/modules/users/users.service.ts -C3 | head -50

Repository: etusdigital/boilerplate

Length of output: 6715


Implement token refresh endpoint to utilize generated refresh tokens.

While refresh token generation is properly implemented in JwtTokenService.generateRefreshToken() with correct 7-day expiry and validation exists via verifyToken(), the implementation is incomplete:

  • Missing refresh endpoint: No endpoint exists to exchange a refresh token for a new access token. Clients receive a refresh token from /users/oauth/google but have no way to use it.
  • Incomplete refresh token payload: The refresh token only contains sub and type fields (line 122 in jwt.service.ts), lacking accountId and role needed to generate a properly scoped new access token.
  • No token rotation strategy: Add a POST /users/refresh endpoint that validates the refresh token and returns a new access token/refresh token pair.
🤖 Prompt for AI Agents
apps/backend/src/modules/users/dto/google-oauth.dto.ts lines 18-19: the review
calls out that there is no /users/refresh endpoint and the refresh token payload
lacks accountId and role, so: update JwtTokenService.generateRefreshToken to
include accountId and role (alongside sub and type), ensure verifyToken can
validate that payload; add a new POST /users/refresh controller action that
accepts a refresh-token DTO, calls JwtTokenService.verifyToken, checks token
type is refresh and not expired/revoked, then issues a new access token and a
new refresh token (using the accountId/role from the verified payload);
implement token rotation by revoking the used refresh token (store a refresh
token id/jti or increment a tokenVersion on the user record) before returning
the new pair; update DTOs to accept/return the refresh token and new tokens
accordingly and add appropriate error handling for invalid/expired/revoked
tokens.

Comment on lines +64 to +79
@Post('/oauth/google')
@UseGuards() // Remove JWT guard for this endpoint
@UsePipes(new ValidationPipe())
@ApiOperation({ summary: 'Authenticate with Google OAuth' })
@ApiResponse({
status: 201,
description: 'Successfully authenticated with Google',
type: OAuthTokenResponse,
})
@ApiResponse({ status: 400, description: 'Invalid Google token.' })
@ApiResponse({ status: 403, description: 'User not found or not allowed.' })
async googleOAuth(
@Body() googleOAuthDto: GoogleOAuthDto,
): Promise<OAuthTokenResponse> {
return await this.usersService.loginWithGoogle(googleOAuthDto.idToken);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Critical: Empty @UseGuards() does not bypass class-level guards.

Line 65 uses @UseGuards() with no arguments, but this does not bypass the class-level guards defined on Line 29 (AuthGuard('jwt') and RolesGuard). The Google OAuth endpoint will still require a valid JWT, which defeats its purpose as a public authentication endpoint.

Solution: Implement a @Public() decorator or move the guards to individual methods.

Option 1 (Recommended): Create a @Public() decorator

Create apps/backend/src/auth/decorators/public.decorator.ts:

import { SetMetadata } from '@nestjs/common';

export const IS_PUBLIC_KEY = 'isPublic';
export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);

Update the JWT strategy or create a guard that checks for the IS_PUBLIC_KEY metadata and skips authentication when present.

Option 2: Move guards to method level

Remove guards from the class level and apply them individually to each protected endpoint (more verbose but explicit).

Then apply the @Public() decorator:

  @Post('/oauth/google')
- @UseGuards() // Remove JWT guard for this endpoint
+ @Public()
  @UsePipes(new ValidationPipe())
🤖 Prompt for AI Agents
In apps/backend/src/modules/users/users.controller.ts around lines 64 to 79, the
googleOAuth endpoint uses @UseGuards() with no args which does not bypass
class-level guards; make this endpoint public by adding a Public decorator
(create apps/backend/src/auth/decorators/public.decorator.ts exporting
IS_PUBLIC_KEY and Public that sets metadata) and update the JWT/Auth guard
pipeline (or JWT strategy) to check for IS_PUBLIC_KEY metadata and skip
authentication when present; alternatively remove class-level guards and apply
AuthGuard('jwt') and RolesGuard on individual protected methods, then annotate
this method with Public to allow unauthenticated access.

Comment on lines +318 to +413
/**
* Authenticate a user via Google OAuth.
* Verifies the Google ID token, finds or creates the user,
* and returns self-issued JWT tokens.
*/
async loginWithGoogle(idToken: string): Promise<OAuthTokenResponse> {
if (!this.googleProvider) {
throw new BadRequestException(
'Google OAuth is not configured. Set GOOGLE_CLIENT_ID in environment.',
);
}

if (!this.jwtTokenService || !this.jwtTokenService.isAvailable()) {
throw new BadRequestException(
'JWT token service is not available. Run `npm run generate:keys`.',
);
}

// Verify the Google ID token
const googleUser = await this.googleProvider.verifyToken(idToken);

// Set transaction ID for audit
this.cls.set('transactionId', uuidv7());

// Find user by provider ID or email
let user = await this.findByProviderId(googleUser.providerId);

if (!user) {
// Try to find by email (user might exist from Auth0 or invitation)
user = await this.userRepository.findOne({
where: { email: googleUser.email },
relations: ['userAccounts'],
});
}

if (!user) {
throw new ForbiddenException(
'User not found. Please contact an administrator to create your account.',
);
}

// Update user status and add Google provider ID if not already present
if (user.status !== 'accepted') {
user.status = 'accepted';
}

if (!user.providerIds.includes(googleUser.providerId)) {
user.addProvider(googleUser.providerId);
}

// Update profile image if not set
if (!user.profileImage && googleUser.picture) {
user.profileImage = googleUser.picture;
}

await this.userRepository.save(user);

// Reload user with relations
user = await this.userRepository.findOne({
where: { id: user.id },
relations: ['userAccounts', 'userAccounts.account'],
});

if (!user) {
throw new ForbiddenException('User not found after save.');
}

// Determine account and role for token
let accountId: string;
let role: Role;

if (user.isSuperAdmin) {
const accounts = await this.accountsService.findAll(user);
accountId = accounts[0]?.id || 'no-account';
role = Role.ADMIN;
} else if (user.userAccounts?.length) {
accountId = user.userAccounts[0].accountId;
role = user.userAccounts[0].role;
} else {
throw new ForbiddenException('User has no account access.');
}

// Generate tokens
const tokens = this.jwtTokenService.generateTokenPair(user, accountId, role);

return {
accessToken: tokens.accessToken,
refreshToken: tokens.refreshToken,
expiresIn: tokens.expiresIn,
user: {
id: user.id,
email: user.email || googleUser.email,
name: user.name,
},
};
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Fix potential crash on providerIds and tighten Google login flow behaviour

The overall flow of loginWithGoogle is solid (token verification → user lookup → status/provider/profile update → token pair), but there are a few issues and improvement opportunities:

  1. Possible runtime error on user.providerIds.includes(...)

If user.providerIds is ever null/undefined (e.g., legacy users, misconfigured entity defaults), this line will throw:

if (!user.providerIds.includes(googleUser.providerId)) {

Safer pattern:

-    if (!user.providerIds.includes(googleUser.providerId)) {
-      user.addProvider(googleUser.providerId);
-    }
+    if (!user.providerIds?.includes(googleUser.providerId)) {
+      user.addProvider(googleUser.providerId);
+    }

This keeps the de‑duplication intent while avoiding a crash if providerIds isn’t initialized.

  1. Ambiguous handling of super admins with no accounts

For super admins you currently do:

const accounts = await this.accountsService.findAll(user);
accountId = accounts[0]?.id || 'no-account';
role = Role.ADMIN;

Issuing tokens with a synthetic 'no-account' ID makes downstream account‑scoped logic harder to reason about and may break the “all operations must have a real account context” expectation. Prefer failing fast if no accounts are configured:

    if (user.isSuperAdmin) {
      const accounts = await this.accountsService.findAll(user);
-      accountId = accounts[0]?.id || 'no-account';
-      role = Role.ADMIN;
+      if (!accounts.length) {
+        throw new ForbiddenException('Super admin has no account access configured.');
+      }
+      accountId = accounts[0].id;
+      role = Role.ADMIN;
  1. CLS/account context consistency with existing login method

The email/password login method sets CLS context for both transactionId and accountId (and user), while loginWithGoogle only sets transactionId. For observability and to stay aligned with the account‑isolation guideline, consider also setting user and accountId once you’ve derived them:

this.cls.set('user', { ...user, userAccounts: null });
this.cls.set('accountId', accountId);

This keeps behaviour consistent regardless of auth mechanism.

  1. Token generation and user lookup re-query

After save(user) you re-query by id with relations. In many cases (especially when the user was found via findByProviderId, which already loads userAccounts) this second query is mostly to ensure relations. Since you only need accountId/role to issue tokens, you could avoid the extra round-trip by ensuring the initial lookup always includes userAccounts and using that instance directly. This is a non-blocking performance simplification you can do later.

  1. Optional: validate googleUser.emailVerified and wrap provider errors

Depending on your security posture, you might want to:

  • Enforce googleUser.emailVerified === true before allowing login, and
  • Wrap verifyToken failures into an UnauthorizedException with a generic message (instead of leaking raw provider errors).

These are policy choices rather than hard bugs, but worth considering for a hardened auth flow.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant