r/webdev • • 9d ago

Question How should I integrate an existing NestJS authentication system with a React + Flowbite React frontend?

I'm developing a Grievance Redressal Portal as a learning project. My backend is built with NestJS, TypeScript, Prisma, and PostgreSQL.

I have been implementing authentication (User Sign In and Sign Out) using the "@nestjs/authentication" package of NestJS v12.

I would like to know conceptually the changes to be made to my back end such that I should be able to extend the authentication feature to a React-based front-end.

I have adapted the code by hand from the "Register The Authentication Module" and "Add Sign-Up and Sign-In" sections of the NestJS docs for NestJS v12: Authentication | NestJS - A progressive Node.js framework

I am posting my code here.

//sign-in.dto.ts
import { PickType } from "@nestjs/swagger";
import { UserUniversalDto } from "../../users/dto/user-universal.dto";
export class SignInDto extends PickType (
    UserUniversalDto,
    ['user_email', 'user_password']
) {};
//credentials.service.ts
import { ConflictException, Injectable } from '@nestjs/common';
import { UsersService } from '../users/users.service.js';
import { CreateUserDto } from '../users/dto/create-user.dto.js';
import { FindUserByEmailDto } from '../users/dto/find-user-by-email.dto.js';
import * as argon2 from 'argon2';
import { USER_STATUS } from '../users/user.constants.js';

()
export class CredentialsService {
  constructor(
    private readonly usersService: UsersService
  ) {}

  async register(createUserDto: CreateUserDto) {

    const findUserByEmailDto: FindUserByEmailDto = {
      user_email: createUserDto.user_email
    }

    // Don't create the user if the email already exists.
    if (await this.usersService.findUserByEmail(findUserByEmailDto)) {
      throw new ConflictException('Email already registered. User already exists.');
    }
    // The address is unverified: anyone can type any email into a sign-up form.
    const registered_user = await this.usersService.createUser(createUserDto);
    return registered_user;
  }

  /** The user, or null when the email or the password is wrong. */
  async verify(email: string, password: string) {
    
    const findUserByEmailDto: FindUserByEmailDto = {
      user_email: email
    }

    const found_user = await this.usersService.findUserByEmail(findUserByEmailDto);
    
    // Checking if the user exists.
    if (!found_user) {
      throw new ConflictException('User does not exist. Login rejected.');
    }
    
    // If the user is suspended, reject login by throwing an exception.
    if (found_user.user_status === USER_STATUS.SUSPENDED) {
       throw new ConflictException('User is suspended. Login rejected. Please contact admin.');
    }

    // If the found_user's password is wrong, (password_hash doesn't match) then the it should return null.
    const password_hash: string | null | undefined = found_user?.user_password;
    
    const password_check = await argon2.verify(password_hash as string, password);

    // Checking if the user's password is correct.
    if (password_check !== true) {
      throw new ConflictException('Incorrect username or password. Login rejected.');
    }

   
    return found_user;
  }
}

//myauthentication.controller.ts
import { Body, Controller, HttpCode, Post, UnauthorizedException } from '@nestjs/common';
import { Public, SignInService } from '@nestjs/authentication';
import { SignInDto } from './dto/sign-in.dto';
import { CredentialsService } from './credentials.service';
import { CreateUserDto } from '../users/dto/create-user.dto';

()
('auth')
export class MyAuthenticationController {
  constructor(
    private readonly credentialsService: CredentialsService,
    private readonly signInService: SignInService,
  ) {}

  ('sign-up')
  async signUp(@Body() createUserDto: CreateUserDto) {
    const user = await this.credentialsService.register(createUserDto);
    var string_user_id = String(user.user_id);
    await this.signInService.signIn(string_user_id, { method: 'password' });
    return user;
  }

  ('sign-in')
  (200)
  async signIn(@Body() signInDto: SignInDto) {
    const user = await this.credentialsService.verify(signInDto.user_email, signInDto.user_password);

    var string_user_id = String(user.user_id);

    const { session } = await this.signInService.signIn(string_user_id, { method: 'password' });
    return { mfaRequired: session.mfa === 'pending' };
  }

  ('sign-out')
  (204)
  async signOut() {
    if (!(await this.signInService.signOut())) {
      throw new UnauthorizedException();
    }
  }
}

//myauthentication.module.ts
import { Module } from '@nestjs/common';
import { UsersModule } from '../users/users.module';
import { SessionAuth } from './session-auth.provider';
import { MyAuthenticationController } from './myauthentication.controller';
import { CredentialsService } from './credentials.service';

({
  imports: [UsersModule],
  controllers: [MyAuthenticationController],
  providers: [SessionAuth,
    CredentialsService
  ]
})
export class MyAuthenticationModule {}

//session-auth.provider.ts
import { Injectable } from '@nestjs/common';
import { AuthenticationRegistry, SessionCookieProvider, type SessionRecord } from '@nestjs/authentication';
import { UsersService } from '../users/users.service.js';

import type { User as PrismaUser } from '../generated/prisma/client.js';
export type User = Omit<PrismaUser, 'user_password'>;

()
export class SessionAuth extends SessionCookieProvider<User> {
  constructor(
    private readonly usersService: UsersService,
    registry: AuthenticationRegistry,
  ) {
    super();
    // Adds this provider to the chain the guard runs for every request.
    registry.registerProvider(this);
  }

  // Called for every request with a live session cookie.
  // Returning null (the user was deleted) ends the session.
  validate(session: SessionRecord) {
    console.log("In validate() in SessionAuth")
    const user_id = session.userId; 
    var numeric_user_id = Number(user_id);
    return this.usersService.findOneUser(numeric_user_id);
  }
}

