Merge branch 'alpha' into passenger/feat/iam

This commit is contained in:
Abubeker Yasin
2026-07-01 11:09:43 +03:00
4 changed files with 41 additions and 6 deletions

View File

@@ -23,7 +23,10 @@ export interface FaydaConfig {
authorizationEndpoint: string;
tokenEndpoint: string;
userInfoEndpoint: string;
/** OAuth redirect_uri sent to eSignet for MOBILE clients. */
redirectUri: string;
/** OAuth redirect_uri sent to eSignet for WEB clients. Falls back to `redirectUri`. */
webRedirectUri: string;
privateJwk: FaydaJwk;
scope: string;
acrValues: string;
@@ -73,6 +76,7 @@ export default registerAs('fayda', (): FaydaConfig => {
const claimsLocales = process.env.FAYDA_CLAIMS_LOCALES ?? 'en am';
const sessionTtl = Number.parseInt(process.env.FAYDA_SESSION_TTL_MINUTES ?? '10', 10);
const redirectUri = process.env.FAYDA_REDIRECT_URI ?? '';
const webRedirectUri = process.env.FAYDA_WEB_REDIRECT_URI || redirectUri;
if (!enabled) {
return {
enabled: false,
@@ -81,6 +85,7 @@ export default registerAs('fayda', (): FaydaConfig => {
tokenEndpoint: process.env.FAYDA_TOKEN_ENDPOINT ?? '',
userInfoEndpoint: process.env.FAYDA_USERINFO_ENDPOINT ?? '',
redirectUri,
webRedirectUri,
privateJwk: { kty: 'RSA', n: '', e: '', d: '' },
scope,
acrValues,
@@ -111,6 +116,7 @@ export default registerAs('fayda', (): FaydaConfig => {
tokenEndpoint: process.env.FAYDA_TOKEN_ENDPOINT!,
userInfoEndpoint: process.env.FAYDA_USERINFO_ENDPOINT!,
redirectUri,
webRedirectUri,
privateJwk: decodePrivateJwk(process.env.FAYDA_PRIVATE_KEY_BASE64!),
scope,
acrValues,

View File

@@ -16,7 +16,7 @@ export class StartVerificationDto {
enum: ['WEB', 'MOBILE'],
default: 'WEB',
description:
'Client platform. Decides where /callback redirects on completion: a web https URL (WEB) or a custom-scheme deep link the Flutter app intercepts (MOBILE).',
'Client platform. Selects which OAuth redirect_uri is sent to eSignet: WEB uses FAYDA_WEB_REDIRECT_URI, MOBILE uses FAYDA_REDIRECT_URI. Both land on the same /complete endpoint with identical handling.',
})
@IsOptional()
@IsIn(['WEB', 'MOBILE'])

View File

@@ -35,6 +35,7 @@ function buildConfig(overrides?: Partial<FaydaConfig>): FaydaConfig {
tokenEndpoint: 'https://esignet.test/token',
userInfoEndpoint: 'https://esignet.test/userinfo',
redirectUri: 'http://localhost:4000/fayda/verification/complete',
webRedirectUri: 'http://localhost:5174/fayda/verification/complete',
privateJwk: { kty: 'RSA', n: '', e: '', d: '' },
scope: 'openid profile email',
acrValues: 'mosip:idp:acr:generated-code',
@@ -101,13 +102,14 @@ describe('VerifaydaService (OIDC, client-callback)', () => {
expect(parsed.origin + parsed.pathname).toBe('https://esignet.test/authorize');
expect(parsed.searchParams.get('client_id')).toBe('edr-test-client');
expect(parsed.searchParams.get('code_challenge_method')).toBe('S256');
// Default platform is WEB → webRedirectUri.
expect(parsed.searchParams.get('redirect_uri')).toBe(
'http://localhost:4000/fayda/verification/complete',
'http://localhost:5174/fayda/verification/complete',
);
expect(parsed.searchParams.get('state')).toBe(created.state);
});
it('uses the same single redirect_uri regardless of platform (platform is only recorded)', async () => {
it('sends the MOBILE redirect_uri (base redirectUri) for MOBILE sessions', async () => {
prisma.faydaVerificationSession.create.mockResolvedValue({});
const url = await service.startVerification({
@@ -122,6 +124,19 @@ describe('VerifaydaService (OIDC, client-callback)', () => {
);
});
it('sends the WEB redirect_uri (webRedirectUri) for WEB sessions', async () => {
prisma.faydaVerificationSession.create.mockResolvedValue({});
const url = await service.startVerification({
purpose: 'VERIFY',
platform: 'WEB',
});
expect(new URL(url).searchParams.get('redirect_uri')).toBe(
'http://localhost:5174/fayda/verification/complete',
);
});
it('throws ServiceUnavailable when fayda integration is disabled', async () => {
const disabledService = new VerifaydaService(
buildConfigService(buildConfig({ enabled: false })),

View File

@@ -161,7 +161,18 @@ export class VerifaydaService {
`Fayda verification started: purpose=${input.purpose} platform=${input.platform ?? 'WEB'} userId=${input.userId ?? 'none'}`,
);
return this.buildAuthorizationUrl({ state, codeChallenge });
return this.buildAuthorizationUrl({
state,
codeChallenge,
redirectUri: this.redirectUriForPlatform(input.platform ?? 'WEB'),
});
}
/** WEB clients use `webRedirectUri`; MOBILE uses the base `redirectUri`. */
private redirectUriForPlatform(platform?: FaydaPlatform): string {
return platform === 'MOBILE'
? this.faydaConfig.redirectUri
: this.faydaConfig.webRedirectUri;
}
@@ -213,6 +224,7 @@ export class VerifaydaService {
const tokens = await this.exchangeCodeForTokens(
query.code,
session.codeVerifier,
this.redirectUriForPlatform(session.platform as FaydaPlatform),
);
const userInfo = await this.fetchUserInfo(tokens.access_token);
const normalized = this.normalizeUserInfo(userInfo);
@@ -307,11 +319,12 @@ export class VerifaydaService {
private buildAuthorizationUrl(args: {
state: string;
codeChallenge: string;
redirectUri: string;
}): string {
const params = new URLSearchParams({
client_id: this.faydaConfig.clientId,
response_type: 'code',
redirect_uri: this.faydaConfig.redirectUri,
redirect_uri: args.redirectUri,
scope: this.faydaConfig.scope,
state: args.state,
code_challenge: args.codeChallenge,
@@ -344,6 +357,7 @@ export class VerifaydaService {
private async exchangeCodeForTokens(
code: string,
codeVerifier: string,
redirectUri: string,
): Promise<FaydaTokenResponse> {
const clientAssertion = await generateClientAssertion({
clientId: this.faydaConfig.clientId,
@@ -354,7 +368,7 @@ export class VerifaydaService {
const body = new URLSearchParams({
grant_type: 'authorization_code',
code,
redirect_uri: this.faydaConfig.redirectUri,
redirect_uri: redirectUri,
client_id: this.faydaConfig.clientId,
client_assertion_type:
'urn:ietf:params:oauth:client-assertion-type:jwt-bearer',