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

151 lines
4.5 KiB
TypeScript

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`,
loginUrl: `${front}/auth/login`,
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) {
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 };