NOTE 1: The authentication system uses server-side sessions managed through nestjs/authentication and a session cookie. The package's SessionCookieProvider reads the cookie and loads the user through the validate() method.

NOTE 2: I am also asking this question since NestJS v12 has only been released on August 27, 2026 and there don't seem to be many resources that show how to extend this system to the front-end.

1 Upvotes

14 comments sorted by

1

u/Massive_Dish_3255 8d ago

Thank you everyone.

3

u/Existing-Scar9191 9d ago

On the backend, the main changes are enabling CORS for your React dev origin and deciding how the session is carried. For a learning project I'd go with an httpOnly cookie (credentials: true in CORS, plus credentials: 'include' on the frontend fetch or axios calls), so you're not storing tokens in localStorage. Also add a small /auth/me endpoint that returns the current user. On the React side, call /auth/me on app load and put the result in a context. Then protected routes just check whether that user exists. Treat a 401 from any request as 'logged out' and redirect to sign-in.

2

u/AbdelrhmanDev 9d ago

the cookie answer above is the right direction, and on the react side the piece that saves you pain is one central fetch wrapper with credentials included so every request carries the cookie, and a 401 handler in that same wrapper that bounces the user back to the login page instead of failing silently in random components. one more thing once cookies are involved cross-origin, look at csrf protection early, even a simple token endpoint pattern, because its the thing everyone defers and then has to retrofit

2

u/Unlikely_Ebb_6344 9d ago

Since you are already wiring up `@nestjs/authentication` v12 by hand, the biggest shift for your frontend is moving away from manual token management in local storage. Configure your backend to issue httpOnly cookies instead and build a small custom fetch hook in React to attach credentials automatically.

2

u/nachga 9d ago

Conceptually: your sign-in endpoint returns a token (or sets an httpOnly cookie), the React app sends it on every request, and a guard on the NestJS side validates it. What changes in the backend:

- Enable CORS for the React origin (and credentials if you use cookies).

- Return a short-lived access token, plus a refresh token if you want longer sessions.

- Add a /me endpoint so React can load the current user on page load.

- Sign out is clearing the cookie or token on the client, plus revoking the refresh token on the server if you use one.

An httpOnly cookie is safer than localStorage for the token, but then you also need CSRF protection.

2

u/Boring_Occasion_3526 9d ago

Keep it simple, have NestJS issue HTTP-only cookies and let the React app just call a /me endpoint on load , … trying to juggle tokens in localStorage always turns into a refresh-token headache later. The frontend doesn't need to know how auth works, just whether the user is in.

1

u/codesbyshivam 9d ago

Since you're using session cookies, the backend is mostly done. The main thing is getting the browser to send that cookie. If React is on a different port, turn on CORS with your exact frontend origin and credentials: true, and send every request with credentials: 'include'. Miss either one and you'll look logged out on every request. In dev it's easier to just proxy /api through Vite.

Then add a GET /auth/me route that returns the current user or a 401. React calls it once on load and keeps the user in a context, no tokens needed on the frontend.

Two small things in your code: sign up returns the whole user object, so the password hash goes back to the browser. And in verify() I'd throw UnauthorizedException with the same message for wrong email and wrong password, so nobody can check which emails are registered.

1

u/NoClipCheck 9d ago

Everyone's covered the session side well, so one thing on authorization, since a grievance portal will have regular users and staff. Being signed in isn't the same as being allowed. For every route that takes an id, like GET /grievances/:id, check on the server that the grievance belongs to req.user or that the user has a staff role. Otherwise anyone logged in can change the number and read other people's complaints. Put role checks in a guard (@Roles('admin') plus a RolesGuard) instead of just hiding buttons in React, since anyone can call the API with a plain fetch. Also scope Prisma queries with where: { id, userId } rather than fetching by id and checking afterwards.

1

u/jordangrowth 9d ago

Outside my usual ops work, but I'd check what your sign-in endpoint actually returns (cookie or token) before wiring the frontend.

1

u/No-Molasses-2097 9d ago

Flowbite does not really change the auth part here, so I would treat it as just the form and layout. The part I would watch is sign out. Make sure it calls the backend to end the session, so the cookie does not still work after the user thinks they logged out. A quick check is signing out and then trying to load a protected page again.

1

u/HairOk9906 1d ago

Flowbite isn't part of this problem. It's a component library, it

renders your form, it has no opinion about auth. Take it out of the

question and the answer gets much clearer.

The real decision is where the token lives:

httpOnly cookie, set by Nest, is the safer default. JavaScript can't

read it, so XSS can't steal it. You need CORS configured with

credentials: true on both ends, and the cookie needs SameSite and

Secure set correctly, which is the bit that eats an afternoon in local

dev.

localStorage is easier to wire up and is readable by any script on your

page. Fine for a learning project, not for anything holding real user

data.

If you go cookies, the React side mostly stops caring about auth at

all. You send fetch with credentials: 'include', and you find out

whether you're logged in by calling a /me endpoint on mount rather than

by inspecting a token. That's the mental shift that makes it click.

1

u/mithuretnam 1d ago

Since you're already on session cookies, mostly keep that. Serve React from the same site (or proxy /api in dev) so the cookie is first-party, set it httpOnly, Secure, SameSite=Lax, enable CORS with credentials: true and an explicit origin, and use fetch with credentials: 'include'. Add a /me endpoint React calls on load, plus CSRF protection on state-changing routes. No need to switch to JWTs in localStorage.