I’ve created a login system in NestJS based on the documentation, but I have a question regarding the authorization approach. According to the documentation, after logging in, the user receives a token that needs to be sent to the API every time for authorization.
My question is: is this really the correct approach? Would it not be better to use HttpOnly cookies to store the token? How should this be implemented in the context of NestJS? AuthService:
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { UsersService } from '../users/users.service';
import { JwtService } from '@nestjs/jwt';
@Injectable()
export class AuthService {
constructor(
private usersService: UsersService,
private jwtService: JwtService
) {}
async signIn(
username: string,
pass: string,
): Promise<{ access_token: string }> {
const user = await this.usersService.findOne(username);
if (user?.password !== pass) {
throw new UnauthorizedException();
}
const payload = { sub: user.userId, username: user.username };
return {
access_token: await this.jwtService.signAsync(payload),
};
}
}
Auth Controller
import {
Body,
Controller,
Get,
HttpCode,
HttpStatus,
Post,
Request,
UseGuards
} from '@nestjs/common';
import { AuthService } from './auth.service';
import { AuthGuard } from './auth.guard';
@Controller('auth')
export class AuthController {
constructor(private authService: AuthService) {}
@HttpCode(HttpStatus.OK)
@Post('login')
signIn(@Body() signInDto: Record<string, any>) {
return this.authService.signIn(signInDto.username, signInDto.password);
}
@UseGuards(AuthGuard)
@Get('profile')
getProfile(@Request() req) {
return req.user;
}
}
Auth Guard
import {
CanActivate,
ExecutionContext,
Injectable,
UnauthorizedException,
} from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { jwtConstants } from './constants';
import { Request } from 'express';
@Injectable()
export class AuthGuard 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: jwtConstants.secret
}
);
request['user'] = payload;
} catch {
throw new UnauthorizedException();
}
return true;
}
private extractTokenFromHeader(request: Request): string | undefined {
const [type, token] = request.headers.authorization?.split(' ') ?? [];
return type === 'Bearer' ? token : undefined;
}
}
Which approach do you think will be better? Should I follow what’s in the documentation and store the token in React, sending it whenever needed, or use HttpOnly cookies?
The method of authentication preferred for your project solely depends on your project's requirements. Both token and cookie based approaches have their own list of pros and cons. Nest.js as a framework gives you the ability to achieve both of these approaches.
Cookie based authentication
This is a stateful approach.
Server authenticates the client, sends out a signed cookie to the client and opens a session. The client sends the cookie along with every subsequent request for authentication.
Server maintains the session information in this approach. The session is flushed out after the client logs out.
Cookie based authentication requires no setup in the client side. Browsers handle that by default. You can also set the cookies to be HttpOnly which makes them inaccessible to the client side javascript, ensuring security.
Cookies are strictly locked to a specific domain. You cannot use cookies between domains, as the session lies on the server.
Token based authentication
This is a stateless approach.
Server authenticates the client and sends out a signed token back to the client, which contains the hashed secret key. Client attaches the token in the header of every request, which is parsed and verified by the server to ensure integrity.
This requires setup in the client side to manage the token. The client is responsible for storing and updating the valid token.
Conclusion
HTTP is a stateless protocol. Token based approach ensures integrity of the client and also ensures a stateless approach. Cookie based authentication results in stateful sessions between client and server.
Tokens can be shared across domains, ensuring a good fit for microservice architectures unlike cookies, which are specific to a domain.
Token based approach results in a more flexible approach, though it requires some manual code to be written from your side.