dependency
pnpm i @types/bcrypt bcrypt @nestjs/passport passport @types/passport-local passport-local @types/express @nestjs/jwt passport-jwt @types/passport-jwt cookie-parser @types/cookie-parser
# or
pnpm i @types/bcrypt bcrypt @nestjs/passport passport @types/passport-local passport-local
pnpm i @types/express @nestjs/jwt passport-jwt @types/passport-jwt cookie-parser @types/cookie-parser
create a user crud resource module, and register it in AppModule; imports array
nest g res modules/users --no-spec
Defining the User entity
// src/modules/users/entities/user.entity.ts
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
@Entity({ name: 'users' })
class UserEntity {
@PrimaryGeneratedColumn()
public id?: number;
@Column({ unique: true })
public email: string;
@Column()
public name: string;
@Column()
public password: string;
}
export default UserEntity;
// src/modules/users/users.service.ts
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';
import { InjectRepository } from '@nestjs/typeorm';
import UserEntity from './entities/user.entity';
import { Repository } from 'typeorm';
@Injectable()
export class UsersService {
constructor(
@InjectRepository(UserEntity)
private usersRepository: Repository<UserEntity>,
) {}
async create(createUserDto: CreateUserDto) {
const newUser = await this.usersRepository.create(createUserDto);
await this.usersRepository.save(newUser);
return newUser;
}
async findOneByEmail(email: string) {
const user = await this.usersRepository.findOneBy({ email });
if (user) {
return user;
}
throw new HttpException(
'User with this email does not exist',
HttpStatus.NOT_FOUND,
);
}
}
// src/modules/users/dto/create-user.dto.ts
export class CreateUserDto {
email: string;
name: string;
password: string;
}
// src/modules/users/users.module.ts
import { Module } from '@nestjs/common';
import { UsersService } from './users.service';
import { UsersController } from './users.controller';
import { TypeOrmModule } from '@nestjs/typeorm';
import UserEntity from './entities/user.entity';
@Module({
imports: [TypeOrmModule.forFeature([UserEntity])],
controllers: [UsersController],
providers: [UsersService],
exports: [UsersService],
})
export class UsersModule {}
Handling passwords
// test/bcrypt.usage.ts
import * as bcrypt from 'bcrypt';
const testBcrypt = async () => {
const passwordInPlaintext = '12345678';
const hashedPassword = await bcrypt.hash(passwordInPlaintext, 10);
const isPasswordMatching = await bcrypt.compare(
passwordInPlaintext,
hashedPassword,
);
console.log(isPasswordMatching); // true
};
testBcrypt();
nest g res modules/authentication --n
o-spec
// src/modules/database/enum/postgresErrorCodes.enum.ts
export enum PostgresErrorCode {
UniqueViolation = '23505',
}
PostgreSQL Error Codes documentation page
// src/modules/authentication/dto/create-register.dto.ts
export class CreateRegisterDto {
email: string;
name: string;
password: string;
}
```ts
//
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
import { CreateAuthenticationDto } from './dto/create-authentication.dto';
import { UpdateAuthenticationDto } from './dto/update-authentication.dto';
import { UsersService } from '../users/users.service';
import * as bcrypt from 'bcrypt';
import { CreateRegisterDto } from './dto/create-register.dto';
import { PostgresErrorCode } from '../database/enum/postgresErrorCodes.enum';
@Injectable()
export class AuthenticationService {
constructor(private readonly usersService: UsersService) {}
public async register(registrationData: CreateRegisterDto) {
const hashedPassword = await bcrypt.hash(registrationData.password, 10);
try {
const createdUser = await this.usersService.create({
...registrationData,
password: hashedPassword,
});
createdUser.password = undefined;
return createdUser;
} catch (error) {
if (error?.code === PostgresErrorCode.UniqueViolation) {
throw new HttpException(
'User with that email already exists',
HttpStatus.BAD_REQUEST,
);
}
throw new HttpException(
'Something went wrong',
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
public async getAuthenticatedUser(email: string, plainTextPassword: string) {
try {
const user = await this.usersService.findOneByEmail(email);
await this.verifyPassword(plainTextPassword, user.password);
user.password = undefined;
return user;
} catch (error) {
throw new HttpException(
'Wrong credentials provided',
HttpStatus.BAD_REQUEST,
);
}
}
private async verifyPassword(
plainTextPassword: string,
hashedPassword: string,
) {
const isPasswordMatching = await bcrypt.compare(
plainTextPassword,
hashedPassword,
);
if (!isPasswordMatching) {
throw new HttpException(
'Wrong credentials provided',
HttpStatus.BAD_REQUEST,
);
}
}
}
Integrating our authentication with Passport
Applications have different approaches to authentication. Passport calls those mechanisms strategies. The first strategy that we want to implement is the passport-local strategy. It is a strategy for authenticating with a username and password
To configure a strategy, we need to provide a set of options specific to a particular strategy. In NestJS, we do it by extending the PassportStrategy class.
// src/modules/authentication/strategy/local.strategy.ts
import { Strategy } from 'passport-local';
import { PassportStrategy } from '@nestjs/passport';
import { Injectable } from '@nestjs/common';
import UserEntity from 'src/modules/users/entities/user.entity';
import { AuthenticationService } from '../authentication.service';
@Injectable()
export class LocalStrategy extends PassportStrategy(Strategy) {
constructor(private authenticationService: AuthenticationService) {
super({
usernameField: 'email',
});
}
async validate(email: string, password: string): Promise<UserEntity> {
// if getAuthenticatedUser return a user, it will be bind to request.user
return this.authenticationService.getAuthenticatedUser(email, password);
}
}
// src/modules/authentication/authentication.module.ts
import { Module } from '@nestjs/common';
import { AuthenticationService } from './authentication.service';
import { AuthenticationController } from './authentication.controller';
import { UsersModule } from '../users/users.module';
import { PassportModule } from '@nestjs/passport';
import { LocalStrategy } from './strategy/local.strategy';
@Module({
imports: [UsersModule, PassportModule],
controllers: [AuthenticationController],
providers: [AuthenticationService, LocalStrategy],
})
export class AuthenticationModule {}
// src/app.module/app.module.ts
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { PostsModule } from '../modules/posts/posts.module';
import { ConfigModule } from '@nestjs/config';
import * as Joi from 'joi';
import { DatabaseModule } from '../modules/database/database.module';
import { UsersModule } from '../modules/users/users.module';
import { AuthenticationModule } from '../modules/authentication/authentication.module';
@Module({
imports: [
PostsModule,
ConfigModule.forRoot({
validationSchema: Joi.object({
POSTGRES_HOST: Joi.string().required(),
POSTGRES_PORT: Joi.number().required(),
POSTGRES_USER: Joi.string().required(),
POSTGRES_PASSWORD: Joi.string().required(),
POSTGRES_DB: Joi.string().required(),
PORT: Joi.number(),
}),
}),
DatabaseModule,
UsersModule,
AuthenticationModule,
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
Using built-in Passport Guards
// src/modules/authentication/guard/localAuthentication.guard.ts
import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
@Injectable()
export class LocalAuthenticationGuard extends AuthGuard('local') {}
// src/modules/authentication/interface/requestWithUser.interface.ts
import { Request } from 'express';
import UserEntity from 'src/modules/users/entities/user.entity';
interface RequestWithUser extends Request {
user: UserEntity;
}
export default RequestWithUser;
The data of the user is attached to the request object, and this is why we extend the Request interface.
// src/modules/authentication/authentication.controller.ts
import {
Controller,
Post,
Body,
UseGuards,
HttpCode,
Req,
} from '@nestjs/common';
import { AuthenticationService } from './authentication.service';
import { CreateAuthenticationDto } from './dto/create-authentication.dto';
import { UpdateAuthenticationDto } from './dto/update-authentication.dto';
import { LocalAuthenticationGuard } from './guard/localAuthentication.guard';
import { CreateRegisterDto } from './dto/create-register.dto';
import RequestWithUser from './interface/requestWithUser.interface';
@Controller('authentication')
export class AuthenticationController {
constructor(private readonly authenticationService: AuthenticationService) {}
@Post('register')
async register(@Body() registrationData: CreateRegisterDto) {
return this.authenticationService.register(registrationData);
}
@HttpCode(200)
@UseGuards(LocalAuthenticationGuard) // encapsulate validation logic in LocalStrategy.validate()
@Post('login')
async logIn(@Req() request: RequestWithUser) {
const user = request.user;
user.password = undefined;
return user;
}
}
summary:
- defining
LocalStrategy(src/modules/authentication/strategy/local.strategy.ts) - defining
LocalAuthenticationGuard(src/modules/authentication/guard/localAuthentication.guard.ts)extends AuthGuard('local') - using
LocalAuthenticationGuardlike this:
@UseGuards(LocalAuthenticationGuard) do all local username and password verifying.
/login route handler logIn() it self doing almost noting.
passport and passort-local usage
passport.use(new LocalStrategy([options,] callback))
argument options is optional, callback is required.
optiona:
usernameFieldsetnamefield, defaultusernamepasswordFieldsetpassworddield, defaultpasswordpassReqToCallbackset whetherrequestis the first parameter, defaultfalse(not the first parameter)sessionset supportsessionor not, keep login state in session, defaulttrue
calback:
done() the first parameter return error message or null, the second parameter return user Object or false
passport.use(new LocalStrategy(
{
usernameField: 'username',
passwordField: 'password',
passReqToCallback: true,
session: false
},
function([req,]username, password, done) {
User.findOne({ username: username, password: password }, function (err, user) {
done(err, user);
});
}
));
@Injectable()
export class LocalStrategy extends PassportStrategy( Strategy ) {
constructor ( private readonly authService: AuthService ) {
// passport-local 用例中,没有配置选项,因此我们的构造函数只是调用 super() ,没有 options 对象。
/**
usernameField 设置 name 字段, 默认 username
passwordField 设置 password 字段, 默认 password
passReqToCallback 设置 request 是否回调函数的第一个参数, 默认 true (是第一个参数)
session 设置 是否支持 session 会话, 保持持久化登录状态, 默认 true
*/
super( {
usernameField: 'firstName'
} );
}
async validate ( firstName: string, password: string ): Promise<any> {
const user = await this.authService.validateUser( firstName, password );
if ( !user ) {
throw new UnauthorizedException();
}
// 返回的值自动创建一个 user 对象,并将其作为 req.user 分配给请求对象
return user;
}
}
Using JSON Web Tokens
.env
POSTGRES_HOST=localhost
POSTGRES_PORT=5432
POSTGRES_USER=admin
POSTGRES_PASSWORD=admin
POSTGRES_DB=nestjs
PORT=5000
JWT_SECRET=JWT_SECRET
# time unit is second
JWT_EXPIRATION_TIME=20000
// src/app.module/app.module.ts
// ...
@Module({
imports: [
PostsModule,
ConfigModule.forRoot({
validationSchema: Joi.object({
// ...
JWT_SECRET: Joi.string().required(),
JWT_EXPIRATION_TIME: Joi.string().required(),
}),
}),
DatabaseModule,
UsersModule,
AuthenticationModule,
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
// src/modules/authentication/authentication.module.ts
import { Module } from '@nestjs/common';
import { AuthenticationService } from './authentication.service';
import { AuthenticationController } from './authentication.controller';
import { UsersModule } from '../users/users.module';
import { PassportModule } from '@nestjs/passport';
import { LocalStrategy } from './strategy/local.strategy';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { JwtModule } from '@nestjs/jwt';
@Module({
imports: [
UsersModule,
PassportModule,
ConfigModule,
JwtModule.registerAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: async (configService: ConfigService) => ({
secret: configService.get('JWT_SECRET'),
signOptions: {
expiresIn: `${configService.get('JWT_EXPIRATION_TIME')}s`,
},
}),
}),
],
controllers: [AuthenticationController],
providers: [AuthenticationService, LocalStrategy],
})
export class AuthenticationModule {}
// src/modules/authentication/interface/tokenPayload.interface.ts
interface TokenPayload {
userId: number;
isSecondFactorAuthenticated?: boolean;
}
export default TokenPayload;
// src/modules/authentication/authentication.service.ts
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
import { CreateAuthenticationDto } from './dto/create-authentication.dto';
import { UpdateAuthenticationDto } from './dto/update-authentication.dto';
import { UsersService } from '../users/users.service';
import * as bcrypt from 'bcrypt';
import { CreateRegisterDto } from './dto/create-register.dto';
import { PostgresErrorCode } from '../database/enum/postgresErrorCodes.enum';
import { JwtService } from '@nestjs/jwt';
import { ConfigService } from '@nestjs/config';
import TokenPayload from './interface/tokenPayload.interface';
@Injectable()
export class AuthenticationService {
constructor(
private readonly usersService: UsersService,
private readonly jwtService: JwtService,
private readonly configService: ConfigService,
) {}
public async register(registrationData: CreateRegisterDto) {
const hashedPassword = await bcrypt.hash(registrationData.password, 10);
try {
const createdUser = await this.usersService.create({
...registrationData,
password: hashedPassword,
});
createdUser.password = undefined;
return createdUser;
} catch (error) {
if (error?.code === PostgresErrorCode.UniqueViolation) {
throw new HttpException(
'User with that email already exists',
HttpStatus.BAD_REQUEST,
);
}
throw new HttpException(
'Something went wrong',
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
public async getAuthenticatedUser(email: string, plainTextPassword: string) {
try {
const user = await this.usersService.findOneByEmail(email);
await this.verifyPassword(plainTextPassword, user.password);
user.password = undefined;
return user;
} catch (error) {
throw new HttpException(
'Wrong credentials provided',
HttpStatus.BAD_REQUEST,
);
}
}
private async verifyPassword(
plainTextPassword: string,
hashedPassword: string,
) {
const isPasswordMatching = await bcrypt.compare(
plainTextPassword,
hashedPassword,
);
if (!isPasswordMatching) {
throw new HttpException(
'Wrong credentials provided',
HttpStatus.BAD_REQUEST,
);
}
}
public getCookieWithJwtToken(userId: number) {
const payload: TokenPayload = { userId };
const token = this.jwtService.sign(payload);
return `Authentication=${token}; HttpOnly; Path=/; Max-Age=${this.configService.get(
'JWT_EXPIRATION_TIME',
)}`;
}
}
at the time i come across a typescript type error, all methods on express.Response not exists. by ref the issues # @types/express Latest types throw Property 'headers', 'body', 'query' does not exist on type 'Request'
solve it by run:
pnpm remove @types/express
pnpm i -D @types/express-serve-static-core
// src/modules/authentication/authentication.controller.ts
//...
import { LocalAuthenticationGuard } from './guard/localAuthentication.guard';
import { CreateRegisterDto } from './dto/create-register.dto';
import RequestWithUser from './interface/requestWithUser.interface';
import { Response } from 'express';
@Controller('authentication')
export class AuthenticationController {
constructor(private readonly authenticationService: AuthenticationService) {}
@Post('register')
async register(@Body() registrationData: CreateRegisterDto) {
return this.authenticationService.register(registrationData);
}
@HttpCode(200)
@UseGuards(LocalAuthenticationGuard)
@Post('login')
async logIn(
@Req() request: RequestWithUser,
@Res({ passthrough: true }) response: Response,
) {
const { user } = request;
const cookie = this.authenticationService.getCookieWithJwtToken(user.id);
response.setHeaders('Set-Cookie', cookie);
user.password = undefined;
return response.send(user);
}
}
Receiving tokens
// src/main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module/app.module';
import * as cookieParser from 'cookie-parser';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.use(cookieParser());
await app.listen(3000);
}
bootstrap();
// src/modules/authentication/strategy/jwt.strategy.ts
import { ExtractJwt, Strategy } from 'passport-jwt';
import { PassportStrategy } from '@nestjs/passport';
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { Request } from 'express';
// import { UsersService } from '../users/users.service';
// import TokenPayload from './tokenPayload.interface';
import { UsersService } from '../../users/users.service';
import TokenPayload from '../interface/tokenPayload.interface';
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(
private readonly configService: ConfigService,
private readonly userService: UsersService,
) {
super({
jwtFromRequest: ExtractJwt.fromExtractors([
(request: Request) => {
return request?.cookies?.Authentication;
},
]),
secretOrKey: configService.get('JWT_SECRET'),
});
}
async validate(payload: TokenPayload) {
return this.userService.findOneById(payload.userId);
}
}
There are a few notable things above. We extend the default JWT strategy by reading the token from the cookie.
When we successfully access the token, we use the id of the user that is encoded inside. With it, we can get the whole user data through the userService.getById method. We also need to add it to our UsersService.
// src/modules/users/users.service.ts
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';
import { InjectRepository } from '@nestjs/typeorm';
import UserEntity from './entities/user.entity';
import { Repository } from 'typeorm';
@Injectable()
export class UsersService {
constructor(
@InjectRepository(UserEntity)
private usersRepository: Repository<UserEntity>,
) {}
async create(createUserDto: CreateUserDto) {
const newUser = await this.usersRepository.create(createUserDto);
await this.usersRepository.save(newUser);
return newUser;
}
async findOneByEmail(email: string) {
const user = await this.usersRepository.findOneBy({ email });
if (user) {
return user;
}
throw new HttpException(
'User with this email does not exist',
HttpStatus.NOT_FOUND,
);
}
async findOneById(id: number) {
const user = await this.usersRepository.findOneBy({ id });
if (user) {
return user;
}
throw new HttpException(
'User with this email does not exist',
HttpStatus.NOT_FOUND,
);
}
}
Thanks to the validate method running under the hood when the token is encoded, we have access to all of the user data.
We now need to add our new JwtStrategy to the AuthenticationModule.
// src/modules/authentication/authentication.module.ts
@Module({
// (...)
providers: [AuthenticationService, LocalStrategy, JwtStrategy]
})
export class AuthenticationModule {}
Requiring authentication from our users
//src/modules/authentication/guard/jwt-authentication.guard.ts
import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
@Injectable()
export default class JwtAuthenticationGuard extends AuthGuard('jwt') {}
// src/modules/posts/posts.controller.ts
// ...
import JwtAuthenticationGuard from '../authentication/guard/jwt-authentication.guard';
@Controller('posts')
export class PostsController {
constructor(private readonly postsService: PostsService) {}
@Post()
@UseGuards(JwtAuthenticationGuard)
create(@Body() createPostDto: CreatePostDto) {
return this.postsService.create(createPostDto);
}
//(...)
}
Logging out
//
//..
@Injectable()
export class AuthenticationService {
//...
public getCookieForLogOut() {
return `Authentication=; HttpOnly; Path=/; Max-Age=0`;
}
//...
}
// src/modules/authentication/authentication.controller.ts
//...
import { Response } from 'express';
import JwtAuthenticationGuard from './guard/jwt-authentication.guard';
@Controller('authentication')
export class AuthenticationController {
//...
@UseGuards(JwtAuthenticationGuard)
@Post('logout')
async logOut(@Req() request: RequestWithUser, @Res() response: Response) {
response.setHeader(
'Set-Cookie',
this.authenticationService.getCookieForLogOut(),
);
return response.sendStatus(200);
}
}
Verifying tokens
One important additional functionality that we need is verifying JSON Web Tokens and returning user data. By doing so, the browser can check if the current token is valid and get the data of the currently logged in user.
// src/modules/authentication/authentication.controller.ts
//...
import { Response } from 'express';
import JwtAuthenticationGuard from './guard/jwt-authentication.guard';
@Controller('authentication')
export class AuthenticationController {
constructor(private readonly authenticationService: AuthenticationService) {}
@UseGuards(JwtAuthenticationGuard)
@Get()
authenticate(@Req() request: RequestWithUser) {
const user = request.user;
user.password = undefined;
return user;
}
}
I am finding Job, if you have a HC, Plz contact me, wechat: ftf2022
ref: