This is an authentication library for express. It uses email-password authentication flow for authentication.
The motivation behind creating this library is to reduce the boilerplate code for authentication that we have to write when implementing authentication in any express application.
We have used two principles to implement this library.
- One is to generate routes for authentication.
- The other is to provide the interface for the persistence layer.
The route generator takes care of generating the routes for authentication. The persistence layer provides the interface for the persistence layer which is implemented by the user.
The following routes are generated for authentication.
This route handles sign up of new user.
Request Body must have email and password properties:
{
"email": "baijan@test.com",
"password": "baijan",
"name": "baijan"
}
{
message: string;
code: TSignUpResponseCodes;
}
When you sign up, we will hash the password and *send the email for verification. We will store the hashed password in the storage using the implementation provided.
This route is used to verify the email after signing up.
Note: Before making request to this route, you need to make a request to
/send-otp
route to send the OTP.
Request Body must have email and otp properties:
{
"email": "baijan@test.com",
"otp": "123456"
}
{
message: string;
code: TVerifyEmailResponseCodes;
}
This route handles login of user.
Request Body must have email and password properties:
{
"email": "baijan@test.com",
"password": "baijan"
}
Note: user will be type of user you return from your handler
{
message: string;
code: TLoginResponseCodes;
data: {
accessToken: string;
refreshToken: string;
user: any;
}
}
This route log outs user from the application. We will invalidate the refresh token.
{
message: string;
code: TLogoutResponseCodes;
}
This route refreshes the access token and refresh token if refresh token is valid.
{
message: string;
code: TRefreshResponseCodes;
data: {
accessToken: string;
refreshToken: string;
}
}
This route returns the details of logged in user.
{
message: string;
code: TMeResponseCodes;
accessToken: string;
data: {
me: any; // data of logged in user: name, email
token: {
name: string;
email: string;
iat: number;
exp: number;
}
}
}
This route resets the password of the logged in user.
{
"oldPassword": "old_password",
"newPassword": "new_password"
}
{
message: string;
code: TResetPasswordResponseCodes;
}
This route is used to change the password of the logged out user.
Note: Before making request to this route, you need to make a request to
/send-otp
route to get the OTP.
Request Body must have email property:
{
"email": "baijan@test.com",
"otp": "123456",
"newPassword": "new_password"
}
{
message: string;
code: TForgotPasswordResponseCodes;
}
This route is used to get an OTP for verification. otplib is used to generate the OTP.
Request Body must have email property:
{
"email": "baijan@test.com"
}
{
message: string;
code: TSendOtpResponseCodes;
}
- Install the dependency.
npm install @baijanstack/express-auth
- Create the auth configuration
import { TConfig } from '@baijanstack/express-auth';
const authConfig: TConfig = {
BASE_PATH: '/v1/auth', // base path for authentication
SALT_ROUNDS: 10, // number of rounds for password hashing
TOKEN_SECRET: 'random_secure_secret_value', // secret for token generation
ACCESS_TOKEN_AGE: 60, // age of access token in seconds
REFRESH_TOKEN_AGE: 240000, // age of refresh token in seconds
OTP_AGE: 30, // age/step of otp in seconds
OTP_SECRET: 'random_secure_secret_value', // secret for otp generation
};
- Implement the
INotifyService
for sending notifications. We will useEmailNotificationService
for sending notifications for different events such asTOKEN_STOLEN
,OTP
andEMAIL_VERIFIED
.
// notifier.ts
import { INotifyService } from '@baijanstack/express-auth';
export class EmailNotificationService implements INotifyService {
async sendTokenStolen(email: string): Promise<void> {
console.log(`Notifying | TOKEN_STOLEN | Email: ${email}`);
}
async sendOtp(email: string, payload: { code: string; generatedAt: number }): Promise<void> {
console.log(`Notifying | OTP | Email: ${email}`, payload);
}
async notifyEmailVerified(email: string): Promise<void> {
console.log(`Notifying | EMAIL_VERIFIED | Email: ${email}`);
}
}
- Create an instance of the route generator.
You need to pass the INotifyService
and TConfig
to the route generator implementations.
import express from 'express';
import { initAuth, RouteGenerator, TConfig } from '@baijanstack/express-auth';
const app = express();
const routeGenerator = new RouteGenerator(app, notificationService, authConfig);
- Initiate the auth library.
import { initAuth } from '@baijanstack/express-auth';
initAuth({
routeGenerator,
// ... need to import all the handlers from
// `handlers.ts` file that we will make in next section
signUpHandler: new SignUpHandler(),
loginHandler: new LoginHandler(),
logoutHandler: new LogoutHandler(),
refreshHandler: new RefreshHandler(),
resetPasswordHandler: new ResetPasswordHandler(),
meRouteHandler: new MeRouteHandler(),
verifyEmailHandler: new VerifyEmailHandler(),
forgotPasswordHandler: new ForgotPasswordHandler(),
verifyOtpHandler: new VerifyOtpHandler(),
});
- When you initiate the auth library, you need to pass handlers for each route. The handlers are independent of storage type - in-memory or database etc.
I will show you how to implement these handlers in the next section using in-memory storage.
Note: You can see an implementation of the handlers in the Test file.
// handlers.ts
import { ISignUpHandler, ILoginHandler, ILogoutHandler, IRefreshHandler, IResetPasswordHandler, IMeRouteHandler, IVerifyEmailHandler, IForgotPasswordHandler, ISendOtpHandler } from '@baijanstack/express-auth';
export type TUser = {
name: string;
email: string;
password: string;
is_email_verified: boolean;
};
let users: TUser[] = [];
type TEmailObj = {
email: string;
};
interface TSignUpBodyInput extends TEmailObj {
name: string;
password: string;
}
export class SignUpHandler implements ISignUpHandler {
constructor() {
console.log('signup persistor init...');
}
doesUserExists: (body: TSignUpBodyInput) => Promise<boolean> = async (body) => {
const user = users.find((user) => user.email === body.email);
return !!user;
};
saveUser: (body: TSignUpBodyInput, hashedPassword: string) => Promise<void> = async (body, hashedPassword) => {
users.push({
name: body.name,
email: body.email,
password: hashedPassword,
/**
* !!for testing...
*/
is_email_verified: true,
});
};
}
export class LoginHandler implements ILoginHandler {
getUserByEmail: (email: string) => Promise<TUser | null> = async (email) => {
const user = await users.find((user) => user.email === email);
if (!user) {
return null;
}
return user;
};
getTokenPayload: (email: string) => Promise<{
name: string;
email: string;
} | null> = async (email) => {
const user = users.find((user) => user.email === email);
if (!user) {
return null;
}
return {
email: user?.email,
name: user?.name,
};
};
}
export class LogoutHandler implements ILogoutHandler {
shouldLogout: () => Promise<boolean> = async () => {
return true;
};
}
export class RefreshHandler implements IRefreshHandler {
getTokenPayload: (email: string) => Promise<{
name: string;
email: string;
} | null> = async (email) => {
const user = users.find((user) => user.email === email);
if (!user) {
return null;
}
return {
email: user?.email,
name: user?.name,
};
};
}
export class ResetPasswordHandler implements IResetPasswordHandler {
saveHashedPassword: (email: string, hashedPassword: string) => Promise<void> = async (email, hashedPassword) => {
const userIdx = users.findIndex((user) => user.email === email);
if (userIdx < 0) {
throw new Error(`User not found`);
}
users[userIdx].password = hashedPassword;
};
getOldPasswordHash: (email: string) => Promise<string> = async (email) => {
const user = users.find((user) => user.email === email);
if (!user) {
return '';
}
return user.password;
};
}
export class MeRouteHandler implements IMeRouteHandler {
getMeByEmail: (email: string) => Promise<{ email: string; name: string } | null> = async (email) => {
const user = users.find((user) => user.email === email);
if (!user) {
return null;
}
return {
name: user?.name,
email: user?.email,
};
};
}
export class VerifyEmailHandler implements IVerifyEmailHandler {
isEmailAlreadyVerified: (email: string) => Promise<boolean> = async (email) => {
const user = users.find((user) => user.email === email);
return !user?.is_email_verified;
};
updateIsEmailVerifiedField: (email: string) => Promise<void> = async (email) => {
users = users.map((u) => {
if (u.email === email) {
return {
...u,
is_email_verified: true,
};
}
return u;
});
};
}
export class SendOtpHandler implements ISendOtpHandler {
doesUserExists: (email: string) => Promise<boolean> = async (email) => {
const user = users.find((user) => user.email === email);
return !!user;
};
}
export class ForgotPasswordHandler implements IForgotPasswordHandler {
saveNewPassword: (email: string, password: string) => Promise<void> = async (email, password) => {
const userIdx = users.findIndex((user) => user.email === email);
if (userIdx < 0) {
throw new Error(`User not found`);
}
users[userIdx].password = password;
};
}
You can protect your routes by using the middleware validateAccesstoken
provided by this library.
Note: You can send the tokens either from the cookie or in headers. Please make sure you are not adding Bearer
before the token else you will get invalid token error. If you are using headers, you must send token as below:
x-access-token: <token_value>
x-refresh-token: <token_value>
import { validateAccessToken } from 'baijanstack/express-auth';
app.get('/protected', validateAccessToken, (req, res) => {
console.log('Logged in user is:', req.user);
res.send('Hello World');
});
Note: If the middlewares return response, they are in following format:
{
message: string;
code: TValidateTokenResponseCodes;
}