feat(fayda): add fayda config and jose dependency

This commit is contained in:
Abubeker Yasin
2026-05-25 14:41:26 +03:00
parent b16f4037f4
commit 57456f0902
4 changed files with 140 additions and 1 deletions

View File

@@ -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",

View File

@@ -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(),

View File

@@ -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<FaydaJwk>;
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,
};
});

8
pnpm-lock.yaml generated
View File

@@ -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: {}