diff --git a/apps/api/src/auth/auth.service.ts b/apps/api/src/auth/auth.service.ts index 3eab476e..37f82001 100644 --- a/apps/api/src/auth/auth.service.ts +++ b/apps/api/src/auth/auth.service.ts @@ -19,8 +19,8 @@ import { RedisService } from 'src/common/redis/redis.service' @Injectable() export class AuthService { - private readonly JWT_ACCESS_TOKEN_TTL - private readonly JWT_REFRESH_TOKEN_TTL + private readonly JWT_ACCESS_TOKEN_TTL: string + private readonly JWT_REFRESH_TOKEN_TTL: string private readonly COOKIE_DOMAIN: string private readonly COOKIE_TTL: string @@ -132,7 +132,8 @@ export class AuthService { } } - this.setCookie(res, 'refreshToken', new Date(0)) + this.setCookie(res, 'refreshToken', '', new Date(0), '/auth/refresh') + this.setCookie(res, 'accessToken', '', new Date(0), '/') return { message: 'Пользователь успешно вышел', success: true } } @@ -158,10 +159,21 @@ export class AuthService { this.setCookie( res, + 'refreshToken', refreshToken, new Date(Date.now() + parseTTLToMs(this.COOKIE_TTL)), + '/auth/refresh', ) + this.setCookie( + res, + 'accessToken', + accessToken, + new Date(Date.now() + parseTTLToMs(this.JWT_ACCESS_TOKEN_TTL)), + '/', + ) + + // TODO: удалить когда фронтенд перейдёт на cookie-only return { accessToken } } @@ -169,23 +181,30 @@ export class AuthService { const payload: JwtPayload = { id: userId } const accessToken = this.jwtService.sign(payload, { - expiresIn: this.JWT_ACCESS_TOKEN_TTL, + expiresIn: parseTTLToMs(this.JWT_ACCESS_TOKEN_TTL) / 1000, }) const refreshToken = this.jwtService.sign(payload, { - expiresIn: this.JWT_REFRESH_TOKEN_TTL, + expiresIn: parseTTLToMs(this.JWT_REFRESH_TOKEN_TTL) / 1000, }) return { accessToken, refreshToken } } - private setCookie(res: Response, value: string, expires: Date) { - res.cookie('refreshToken', value, { + private setCookie( + res: Response, + name: string, + value: string, + expires: Date, + path = '/', + ) { + res.cookie(name, value, { httpOnly: true, secure: !isDev(this.configService), sameSite: isDev(this.configService) ? 'lax' : 'strict', domain: this.COOKIE_DOMAIN, expires, + path, }) } } diff --git a/apps/api/src/strategies/jwt.strategy.ts b/apps/api/src/strategies/jwt.strategy.ts index 0ea23765..6a5e8dda 100644 --- a/apps/api/src/strategies/jwt.strategy.ts +++ b/apps/api/src/strategies/jwt.strategy.ts @@ -2,6 +2,7 @@ import { Injectable } from '@nestjs/common' import { ConfigService } from '@nestjs/config' import { PassportStrategy } from '@nestjs/passport' import { ExtractJwt, Strategy } from 'passport-jwt' +import type { Request } from 'express' import { AuthService } from 'src/auth/auth.service' import { JwtPayload } from 'src/auth/interfaces/jwt.interface' @@ -12,7 +13,10 @@ export class JwtStrategy extends PassportStrategy(Strategy) { private readonly configService: ConfigService, ) { super({ - jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), + jwtFromRequest: ExtractJwt.fromExtractors([ + ExtractJwt.fromAuthHeaderAsBearerToken(), + (req: Request) => req?.cookies?.['accessToken'] ?? null, + ]), ignoreExpiration: false, secretOrKey: configService.getOrThrow('JWT_SECRET'), algorithms: ['HS256'], diff --git a/apps/web/proxy.ts b/apps/web/proxy.ts index 3b8febe9..0c65fb66 100644 --- a/apps/web/proxy.ts +++ b/apps/web/proxy.ts @@ -8,15 +8,15 @@ import { NextResponse, type NextRequest, type ProxyConfig } from 'next/server' export function proxy(request: NextRequest) { const { pathname, search } = request.nextUrl - const refreshToken = !!request.cookies.get('refreshToken')?.value + const accessToken = !!request.cookies.get('accessToken')?.value - if (!refreshToken && isProtectedRoute(pathname)) { + if (!accessToken && isProtectedRoute(pathname)) { const url = new URL(ROUTES.login, request.url) url.searchParams.set(ROUTE_QUERY_PARAMS.from, `${pathname}${search}`) return NextResponse.redirect(url) } - if (refreshToken && isAuthRoute(pathname)) { + if (accessToken && isAuthRoute(pathname)) { return NextResponse.redirect(new URL(ROUTES.teams, request.url)) }