Description
Identity access management module for Nest that provides a customizable
authentication service interface supporting multiple authentication methods and
role-based access control.
The goal of this module is to provide a simple interface that can be used to
implement authentication and authorization in your application. It is not meant
to be a full-fledged identity access management solution. It does not provide
any user management functionality for example but instead relies on your own
implementation.
Installation
$ npm i --save @fastnloud/nest-iam
Setup
Create a service that implements the IAuthService
interface in your project:
import { IAuthService, IToken, IUser } from '@fastnloud/nest-iam';
import { Injectable } from '@nestjs/common';
@Injectable()
export class AuthService implements IAuthService {
public async findOneUserOrFail(id: string): Promise<IUser> {}
public async findOneValidTokenOrFail(
id: string,
type: string,
): Promise<IToken> {}
public async findOneValidUserOrFail(username: string): Promise<IUser> {}
public async removeTokenOrFail(id: string): Promise<void> {}
public async saveTokenOrFail(userId: string, token: IToken): Promise<void> {}
}
Import the IamModule
and register it in your AppModule
:
import { IAuthService, IamModule } from '@fastnloud/nest-iam';
import { Module } from '@nestjs/common';
import { CqrsModule } from '@nestjs/cqrs';
import { AuthService } from './services/auth.service';
@Module({
imports: [
IamModule.forRootAsync({
imports: [UserModule],
useFactory: (authService: IAuthService) => {
return {
authService,
};
},
inject: [AuthService],
}),
],
exports: [AuthService],
providers: [AuthService],
})
export class AppModule {}
Configuration
Add the following environment variables to your .env
file and change the
values to your needs:
# defaults to: basic
IAM_AUTH_METHODS=basic,passwordless
# defaults to: 300
IAM_AUTH_PASSWORDLESS_TOKEN_TTL=150
# defaults to: strict
IAM_COOKIE_SAME_SITE=lax
# defaults to: 1
IAM_COOKIE_SECURE=0
# defaults to: 600
IAM_JWT_ACCESS_TOKEN_TTL=3600
# defaults to: 604800
IAM_JWT_REFRESH_TOKEN_TTL=86400
IAM_JWT_SECRET=superSecretString
Minimal configuration:
IAM_JWT_SECRET=superSecretString
When in local development, you can set IAM_COOKIE_SECURE
to 0
to disable the
secure flag on the cookie. This will allow you to use the cookie over HTTP
instead of HTTPS.
The IAM_ROUTE_PATH_PREFIX
environment variable is optional and will be left
empty if not set. Whenever this module is served from another path this value
should be updated accordingly.
Example
This is an example of a service that implements the IAuthService
interface
using TypeORM:
import { CookieName, IAuthService, IToken, IUser, TokenType } from '@fastnloud/nest-iam';
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { MoreThan, Repository } from 'typeorm';
import { UserToken } from './entities/user-token.entity';
import { User } from './entities/user.entity';
@Injectable()
export class AuthService implements IAuthService {
constructor(
@InjectRepository(User)
private readonly userRepository: Repository<User>,
@InjectRepository(UserToken)
private readonly userTokenRepository: Repository<UserToken>,
) {}
public async findOneValidTokenOrFail(
id: string,
type: string,
context?: string,
): Promise<IToken> {
const where = {
id,
type,
expiresAt: MoreThan(new Date()),
};
if (type === TokenType.PasswordlessLoginToken) {
where.requestId = context?.request?.cookies[CookieName.PasswordlessLoginToken];
}
return await this.userTokenRepository.findOneOrFail({ where });
}
public async findOneValidUserOrFail(username: string): Promise<IUser> {
const user = await this.userRepository.findOneOrFail({
where: {
email: username,
},
});
if (!user.isActive()) {
throw new UnauthorizedException();
}
return user;
}
public async findOneUserOrFail(id: string): Promise<IUser> {
return await this.userRepository.findOneOrFail({
where: {
id: +id,
},
});
}
public async removeTokenOrFail(id: string): Promise<void> {
const token = await this.userTokenRepository.findOneOrFail({
where: {
id,
},
});
await this.userTokenRepository.remove(token);
}
public async saveTokenOrFail(token: IToken): Promise<void> {
await this.userTokenRepository.save({
user: {
id: +token.getUserId(),
},
id: token.getId(),
requestId: token.getRequestId(),
type: token.getType(),
expiresAt: token.getExpiresAt(),
});
if (token.getType() === TokenType.Passwordless) {
}
}
}
Usage
All routes will be set to private by default. Use the Auth
decorator provided
by this module to change the AuthType
of specific routes.
For example:
import { Auth, AuthType } from '@fastnloud/nest-iam';
import { Controller } from '@nestjs/common';
@Controller('/public')
@Auth(AuthType.None)
export class MyController {}
Alternatively, you can use the publicRoutes
configuration option to set
specific routes to be public.
Roles (if implemented) can be applied similarly by using the Roles
decorator:
import { Roles } from '@fastnloud/nest-iam';
import { Controller } from '@nestjs/common';
@Controller('/admin')
@Roles('admin', 'superAdmin')
export class MyController {}
Basic authentication
To login, logout or to refresh tokens you can use these endpoints:
fetch(
new Request('http://localhost:3000/auth/login', {
method: 'POST',
body: JSON.stringify({ username, password }),
headers: new Headers({ 'Content-Type': 'application/json' }),
credentials: 'include',
mode: 'cors',
}),
);
fetch(
new Request('http://localhost:3000/auth/logout', {
method: 'GET',
credentials: 'include',
mode: 'cors',
}),
);
fetch(
new Request('http://localhost:3000/auth/refresh_tokens', {
method: 'GET',
credentials: 'include',
mode: 'cors',
}),
);
The login
and refresh_tokens
endpoints will return an access token (JWT) in
the response. The logout endpoint will return a 204 (No Content) given that no
errors are thrown.
A decoded JWT payload may look something like this:
{
"sub": "1",
"username": "john.doe@example.com",
"roles": ["guest"],
"iat": 1681660389,
"exp": 1681663989,
"aud": "localhost",
"iss": "localhost"
}
Additionally an activeUser
cookie will be set containing user information that
can be used on the client side.
Decoded cookie payload:
{
"id": "1",
"username": "john.doe@example.com",
"roles": ["guest"]
}
Run npx hash-password
to hash passwords via the CLI. This can be useful when
you need to insert users into the database directly.
This module also provides a bycrypt hasher that can be used to hash passwords
programmatically.
Passwordless authentication
In contrast to basic authentication, passwordless authentication does not
require a password. Instead, a token is sent to the user that can be used to
login. This token can be sent via email for example. This logic you must
implement yourself when the token is saved. This allows you full control over
how the token is sent to the user.
To request a token, use this endpoint:
fetch(
new Request('http://localhost:3000/auth/passwordless_login', {
method: 'POST',
body: JSON.stringify({ username }),
headers: new Headers({ 'Content-Type': 'application/json' }),
credentials: 'include',
mode: 'cors',
}),
);
Note that a cookie will be set that contains the requestId
of the token that
was requested. This cookie is used to ensure the token can only be used by the
same client that requested it. This limits the chance of the token being
intercepted and used by someone else.
To login with the requested token, use this endpoint:
fetch(
new Request('http://localhost:3000/auth/passwordless_login/:token', {
method: 'GET',
credentials: 'include',
mode: 'cors',
}),
);
License
nest-iam is MIT licensed.