feat: ( fayda ) use platform-specific Fayda redirect_uri

This commit is contained in:
Abubeker Yasin
2026-07-01 10:24:42 +03:00
parent 0db0d3af78
commit 3308c775e6
4 changed files with 41 additions and 6 deletions

View File

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

View File

@@ -16,7 +16,7 @@ export class StartVerificationDto {
enum: ['WEB', 'MOBILE'], enum: ['WEB', 'MOBILE'],
default: 'WEB', default: 'WEB',
description: 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() @IsOptional()
@IsIn(['WEB', 'MOBILE']) @IsIn(['WEB', 'MOBILE'])

View File

@@ -35,6 +35,7 @@ function buildConfig(overrides?: Partial<FaydaConfig>): FaydaConfig {
tokenEndpoint: 'https://esignet.test/token', tokenEndpoint: 'https://esignet.test/token',
userInfoEndpoint: 'https://esignet.test/userinfo', userInfoEndpoint: 'https://esignet.test/userinfo',
redirectUri: 'http://localhost:4000/fayda/verification/complete', redirectUri: 'http://localhost:4000/fayda/verification/complete',
webRedirectUri: 'http://localhost:5174/fayda/verification/complete',
privateJwk: { kty: 'RSA', n: '', e: '', d: '' }, privateJwk: { kty: 'RSA', n: '', e: '', d: '' },
scope: 'openid profile email', scope: 'openid profile email',
acrValues: 'mosip:idp:acr:generated-code', 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.origin + parsed.pathname).toBe('https://esignet.test/authorize');
expect(parsed.searchParams.get('client_id')).toBe('edr-test-client'); expect(parsed.searchParams.get('client_id')).toBe('edr-test-client');
expect(parsed.searchParams.get('code_challenge_method')).toBe('S256'); expect(parsed.searchParams.get('code_challenge_method')).toBe('S256');
// Default platform is WEB → webRedirectUri.
expect(parsed.searchParams.get('redirect_uri')).toBe( 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); 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({}); prisma.faydaVerificationSession.create.mockResolvedValue({});
const url = await service.startVerification({ 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 () => { it('throws ServiceUnavailable when fayda integration is disabled', async () => {
const disabledService = new VerifaydaService( const disabledService = new VerifaydaService(
buildConfigService(buildConfig({ enabled: false })), 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'}`, `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( const tokens = await this.exchangeCodeForTokens(
query.code, query.code,
session.codeVerifier, session.codeVerifier,
this.redirectUriForPlatform(session.platform as FaydaPlatform),
); );
const userInfo = await this.fetchUserInfo(tokens.access_token); const userInfo = await this.fetchUserInfo(tokens.access_token);
const normalized = this.normalizeUserInfo(userInfo); const normalized = this.normalizeUserInfo(userInfo);
@@ -307,11 +319,12 @@ export class VerifaydaService {
private buildAuthorizationUrl(args: { private buildAuthorizationUrl(args: {
state: string; state: string;
codeChallenge: string; codeChallenge: string;
redirectUri: string;
}): string { }): string {
const params = new URLSearchParams({ const params = new URLSearchParams({
client_id: this.faydaConfig.clientId, client_id: this.faydaConfig.clientId,
response_type: 'code', response_type: 'code',
redirect_uri: this.faydaConfig.redirectUri, redirect_uri: args.redirectUri,
scope: this.faydaConfig.scope, scope: this.faydaConfig.scope,
state: args.state, state: args.state,
code_challenge: args.codeChallenge, code_challenge: args.codeChallenge,
@@ -344,6 +357,7 @@ export class VerifaydaService {
private async exchangeCodeForTokens( private async exchangeCodeForTokens(
code: string, code: string,
codeVerifier: string, codeVerifier: string,
redirectUri: string,
): Promise<FaydaTokenResponse> { ): Promise<FaydaTokenResponse> {
const clientAssertion = await generateClientAssertion({ const clientAssertion = await generateClientAssertion({
clientId: this.faydaConfig.clientId, clientId: this.faydaConfig.clientId,
@@ -354,7 +368,7 @@ export class VerifaydaService {
const body = new URLSearchParams({ const body = new URLSearchParams({
grant_type: 'authorization_code', grant_type: 'authorization_code',
code, code,
redirect_uri: this.faydaConfig.redirectUri, redirect_uri: redirectUri,
client_id: this.faydaConfig.clientId, client_id: this.faydaConfig.clientId,
client_assertion_type: client_assertion_type:
'urn:ietf:params:oauth:client-assertion-type:jwt-bearer', 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer',