Files
IdP/apps/api-gateway/src/lib/fedcm-config.ts
2026-07-01 12:14:50 +03:00

119 lines
4.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import type { Request } from 'express';
import { firstValueFrom } from 'rxjs';
import { CoreGrpcService } from '../core-grpc.service';
import { resolveFrontendUrl, resolveOAuthIssuer } from './oauth-issuer';
import {
normalizePublicBaseUrl,
preferCanonicalBase,
resolveFedcmWebIdentityOrigin,
resolvePublicApiBaseFromRequest,
resolvePublicFrontendBaseFromRequest
} from './public-url';
export interface FedcmEndpoints {
issuer: string;
frontendUrl: string;
projectName: string;
configUrl: string;
accountsEndpoint: string;
clientMetadataEndpoint: string;
idAssertionEndpoint: string;
loginUrl: string;
signupUrl: string;
webIdentityUrl: string;
}
function buildFedcmEndpointUrls(issuer: string, frontendUrl: string) {
const base = normalizePublicBaseUrl(issuer);
const front = normalizePublicBaseUrl(frontendUrl);
const webIdentityOrigin = resolveFedcmWebIdentityOrigin(base, front);
return {
configUrl: `${base}/fedcm/config.json`,
accountsEndpoint: `${base}/fedcm/accounts`,
clientMetadataEndpoint: `${base}/fedcm/client_metadata`,
idAssertionEndpoint: `${base}/fedcm/id_assertion`,
loginUrl: `${front}/auth/login`,
signupUrl: `${front}/auth/register`,
webIdentityUrl: `${webIdentityOrigin}/.well-known/web-identity`
};
}
export async function resolveFedcmEndpoints(core: CoreGrpcService, req?: Request): Promise<FedcmEndpoints> {
const storedIssuer = await resolveOAuthIssuer(core);
const storedFrontend = await resolveFrontendUrl(core);
// ВАЖНО для FedCM: configUrl должен быть стабильным независимо от того, с какого
// хоста пришёл запрос. well-known запрашивается браузером всегда с eTLD+1
// (например https://idpmvk.lpr/.well-known/web-identity), а discover.json — с
// поддомена (https://sso.idpmvk.lpr/idp-api/...). Оба обязаны вернуть ОДИН и тот
// же configUrl, иначе браузер отвергнет провайдера. Поэтому берём канонический
// публичный URL из настроек, а не из текущего хоста запроса.
const issuer = preferCanonicalBase(storedIssuer, resolvePublicApiBaseFromRequest(req));
const frontendUrl = preferCanonicalBase(storedFrontend, resolvePublicFrontendBaseFromRequest(req));
let projectName = 'MVK ID';
try {
const setting = (await firstValueFrom(core.settings.GetSetting({ key: 'PROJECT_NAME' }))) as { value?: string };
if (setting.value?.trim()) {
projectName = setting.value.trim();
}
} catch {
// fallback
}
return {
issuer,
frontendUrl,
projectName,
...buildFedcmEndpointUrls(issuer, frontendUrl)
};
}
export function buildFedcmProviderConfig(endpoints: FedcmEndpoints) {
return {
accounts_endpoint: endpoints.accountsEndpoint,
client_metadata_endpoint: endpoints.clientMetadataEndpoint,
id_assertion_endpoint: endpoints.idAssertionEndpoint,
login_url: endpoints.loginUrl,
branding: {
background_color: '#ffffff',
color: '#1f2430',
icons: [{ url: `${endpoints.frontendUrl}/favicon.ico`, size: 32 }]
}
};
}
export function buildFedcmWebIdentityManifest(endpoints: FedcmEndpoints) {
return {
provider_urls: [endpoints.configUrl],
accounts_endpoint: endpoints.accountsEndpoint,
login_url: endpoints.loginUrl
};
}
export function buildFedcmDiscoverPayload(endpoints: FedcmEndpoints, enabled = true) {
return {
enabled,
apiBase: endpoints.issuer,
frontendUrl: endpoints.frontendUrl,
projectName: endpoints.projectName,
configUrl: endpoints.configUrl,
webIdentityUrl: endpoints.webIdentityUrl,
accountsEndpoint: endpoints.accountsEndpoint,
loginUrl: endpoints.loginUrl
};
}
async function readOneTapEnabled(core: CoreGrpcService): Promise<boolean> {
try {
const setting = (await firstValueFrom(core.settings.GetSetting({ key: 'ONE_TAP_ENABLED' }))) as { value?: string };
const raw = setting.value?.trim().toLowerCase();
if (!raw) return true;
return ['true', '1', 'yes'].includes(raw);
} catch {
return true;
}
}
export { readOneTapEnabled };