186 lines
5.3 KiB
TypeScript
186 lines
5.3 KiB
TypeScript
import type { Request } from 'express';
|
|
import { firstValueFrom } from 'rxjs';
|
|
import { CoreGrpcService } from '../core-grpc.service';
|
|
import { resolveFrontendUrl, resolveOAuthIssuer } from './oauth-issuer';
|
|
import {
|
|
hostsMatch,
|
|
normalizePublicBaseUrl,
|
|
preferBrowserReachableBase,
|
|
readRequestHost,
|
|
readRequestProto,
|
|
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);
|
|
|
|
let webIdentityOrigin = front;
|
|
try {
|
|
webIdentityOrigin = new URL(base).origin;
|
|
} catch {
|
|
// keep frontend origin
|
|
}
|
|
|
|
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`
|
|
};
|
|
}
|
|
|
|
function resolveSameOriginFrontend(storedFrontend: string, req?: Request): string {
|
|
const fromRequest = resolvePublicFrontendBaseFromRequest(req);
|
|
const preferred = preferBrowserReachableBase(storedFrontend, fromRequest);
|
|
if (preferred !== storedFrontend) {
|
|
return preferred;
|
|
}
|
|
|
|
const host = readRequestHost(req);
|
|
if (!host) {
|
|
return storedFrontend;
|
|
}
|
|
|
|
const requestOrigin = normalizePublicBaseUrl(`${readRequestProto(req)}://${host}`);
|
|
|
|
try {
|
|
const frontendUrl = new URL(storedFrontend);
|
|
const requestUrl = new URL(requestOrigin);
|
|
|
|
if (hostsMatch(frontendUrl.hostname, requestUrl.hostname)) {
|
|
return requestOrigin;
|
|
}
|
|
} catch {
|
|
return storedFrontend;
|
|
}
|
|
|
|
return storedFrontend;
|
|
}
|
|
|
|
export function resolveSameOriginIssuer(storedIssuer: string, storedFrontend: string, req?: Request): string {
|
|
const fromRequest = resolvePublicApiBaseFromRequest(req);
|
|
const preferred = preferBrowserReachableBase(storedIssuer, fromRequest);
|
|
if (preferred !== storedIssuer) {
|
|
return preferred;
|
|
}
|
|
|
|
const host = readRequestHost(req);
|
|
if (!host) {
|
|
return storedIssuer;
|
|
}
|
|
|
|
const requestOrigin = normalizePublicBaseUrl(`${readRequestProto(req)}://${host}`);
|
|
const sameOriginApi = `${requestOrigin}/idp-api`;
|
|
|
|
try {
|
|
const issuerUrl = new URL(storedIssuer);
|
|
const frontendUrl = new URL(storedFrontend);
|
|
const requestUrl = new URL(requestOrigin);
|
|
|
|
const hostMatches =
|
|
hostsMatch(issuerUrl.hostname, requestUrl.hostname) || hostsMatch(frontendUrl.hostname, requestUrl.hostname);
|
|
|
|
if (!hostMatches) {
|
|
return storedIssuer;
|
|
}
|
|
|
|
if (normalizePublicBaseUrl(storedIssuer) !== sameOriginApi) {
|
|
return sameOriginApi;
|
|
}
|
|
} catch {
|
|
return fromRequest ?? storedIssuer;
|
|
}
|
|
|
|
return storedIssuer;
|
|
}
|
|
|
|
export async function resolveFedcmEndpoints(core: CoreGrpcService, req?: Request): Promise<FedcmEndpoints> {
|
|
const storedIssuer = await resolveOAuthIssuer(core);
|
|
const storedFrontend = await resolveFrontendUrl(core);
|
|
const frontendUrl = resolveSameOriginFrontend(storedFrontend, req);
|
|
const issuer = resolveSameOriginIssuer(storedIssuer, storedFrontend, 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: '#3390ec',
|
|
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 };
|