fix and update
This commit is contained in:
@@ -494,6 +494,19 @@ export async function refreshAuthSession(): Promise<RefreshSessionResponse> {
|
||||
return response.json() as Promise<RefreshSessionResponse>;
|
||||
}
|
||||
|
||||
export async function syncFedcmSession(accessToken: string) {
|
||||
if (typeof window === 'undefined' || !accessToken.trim()) return;
|
||||
try {
|
||||
await fetch(`${getApiUrl()}/fedcm/session/sync`, {
|
||||
method: 'POST',
|
||||
headers: { Authorization: `Bearer ${accessToken.trim()}` },
|
||||
credentials: 'include'
|
||||
});
|
||||
} catch {
|
||||
// фоновая синхронизация FedCM cookie
|
||||
}
|
||||
}
|
||||
|
||||
async function trySilentTokenRefresh(): Promise<RefreshSessionResponse | null> {
|
||||
try {
|
||||
const refreshed = await refreshAuthSession();
|
||||
|
||||
35
apps/frontend/lib/oauth-popup-bridge.ts
Normal file
35
apps/frontend/lib/oauth-popup-bridge.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
export const ONE_TAP_MESSAGE_TYPE = 'lendry-sso-onetap';
|
||||
|
||||
export function parsePopupOAuthParams(searchParams: URLSearchParams) {
|
||||
const display = searchParams.get('display');
|
||||
const popupOrigin = searchParams.get('popup_origin');
|
||||
if (display !== 'popup' || !popupOrigin?.trim()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return { popupOrigin: new URL(popupOrigin.trim()).origin };
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function postOneTapResult(popupOrigin: string, payload: Record<string, unknown>) {
|
||||
if (!window.opener) {
|
||||
return false;
|
||||
}
|
||||
|
||||
window.opener.postMessage({ type: ONE_TAP_MESSAGE_TYPE, ...payload }, popupOrigin);
|
||||
window.close();
|
||||
return true;
|
||||
}
|
||||
|
||||
export function parseAuthorizationRedirect(redirectUrl: string) {
|
||||
const url = new URL(redirectUrl);
|
||||
return {
|
||||
code: url.searchParams.get('code'),
|
||||
state: url.searchParams.get('state'),
|
||||
error: url.searchParams.get('error'),
|
||||
errorDescription: url.searchParams.get('error_description')
|
||||
};
|
||||
}
|
||||
@@ -2,12 +2,29 @@ export function normalizeBaseUrl(url: string) {
|
||||
return url.trim().replace(/\/+$/, '');
|
||||
}
|
||||
|
||||
export function resolveOAuthApiBase(settings: Record<string, string>, fallback = 'http://localhost:3000') {
|
||||
export function resolveOAuthApiBase(settings: Record<string, string>, fallback = 'http://localhost:3002/idp-api') {
|
||||
const publicApi = settings.PUBLIC_API_URL?.trim();
|
||||
if (publicApi) {
|
||||
return normalizeBaseUrl(publicApi);
|
||||
}
|
||||
|
||||
const domain = settings.PROJECT_DOMAIN?.trim();
|
||||
if (domain) {
|
||||
if (domain.startsWith('http://') || domain.startsWith('https://')) {
|
||||
return normalizeBaseUrl(domain);
|
||||
}
|
||||
return `https://${domain.replace(/^\/+/, '')}/idp-api`;
|
||||
}
|
||||
|
||||
return normalizeBaseUrl(fallback);
|
||||
}
|
||||
|
||||
export function resolveFrontendBase(settings: Record<string, string>, fallback = 'http://localhost:3002') {
|
||||
const publicFrontend = settings.PUBLIC_FRONTEND_URL?.trim();
|
||||
if (publicFrontend) {
|
||||
return normalizeBaseUrl(publicFrontend);
|
||||
}
|
||||
|
||||
const domain = settings.PROJECT_DOMAIN?.trim();
|
||||
if (domain) {
|
||||
if (domain.startsWith('http://') || domain.startsWith('https://')) {
|
||||
@@ -26,20 +43,48 @@ export interface OAuthEndpoints {
|
||||
userInfoEndpoint: string;
|
||||
openIdConfigurationUrl: string;
|
||||
jwksUrl: string;
|
||||
webIdentityUrl: string;
|
||||
fedcmConfigUrl: string;
|
||||
fedcmDiscoverUrl: string;
|
||||
fedcmAccountsUrl: string;
|
||||
fedcmIdAssertionUrl: string;
|
||||
widgetUrl: string;
|
||||
}
|
||||
|
||||
export function buildOAuthEndpoints(apiBase: string): OAuthEndpoints {
|
||||
export function buildOAuthEndpoints(apiBase: string, frontendBase?: string): OAuthEndpoints {
|
||||
const base = normalizeBaseUrl(apiBase);
|
||||
const front = normalizeBaseUrl(frontendBase ?? deriveFrontendBaseFromApi(base));
|
||||
let webIdentityOrigin = front;
|
||||
|
||||
try {
|
||||
webIdentityOrigin = new URL(base).origin;
|
||||
} catch {
|
||||
// keep frontend origin
|
||||
}
|
||||
|
||||
return {
|
||||
issuer: base,
|
||||
authorizationEndpoint: `${base}/oauth/authorize`,
|
||||
tokenEndpoint: `${base}/oauth/token`,
|
||||
userInfoEndpoint: `${base}/oauth/userinfo`,
|
||||
openIdConfigurationUrl: `${base}/.well-known/openid-configuration`,
|
||||
jwksUrl: `${base}/.well-known/jwks.json`
|
||||
jwksUrl: `${base}/.well-known/jwks.json`,
|
||||
webIdentityUrl: `${webIdentityOrigin}/.well-known/web-identity`,
|
||||
fedcmConfigUrl: `${base}/fedcm/config.json`,
|
||||
fedcmDiscoverUrl: `${base}/fedcm/discover.json`,
|
||||
fedcmAccountsUrl: `${base}/fedcm/accounts`,
|
||||
fedcmIdAssertionUrl: `${base}/fedcm/id_assertion`,
|
||||
widgetUrl: `${front}/sso-widget.js`
|
||||
};
|
||||
}
|
||||
|
||||
function deriveFrontendBaseFromApi(apiBase: string) {
|
||||
if (apiBase.endsWith('/idp-api')) {
|
||||
return apiBase.replace(/\/idp-api$/, '');
|
||||
}
|
||||
return apiBase;
|
||||
}
|
||||
|
||||
export function buildAuthorizeUrl(
|
||||
apiBase: string,
|
||||
params: { clientId: string; redirectUri: string; scope: string; userId?: string; state?: string; useStandardParams?: boolean }
|
||||
|
||||
130
apps/frontend/lib/one-tap-examples.ts
Normal file
130
apps/frontend/lib/one-tap-examples.ts
Normal file
@@ -0,0 +1,130 @@
|
||||
export interface OneTapUrls {
|
||||
apiBase: string;
|
||||
frontendBase: string;
|
||||
widgetUrl: string;
|
||||
fedcmConfigUrl: string;
|
||||
webIdentityUrl: string;
|
||||
fedcmDiscoverUrl: string;
|
||||
projectName: string;
|
||||
}
|
||||
|
||||
export interface OneTapExample {
|
||||
id: string;
|
||||
label: string;
|
||||
language: string;
|
||||
code: string;
|
||||
}
|
||||
|
||||
export function buildOneTapUrls(
|
||||
apiBase: string,
|
||||
frontendBase: string,
|
||||
projectName = 'MVK ID'
|
||||
): OneTapUrls {
|
||||
const base = apiBase.replace(/\/+$/, '');
|
||||
const front = frontendBase.replace(/\/+$/, '');
|
||||
let webIdentityOrigin = front;
|
||||
|
||||
try {
|
||||
webIdentityOrigin = new URL(base).origin;
|
||||
} catch {
|
||||
// keep frontend origin
|
||||
}
|
||||
|
||||
return {
|
||||
apiBase: base,
|
||||
frontendBase: front,
|
||||
widgetUrl: `${front}/sso-widget.js`,
|
||||
fedcmConfigUrl: `${base}/fedcm/config.json`,
|
||||
fedcmDiscoverUrl: `${base}/fedcm/discover.json`,
|
||||
webIdentityUrl: `${webIdentityOrigin}/.well-known/web-identity`,
|
||||
projectName
|
||||
};
|
||||
}
|
||||
|
||||
export function buildOneTapExamples(urls: OneTapUrls, clientId: string, redirectUri?: string): OneTapExample[] {
|
||||
const { apiBase, frontendBase, widgetUrl, fedcmConfigUrl, webIdentityUrl, projectName } = urls;
|
||||
const callbackUri = redirectUri?.trim() || 'https://app.example.com/auth/callback';
|
||||
|
||||
return [
|
||||
{
|
||||
id: 'widget-script',
|
||||
label: 'Виджет (script)',
|
||||
language: 'html',
|
||||
code: `<script
|
||||
src="${widgetUrl}"
|
||||
data-client-id="${clientId}"
|
||||
data-idp-url="${apiBase}"
|
||||
data-idp-frontend-url="${frontendBase}"
|
||||
data-provider-name="${projectName}"
|
||||
data-redirect-uri="${callbackUri}"
|
||||
data-on-success="handleLendryLogin"
|
||||
></script>
|
||||
|
||||
<script>
|
||||
function handleLendryLogin(payload) {
|
||||
// payload.method — 'fedcm' | 'popup'
|
||||
// payload.token / payload.idToken / payload.code
|
||||
console.log('Вход через', payload.method, payload);
|
||||
}
|
||||
</script>`
|
||||
},
|
||||
{
|
||||
id: 'sdk-manual',
|
||||
label: 'SDK вручную',
|
||||
language: 'javascript',
|
||||
code: `<script src="${widgetUrl}" data-auto-init="false"></script>
|
||||
<script>
|
||||
LendryIdOneTap.init({
|
||||
clientId: '${clientId}',
|
||||
idpUrl: '${apiBase}',
|
||||
frontendUrl: '${frontendBase}',
|
||||
providerName: '${projectName}',
|
||||
redirectUri: '${callbackUri}'
|
||||
});
|
||||
|
||||
window.addEventListener('lendry-sso-onetap-success', (event) => {
|
||||
console.log(event.detail);
|
||||
});
|
||||
</script>`
|
||||
},
|
||||
{
|
||||
id: 'fedcm-native',
|
||||
label: 'FedCM API',
|
||||
language: 'javascript',
|
||||
code: `const credential = await navigator.credentials.get({
|
||||
identity: {
|
||||
providers: [{
|
||||
configURL: '${fedcmConfigUrl}',
|
||||
clientId: '${clientId}'
|
||||
}]
|
||||
},
|
||||
mediation: 'optional'
|
||||
});
|
||||
|
||||
const idToken = credential?.token; // JWT — проверьте на backend`
|
||||
},
|
||||
{
|
||||
id: 'postmessage',
|
||||
label: 'Popup postMessage',
|
||||
language: 'javascript',
|
||||
code: `window.addEventListener('message', (event) => {
|
||||
if (event.origin !== '${frontendBase}') return;
|
||||
if (event.data?.type !== 'lendry-sso-onetap') return;
|
||||
|
||||
const { code, idToken, token, error } = event.data;
|
||||
if (error) return console.error(error);
|
||||
|
||||
// code — обменяйте на POST ${apiBase}/oauth/token
|
||||
// idToken / token — проверьте JWT (issuer = ${apiBase})
|
||||
});`
|
||||
},
|
||||
{
|
||||
id: 'curl-fedcm',
|
||||
label: 'Диагностика (curl)',
|
||||
language: 'bash',
|
||||
code: `curl -sH "Sec-Fetch-Dest: webidentity" ${webIdentityUrl}
|
||||
curl -sH "Sec-Fetch-Dest: webidentity" ${fedcmConfigUrl}
|
||||
curl -s ${apiBase}/fedcm/discover.json`
|
||||
}
|
||||
];
|
||||
}
|
||||
@@ -13,6 +13,7 @@ export interface SystemSettingMeta {
|
||||
|
||||
export const SYSTEM_SETTING_GROUPS: Record<string, string> = {
|
||||
general: 'Проект и брендинг',
|
||||
onetap: 'One Tap Login / FedCM',
|
||||
pin: 'PIN-код и блокировка сессии',
|
||||
auth: 'Аутентификация и регистрация',
|
||||
ldap: 'LDAP / Active Directory',
|
||||
@@ -30,7 +31,37 @@ export const SYSTEM_SETTING_CATALOG: SystemSettingMeta[] = [
|
||||
label: 'URL API (OAuth issuer)',
|
||||
group: 'general',
|
||||
type: 'text',
|
||||
hint: 'Базовый URL API для OAuth/OIDC. Пример: https://sso.example.ru/idp-api или https://api.example.ru'
|
||||
hint: 'Базовый URL API для OAuth/OIDC и FedCM. При same-origin деплое: https://ваш-домен/idp-api'
|
||||
},
|
||||
{
|
||||
key: 'PUBLIC_FRONTEND_URL',
|
||||
label: 'URL Frontend IdP',
|
||||
group: 'general',
|
||||
type: 'text',
|
||||
hint: 'Публичный адрес интерфейса IdP. Используется для login_url FedCM и popup OAuth'
|
||||
},
|
||||
{
|
||||
key: 'ONE_TAP_ENABLED',
|
||||
label: 'One Tap Login включён',
|
||||
group: 'onetap',
|
||||
type: 'boolean',
|
||||
hint: 'FedCM и виджет sso-widget.js. При отключении остаётся только стандартный OAuth redirect'
|
||||
},
|
||||
{
|
||||
key: 'FEDCM_PRIVACY_POLICY_URL',
|
||||
label: 'FedCM — политика конфиденциальности',
|
||||
group: 'onetap',
|
||||
type: 'text',
|
||||
hint: 'URL для UI FedCM. Пусто — используется страница «Данные» IdP',
|
||||
showWhen: { key: 'ONE_TAP_ENABLED', equals: 'true' }
|
||||
},
|
||||
{
|
||||
key: 'FEDCM_TERMS_URL',
|
||||
label: 'FedCM — условия использования',
|
||||
group: 'onetap',
|
||||
type: 'text',
|
||||
hint: 'URL для UI FedCM. Пусто — используется страница «Данные» IdP',
|
||||
showWhen: { key: 'ONE_TAP_ENABLED', equals: 'true' }
|
||||
},
|
||||
{ key: 'PIN_LOCK_TIMEOUT_MINUTES', label: 'Таймаут блокировки PIN', group: 'pin', type: 'number', unit: 'мин' },
|
||||
{ key: 'PIN_DELETE_GRACE_MINUTES', label: 'Задержка удаления PIN', group: 'pin', type: 'number', unit: 'мин', hint: 'Сколько ждать после запроса удаления PIN-кода' },
|
||||
|
||||
Reference in New Issue
Block a user