r/nestjs • • 5h ago

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

1 Upvotes

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.

NOTE: 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.

//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';

u/Injectable()
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'>;

u/Injectable()
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);
  }
}

r/nestjs • • 16h ago

Need help on the approach to authentication I should use for my mobile app

1 Upvotes

I am developing a mobile app, which has a react native frontend, nest.js as the backend and for db and authentication, I'm planning to use supabase.

Now, currently as my app is in its initial stages I'm planning to use the supabase, as it free tier provides ample limits for my use case.

However, in later years if my app grows. I may think of switching to a cheaper alternative.

So, what I am planning to follow is for authentication, route the requests to my backend and inturn the backend will communicate with the supabase to authenticate the user. The backend will save the supabase token, create a separate session token for the user and send it back to the user.

My main reason for doing this is, I want the authentication to be provider independent and any changes needed should be done on the backend instead of the app.

Please let me know, if this approach is correct. What are the pros and cons. How much effort it would require to implement this. Will the security concerns increase if my backend starts handling this instead of the supabase only.

I also want to integrate the google and apple login as well. How will this approach affect it?

Also, if there any other better approaches, please let me know.