fix and update
This commit is contained in:
@@ -1,11 +1,13 @@
|
||||
import { Body, Controller, Get, Headers, Param, Post, Req } from '@nestjs/common';
|
||||
import { Body, Controller, ForbiddenException, Get, Headers, Param, Post, Req } from '@nestjs/common';
|
||||
import type { Request } from 'express';
|
||||
import { firstValueFrom } from 'rxjs';
|
||||
import { enrichAuthClientMeta } from '../client-request.util';
|
||||
import { JwtService } from '@nestjs/jwt';
|
||||
import { ApiBearerAuth, ApiBody, ApiOperation, ApiParam, ApiResponse, ApiTags } from '@nestjs/swagger';
|
||||
import { CoreGrpcService } from '../core-grpc.service';
|
||||
import { QrSessionDto, WebAuthnDto } from '../dto/identity.dto';
|
||||
import { resolveAuthorizedPayload } from '../session-auth';
|
||||
import { resolveFrontendUrl } from '../lib/oauth-issuer';
|
||||
|
||||
@ApiTags('Биометрия и QR-вход')
|
||||
@Controller('auth/advanced')
|
||||
@@ -53,6 +55,30 @@ export class AdvancedAuthController {
|
||||
return this.core.advancedAuth.PollQrSession({ sessionId });
|
||||
}
|
||||
|
||||
@Post('qr/session/:sessionId/claim')
|
||||
@ApiOperation({
|
||||
summary: 'Привязать QR-сессию к устройству',
|
||||
description: 'Новое устройство подтверждает сканирование QR-кода для подключения из раздела «Безопасность».'
|
||||
})
|
||||
@ApiParam({ name: 'sessionId', description: 'ID QR-сессии' })
|
||||
@ApiBody({ type: QrSessionDto })
|
||||
@ApiResponse({ status: 201, description: 'QR-сессия привязана к устройству' })
|
||||
claimQr(@Param('sessionId') sessionId: string, @Body() dto: QrSessionDto, @Req() req: Request) {
|
||||
const enriched = enrichAuthClientMeta(req, {
|
||||
deviceName: dto.deviceName,
|
||||
fingerprint: dto.fingerprint,
|
||||
deviceType: dto.deviceType ?? 'WEB'
|
||||
});
|
||||
return this.core.advancedAuth.ClaimQrSession({
|
||||
sessionId,
|
||||
deviceName: enriched.deviceName,
|
||||
fingerprint: enriched.fingerprint,
|
||||
deviceType: enriched.deviceType,
|
||||
ipAddress: enriched.ipAddress,
|
||||
userAgent: enriched.userAgent
|
||||
});
|
||||
}
|
||||
|
||||
@Post('qr/session/:sessionId/approve')
|
||||
@ApiBearerAuth()
|
||||
@ApiOperation({ summary: 'Подтвердить QR-вход', description: 'Подтверждает QR-сессию с мобильного приложения уже авторизованным пользователем.' })
|
||||
|
||||
@@ -31,6 +31,7 @@ import {
|
||||
resolveFedcmEndpoints
|
||||
} from '../lib/fedcm-config';
|
||||
import { resolveFedcmSessionFromRequest, setFedcmSessionCookie } from '../lib/fedcm-cookie';
|
||||
import { resolveFedcmSessionPinState } from '../lib/fedcm-session';
|
||||
import { verifyAccessToken } from '../session-auth';
|
||||
|
||||
type FedcmAccountsResponse = {
|
||||
@@ -128,12 +129,17 @@ export class FedcmController {
|
||||
applyFedcmCorsHeaders(res, origin);
|
||||
res.setHeader('Content-Type', 'application/json; charset=utf-8');
|
||||
|
||||
const session = await resolveFedcmSessionFromRequest(this.jwt, req.headers.cookie);
|
||||
const { session, requiresPin } = await resolveFedcmSessionPinState(this.jwt, this.core, req.headers.cookie);
|
||||
if (!session) {
|
||||
applyFedcmLoginStatus(res, false);
|
||||
return { accounts: [] };
|
||||
}
|
||||
|
||||
if (requiresPin) {
|
||||
applyFedcmLoginStatus(res, false);
|
||||
return { accounts: [] };
|
||||
}
|
||||
|
||||
try {
|
||||
const result = (await firstValueFrom(
|
||||
this.core.fedcm.GetAccounts({ userId: session.sub, sessionId: session.sessionId })
|
||||
@@ -180,11 +186,16 @@ export class FedcmController {
|
||||
applyFedcmCorsHeaders(res, rpOrigin);
|
||||
res.setHeader('Content-Type', 'application/json; charset=utf-8');
|
||||
|
||||
const session = await resolveFedcmSessionFromRequest(this.jwt, req.headers.cookie);
|
||||
const { session, requiresPin } = await resolveFedcmSessionPinState(this.jwt, this.core, req.headers.cookie);
|
||||
if (!session) {
|
||||
throw new UnauthorizedException('Сессия FedCM не найдена');
|
||||
}
|
||||
|
||||
if (requiresPin) {
|
||||
applyFedcmLoginStatus(res, false);
|
||||
throw new UnauthorizedException('Требуется подтверждение PIN-кода');
|
||||
}
|
||||
|
||||
const body = (req.body ?? {}) as Record<string, unknown>;
|
||||
const clientId = String(body.client_id ?? body.clientId ?? '').trim();
|
||||
const accountId = String(body.account_id ?? body.accountId ?? '').trim();
|
||||
@@ -298,8 +309,8 @@ export class FedcmController {
|
||||
'Минимальная HTML-страница на API-домене: Set-Login + navigator.login.setStatus для Chrome FedCM (origin login_url).'
|
||||
})
|
||||
async loginStatus(@Req() req: Request, @Res() res: Response) {
|
||||
const session = await resolveFedcmSessionFromRequest(this.jwt, req.headers.cookie);
|
||||
const loggedIn = Boolean(session);
|
||||
const { session, requiresPin } = await resolveFedcmSessionPinState(this.jwt, this.core, req.headers.cookie);
|
||||
const loggedIn = Boolean(session && !requiresPin);
|
||||
applyFedcmLoginStatus(res, loggedIn);
|
||||
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
||||
res.setHeader('Cache-Control', 'no-store');
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { Body, Controller, Get, Headers, Param, Post } from '@nestjs/common';
|
||||
import { Body, Controller, ForbiddenException, Get, Headers, Param, Post } from '@nestjs/common';
|
||||
import { JwtService } from '@nestjs/jwt';
|
||||
import { ApiBearerAuth, ApiBody, ApiOperation, ApiParam, ApiResponse, ApiTags } from '@nestjs/swagger';
|
||||
import { firstValueFrom } from 'rxjs';
|
||||
import { CoreGrpcService } from '../core-grpc.service';
|
||||
import { OptionalPinDto, PinDto, TotpCodeDto, VerifySecurityPinDto } from '../dto/security.dto';
|
||||
import { resolveAuthorizedPayload } from '../session-auth';
|
||||
import { resolveFrontendUrl } from '../lib/oauth-issuer';
|
||||
|
||||
@ApiTags('Безопасность')
|
||||
@ApiBearerAuth()
|
||||
@@ -148,4 +150,20 @@ export class SecurityController {
|
||||
const payload = await resolveAuthorizedPayload(this.jwt, this.core, authorization);
|
||||
return this.core.security.RevokeAllSessions({ userId, exceptSessionId: payload.sessionId });
|
||||
}
|
||||
|
||||
@Post('users/:userId/device-link/session')
|
||||
@ApiOperation({
|
||||
summary: 'Создать QR для подключения устройства',
|
||||
description: 'Генерирует QR-код (5 минут) для входа на новом устройстве через раздел «Безопасность».'
|
||||
})
|
||||
@ApiParam({ name: 'userId', description: 'ID пользователя' })
|
||||
@ApiResponse({ status: 201, description: 'QR-сессия для подключения устройства создана' })
|
||||
async createDeviceLinkSession(@Param('userId') userId: string, @Headers('authorization') authorization?: string) {
|
||||
const payload = await resolveAuthorizedPayload(this.jwt, this.core, authorization);
|
||||
if (payload.sub !== userId) {
|
||||
throw new ForbiddenException('Недостаточно прав для подключения устройства');
|
||||
}
|
||||
const frontendUrl = await resolveFrontendUrl(this.core);
|
||||
return firstValueFrom(this.core.advancedAuth.CreateDeviceLinkSession({ userId, frontendUrl }));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ function buildFedcmEndpointUrls(issuer: string, frontendUrl: string) {
|
||||
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`,
|
||||
loginUrl: `${base}/auth/login?fedcm=1`,
|
||||
signupUrl: `${front}/auth/register`,
|
||||
webIdentityUrl: `${webIdentityOrigin}/.well-known/web-identity`
|
||||
};
|
||||
|
||||
26
apps/api-gateway/src/lib/fedcm-session.ts
Normal file
26
apps/api-gateway/src/lib/fedcm-session.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { JwtService } from '@nestjs/jwt';
|
||||
import { firstValueFrom } from 'rxjs';
|
||||
import { CoreGrpcService } from '../core-grpc.service';
|
||||
import { FedcmSessionPayload, resolveFedcmSessionFromRequest } from './fedcm-cookie';
|
||||
|
||||
export async function resolveFedcmSessionPinState(
|
||||
jwt: JwtService,
|
||||
core: CoreGrpcService,
|
||||
cookieHeader?: string
|
||||
): Promise<{ session: FedcmSessionPayload | null; requiresPin: boolean }> {
|
||||
const session = await resolveFedcmSessionFromRequest(jwt, cookieHeader);
|
||||
if (!session) {
|
||||
return { session: null, requiresPin: false };
|
||||
}
|
||||
|
||||
const validation = (await firstValueFrom(
|
||||
core.auth.ValidateSession({
|
||||
userId: session.sub,
|
||||
sessionId: session.sessionId,
|
||||
touchActivity: false
|
||||
})
|
||||
)) as { requiresPin: boolean; pinVerified?: boolean };
|
||||
|
||||
const requiresPin = Boolean(validation.requiresPin || !session.pinVerified);
|
||||
return { session, requiresPin };
|
||||
}
|
||||
Reference in New Issue
Block a user