From 57456f0902eaff79853ac5b06c7846f1dabff81d Mon Sep 17 00:00:00 2001 From: Abubeker Yasin Date: Mon, 25 May 2026 14:41:26 +0300 Subject: [PATCH] feat(fayda): add fayda config and jose dependency --- apps/edr-passenger-api/package.json | 1 + apps/edr-passenger-api/src/app.module.ts | 12 +- .../src/config/fayda.config.ts | 120 ++++++++++++++++++ pnpm-lock.yaml | 8 ++ 4 files changed, 140 insertions(+), 1 deletion(-) create mode 100644 apps/edr-passenger-api/src/config/fayda.config.ts diff --git a/apps/edr-passenger-api/package.json b/apps/edr-passenger-api/package.json index ae10747f5..e8704d09e 100644 --- a/apps/edr-passenger-api/package.json +++ b/apps/edr-passenger-api/package.json @@ -36,6 +36,7 @@ "bcrypt": "^5.1.1", "class-transformer": "^0.5.1", "class-validator": "^0.14.0", + "jose": "^6.2.3", "passport": "^0.7.0", "passport-jwt": "^4.0.1", "qrcode": "^1.5.3", diff --git a/apps/edr-passenger-api/src/app.module.ts b/apps/edr-passenger-api/src/app.module.ts index 261463e27..e1626bef5 100644 --- a/apps/edr-passenger-api/src/app.module.ts +++ b/apps/edr-passenger-api/src/app.module.ts @@ -13,6 +13,7 @@ import cbeConfig from './config/cbe.config'; import ebirrConfig from './config/ebirr.config'; import cardConfig from './config/card.config'; import waafiConfig from './config/waafi.config'; +import faydaConfig from './config/fayda.config'; import { AuthModule } from './modules/auth/auth.module'; import { StationsModule } from './modules/stations/stations.module'; import { FleetModule } from './modules/fleet/fleet.module'; @@ -40,7 +41,16 @@ import { SeatClassesModule } from './modules/seat-classes/seat-classes.module'; imports: [ ConfigModule.forRoot({ isGlobal: true, - load: [appConfig, dbConfig, telebirrConfig, cbeConfig, ebirrConfig, cardConfig, waafiConfig], + load: [ + appConfig, + dbConfig, + telebirrConfig, + cbeConfig, + ebirrConfig, + cardConfig, + waafiConfig, + faydaConfig, + ], }), ScheduleModule.forRoot(), EventEmitterModule.forRoot(), diff --git a/apps/edr-passenger-api/src/config/fayda.config.ts b/apps/edr-passenger-api/src/config/fayda.config.ts new file mode 100644 index 000000000..6e317760b --- /dev/null +++ b/apps/edr-passenger-api/src/config/fayda.config.ts @@ -0,0 +1,120 @@ +import { registerAs } from '@nestjs/config'; + +export interface FaydaJwk { + kty: 'RSA'; + use?: string; + kid?: string; + alg?: string; + n: string; + e: string; + d: string; + p?: string; + q?: string; + dp?: string; + dq?: string; + qi?: string; +} + +export interface FaydaConfig { + enabled: boolean; + clientId: string; + authorizationEndpoint: string; + tokenEndpoint: string; + userInfoEndpoint: string; + redirectUri: string; + privateJwk: FaydaJwk; + successRedirectUrl: string; + failureRedirectUrl: string; + scope: string; + acrValues: string; + claimsLocales: string; + sessionTtlMinutes: number; +} + +const REQUIRED_VARS = [ + 'FAYDA_CLIENT_ID', + 'FAYDA_AUTHORIZATION_ENDPOINT', + 'FAYDA_TOKEN_ENDPOINT', + 'FAYDA_USERINFO_ENDPOINT', + 'FAYDA_REDIRECT_URI', + 'FAYDA_PRIVATE_KEY_BASE64', + 'FAYDA_SUCCESS_REDIRECT_URL', + 'FAYDA_FAILURE_REDIRECT_URL', +] as const; + +function decodePrivateJwk(base64: string): FaydaJwk { + let jwk: unknown; + try { + const json = Buffer.from(base64, 'base64').toString('utf8'); + jwk = JSON.parse(json); + } catch (err) { + throw new Error( + `FAYDA_PRIVATE_KEY_BASE64 is not valid Base64-encoded JSON: ${(err as Error).message}`, + ); + } + if (!jwk || typeof jwk !== 'object') { + throw new Error('FAYDA_PRIVATE_KEY_BASE64 must decode to a JSON object'); + } + const candidate = jwk as Partial; + if (candidate.kty !== 'RSA') { + throw new Error('FAYDA_PRIVATE_KEY_BASE64 JWK must have kty="RSA"'); + } + if (!candidate.n || !candidate.e || !candidate.d) { + throw new Error( + 'FAYDA_PRIVATE_KEY_BASE64 JWK is missing required RSA private-key fields (n, e, d)', + ); + } + return candidate as FaydaJwk; +} + +export default registerAs('fayda', (): FaydaConfig => { + const enabled = (process.env.FAYDA_ENABLED ?? 'false').toLowerCase() === 'true'; + const scope = process.env.FAYDA_SCOPE ?? 'openid profile email'; + const acrValues = process.env.FAYDA_ACR_VALUES ?? 'mosip:idp:acr:generated-code'; + const claimsLocales = process.env.FAYDA_CLAIMS_LOCALES ?? 'en am'; + const sessionTtl = Number.parseInt(process.env.FAYDA_SESSION_TTL_MINUTES ?? '10', 10); + + if (!enabled) { + return { + enabled: false, + clientId: process.env.FAYDA_CLIENT_ID ?? '', + authorizationEndpoint: process.env.FAYDA_AUTHORIZATION_ENDPOINT ?? '', + tokenEndpoint: process.env.FAYDA_TOKEN_ENDPOINT ?? '', + userInfoEndpoint: process.env.FAYDA_USERINFO_ENDPOINT ?? '', + redirectUri: process.env.FAYDA_REDIRECT_URI ?? '', + privateJwk: { kty: 'RSA', n: '', e: '', d: '' }, + successRedirectUrl: process.env.FAYDA_SUCCESS_REDIRECT_URL ?? '', + failureRedirectUrl: process.env.FAYDA_FAILURE_REDIRECT_URL ?? '', + scope, + acrValues, + claimsLocales, + sessionTtlMinutes: Number.isNaN(sessionTtl) || sessionTtl <= 0 ? 10 : sessionTtl, + }; + } + + const missing = REQUIRED_VARS.filter((name) => !process.env[name]); + if (missing.length > 0) { + throw new Error( + `Fayda integration is enabled (FAYDA_ENABLED=true) but the following env vars are missing: ${missing.join(', ')}`, + ); + } + if (Number.isNaN(sessionTtl) || sessionTtl <= 0) { + throw new Error('FAYDA_SESSION_TTL_MINUTES must be a positive integer'); + } + + return { + enabled: true, + clientId: process.env.FAYDA_CLIENT_ID!, + authorizationEndpoint: process.env.FAYDA_AUTHORIZATION_ENDPOINT!, + tokenEndpoint: process.env.FAYDA_TOKEN_ENDPOINT!, + userInfoEndpoint: process.env.FAYDA_USERINFO_ENDPOINT!, + redirectUri: process.env.FAYDA_REDIRECT_URI!, + privateJwk: decodePrivateJwk(process.env.FAYDA_PRIVATE_KEY_BASE64!), + successRedirectUrl: process.env.FAYDA_SUCCESS_REDIRECT_URL!, + failureRedirectUrl: process.env.FAYDA_FAILURE_REDIRECT_URL!, + scope, + acrValues, + claimsLocales, + sessionTtlMinutes: sessionTtl, + }; +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 30f7e543d..f9b6b377a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -320,6 +320,9 @@ importers: class-validator: specifier: ^0.14.0 version: 0.14.4 + jose: + specifier: ^6.2.3 + version: 6.2.3 passport: specifier: ^0.7.0 version: 0.7.0 @@ -7229,6 +7232,9 @@ packages: jose@5.10.0: resolution: {integrity: sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg==} + jose@6.2.3: + resolution: {integrity: sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw==} + jquery@3.7.1: resolution: {integrity: sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg==} @@ -19456,6 +19462,8 @@ snapshots: jose@5.10.0: {} + jose@6.2.3: {} + jquery@3.7.1: {} js-cookie@3.0.7: {}