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

161 lines
5.1 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,
resolveProjectDomainApiUrl,
resolveProjectDomainFrontendUrl
} from './oauth-issuer';
import {
isLocalDevBaseUrl,
isValidFedcmEndpointUrl,
normalizePublicBaseUrl,
pickBestPublicBase,
resolveFedcmWebIdentityOrigin
} 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`,
// FedCM: login_url MUST be same-origin with config.json (W3C FedCM / Chrome).
// На split-domain UI проксируется через nginx: api.idpmvk.lpr/auth/login → frontend.
loginUrl: `${base}/auth/login?fedcm=1`,
signupUrl: `${front}/auth/register`,
webIdentityUrl: `${webIdentityOrigin}/.well-known/web-identity`
};
}
async function resolveCanonicalFedcmIssuer(core: CoreGrpcService): Promise<string> {
const stored = await resolveOAuthIssuer(core);
const fromProject = await resolveProjectDomainApiUrl(core);
const issuer = pickBestPublicBase(
[stored, fromProject],
stored
);
if (isValidFedcmEndpointUrl(issuer)) {
return issuer;
}
if (fromProject && isValidFedcmEndpointUrl(fromProject)) {
return fromProject;
}
return issuer;
}
async function resolveCanonicalFedcmFrontend(core: CoreGrpcService): Promise<string> {
const stored = await resolveFrontendUrl(core);
const fromProject = await resolveProjectDomainFrontendUrl(core);
const frontend = pickBestPublicBase(
[stored, fromProject],
stored
);
if (isValidFedcmEndpointUrl(frontend) && !isLocalDevBaseUrl(frontend)) {
return frontend;
}
if (fromProject && isValidFedcmEndpointUrl(fromProject)) {
return fromProject;
}
return frontend;
}
export async function resolveFedcmEndpoints(core: CoreGrpcService, _req?: Request): Promise<FedcmEndpoints> {
// FedCM: web-identity (idpmvk.lpr) и config.json (api.idpmvk.lpr) ОБЯЗАНЫ
// возвращать одинаковые login_url / accounts_endpoint / configUrl.
// Хост запроса разный — никогда не выводим URL из req.headers.host.
const issuer = await resolveCanonicalFedcmIssuer(core);
const frontendUrl = await resolveCanonicalFedcmFrontend(core);
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) {
const base = normalizePublicBaseUrl(endpoints.issuer);
const front = normalizePublicBaseUrl(endpoints.frontendUrl);
return {
accounts_endpoint: `${base}/fedcm/accounts`,
client_metadata_endpoint: `${base}/fedcm/client_metadata`,
id_assertion_endpoint: `${base}/fedcm/id_assertion`,
login_url: endpoints.loginUrl,
branding: {
name: endpoints.projectName,
background_color: '#ffffff',
color: '#1f2430',
icons: [
{ url: `${front}/icon.svg`, size: 40 },
{ url: `${base}/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,
// Поля для navigator.credentials.get({ identity: { fields: [...] } }) на стороне RP (Chrome 132+).
suggestedFields: ['name', 'email', 'picture', 'tel']
};
}
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 };