احراز هویت NestJS با OAuth2.0: Fastify Local OAuth REST API

معرفی سری
این مجموعه اجرای کامل OAuth2.0 Authentication در NestJS را برای انواع API های زیر پوشش می دهد:
و به 5 قسمت تقسیم می شود:
- پیکربندی و عملیات؛
- Express Local OAuth REST API؛
- Fastify Local OAuth REST API؛
- Apollo Local OAuth GraphQL API؛
- افزودن ارائه دهندگان OAuth خارجی به API ما؛
بیایید قسمت سوم این مجموعه را شروع کنیم.
معرفی آموزش
در این آموزش ما آداپتور را از REST API قبلی خود، از Express به Fastify تغییر می دهیم.
TLDR: اگر 25 دقیقه برای خواندن مقاله ندارید، کد را می توانید در این مخزن پیدا کنید
برپایی
با حذف express و وابستگی های آن شروع کنید:
$ yarn remove @types/express @types/express-serve-static-core @nestjs/platform-express cookie-parser helmet @types/cookie-parser
و fastify را نصب کنید:
$ yarn add @nestjs/platform-fastify fastify @fastify/cookie @fastify/cors @fastify/csrf-protection @fastify/helmet
ماژول احراز هویت
نگهبانان
باید حذف کنیم express.d.ts
و اضافه کنید fastify.d.ts
:
import { FastifyRequest as Request } from 'fastify';
declare module 'fastify' {
interface FastifyRequest extends Request {
user?: number;
}
}
گارد احراز هویت
فقط یکی از انواع تغییر می کند، از Request
به FastifyRequest
:
import {
CanActivate,
ExecutionContext,
Injectable,
UnauthorizedException,
} from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { isJWT } from 'class-validator';
import { FastifyRequest } from 'fastify';
import { isNull, isUndefined } from '../../common/utils/validation.util';
import { TokenTypeEnum } from '../../jwt/enums/token-type.enum';
import { JwtService } from '../../jwt/jwt.service';
import { IS_PUBLIC_KEY } from '../decorators/public.decorator';
@Injectable()
export class AuthGuard implements CanActivate {
constructor(
private readonly reflector: Reflector,
private readonly jwtService: JwtService,
) {}
public async canActivate(context: ExecutionContext): Promise<boolean> {
const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
context.getHandler(),
context.getClass(),
]);
const activate = await this.setHttpHeader(
context.switchToHttp().getRequest<FastifyRequest>(),
isPublic,
);
if (!activate) {
throw new UnauthorizedException();
}
return activate;
}
/**
* Sets HTTP Header
*
* Checks if the header has a valid Bearer token, validates it and sets the User ID as the user.
*/
private async setHttpHeader(
req: FastifyRequest,
isPublic: boolean,
): Promise<boolean> {
const auth = req.headers?.authorization;
if (isUndefined(auth) || isNull(auth) || auth.length === 0) {
return isPublic;
}
const authArr = auth.split(' ');
const bearer = authArr[0];
const token = authArr[1];
if (isUndefined(bearer) || isNull(bearer) || bearer !== 'Bearer') {
return isPublic;
}
if (isUndefined(token) || isNull(token) || !isJWT(token)) {
return isPublic;
}
try {
const { id } = await this.jwtService.verifyToken(
token,
TokenTypeEnum.ACCESS,
);
req.user = id;
return true;
} catch (_) {
return isPublic;
}
}
}
(اختیاری) گارد دریچه گاز
ما دیگر نمی توانیم از پیش فرض NestJS استفاده کنیم ThrottlerGuard
همانطور که آن یکی برای Express پیاده سازی شده است، پس یک سفارشی ایجاد کنید:
// fastify-throttler.guard.ts
import { ExecutionContext, Injectable } from '@nestjs/common';
import { ThrottlerGuard } from '@nestjs/throttler';
import { FastifyReply, FastifyRequest } from 'fastify';
@Injectable()
export class FastifyThrottlerGuard extends ThrottlerGuard {
public getRequestResponse(context: ExecutionContext) {
const http = context.switchToHttp();
return {
req: http.getRequest<FastifyRequest>(),
res: http.getResponse<FastifyReply>(),
};
}
}
و آن را به کنترل کننده اعتبار اضافه کنید:
// ...
import { FastifyThrottlerGuard } from './guards/fastify-throttler.guard';
// ...
@ApiTags('Auth')
@Controller('api/auth')
@UseGuards(FastifyThrottlerGuard)
export class AuthController {
// ...
}
کنترل کننده
روش های خصوصی
تغییر جزئی در آن وجود دارد refresTokenFromReq
روش:
import {
// ...
Controller,
// ...
UseGuards,
} from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import {
// ...
ApiTags,
// ...
} from '@nestjs/swagger';
import { FastifyReply, FastifyRequest } from 'fastify';
// ...
import { isNull, isUndefined } from '../common/utils/validation.util';
// ...
@ApiTags('Auth')
@Controller('api/auth')
@UseGuards(FastifyThrottlerGuard)
export class AuthController {
// ...
private refreshTokenFromReq(req: FastifyRequest): string {
const token: string | undefined = req.cookies[this.cookieName];
if (isUndefined(token) || isNull(token)) {
throw new UnauthorizedException();
}
const { valid, value } = req.unsignCookie(token);
if (!valid) {
throw new UnauthorizedException();
}
return value;
}
// ...
}
و از آنجایی که fastify ندارد json
روشی که باید تنظیم کنیم Content-Type
سربرگ به application/json
:
// ...
@ApiTags('Auth')
@Controller('api/auth')
@UseGuards(FastifyThrottlerGuard)
export class AuthController {
// ...
private saveRefreshCookie(
res: FastifyReply,
refreshToken: string,
): FastifyReply {
return res
.cookie(this.cookieName, refreshToken, {
secure: !this.testing,
httpOnly: true,
signed: true,
path: this.cookiePath,
expires: new Date(Date.now() + this.refreshTime * 1000),
})
.header('Content-Type', 'application/json');
}
}
اشاره می کند
تغییرات زیادی وجود ندارد که باید در API اکسپرس قبلی خود اعمال کنیم، فقط باید از موارد زیر تغییر کنیم:
-
Request
بهFastifyRequest
بر رویReq
دکوراتور؛ -
Response
بهFastifyReply
بر رویRes
دکوراتور؛ -
json
بهsend
روش.
// ...
@ApiTags('Auth')
@Controller('api/auth')
@UseGuards(FastifyThrottlerGuard)
export class AuthController {
// ...
@Public()
@Post('/sign-up')
@ApiCreatedResponse({
type: MessageMapper,
description: 'The user has been created and is waiting confirmation',
})
@ApiConflictResponse({
description: 'Email already in use',
})
@ApiBadRequestResponse({
description: 'Something is invalid on the request body',
})
public async signUp(
@Origin() origin: string | undefined,
@Body() signUpDto: SignUpDto,
): Promise<IMessage> {
return await this.authService.signUp(signUpDto, origin);
}
@Public()
@Post('/sign-in')
@ApiOkResponse({
type: AuthResponseMapper,
description: 'Logs in the user and returns the access token',
})
@ApiBadRequestResponse({
description: 'Something is invalid on the request body',
})
@ApiUnauthorizedResponse({
description: 'Invalid credentials or User is not confirmed',
})
public async signIn(
@Res() res: FastifyReply,
@Origin() origin: string | undefined,
@Body() singInDto: SignInDto,
): Promise<void> {
const result = await this.authService.signIn(singInDto, origin);
this.saveRefreshCookie(res, result.refreshToken)
.status(HttpStatus.OK)
.send(AuthResponseMapper.map(result));
}
@Public()
@Post('/refresh-access')
@ApiOkResponse({
type: AuthResponseMapper,
description: 'Refreshes and returns the access token',
})
@ApiUnauthorizedResponse({
description: 'Invalid token',
})
@ApiBadRequestResponse({
description:
'Something is invalid on the request body, or Token is invalid or expired',
})
public async refreshAccess(
@Req() req: FastifyRequest,
@Res() res: FastifyReply,
): Promise<void> {
const token = this.refreshTokenFromReq(req);
const result = await this.authService.refreshTokenAccess(
token,
req.headers.origin,
);
this.saveRefreshCookie(res, result.refreshToken)
.status(HttpStatus.OK)
.send(AuthResponseMapper.map(result));
}
@Post('/logout')
@ApiOkResponse({
type: MessageMapper,
description: 'The user is logged out',
})
@ApiBadRequestResponse({
description: 'Something is invalid on the request body',
})
@ApiUnauthorizedResponse({
description: 'Invalid token',
})
public async logout(
@Req() req: FastifyRequest,
@Res() res: FastifyReply,
): Promise<void> {
const token = this.refreshTokenFromReq(req);
const message = await this.authService.logout(token);
res
.clearCookie(this.cookieName, { path: this.cookiePath })
.header('Content-Type', 'application/json')
.status(HttpStatus.OK)
.send(message);
}
@Public()
@Post('/confirm-email')
@ApiOkResponse({
type: AuthResponseMapper,
description: 'Confirms the user email and returns the access token',
})
@ApiUnauthorizedResponse({
description: 'Invalid token',
})
@ApiBadRequestResponse({
description:
'Something is invalid on the request body, or Token is invalid or expired',
})
public async confirmEmail(
@Origin() origin: string | undefined,
@Body() confirmEmailDto: ConfirmEmailDto,
@Res() res: FastifyReply,
): Promise<void> {
const result = await this.authService.confirmEmail(confirmEmailDto);
this.saveRefreshCookie(res, result.refreshToken)
.status(HttpStatus.OK)
.send(AuthResponseMapper.map(result));
}
@Public()
@Post('/forgot-password')
@HttpCode(HttpStatus.OK)
@ApiOkResponse({
type: MessageMapper,
description:
'An email has been sent to the user with the reset password link',
})
public async forgotPassword(
@Origin() origin: string | undefined,
@Body() emailDto: EmailDto,
): Promise<IMessage> {
return this.authService.resetPasswordEmail(emailDto, origin);
}
@Public()
@Post('/reset-password')
@HttpCode(HttpStatus.OK)
@ApiOkResponse({
type: MessageMapper,
description: 'The password has been reset',
})
@ApiBadRequestResponse({
description:
'Something is invalid on the request body, or Token is invalid or expired',
})
public async resetPassword(
@Body() resetPasswordDto: ResetPasswordDto,
): Promise<IMessage> {
return this.authService.resetPassword(resetPasswordDto);
}
@Patch('/update-password')
@ApiOkResponse({
type: AuthResponseMapper,
description: 'The password has been updated',
})
@ApiUnauthorizedResponse({
description: 'The user is not logged in.',
})
public async updatePassword(
@CurrentUser() userId: number,
@Origin() origin: string | undefined,
@Body() changePasswordDto: ChangePasswordDto,
@Res() res: FastifyReply,
): Promise<void> {
const result = await this.authService.updatePassword(
userId,
changePasswordDto,
origin,
);
this.saveRefreshCookie(res, result.refreshToken)
.status(HttpStatus.OK)
.send(AuthResponseMapper.map(result));
}
@Get('/me')
@ApiOkResponse({
type: AuthResponseUserMapper,
description: 'The user is found and returned.',
})
@ApiUnauthorizedResponse({
description: 'The user is not logged in.',
})
public async getMe(@CurrentUser() id: number): Promise<IAuthResponseUser> {
const user = await this.usersService.findOneById(id);
return AuthResponseUserMapper.map(user);
}
// ...
}
ماژول کاربر
کنترل کننده
فقط یک تغییر در نوع برگشتی وجود دارد Res
دکوراتور (از Response
به FastifyReply
) در نقطه پایانی حذف:
// ...
import { FastifyReply } from 'fastify';
// ...
@ApiTags('Users')
@Controller('api/users')
export class UsersController {
// ...
@Delete()
@ApiNoContentResponse({
description: 'The user is deleted.',
})
@ApiBadRequestResponse({
description: 'Something is invalid on the request body, or wrong password.',
})
@ApiUnauthorizedResponse({
description: 'The user is not logged in.',
})
public async deleteUser(
@CurrentUser() id: number,
@Body() dto: PasswordDto,
@Res() res: FastifyReply,
): Promise<void> {
await this.usersService.delete(id, dto);
res
.clearCookie(this.cookieName, { path: this.cookiePath })
.status(HttpStatus.NO_CONTENT)
.send();
}
}
اصلی
در نهایت تغییر دهید main
فایل به a NestFastifyApplication
، و تمام افزونه هایی را که قبلا نصب کرده ایم ثبت کنید:
import fastifyCookie from '@fastify/cookie';
import fastifyCors from '@fastify/cors';
import fastifyCsrfProtection from '@fastify/csrf-protection';
import fastifyHelmet from '@fastify/helmet';
import { ValidationPipe } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { NestFactory } from '@nestjs/core';
import {
FastifyAdapter,
NestFastifyApplication,
} from '@nestjs/platform-fastify';
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create<NestFastifyApplication>(
AppModule,
new FastifyAdapter(),
);
const configService = app.get(ConfigService);
app.register(fastifyCookie, {
secret: configService.get<string>('COOKIE_SECRET'),
});
app.register(fastifyHelmet);
app.register(fastifyCsrfProtection, { cookieOpts: { signed: true } });
app.register(fastifyCors, {
credentials: true,
origin: `https://${configService.get<string>('domain')}`,
});
app.useGlobalPipes(
new ValidationPipe({
transform: true,
}),
);
const swaggerConfig = new DocumentBuilder()
.setTitle('NestJS Authentication API')
.setDescription('An OAuth2.0 authentication API made with NestJS')
.setVersion('0.0.1')
.addBearerAuth()
.addTag('Authentication API')
.build();
const document = SwaggerModule.createDocument(app, swaggerConfig);
SwaggerModule.setup('api/docs', app, document);
await app.listen(
configService.get<number>('port'),
configService.get<boolean>('testing') ? '127.0.0.1' : '0.0.0.0',
);
}
bootstrap();
نتیجه
امیدوارم که نشان داده باشم چقدر آسان است که API را از Express به Fastify با NestJS تغییر دهید.
github برای این آموزش را می توانید در اینجا پیدا کنید.
درباره نویسنده
سلام، نام من Afonso Barracha است، من یک توسعه دهنده بک اند اقتصاد سنجی هستم که علاقه زیادی به GraphQL دارم.
من قبلاً سعی می کردم هفته ای یک بار پست بگذارم، اما در حال غوطه ور شدن در موضوعات پیشرفته تر هستم که نوشتن آنها زمان بیشتری می برد، اما هنوز هم سعی می کنم حداقل دو بار در ماه پست کنم.
اگر نمیخواهید هیچ یک از پستهای من را از دست بدهید، من را اینجا در توسعهدهنده یا لینکدین دنبال کنید.