From ab5187349398bf83fe6754522aa1574cd7ffc0c0 Mon Sep 17 00:00:00 2001 From: M1000fr Date: Mon, 2 Dec 2024 16:39:18 +0100 Subject: [PATCH] feat: Implement refresh jwt token --- src/modules/auth/Dto/refresh.dto.ts | 8 +++ src/modules/auth/Guards/refresh.guard.ts | 5 ++ src/modules/auth/Strategy/discord.strategy.ts | 15 +++-- src/modules/auth/Strategy/jwt.strategy.ts | 2 +- src/modules/auth/Strategy/refresh.strategy.ts | 30 ++++++++++ src/modules/auth/auth.controller.ts | 58 +++++++++++++++++-- src/modules/auth/auth.module.ts | 3 +- src/modules/auth/auth.service.ts | 31 +++++++++- 8 files changed, 137 insertions(+), 15 deletions(-) create mode 100644 src/modules/auth/Dto/refresh.dto.ts create mode 100644 src/modules/auth/Guards/refresh.guard.ts create mode 100644 src/modules/auth/Strategy/refresh.strategy.ts diff --git a/src/modules/auth/Dto/refresh.dto.ts b/src/modules/auth/Dto/refresh.dto.ts new file mode 100644 index 0000000..ba2848e --- /dev/null +++ b/src/modules/auth/Dto/refresh.dto.ts @@ -0,0 +1,8 @@ +import { ApiProperty } from "@nestjs/swagger"; +import { IsJWT } from "class-validator"; + +export class ResfreshDTO { + @ApiProperty() + @IsJWT() + refreshToken: string; +} diff --git a/src/modules/auth/Guards/refresh.guard.ts b/src/modules/auth/Guards/refresh.guard.ts new file mode 100644 index 0000000..babd545 --- /dev/null +++ b/src/modules/auth/Guards/refresh.guard.ts @@ -0,0 +1,5 @@ +import { Injectable } from "@nestjs/common"; +import { AuthGuard } from "@nestjs/passport"; + +@Injectable() +export class RefreshJwtAuthGuard extends AuthGuard("refresh") {} diff --git a/src/modules/auth/Strategy/discord.strategy.ts b/src/modules/auth/Strategy/discord.strategy.ts index 7d7e5cf..d61ff8b 100644 --- a/src/modules/auth/Strategy/discord.strategy.ts +++ b/src/modules/auth/Strategy/discord.strategy.ts @@ -1,13 +1,15 @@ import { Injectable } from "@nestjs/common"; import { ConfigService } from "@nestjs/config"; -import { JwtService } from "@nestjs/jwt"; import { PassportStrategy } from "@nestjs/passport"; import { Profile, Strategy } from "passport-discord"; +import { AuthService } from "../auth.service"; @Injectable() export class DiscordStrategy extends PassportStrategy(Strategy, "discord") { + configService: ConfigService; + constructor( - private readonly jwtService: JwtService, + private readonly authService: AuthService, configService: ConfigService, ) { super({ @@ -20,6 +22,8 @@ export class DiscordStrategy extends PassportStrategy(Strategy, "discord") { ), scope: ["identify", "email"], }); + + this.configService = configService; } async validate( @@ -28,10 +32,9 @@ export class DiscordStrategy extends PassportStrategy(Strategy, "discord") { profile: Profile, done: Function, ) { - const jwt = this.jwtService.sign({ - id: profile.id, - }); + const accessToken = this.authService.accessToken({ id: profile.id }); + const refreshToken = this.authService.refreshToken({ id: profile.id }); - done(null, { jwt }); + done(null, { accessToken, refreshToken }); } } diff --git a/src/modules/auth/Strategy/jwt.strategy.ts b/src/modules/auth/Strategy/jwt.strategy.ts index 91a02c3..af5e5d1 100644 --- a/src/modules/auth/Strategy/jwt.strategy.ts +++ b/src/modules/auth/Strategy/jwt.strategy.ts @@ -9,7 +9,7 @@ import { ConfigService } from "@nestjs/config"; export class JWTStrategy extends PassportStrategy(Strategy) { constructor( private readonly userService: UserService, - private readonly configService: ConfigService, + configService: ConfigService, ) { super({ jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), diff --git a/src/modules/auth/Strategy/refresh.strategy.ts b/src/modules/auth/Strategy/refresh.strategy.ts new file mode 100644 index 0000000..5c7258d --- /dev/null +++ b/src/modules/auth/Strategy/refresh.strategy.ts @@ -0,0 +1,30 @@ +import { Injectable, UnauthorizedException } from "@nestjs/common"; +import { PassportStrategy } from "@nestjs/passport"; +import { User } from "@prisma/client"; +import { Strategy, ExtractJwt } from "passport-jwt"; +import { UserService } from "@Modules/user/user.service"; +import { ConfigService } from "@nestjs/config"; + +@Injectable() +export class RefreshJWTStrategy extends PassportStrategy(Strategy, "refresh") { + constructor( + private readonly userService: UserService, + configService: ConfigService, + ) { + super({ + jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), + ignoreExipration: false, + secretOrKey: configService.get("JWT.refresh.secret"), + }); + } + + async validate(payload: any): Promise { + const user = await this.userService.findById(payload.id); + + if (!user) { + throw new UnauthorizedException("User not found"); + } + + return user; + } +} diff --git a/src/modules/auth/auth.controller.ts b/src/modules/auth/auth.controller.ts index 762a6a5..6c1ce3f 100644 --- a/src/modules/auth/auth.controller.ts +++ b/src/modules/auth/auth.controller.ts @@ -1,18 +1,47 @@ -import { Controller, Get, Req, Res, UseGuards } from "@nestjs/common"; +import { + Body, + Controller, + Get, + Post, + Req, + Res, + UseGuards, +} from "@nestjs/common"; import { ConfigService } from "@nestjs/config"; +import { ApiBearerAuth, ApiBody, ApiOkResponse } from "@nestjs/swagger"; + import { URLSearchParams } from "node:url"; + +import { ResfreshDTO } from "./Dto/refresh.dto"; + import { DiscordAuthGuard } from "./Guards/discord.guard"; +import { RefreshJwtAuthGuard } from "./Guards/refresh.guard"; +import { AuthService } from "./auth.service"; @Controller("auth") export class AuthController { - constructor(private readonly configService: ConfigService) {} + constructor( + private readonly configService: ConfigService, + private readonly authService: AuthService, + ) {} @Get("providers") - getProviders() { + @ApiOkResponse({ + description: "List of OAuth2 providers", + example: { + discord: { url: "https://discord.com/oauth2/authorize?..." }, + keycloak: { url: "https://keycloak.com/oauth2/authorize?..." }, + }, + }) + getProviders() { const discordOauth2Params = new URLSearchParams({ - client_id: this.configService.get("oauth2.discord.clientId"), + client_id: this.configService.get( + "oauth2.discord.clientId", + ), response_type: "code", - redirect_uri: this.configService.get("oauth2.discord.callbackUrl"), + redirect_uri: this.configService.get( + "oauth2.discord.callbackUrl", + ), scope: "identify email", }); @@ -25,9 +54,26 @@ export class AuthController { @Get("discord/callback") @UseGuards(DiscordAuthGuard) - CallbackDiscord(@Req() req, @Res() res) { + @ApiOkResponse({ + description: "Return JWT token", + example: { + accessToken: "Hello World!", + refreshtoken: "Hello World!", + }, + }) + callbackDiscord(@Req() req, @Res() res) { const { user } = req; res.send(user); } + + @Get("refresh") + @UseGuards(RefreshJwtAuthGuard) + @ApiBearerAuth() + @ApiBody({ + type: ResfreshDTO, + }) + refresh(@Req() req) { + return this.authService.accessToken(req.user); + } } diff --git a/src/modules/auth/auth.module.ts b/src/modules/auth/auth.module.ts index 66c44ee..d0692da 100644 --- a/src/modules/auth/auth.module.ts +++ b/src/modules/auth/auth.module.ts @@ -5,10 +5,11 @@ import { AuthService } from "./auth.service"; import { UserModule } from "@Modules/user/user.module"; import { DiscordStrategy } from "./Strategy/discord.strategy"; import { JWTStrategy } from "./Strategy/jwt.strategy"; +import { RefreshJWTStrategy } from "./Strategy/refresh.strategy"; @Module({ imports: [UserModule], controllers: [AuthController], - providers: [AuthService, DiscordStrategy, JWTStrategy], + providers: [AuthService, DiscordStrategy, JWTStrategy, RefreshJWTStrategy], }) export class AuthModule {} diff --git a/src/modules/auth/auth.service.ts b/src/modules/auth/auth.service.ts index 58b5750..a14406a 100644 --- a/src/modules/auth/auth.service.ts +++ b/src/modules/auth/auth.service.ts @@ -1,4 +1,33 @@ import { Injectable } from "@nestjs/common"; +import { ConfigService } from "@nestjs/config"; +import { JwtService } from "@nestjs/jwt"; @Injectable() -export class AuthService {} +export class AuthService { + constructor( + private readonly jwtService: JwtService, + private readonly configService: ConfigService, + ) {} + + accessToken(user: { id: string }) { + const payload = { id: user.id }; + return { + accessToken: this.jwtService.sign(payload, { + secret: this.configService.get("JWT.secret"), + expiresIn: this.configService.get("JWT.expiresIn"), + }), + }; + } + + refreshToken(user: { id: string }) { + const payload = { id: user.id }; + return { + refreshToken: this.jwtService.sign(payload, { + secret: this.configService.get("JWT.refresh.secret"), + expiresIn: this.configService.get( + "JWT.refresh.expiresIn", + ), + }), + }; + } +}