Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion backend/.env.example
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
NODE_ENV=development
# NODE_ENV: Set to 'local' to bypass SSO/JWT verification for local development
# Values: local (bypass SSO), development, production
NODE_ENV=local

# Database
POSTGRES_HOST=localhost
Expand Down
64 changes: 0 additions & 64 deletions backend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

16 changes: 15 additions & 1 deletion backend/src/api/common/guards/csa.guard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
Logger,
UnauthorizedException,
} from '@nestjs/common'
import { ConfigService } from '@nestjs/config'
import { Reflector } from '@nestjs/core'
import { Request } from 'express'
import { JwtVerificationService } from 'src/common/auth/jwt-verification.service'
Expand All @@ -31,16 +32,23 @@ export const SKIP_CSA_CHECK_KEY = 'skipCSACheck'
/**
* Guard that validates JWT token and verifies CSA access via ICM
* Uses caching to avoid hitting ICM on every request
*
* When SKIP_SSO_VERIFICATION=true (local development), token verification is skipped
* and CSA access check is bypassed.
*/
@Injectable()
export class CSAGuard implements CanActivate {
private readonly logger = new Logger(CSAGuard.name)
private readonly skipSsoVerification: boolean

constructor(
private readonly adminService: AdminService,
private readonly reflector: Reflector,
private readonly jwtVerificationService: JwtVerificationService,
) {}
private readonly configService: ConfigService,
) {
this.skipSsoVerification = this.configService.get<string>('NODE_ENV') === 'local'
}

async canActivate(context: ExecutionContext): Promise<boolean> {
// Check if route is marked to skip CSA check
Expand Down Expand Up @@ -76,6 +84,12 @@ export class CSAGuard implements CanActivate {
return true
}

// In local development mode, skip ICM CSA access verification
if (this.skipSsoVerification) {
this.logger.debug(`Skipping CSA access verification for user: ${username} (local mode)`)
return true
}

// Check cache first
const cached = csaAccessCache.get(username)
if (cached && cached.expiresAt > Date.now()) {
Expand Down
3 changes: 2 additions & 1 deletion backend/src/api/main.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Logger } from '@nestjs/common'
import type { NestExpressApplication } from '@nestjs/platform-express'
import 'dotenv/config'
import { bootstrap } from './app'
import { Logger } from '@nestjs/common'
const logger = new Logger('NestApplication')
bootstrap()
.then(async (app: NestExpressApplication) => {
Expand Down
55 changes: 54 additions & 1 deletion backend/src/common/auth/jwt-verification.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,28 @@ import jwksClient from 'jwks-rsa'
/**
* Service for verifying JWT signatures using JWKS (JSON Web Key Set)
* Fetches public keys from the SSO Keycloak server to verify frontend tokens
*
* When SKIP_SSO_VERIFICATION=true (local development), token signatures are not verified
* but the token is still decoded to extract user information.
*/
@Injectable()
export class JwtVerificationService {
private readonly logger = new Logger(JwtVerificationService.name)
private readonly jwksClient: jwksClient.JwksClient
private readonly jwksClient: jwksClient.JwksClient | null = null
private readonly skipVerification: boolean

constructor(private readonly configService: ConfigService) {
const nodeEnv = this.configService.get<string>('NODE_ENV')
this.skipVerification = nodeEnv === 'local'

if (this.skipVerification) {
this.logger.warn(
'⚠️ SSO verification is DISABLED (NODE_ENV=local). ' +
'This should only be used for local development!',
)
return
}

const jwksUri = this.configService.get<string>('admin.ssoKeycloakJwksUrl')

if (!jwksUri) {
Expand All @@ -37,6 +52,11 @@ export class JwtVerificationService {
* @throws UnauthorizedException if verification fails
*/
async verifyToken(token: string): Promise<jwt.JwtPayload> {
// In local development mode, skip signature verification
if (this.skipVerification) {
return this.decodeTokenWithoutVerification(token)
}

try {
// First decode the header to get the key ID (kid)
const decodedHeader = jwt.decode(token, { complete: true })
Expand Down Expand Up @@ -78,10 +98,43 @@ export class JwtVerificationService {
}
}

/**
* Decode token without signature verification (for local development only)
* Still validates token structure and expiration
*/
private decodeTokenWithoutVerification(token: string): jwt.JwtPayload {
try {
const decoded = jwt.decode(token, { complete: true })

if (!decoded || typeof decoded === 'string') {
throw new UnauthorizedException('Invalid token: Unable to decode')
}

const payload = decoded.payload as jwt.JwtPayload

// Check expiration even in local mode
if (payload.exp && payload.exp < Math.floor(Date.now() / 1000)) {
throw new UnauthorizedException('Token has expired. Please login again.')
}

this.logger.debug('Token decoded without verification (local development mode)')
return payload
} catch (error) {
if (error instanceof UnauthorizedException) {
throw error
}
this.logger.error(`Token decode error: ${error}`)
throw new UnauthorizedException('Invalid token format')
}
}

/**
* Get the signing key for a given key ID
*/
private async getSigningKey(kid: string): Promise<string> {
if (!this.jwksClient) {
throw new Error('JWKS client not initialized - SSO verification is disabled')
}
try {
const key = await this.jwksClient.getSigningKey(kid)
return key.getPublicKey()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { ConfigService } from '@nestjs/config'
import type { TestingModule } from '@nestjs/testing'
import { Test } from '@nestjs/testing'
import { beforeEach, describe, expect, it } from 'vitest'
import { AdminService } from 'src/api/admin/admin.service'
import { JwtVerificationService } from 'src/common/auth/jwt-verification.service'
import { beforeEach, describe, expect, it } from 'vitest'
import { CSA_STATUS } from './constants'
import { StateMachineController } from './state-machine.controller'

Expand All @@ -15,6 +16,7 @@ describe('StateMachineController', () => {
providers: [
{ provide: AdminService, useValue: {} },
{ provide: JwtVerificationService, useValue: {} },
{ provide: ConfigService, useValue: { get: () => undefined } },
],
}).compile()

Expand Down
Loading
Loading