Authenticate requests using username/email and password against a local or remote data source.
- Tutorials
-
How-To Guides
- 1. Registering AuthLocalModule Synchronously
- 2. Registering AuthLocalModule Asynchronously
- 3. Global Registering AuthLocalModule Asynchronously
- 4. Implementing User Lookup Service
- 5. Implementing custom token issuance service
- 6. Implementing a custom user validation service
- 7. Implementing a custom password validation service
- 8. Overriding the Settings
- 9. Integration with Other NestJS Modules
- Reference
- Explanation
The AuthLocalModule
is a robust NestJS module designed for implementing
local authentication using username and password. This module leverages the
passport-local
strategy
to authenticate users locally within your application.
-
Local Authentication: Provides a straightforward way to implement local authentication using username and password.
-
Synchronous and Asynchronous Registration: Flexibly register the module either synchronously or asynchronously, depending on your application's needs.
-
Global and Feature-Specific Registration: Use the module globally across your application or tailor it for specific features.
-
Customizable: Easily customize various aspects such as user validation, token issuance, and password validation.
npm install class-transformer
npm install class-validator
npm install @concepta/ts-core
npm install @concepta/nestjs-authentication
npm install @concepta/nestjs-password
npm install @concepta/nestjs-jwt
npm install @concepta/nestjs-auth-local
or
yarn add class-transformer
yarn add class-validator
yarn add @concepta/ts-core
yarn add @concepta/nestjs-authentication
yarn add @concepta/nestjs-password
yarn add @concepta/nestjs-jwt
yarn add @concepta/nestjs-auth-local
Import the AuthLocalModule
and required services in your application module.
Ensure to provide the necessary configuration options at
AuthLocalOptionsInterface
.
The AuthLocalOptionsInterface
defines the configuration options for the
local authentication strategy within a NestJS application using the
@concepta/nestjs-auth-local
package. This interface allows for the customization
of userLookupService
, issueTokenService
, validateUserService
, and
passwordValidationService
. Please see Reference for more
details.
Optional fields utilize default implementations, enabling straightforward integration and flexibility to override with custom implementations as needed. This setup ensures that developers can tailor the authentication process to specific requirements while maintaining a robust and secure authentication framework.
To test this scenario, we will set up an application where users can log in using a username and password. We will create the necessary entities, services, module configurations.
Note: The
@concepta/nestjs-user
module can be used in place of our exampleUser
related prerequisites.
First, create the User
entity.
// user.entity.ts
export class User {
id: number;
username: string;
password: string;
}
Next, you need to create the UserLookupService
. This
service is responsible for the business logic related to
retrieving user data. It should implement the
AuthLocalUserLookupServiceInterface
.
Within this service, implement the byUsername
method to
fetch user details by their username (or email). Ensure that
the method returns a User
object containing passwordHash
and
passwordSalt
.
These attributes are crucial as they are used by the
validateUser
method in the passwordValidationService
to authenticate the user, which is a configurable option
in the AuthLocalModule
.
// user-lookup.service.ts
import { Injectable } from '@nestjs/common';
import { ReferenceUsername } from '@concepta/ts-core';
import { AuthLocalUserLookupServiceInterface } from '@concepta/nestjs-auth-local';
import { AuthLocalCredentialsInterface } from '@concepta/nestjs-auth-local/dist/interfaces/auth-local-credentials.interface';
@Injectable()
export class UserLookupService implements AuthLocalUserLookupServiceInterface {
async byUsername(
username: ReferenceUsername,
): Promise<AuthLocalCredentialsInterface | null> {
// make sure this method will return a valid user with
// correct passwordHash and passwordSalt
// let's user this mock data for the purposes of this tutorial
return {
id: '5b3f5fd3-9426-4c4d-a06d-b4d55079034d',
username: username,
passwordHash:
'$2b$12$9rQ4qZx8gpTaTR4ic3LQ.OkebyVBa48DP42jErL1zfqF17WeG4hHC',
passwordSalt: '$2b$12$9rQ4qZx8gpTaTR4ic3LQ.O',
active: true,
};
}
}
Configure the module to include the necessary services userLookupService
.
// app.module.ts
import { Module } from '@nestjs/common';
import { AuthLocalModule } from '@concepta/nestjs-auth-local';
import { JwtModule } from '@concepta/nestjs-jwt';
import { UserLookupService } from './user-lookup.service';
@Module({
imports: [
JwtModule.forRoot({}),
AuthLocalModule.forRoot({
userLookupService: new UserLookupService(),
}),
],
controllers: [],
providers: [],
})
export class AppModule {}
To validate the setup, you can use curl
commands to simulate frontend
requests. Here are the steps to test the login endpoint:
Assuming you have an endpoint to obtain a JWT token, use curl
to get
the token. Replace auth-url
with your actual authentication URL, and
username
and password
with valid credentials.
curl -X POST http://localhost:3000/auth/login \
-H "Content-Type: application/json" \
-d '{"username": "testuser", "password": "testpassword"}'
This should return a response with a login message.
Here is an example sequence of curl
commands to validate the login setup:
- Login Request:
Command:
curl -X POST http://localhost:3000/auth/login \
-H "Content-Type: application/json" \
-d '{"username": "username", "password": "Test1234"}'
Response (example):
{
"accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI0NjZkMTAyNS1iZGNkLTRiNWItYTYxMi0yYThiZTU2MDhlNjIiLCJpYXQiOjE3MTgwNDg1NDQsImV4cCI6MTcxODA1MjE0NH0.Zl2i59w89cgJxfI4lXn6VmOhC5GLEqMm2nWkiVKpEUs",
"refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI0NjZkMTAyNS1iZGNkLTRiNWItYTYxMi0yYThiZTU2MDhlNjIiLCJpYXQiOjE3MTgwNDg1NDQsImV4cCI6NDg0MjI1MDk0NH0.xEF7kObwkztrMF7J83S-xvDarABmjXYkqLFINPWbx6g"
}
- Invalid Credentials Request:
Command:
curl -X POST http://localhost:3000/auth/login \
-H "Content-Type: application/json" \
-d '{"username": "testuser", "password": "wrongpassword"}'
Response (example):
{
"statusCode": 401,
"message": "Unauthorized"
}
// app.module.ts
//...
AuthLocalModule.register({
userLookupService: new MyUserLookupService(), // required
}),
//...
// app.module.ts
import { MyUserLookupService } from './services/my-user-lookup.service.ts';
//...
AuthLocalModule.registerAsync({
useFactory: async (userLookupService: MyUserLookupService) => ({
userLookupService, // required
}),
inject: [MyUserLookupService],
}),
//...
// app.module.ts
//...
AuthLocalModule.forRootAsync({
useFactory: async (userLookupService: MyUserLookupService) => ({
userLookupService,
}),
inject: [MyUserLookupService],
}),
//...
// my-user-lookup.service.ts
import { Injectable } from '@nestjs/common';
import {
AuthLocalUserLookupServiceInterface,
AuthLocalCredentialsInterface
} from '@concepta/nestjs-auth-local';
@Injectable()
export class MyUserLookupService
implements AuthLocalUserLookupServiceInterface {
async byUsername(username: string): Promise<AuthLocalCredentialsInterface | null> {
// implement custom logic to return the user's credentials
return null;
}
}
There are two ways to implementing the custom token issue service. You can take advantage of the default service, as seen here:
// my-jwt-issue.service.ts
import { Injectable } from '@nestjs/common';
import {
JwtIssueService,
JwtIssueServiceInterface,
JwtSignService,
} from '@concepta/nestjs-jwt';
@Injectable()
export class MyJwtIssueService extends JwtIssueService {
constructor(protected readonly jwtSignService: JwtSignService) {
super(jwtSignService);
}
async accessToken(
...args: Parameters<JwtIssueServiceInterface['accessToken']>
) {
// your custom code
return super.accessToken(...args);
}
async refreshToken(
...args: Parameters<JwtIssueServiceInterface['refreshToken']>
) {
// your custom code
return super.refreshToken(...args);
}
}
Or you can completely replace the default implementation:
// my-jwt-issue.service.ts
import { Injectable } from '@nestjs/common';
import { JwtIssueServiceInterface } from '@concepta/nestjs-jwt';
@Injectable()
export class MyJwtIssueService implements JwtIssueServiceInterface {
constructor() {}
async accessToken(
...args: Parameters<JwtIssueServiceInterface['accessToken']>
) {
// your custom code
}
async refreshToken(
...args: Parameters<JwtIssueServiceInterface['refreshToken']>
) {
// your custom code
}
}
The same approach can be done for AuthLocalValidateUserService
you can
either completely override the default implementation or you can take
advantage of the default implementation.
// my-auth-local-validate-user.service.ts
import { Injectable } from '@nestjs/common';
import { ReferenceActiveInterface, ReferenceIdInterface } from '@concepta/ts-core';
import {
AuthLocalValidateUserInterface,
AuthLocalValidateUserService
} from '@concepta/nestjs-auth-local';
@Injectable()
export class MyAuthLocalValidateUserService
extends AuthLocalValidateUserService
{
async validateUser(
dto: AuthLocalValidateUserInterface,
): Promise<ReferenceIdInterface> {
// customize as needed
return super.validateUser(dto);
}
async isActive(
user: ReferenceIdInterface<string> & ReferenceActiveInterface<boolean>,
): Promise<boolean> {
// customize as needed
return super.isActive(user);
}
}
// my-auth-local-validate-user.service.ts
import { Injectable } from '@nestjs/common';
import { ReferenceActiveInterface, ReferenceIdInterface } from '@concepta/ts-core';
import {
AuthLocalValidateUserInterface,
AuthLocalValidateUserServiceInterface
} from '@concepta/nestjs-auth-local';
@Injectable()
export class MyAuthLocalValidateUserService
implements AuthLocalValidateUserServiceInterface
{
async validateUser(
dto: AuthLocalValidateUserInterface,
): Promise<ReferenceIdInterface> {
// your custom code
return {
id: '[userId]',
//...
}
}
async isActive(
user: ReferenceIdInterface<string> & ReferenceActiveInterface<boolean>,
): Promise<boolean> {
// customize as needed
return true;
}
}
The PasswordValidationService
in the @concepta/nestjs-password
module
provides a default implementation using bcrypt for hashing and verifying passwords.
However, depending on your application's requirements, you might need to use a
different method for password hashing or add additional validation logic.
You can either extend the existing PasswordValidationService
to leverage its
built-in functionalities while adding your enhancements, or completely
override it with your custom implementation.
Overriding the Default Implementation:
If your application requires a different hashing algorithm , you can replace the default implementation with one that suits your needs.
// my-password-validation.service.ts
import { Injectable } from '@nestjs/common';
import {
PasswordStorageInterface,
PasswordValidationServiceInterface,
} from '@concepta/nestjs-password';
@Injectable()
export class MyPasswordValidationService
implements PasswordValidationServiceInterface
{
async validate(options: {
password: string;
passwordHash: string;
passwordSalt: string;
}): Promise<boolean> {
// customize as needed
return true;
}
async validateObject<T extends PasswordStorageInterface>(
password: string,
object: T,
): Promise<boolean> {
// customize as needed
return true;
}
}
Extending the Default Service:
If you want to add additional validation logic while keeping the current hashing and validation, you can extend the default service:
// my-password-validation.service.ts
import {
PasswordStorageInterface,
PasswordValidationService,
} from '@concepta/nestjs-password';
import { Injectable } from '@nestjs/common';
@Injectable()
export class MyPasswordValidationService extends PasswordValidationService {
async validate(options: {
password: string;
passwordHash: string;
passwordSalt: string;
}): Promise<boolean> {
// customize as neeeded
return super.validate(options);
}
async validateObject<T extends PasswordStorageInterface>(
password: string,
object: T,
): Promise<boolean> {
// customize as neeeded
return super.validateObject(password, object);
}
}
import { Type } from '@nestjs/common';
export class CustomLoginDto {
email: string;
password: string;
}
export const localSettings = {
loginDto: CustomLoginDto,
usernameField: 'email',
passwordField: 'password'
};
AuthLocalModule.forRoot({
userLookupService: new UserLookupService(),
issueTokenService: new MyIssueTokenService(), // <- optional
passwordValidationService: new PasswordValidationService(), // <- optional
settings: localSettings
}),
Integrate nestjs-auth-local
with other NestJS modules like,
@concepta/nestjs-authentication
, @concepta/nestjs-auth-jwt
,
@concepta/nestjs-auth-refresh
for a comprehensive authentication system.
For detailed information on the properties, methods, and classes used in the
@concepta/nestjs-auth-local
, please refer to the API documentation
available at AuthLocalModule API Documentation. This documentation provides
comprehensive details on the interfaces and services that you can utilize to
customize and extend the authentication functionality within your NestJS
application.
Local Authentication is a method of verifying user identity based on credentials (username and password) stored locally within the application or in a connected database.
- Simplicity: Easy to implement and manage.
- Control: Full control over user authentication and data.
- Security: When properly implemented, provides a secure way to authenticate users.
Local Authentication is ideal for applications that need to manage user authentication directly within the application without relying on external identity providers.
-
Synchronous Registration: Used when configuration options are static and available at startup.
-
Asynchronous Registration: Used when configuration options need to be retrieved from external sources at runtime.
-
Global Registration: Makes the module available throughout the entire application.
-
Feature-Specific Registration: Allows the module to be registered only for specific features or modules within the application.