Authentification de bout en bout : concevoir un système de connexion sécurisé
Une authentification bien faite touche simultanément le frontend, le backend, la base de données et l'infrastructure. Ce guide parcourt la stack complète — flux de session, validation JWT, modèles utilisateur, et les décisions de durcissement qui la rendent sûre pour la production.
Construire un système d'authentification pour une architecture découplée où Next.js gère le frontend et NestJS alimente l'API backend nécessite une orchestration soignée. Ce guide couvre l'implémentation complète d'un système d'authentification prêt pour la production avec NextAuth, PostgreSQL et Prisma — en mettant l'accent sur les bonnes pratiques de sécurité et les architectures scalables.
Prérequis
Pour tirer le meilleur parti de ce guide, vous devez être familier avec :
- Next.js 13/14 : App Router et Server Components.
- NestJS : Modules, Controllers et Services.
- Docker : Gestion basique des conteneurs (pour exécuter les microservices).
- Node.js : v18 ou version ultérieure.
Partie 1 : Vue d'ensemble de l'architecture
Avant de plonger dans le code, il est essentiel de comprendre l'architecture de haut niveau. Nous allons examiner comment les différentes parties de notre système — le frontend Next.js, l'API Gateway et le microservice d'authentification — interagissent pour offrir une expérience utilisateur fluide et sécurisée.
1.1 Le défi
Lors de la construction d'une application SaaS moderne, on souhaite souvent le meilleur des deux mondes : Next.js pour son rendu côté serveur et son excellente expérience développeur, et NestJS pour son architecture backend robuste et scalable. Le défi consiste à leur faire partager l'état d'authentification de manière sécurisée.
NextAuth.js est conçu principalement pour Next.js et gère les sessions selon ses propres règles. NestJS s'appuie généralement sur des tokens JWT Bearer standard. Combler cet écart — faire en sorte que NestJS « fasse confiance » aux sessions créées par NextAuth sans partage direct de base de données — est le problème central que nous résolvons.
1.2 Composants du système
- Frontend (Next.js 14) : Gère les flux OAuth via NextAuth. Gère l'état de session et transfère les requêtes à l'API Gateway.
- API Gateway (NestJS) : Point d'entrée sans état qui route les requêtes vers les microservices.
- Microservice d'authentification (NestJS) : Service dédié gérant la logique d'authentification et les interactions avec la base de données via Prisma.
- Base de données (Serveur PostgreSQL) : Source unique de vérité gérée exclusivement par le microservice d'authentification.
1.3 Flux de communication
┌──────────────┐ HTTP ┌──────────────┐ TCP ┌──────────────┐
│ Next.js │ ──────────────▶ │ API Gateway │ ──────────────▶ │ Auth │
│ REST Adapter │ ◀────────────── │ Controller │ ◀────────────── │ Microservice │
└──────────────┘ └──────────────┘ └──────┬───────┘
│
│ Prisma
▼
┌──────────────┐
│ PostgreSQL │
└──────────────┘
1.4 Séquence d'authentification
User Next.js (Client) API Gateway Auth Service Database
│ │ │ │ │
├─── Login ──────▶│ │ │ │
│ │── POST /login ────▶│ │ │
│ │ │── TCP /login ────▶│ │
│ │ │ │─── Query User ─▶│
│ │ │ │◀── User Data ───│
│ │ │◀── JWT Tokens ────│ │
│ │◀── JWT Tokens ─────│ │ │
│◀── Session ─────│ │ │ │
│ (Cookie) │ │ │ │
Partie 2 : Implémentation du Frontend
Le frontend est le point d'entrée de l'utilisateur. Ici, nous allons configurer NextAuth.js pour gérer les complexités d'OAuth et de la gestion de session, mais avec une particularité : au lieu de communiquer directement avec une base de données, nous allons construire un adaptateur personnalisé qui communique avec notre API backend.
2.1 Adaptateur REST personnalisé
Dans une application Next.js classique, on utilise PrismaAdapter pour laisser NextAuth communiquer directement avec la base de données. Mais dans une architecture microservices, la base de données doit rester privée aux services backend. Le frontend ne doit jamais avoir accès direct aux identifiants de base de données.
2.2 Construction de l'adaptateur REST (Next.js)
Nous implémentons l'interface complète de l'adaptateur NextAuth en utilisant des appels HTTP vers notre API Gateway NestJS. L'adaptateur inclut une fonction utilitaire pour gérer l'analyse des dates dans les réponses JSON :
// apps/client/src/lib/auth/api-adapter.ts
import axios from 'axios';
import { Adapter, AdapterAccount, AdapterSession, AdapterUser, VerificationToken } from 'next-auth/adapters';
const isDate = (value: unknown): value is string =>
typeof value === 'string' ? new Date(value).toString() !== 'Invalid Date' && !Number.isNaN(Date.parse(value)) : false;
function format<T>(obj: Record<string, unknown>): T {
return Object.entries(obj).reduce((result, [key, value]) => {
const newResult = result;
if (value === null) {
return newResult;
}
if (isDate(value)) {
newResult[key] = new Date(value);
} else {
newResult[key] = value;
}
return newResult;
}, {} as Record<string, unknown>) as T;
}
function AuthRestAdapter(): Adapter {
const client = axios.create({
baseURL: `${process.env.NEXT_PUBLIC_API_GATEWAY_URL}/adapter`,
headers: {
'Content-Type': 'application/json',
'x-auth-secret': process.env.NEXTAUTH_SECRET || ''
}
});
return {
createUser: async (user: Omit<AdapterUser, 'id'>) => {
const response = await client.post('/signup', user);
return format<AdapterUser>(response.data);
},
getUserByEmail: async (email: string) => {
const response = await client.get('/get-user-by-email', { params: { email } });
return response.data ? format<AdapterUser>(response.data) : response.data;
},
getUserByAccount: async ({
providerAccountId,
provider
}: Pick<AdapterAccount, 'provider' | 'providerAccountId'>) => {
const response = await client.get(`/get-user-by-account/${provider}/${providerAccountId}`);
return response.data ? format<AdapterUser>(response.data) : response.data;
},
getUser: async (id: string) => {
const response = await client.get(`/${id}`);
return response.data ? format<AdapterUser>(response.data) : response.data;
},
updateUser: async (user: Partial<AdapterUser> & Pick<AdapterUser, 'id'>) => {
const response = await client.patch('/', user);
return format<AdapterUser>(response.data);
},
deleteUser: async (userId: string) => {
const response = await client.delete(`/${userId}`);
return response.data ? format<AdapterUser>(response.data) : response.data;
},
linkAccount: async (account: AdapterAccount) => {
const response = await client.post('/link-account', account);
return response.data ? format<AdapterAccount>(response.data) : response.data;
},
unlinkAccount: async ({ providerAccountId, provider }: Pick<AdapterAccount, 'provider' | 'providerAccountId'>) => {
const response = await client.delete(`/unlink-account/${provider}/${providerAccountId}`);
return response.data ? format<AdapterAccount>(response.data) : response.data;
},
createSession: async (session: { sessionToken: string; userId: string; expires: Date }) => {
const response = await client.post('/create-session', session);
return response.data ? format<AdapterSession>(response.data) : response.data;
},
getSessionAndUser: async (sessionToken: string) => {
const response = await client.get(`/get-session/${sessionToken}`);
if (!response.data) {
return response.data;
}
const session = format<AdapterSession>(response.data.session);
const user = format<AdapterUser>(response.data.user);
return { session, user };
},
updateSession: async (session: Partial<AdapterSession> & Pick<AdapterSession, 'sessionToken'>) => {
const response = await client.patch('/update-session', session);
return response.data ? format<AdapterSession>(response.data) : response.data;
},
deleteSession: async (sessionToken: string) => {
const response = await client.delete(`/delete-session/${sessionToken}`);
return response.data ? format<AdapterSession>(response.data) : response.data;
},
createVerificationToken: async (verificationToken: VerificationToken) => {
const response = await client.post('/create-verification-token', verificationToken);
return response.data ? format<VerificationToken>(response.data) : response.data;
},
useVerificationToken: async (params: { identifier: string; token: string }) => {
const response = await client.patch('/use-verification-token', params);
return response.data ? format<VerificationToken>(response.data) : response.data;
}
};
}
export default AuthRestAdapter;
2.3 Configuration des options NextAuth
// apps/client/src/lib/auth/auth-options.ts
import { NextAuthOptions } from "next-auth";
import GoogleProvider from "next-auth/providers/google";
import CredentialsProvider from "next-auth/providers/credentials";
import { JWT } from "next-auth/jwt";
import AuthRestAdapter from "../utils/apiAdapter";
async function refreshToken(token: JWT): Promise<JWT> {
try {
const res = await fetch(`${process.env.NEXT_PUBLIC_API_GATEWAY_URL}/auth/refresh`, {
method: "POST",
body: JSON.stringify({
username: token.user.email,
sub: token.user.name,
}),
headers: {
authorization: `Refresh ${token.backendTokens.refreshToken}`,
"Content-Type": "application/json",
},
});
if (!res.ok) {
throw new Error("Failed to refresh token");
}
const response = await res.json();
return {
...token,
backendTokens: response,
};
} catch (error) {
console.error("Token refresh failed:", error);
return token;
}
}
const authOptions : NextAuthOptions = {
adapter: AuthRestAdapter(),
providers: [
GoogleProvider({
clientId: process.env.GOOGLE_CLIENT_ID || "",
clientSecret: process.env.GOOGLE_CLIENT_SECRET || "",
}),
CredentialsProvider({
name: 'Credentials',
credentials: {
email: {
label: "Email",
type: "text"
},
password: {
label: "Password",
type: "password"
},
},
async authorize(credentials) {
if(!credentials?.email || !credentials?.password) {
return null
}
const { email, password } = credentials;
const res = await fetch( `${ process.env.NEXT_PUBLIC_API_GATEWAY_URL }/auth/login`, {
method: "POST",
body: JSON.stringify({
email,
password,
}),
headers: {
"Content-Type": "application/json",
},
});
if(res.status === 400) {
return null
}
const user = await res.json();
return user;
},
}),
],
session: {
strategy: "jwt",
maxAge: 30 * 24 * 60 * 60
},
callbacks: {
async jwt({token, user, account}: {token: any, user: any, account: any}) {
if (account?.type === 'oauth'){
const res = await fetch(`${process.env.NEXT_PUBLIC_API_GATEWAY_URL}/auth/oAuthlogin`, {
method: "POST",
body: JSON.stringify({
email: user.email,
}),
headers: {
"Content-Type": "application/json",
},
});
if(res.status === 401) {
return null
}
const oAuthUserData = await res.json();
return {...token, ...oAuthUserData};
}
if (user && account?.type === 'credentials') {
return {...token, ...user};
}
if (token?.backendTokens?.expiresIn && new Date().getTime() < token.backendTokens.expiresIn) {
return token;
}
return refreshToken(token);
},
async session({ token, session }: {token: any, session: any}) {
const newSession = { ...session };
newSession.user = token.user;
newSession.backendTokens = token.backendTokens;
return newSession;
},
},
pages: {
signIn: "/signin",
},
secret: process.env.NEXTAUTH_SECRET,
};
export default authOptions;
2.4 Extensions de types
// apps/client/src/types/next-auth.d.ts
/* eslint-disable no-unused-vars */
/* eslint-disable @typescript-eslint/no-unused-vars */
import NextAuth from "next-auth";
import { JWT } from "next-auth/jwt";
declare module "next-auth" {
interface Session {
user: {
id: string;
email: string;
name: string;
background: string;
companyName: string;
about: string;
image: string;
country: string;
website: string;
region: string;
annualOperatingBudget: string;
industry: string;
foundedYear: string;
headquarter: string;
employeesNumber: string;
created_at: Date;
updated_at: Date;
};
backendTokens: {
accessToken: string;
refreshToken: string;
expiresIn: number;
};
}
}
declare module "next-auth/jwt" {
interface JWT {
user: {
id: string;
email: string;
name: string;
background: string;
companyName: string;
about: string;
image: string;
country: string;
website: string;
region: string;
annualOperatingBudget: string;
industry: string;
foundedYear: string;
headquarter: string;
employeesNumber: string;
created_at: Date;
updated_at: Date;
};
backendTokens: {
accessToken: string;
refreshToken: string;
expiresIn: number;
};
}
}
Partie 3 : Conception du schéma de base de données
Un modèle de données solide est le fondement de tout système d'authentification. Nous allons définir un schéma Prisma qui répond aux exigences standard de NextAuth tout en étant suffisamment extensible pour notre architecture microservices.
3.1 Schéma Prisma
Le schéma de base de données étend les tables standard de NextAuth avec des champs spécifiques à l'application. L'utilisation de Prisma garantit la sécurité des types sur l'ensemble de la stack.
generator client {
provider = "prisma-client-js"
output = "./generated/companyAuthClient"
binaryTargets = ["native", "linux-musl-openssl-3.0.x"]
}
datasource db {
provider = "postgresql"
url = env("COMPANY_AUTH_DATABASE_URL")
}
model Account {
id String @id @default(cuid())
userId String
providerType String
providerId String
providerAccountId String
refreshToken String?
accessToken String?
accessTokenExpires DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user User @relation(fields: [userId], references: [id])
@@unique([providerId, providerAccountId])
}
model Session {
id String @id @default(cuid())
userId String
expires DateTime
sessionToken String @unique
accessToken String @unique
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user User @relation(fields: [userId], references: [id])
}
model User {
id String @id @default(cuid())
name String?
email String? @unique
emailVerified DateTime?
image String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
accounts Account[]
sessions Session[]
}
model VerificationRequest {
id String @id @default(cuid())
identifier String
token String @unique
expires DateTime
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([identifier, token])
}
Partie 4 : Implémentation du Backend
C'est ici que se passe l'essentiel du travail. Nous allons implémenter la logique backend à travers l'API Gateway et le microservice d'authentification, en mettant en place des canaux de communication sécurisés, la génération de JWT et des gardes de routes pour protéger nos endpoints API.
4.1 API Gateway : Contrôleur de l'adaptateur
Le backend expose des endpoints qui reflètent l'interface de l'adaptateur NextAuth :
// apps/api/src/auth/adapter/adapter.controller.ts
import {
Body,
Controller,
Delete,
Get,
Param,
Post,
Query,
} from "@nestjs/common";
import { AdapterService } from "./adapter.service";
import { ApiTags } from "@nestjs/swagger";
@ApiTags("adapter")
@Controller("adapter")
export class AdapterController {
constructor(private readonly adapterService: AdapterService) {}
@Post("signup")
async createUser(@Body() user: any): Promise<any> {
const res = await this.adapterService.createUser(user);
return res;
}
@Get("get-user/:id")
async getUserById(@Param("id") id: string): Promise<any> {
const res = await this.adapterService.getUserById(id);
return res;
}
@Get("get-user-by-email/")
async getUserByEmail(@Query("email") email: string): Promise<any> {
const res = await this.adapterService.getUserByEmail(email);
return res;
}
@Get("get-user-by-account/:provider/:providerAccountId")
async getUserByAccount(
@Param("provider") provider: string,
@Param("providerAccountId") providerAccountId: string,
): Promise<any> {
const res = await this.adapterService.getUserByAccount(
provider,
providerAccountId,
);
return res;
}
@Post("update-user")
async updateUser(@Body() user: any): Promise<any> {
const res = await this.adapterService.updateUser(user);
return res;
}
@Delete("delete-user/:id")
async deleteUser(@Param("id") id: string): Promise<any> {
const res = await this.adapterService.deleteUser(id);
return res;
}
@Post("link-account")
async linkAccount(@Body() account: any): Promise<any> {
const res = await this.adapterService.linkAccount(account);
return res;
}
@Delete("unlink-account/:provider/:providerAccountId")
async unlinkAccount(
@Param("provider") provider: string,
@Param("providerAccountId") providerAccountId: string,
): Promise<any> {
const res = await this.adapterService.unlinkAccount(
provider,
providerAccountId,
);
return res;
}
}
4.2 API Gateway : Service de l'adaptateur
// apps/api/src/auth/adapter/adapter.service.ts
import { HttpException, Inject, Injectable } from "@nestjs/common";
import { ClientProxy } from "@nestjs/microservices";
import { firstValueFrom } from "rxjs";
@Injectable()
export class AdapterService {
constructor(@Inject("AUTH") private authMicroservice: ClientProxy) {}
async createUser(user: any): Promise<any> {
try {
const pattern = { cmd: "createUser" };
const res = await firstValueFrom(
this.authMicroservice.send<string, any>(pattern.cmd, user)
);
return res;
} catch (error) {
throw new HttpException(error, 400);
}
}
async getUserById(id: string): Promise<any> {
try {
const pattern = { cmd: "getUserById" };
const res = await firstValueFrom(
this.authMicroservice.send<string, string>(pattern.cmd, id)
);
return res;
} catch (error) {
throw new HttpException(error, 400);
}
}
async getUserByEmail(email: string): Promise<any> {
try {
const pattern = { cmd: "getUserByEmail" };
const res = await firstValueFrom(
this.authMicroservice.send<string, string>(pattern.cmd, email)
);
return res;
} catch (error) {
throw new HttpException(error, 400);
}
}
async getUserByAccount(provider: string, providerAccountId: string): Promise<any> {
try {
const pattern = { cmd: "getUserByAccount" };
const payload = { provider, providerAccountId };
const res = await firstValueFrom(
this.authMicroservice.send<string, any>(pattern.cmd, payload)
);
return res;
} catch (error) {
throw new HttpException(error, 400);
}
}
async updateUser(user: any): Promise<any> {
try {
const pattern = { cmd: "updateUser" };
const res = await firstValueFrom(
this.authMicroservice.send<string, any>(pattern.cmd, user)
);
return res;
} catch (error) {
throw new HttpException(error, 400);
}
}
async deleteUser(id: string): Promise<any> {
try {
const pattern = { cmd: "deleteUser" };
const res = await firstValueFrom(
this.authMicroservice.send<string, string>(pattern.cmd, id)
);
return res;
} catch (error) {
throw new HttpException(error, 400);
}
}
async linkAccount(account: any): Promise<any> {
try {
const pattern = { cmd: "linkAccount" };
const res = await firstValueFrom(
this.authMicroservice.send<string, any>(pattern.cmd, account)
);
return res;
} catch (error) {
throw new HttpException(error, 400);
}
}
async unlinkAccount(provider: string, providerAccountId: string): Promise<any> {
try {
const pattern = { cmd: "unlinkAccount" };
const payload = { provider, providerAccountId };
const res = await firstValueFrom(
this.authMicroservice.send<string, any>(pattern.cmd, payload)
);
return res;
} catch (error) {
throw new HttpException(error, 400);
}
}
}
4.3 Microservice d'authentification : Contrôleur de l'adaptateur
// apps/auth/src/adapter/adapter.controller.ts
import { Controller } from "@nestjs/common";
import { MessagePattern, Payload } from "@nestjs/microservices";
import { AdapterService } from "./adapter.service";
@Controller()
export class AdapterController {
constructor(private readonly adapterService: AdapterService) {}
@MessagePattern("createUser")
async createUser(@Payload() user: any): Promise<any> {
const res = await this.adapterService.createUser(user);
return res;
}
@MessagePattern("getUserById")
async getUserById(@Payload() id: string): Promise<any> {
const res = await this.adapterService.getUserById(id);
return res;
}
@MessagePattern("getUserByEmail")
async getUserByEmail(@Payload() email: string): Promise<any> {
const res = await this.adapterService.getUserByEmail(email);
return res;
}
@MessagePattern("getUserByAccount")
async getUserByAccount(@Payload() provider: any): Promise<any> {
const res = await this.adapterService.getUserByAccount(
provider.provider,
provider.providerAccountId,
);
return res;
}
@MessagePattern("updateUser")
async updateUser(@Payload() user: any): Promise<any> {
const res = await this.adapterService.updateUser(user);
return res;
}
@MessagePattern("deleteUser")
async deleteUser(@Payload() id: string): Promise<any> {
const res = await this.adapterService.deleteUser(id);
return res;
}
@MessagePattern("linkAccount")
async linkAccount(@Payload() account: any): Promise<any> {
const res = await this.adapterService.linkAccount(account);
return res;
}
@MessagePattern("unlinkAccount")
async unlinkAccount(@Payload() param: any): Promise<any> {
const res = await this.adapterService.unlinkAccount(
param.provider,
param.providerAccountId,
);
return res;
}
}
4.4 Microservice d'authentification : Service de l'adaptateur
// apps/auth/src/adapter/adapter.service.ts
import { Injectable } from "@nestjs/common";
import { EEmailVerificationStatus } from "src/auth/dto/request-email-verification.dto";
import { PrismaService } from "src/persistence/prisma/prisma.service";
@Injectable()
export class AdapterService {
constructor(private prisma: PrismaService) {}
async createUser(user: any): Promise<any> {
const res = await this.prisma.user.create({
data: {
...user,
emailVerified: EEmailVerificationStatus.VERIFIED,
},
});
return res;
}
async getUserById(id: string) {
const res = await this.prisma.user.findUnique({
where: {
id: id,
},
});
return res;
}
async getUserByEmail(email: string) {
const res = await this.prisma.user.findUnique({
where: {
email: email,
},
});
return res;
}
async getUserByAccount(provider: string, providerAccountId: string) {
const account = await this.prisma.account.findUnique({
where: {
provider_providerAccountId: {
provider: provider,
providerAccountId: providerAccountId,
},
},
});
if (account) {
const res = await this.prisma.user.findUnique({
where: {
id: account.userId,
},
});
return res;
}
return null;
}
async updateUser(user: any): Promise<any> {
const res = await this.prisma.user.update({
where: {
id: user.id,
},
data: {
...user,
},
});
return res;
}
async deleteUser(id: string): Promise<any> {
const res = await this.prisma.user.delete({
where: {
id: id,
},
});
return res;
}
async linkAccount(account: any): Promise<any> {
const res = await this.prisma.account.create({
data: {
userId: account.userId,
type: account.type,
provider: account.provider,
providerAccountId: account.providerAccountId,
access_token: account.access_token,
expires_at: account.expires_at,
token_type: account.token_type,
id_token: account.id_token,
},
});
return res;
}
async unlinkAccount(
provider: string,
providerAccountId: string,
): Promise<any> {
const res = await this.prisma.account.delete({
where: {
provider_providerAccountId: {
provider: provider,
providerAccountId: providerAccountId,
},
},
});
return res;
}
}
4.5 Auth JWT : Contrôleur Auth
// apps/api/src/auth/auth.controller.ts
import {
Body,
Controller,
Get,
HttpStatus,
Param,
Post,
Put,
Res,
UploadedFile,
UseGuards,
UseInterceptors,
} from "@nestjs/common";
import { CreateUserDto } from "./dto/create-user.dto";
import { UpdateUserDto } from "./dto/update-user.dto";
import { CreateAuthDto } from "./dto/create-auth.dto";
import { CreateRefreshDto } from "./dto/create-refresh.dto";
import { AuthService } from "./auth.service";
import { FileInterceptor } from "@nestjs/platform-express";
import { AzureBlobService } from "./azure-blob.service";
import { ApiTags } from "@nestjs/swagger";
import { ForgotPasswordDto } from "./dto/forgot-password.dto";
import { Response } from "express";
import { RequestEmailVerificationDto } from "./dto/request-email-verification.dto";
import { JwtGuard } from "src/guards/jwt.guard";
import { RefreshJwtGuard } from "src/guards/refresh-jwt.guard";
@ApiTags("auth")
@Controller("auth")
export class AuthController {
constructor(
private readonly authService: AuthService,
private readonly azureblobService: AzureBlobService,
) {}
@Post("register")
async register(@Body() createUserDto: CreateUserDto): Promise<any> {
const res = await this.authService.register(createUserDto);
return res;
}
@Post("login")
async login(@Body() createAuthDto: CreateAuthDto): Promise<any> {
const res = await this.authService.login(createAuthDto);
return res;
}
@Post("oAuthlogin")
async oAuthlogin(@Body() createAuthDto: CreateAuthDto): Promise<any> {
const res = await this.authService.oAuthlogin(createAuthDto.email);
return res;
}
@UseGuards(RefreshJwtGuard)
@Post("refresh")
async refreshToken(@Body() createRefreshDto: CreateRefreshDto): Promise<any> {
const refreshToken = await this.authService.refreshToken(createRefreshDto);
return refreshToken;
}
@Get(":id")
async findOneUser(@Param("id") id: string): Promise<any> {
const company = await this.authService.findOneUser(id);
return company;
}
@Put(":id")
async updateUser(
@Param("id") id: string,
@Body() updateUserDto: UpdateUserDto,
): Promise<any> {
const company = await this.authService.updateUser(id, updateUserDto);
return company;
}
@Put("upload-image/:id")
@UseInterceptors(FileInterceptor("image"))
async updateUserImage(
@Param("id") id: string,
@UploadedFile() image: Express.Multer.File,
): Promise<any> {
const containerName = process.env.AZURE_STORAGE_CONTAINER_NAME;
const imageUrl = await this.azureblobService.upload(image, containerName);
return { imageUrl };
}
@Get()
async findAllUsersWithId() {
const users = await this.authService.findAllUsersWithId();
return users;
}
@Post("forgot-password")
async forgotPassword(
@Body() forgotPasswordDto: ForgotPasswordDto,
@Res() response: Response,
): Promise<any> {
try {
await this.authService.forgotPassword(forgotPasswordDto);
return response.status(HttpStatus.OK).json({
message:
"If this user exists, they will receive an email for restoring their password",
});
} catch (error) {
return response.status(HttpStatus.BAD_REQUEST).json({
message:
"If this user exists, they will receive an email for restoring their password",
});
}
}
@Post("reset-password/:token")
async resetPassword(
@Param("token") token: string,
@Body() resetPasswordDto: { newPassword: string },
@Res() response: Response,
): Promise<any> {
try {
await this.authService.resetPassword(token, resetPasswordDto.newPassword);
return response.status(HttpStatus.OK).json({
message: "Password has been successfully reset",
});
} catch (error) {
return response.status(HttpStatus.BAD_REQUEST).json({
message: error.message || "Failed to reset password",
});
}
}
@UseGuards(JwtGuard)
@Post("request-email-verification")
async requestEmailVerification(
@Body() body: RequestEmailVerificationDto,
@Res() response: Response,
) {
try {
await this.authService.requestEmailVerification(body.userId, body.email);
return response.status(HttpStatus.OK).json({
message: "Email verification requested successfully",
});
} catch (error) {
return response.status(HttpStatus.BAD_REQUEST).json({
message: error.message || "Failed to request email verification",
});
}
}
@Post("verify-email/:token")
async verifyEmail(@Param("token") token: string, @Res() response: Response) {
try {
await this.authService.verifyEmail(token);
return response.status(HttpStatus.OK).json({
message: "Email has been successfully verified",
});
} catch (error) {
return response.status(HttpStatus.BAD_REQUEST).json({
message: error.message || "Failed to verify email",
});
}
}
}
4.6 Auth JWT : Service Auth
// apps/api/src/auth/auth.service.ts
import { HttpException, Inject, Injectable } from "@nestjs/common";
import { CreateUserDto } from "./dto/create-user.dto";
import { ClientProxy } from "@nestjs/microservices";
import { CreateAuthDto } from "./dto/create-auth.dto";
import { CreateRefreshDto } from "./dto/create-refresh.dto";
import { UpdateUserDto } from "./dto/update-user.dto";
import { ForgotPasswordDto } from "./dto/forgot-password.dto";
import { RequestEmailVerificationDto } from "./dto/request-email-verification.dto";
import { firstValueFrom } from "rxjs";
@Injectable()
export class AuthService {
constructor(@Inject("AUTH") private authMicroservice: ClientProxy) {}
async register(createUserDto: CreateUserDto): Promise<any> {
try {
const pattern = { cmd: "register" };
const res = await firstValueFrom(
this.authMicroservice.send<string, CreateUserDto>(pattern.cmd, createUserDto)
);
return res;
} catch (error) {
throw new HttpException(error, 400);
}
}
async login(createAuthDto: CreateAuthDto): Promise<any> {
try {
const pattern = { cmd: "login" };
const res = await firstValueFrom(
this.authMicroservice.send<string, CreateAuthDto>(pattern.cmd, createAuthDto)
);
return res;
} catch (error) {
throw new HttpException(error, 400);
}
}
async oAuthlogin(email: string): Promise<any> {
try {
const pattern = { cmd: "oAuthlogin" };
const res = await firstValueFrom(
this.authMicroservice.send<string, string>(pattern.cmd, email)
);
return res;
} catch (error) {
throw new HttpException(error, 400);
}
}
async refreshToken(createRefreshDto: CreateRefreshDto): Promise<any> {
try {
const pattern = { cmd: "refresh" };
const res = await firstValueFrom(
this.authMicroservice.send<string, CreateRefreshDto>(pattern.cmd, createRefreshDto)
);
return res;
} catch (error) {
throw new HttpException(error, 400);
}
}
async findOneUser(id: string): Promise<any> {
try {
const pattern = { cmd: "findOneUser" };
const res = await firstValueFrom(
this.authMicroservice.send<string, string>(pattern.cmd, id)
);
return res;
} catch (error) {
throw new HttpException(error, 400);
}
}
async updateUser(id: string, updateUserDto: UpdateUserDto) {
try {
const pattern = { cmd: "UpdateUser" };
const payload = { id, ...updateUserDto };
const res = await firstValueFrom(
this.authMicroservice.send<string, any>(pattern.cmd, payload)
);
return res;
} catch (error) {
throw new HttpException(error, 400);
}
}
async updateUserImage(id: string, imageUrl: string) {
try {
const pattern = { cmd: "updateUserImage" };
const payload = { id, imageUrl };
const res = await firstValueFrom(
this.authMicroservice.send<string, any>(pattern.cmd, payload)
);
return res;
} catch (error) {
throw new HttpException(error, 400);
}
}
async findAllUsersWithId() {
try {
const pattern = { cmd: "findAllUsersWithId" };
const res = await firstValueFrom(
this.authMicroservice.send<string, any>(pattern.cmd, {})
);
return res;
} catch (error) {
throw new HttpException(error, 400);
}
}
async forgotPassword(forgotPasswordDto: ForgotPasswordDto): Promise<any> {
try {
const pattern = { cmd: "password.v1.forgot" };
const res = await firstValueFrom(
this.authMicroservice.send<string, ForgotPasswordDto>(pattern.cmd, forgotPasswordDto)
);
return res;
} catch (error) {
throw new HttpException(error, 400);
}
}
async resetPassword(token: string, newPassword: string): Promise<any> {
try {
const pattern = { cmd: "password.v1.reset" };
const payload = { token, newPassword };
const res = await firstValueFrom(
this.authMicroservice.send<string, any>(pattern.cmd, payload)
);
return res;
} catch (error) {
throw new HttpException(error, 400);
}
}
async requestEmailVerification(userId: string, email: string): Promise<any> {
try {
const pattern = { cmd: "email.v1.requestVerification" };
const payload = { userId, email };
const res = await firstValueFrom(
this.authMicroservice.send<string, RequestEmailVerificationDto>(pattern.cmd, payload)
);
return res;
} catch (error) {
throw new HttpException(error, 400);
}
}
async verifyEmail(token: string): Promise<any> {
try {
const pattern = { cmd: "email.v1.verify" };
const res = await firstValueFrom(
this.authMicroservice.send<string, string>(pattern.cmd, token)
);
return res;
} catch (error) {
throw new HttpException(error, 400);
}
}
}
4.7 Microservice d'authentification : Contrôleur principal
// apps/auth/src/auth/auth.controller.ts
import { Body, Controller } from "@nestjs/common";
import { MessagePattern, RpcException } from "@nestjs/microservices";
import { AuthService } from "./auth.service";
import { CreateAuthDto } from "./dto/create-auth.dto";
import { UserService } from "src/user/user.service";
import { CreateUserDto } from "src/user/dto/create-user.dto";
import { ForgotPasswordDto } from "./dto/forgot-password.dto";
import { RequestEmailVerificationDto } from "./dto/request-email-verification.dto";
@Controller()
export class AuthController {
constructor(
private readonly authService: AuthService,
private readonly userService: UserService,
) {}
@MessagePattern("register")
async registerUser(@Body() createUserDto: CreateUserDto) {
try {
const user = await this.userService.createUser(createUserDto);
if (!user) {
throw new RpcException("User creation failed");
}
const res = await this.authService.requestEmailVerification(
user.id,
user.email,
);
if (!res) {
throw new RpcException("Email verification request failed");
}
return user;
} catch (error) {
throw new RpcException(error);
}
}
@MessagePattern("login")
async loginUser(@Body() createAuthDto: CreateAuthDto): Promise<any> {
const res = await this.authService.login(createAuthDto);
return res;
}
@MessagePattern("oAuthlogin")
async oAuthlogin(@Body() email: string): Promise<any> {
const res = await this.authService.oAuthlogin(email);
return res;
}
@MessagePattern("refresh")
async refreshToken(@Body() user: any) {
const res = await this.authService.refreshToken(user);
return res;
}
@MessagePattern("password.v1.forgot")
async forgotPassword(@Body() forgotPasswordDto: ForgotPasswordDto) {
try {
const results = await this.authService.forgotPassword(forgotPasswordDto);
return results;
} catch (error) {
throw new RpcException(error);
}
}
@MessagePattern("password.v1.reset")
async resetPassword(@Body() resetPasswordDto: any) {
try {
const results = await this.authService.resetPassword(
resetPasswordDto.token,
resetPasswordDto.newPassword,
);
return results;
} catch (error) {
throw new RpcException(error);
}
}
@MessagePattern("email.v1.requestVerification")
async requestEmailVerification(
@Body() requestEmailVerificationDto: RequestEmailVerificationDto,
): Promise<any> {
try {
const results = await this.authService.requestEmailVerification(
requestEmailVerificationDto.userId,
requestEmailVerificationDto.email,
);
return results;
} catch (error) {
throw new RpcException(error);
}
}
@MessagePattern("email.v1.verify")
async verifyEmail(@Body() token: string): Promise<any> {
try {
const results = await this.authService.verifyEmail(token);
return results;
} catch (error) {
throw new RpcException(error);
}
}
}
4.8 Microservice d'authentification : Service principal
// apps/auth/src/auth/auth.service.ts
import { Injectable } from "@nestjs/common";
import { CreateAuthDto } from "./dto/create-auth.dto";
import { compare } from "bcrypt";
import { JwtService } from "@nestjs/jwt";
import { RpcException } from "@nestjs/microservices";
import { UserService } from "src/user/user.service";
import { ForgotPasswordDto } from "./dto/forgot-password.dto";
import { MailService } from "src/mail/mail.service";
import { hash } from "bcrypt";
import { EEmailVerificationStatus } from "./dto/request-email-verification.dto";
const EXPIRE_TIME = 24 * 60 * 60 * 1000;
@Injectable()
export class AuthService {
constructor(
private readonly userService: UserService,
private jwtService: JwtService,
private readonly mailService: MailService,
) {}
async login(createAuthDto: CreateAuthDto): Promise<any> {
const user = await this.userService.findCompanyByEmail(createAuthDto.email);
if (user && (await compare(createAuthDto.password, user.password))) {
const { password, ...safeUser } = user;
const payload = {
username: user.email,
emailVerified: user.emailVerified,
sub: {
name: user.name,
},
};
return {
user: safeUser,
backendTokens: {
accessToken: await this.jwtService.signAsync(payload, {
expiresIn: "1d",
secret: process.env.JWT_SECRET_KEY,
}),
refreshToken: await this.jwtService.signAsync(payload, {
expiresIn: "7d",
secret: process.env.JWT_REFRESH_TOKEN,
}),
expiresIn: new Date().setTime(new Date().getTime() + EXPIRE_TIME),
},
};
}
throw new RpcException("UnauthorizedException");
}
async oAuthlogin(email: string): Promise<any> {
const user = await this.userService.findCompanyByEmail(email);
if (user) {
const { password, ...safeUser } = user;
const payload = {
username: email,
emailVerified: user.emailVerified,
sub: {
name: user.name,
},
};
return {
user: safeUser,
backendTokens: {
accessToken: await this.jwtService.signAsync(payload, {
expiresIn: "1d",
secret: process.env.JWT_SECRET_KEY,
}),
refreshToken: await this.jwtService.signAsync(payload, {
expiresIn: "7d",
secret: process.env.JWT_REFRESH_TOKEN,
}),
},
};
}
throw new RpcException("UnauthorizedException");
}
async validateUser(createAuthDto: CreateAuthDto) {
const user = await this.userService.findCompanyByEmail(createAuthDto.email);
if (user && (await compare(createAuthDto.password, user.password))) {
return user;
}
throw new RpcException("UnauthorizedException");
}
async refreshToken(user: any) {
const payload = {
username: user.username,
sub: user.sub,
};
return {
accessToken: await this.jwtService.signAsync(payload, {
expiresIn: "1d",
secret: process.env.JWT_SECRET_KEY,
}),
refreshToken: await this.jwtService.signAsync(payload, {
expiresIn: "7d",
secret: process.env.JWT_REFRESH_TOKEN,
}),
expiresIn: new Date().setTime(new Date().getTime() + EXPIRE_TIME),
};
}
async forgotPassword(forgotPasswordDto: ForgotPasswordDto) {
const email = forgotPasswordDto.email;
try {
const user = await this.userService.findUserByEmail(email);
if (!user) {
return { message: "If an account exists, a password reset email has been sent" };
}
const expire = new Date();
expire.setHours(expire.getHours() + 1);
const token = this.jwtService.sign(
{ id: user.id, email: user.email },
{
secret: process.env.JWT_SECRET_KEY,
expiresIn: expire.getTime() - new Date().getTime(),
},
);
await this.userService.saveRestorePasswordToken({
userID: user.id,
token: token,
expires: expire,
isUsed: false,
});
await this.mailService.sendForgotPasswordEmail(user.email, token);
return { message: "If an account exists, a password reset email has been sent" };
} catch (error) {
return { message: "If an account exists, a password reset email has been sent" };
}
}
async resetPassword(token: string, newPassword: string): Promise<any> {
try {
const payload = this.jwtService.verify(token, {
secret: process.env.JWT_SECRET_KEY,
});
const user = await this.userService.findUSerByID(payload.id);
if (!user) {
throw new RpcException("User does not exist");
}
const storedToken = await this.userService.findRestorePasswordToken(
user.id,
);
if (!storedToken || token !== storedToken.token) {
throw new RpcException("Invalid token");
}
if (storedToken.expires < new Date() || storedToken.isUsed) {
throw new RpcException("Token expired or already used");
}
const hashedPassword = await hash(newPassword, 10);
await this.userService.updateUserOnly({
id: user.id,
password: hashedPassword,
});
await this.userService.updateRestorePasswordToken({
id: storedToken.id,
isUsed: true,
});
return { message: "Password has been reset successfully" };
} catch (error) {
throw new RpcException(error.message || "Failed to reset password");
}
}
async requestEmailVerification(userId: string, email: string): Promise<any> {
try {
const user = await this.userService.findUSerByID(userId);
if (!user) throw new RpcException("User does not exist");
if (user.email !== email)
throw new RpcException("Email does not match user");
if (
!user.emailVerified ||
user.emailVerified !== EEmailVerificationStatus.PENDING
)
throw new RpcException(
"Email already verified or pending verification",
);
const expire = new Date();
expire.setHours(expire.getHours() + 24);
const token = this.jwtService.sign(
{ id: user.id, email: user.email },
{
secret: process.env.JWT_SECRET_KEY,
expiresIn: expire.getTime() - new Date().getTime(),
},
);
await this.userService.createEmailVerificationToken({
userId: user.id,
token: token,
expires: expire,
});
const verificationUrl = `${process.env.DNAME}/verify-email?token=${token}`;
await this.mailService.sendEmailVerificationEmail(
user.email,
verificationUrl,
);
return { message: "Email verification requested successfully" };
} catch (error) {
throw new RpcException(
error.message || "Failed to request email verification",
);
}
}
async verifyEmail(token: string): Promise<any> {
try {
const payload = this.jwtService.verify(token, {
secret: process.env.JWT_SECRET_KEY,
});
const user = await this.userService.findUSerByID(payload.id);
if (!user) {
throw new RpcException("User does not exist");
}
const storedToken =
await this.userService.findEmailVerificationTokenByUserId(user.id);
if (!storedToken || token !== storedToken.token) {
throw new RpcException("Invalid token");
}
if (storedToken.expires < new Date()) {
throw new RpcException("Token expired");
}
await this.userService.updateUserOnly({
id: user.id,
emailVerified: EEmailVerificationStatus.VERIFIED,
});
await this.userService.deleteEmailVerificationToken(storedToken.id);
return { message: "Email successfully verified" };
} catch (error) {
throw new RpcException(error.message || "Failed to verify email");
}
}
}
4.9 Garde API : JWT Guard
// apps/api/src/auth/guards/jwt.guard.ts
import {
CanActivate,
ExecutionContext,
Injectable,
UnauthorizedException,
} from "@nestjs/common";
import { JwtService } from "@nestjs/jwt";
import { Request } from "express";
@Injectable()
export class JwtGuard implements CanActivate {
constructor(private jwtService: JwtService) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest();
const token = this.extractTokenFromHeader(request);
if (!token) {
throw new UnauthorizedException();
}
try {
const payload = await this.jwtService.verifyAsync(token, {
secret: process.env.JWT_SECRET_KEY,
});
request["user"] = payload;
} catch (error) {
throw new UnauthorizedException();
}
return true;
}
private extractTokenFromHeader(request: Request) {
const authHeader = request.headers.authorization;
if (!authHeader) {
return undefined;
}
const [type, token] = authHeader.split(" ") ?? [];
return type === "Bearer" ? token : undefined;
}
}
4.10 Garde API : Refresh JWT Guard
// apps/api/src/auth/guards/refresh-jwt.guard.ts
import {
CanActivate,
ExecutionContext,
Injectable,
UnauthorizedException,
} from "@nestjs/common";
import { JwtService } from "@nestjs/jwt";
import { Request } from "express";
const getJwtRefreshSecret = () => process.env.JWT_REFRESH_TOKEN;
@Injectable()
export class RefreshJwtGuard implements CanActivate {
constructor(private jwtService: JwtService) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest();
const token = this.extractTokenFromHeader(request);
if (!token) {
throw new UnauthorizedException();
}
try {
const payload = await this.jwtService.verifyAsync(token, {
secret: getJwtRefreshSecret(),
});
request["user"] = payload;
} catch {
throw new UnauthorizedException();
}
return true;
}
private extractTokenFromHeader(request: Request) {
const authHeader = request.headers.authorization;
if (!authHeader) {
return undefined;
}
const [type, token] = authHeader?.split(" ") ?? [];
return type === "Refresh" ? token : undefined;
}
}
Conclusion
En combinant la gestion de session centrée sur l'utilisateur de Next.js avec un microservice d'authentification NestJS spécialisé, nous avons créé un système à la fois sécurisé et scalable. Cette architecture garantit que les identifiants sensibles restent côté serveur, que les tokens sont gérés sans état, et que le frontend reste léger et focalisé sur l'expérience utilisateur. Si la mise en place initiale est plus complexe qu'une approche monolithique, les bénéfices à long terme en termes de séparation des responsabilités et de maintenabilité en font un investissement rentable pour les applications en production.
Écrit par

Tech Lead et Ingénieur Full Stack pilotant une équipe de 5 ingénieurs chez Fygurs (Paris, Remote) sur un SaaS cloud-native Azure. Diplômé de 1337 Coding School (42 Network / UM6P). Écrit sur l'architecture, l'infrastructure cloud et le leadership technique.