guide authentication domain - wwwsolutions/nestjs-starter-kit GitHub Wiki
Example from Prisma-GraphQL integration
Each library has a single responsibility. Authentication is utilized via JWT token.
npm install @nestjs/passport passport passport-jwt passport-local
npm i -D @types/passport-jwt @types/passport-local
-
libs/api/domain/authentication
DOMAIN-
data-access
DATA LAYER -
feature
API LAYER-
src
-
lib
-
decorators
-
guards
-
resolvers
-
strategies
- api-domain-authentication-feature.module.ts
-
- index.ts
-
-
-
# apps/api/.env.development
##### JWT CONFIGURATION ################################################################################
### REQUIRED
JWT_SECRET='My$deepest$Secret#123456789'
### OPTIONAL
# OVERRIDE DEFAULT VALUES FROM `libs/api/config/features/src/lib/validation.schema.ts`
# JWT_SIGN_OPTIONS_EXPIRES_IN=3600
# libs/api/config/features/src/lib/validation.schema.ts
/* --------------------------------------------------------------
JWT
api/config/features/src/lib/configs/jwt.configuration.ts
--------------------------------------------------------------- */
// REQUIRED
JWT_SECRET: joiPassword
.string()
.required()
.minOfSpecialCharacters(2)
.minOfLowercase(2)
.minOfUppercase(2)
.minOfNumeric(2)
.noWhiteSpaces()
.description('JWT secret'),
// OPTIONAL
JWT_SIGN_OPTIONS_EXPIRES_IN: Joi.number()
.positive()
.default(3600)
.description('JWT sign options expires'),
// libs/api/config/features/src/lib/configs/jwt.configuration.ts
import { Inject } from '@nestjs/common';
import { ConfigType, registerAs } from '@nestjs/config';
import { JwtModuleOptions } from '@nestjs/jwt';
export const jwtConfiguration = registerAs('jwt', () => ({
secret: process.env.JWT_SECRET,
expiresIn: Number(process.env.JWT_SIGN_OPTIONS_EXPIRES_IN),
get options(): JwtModuleOptions {
return {
secret: process.env.JWT_SECRET,
signOptions: {
expiresIn: Number(process.env.JWT_SIGN_OPTIONS_EXPIRES_IN),
},
};
},
}));
export type JwtConfiguration = ConfigType<typeof jwtConfiguration>;
export const InjectJwtConfig = () => Inject(jwtConfiguration.KEY);
// libs/api/config/features/src/lib/registrations/business-logic-configs.registration.ts
import { jwtConfiguration } from '../configs/jwt.configuration';
export const businessLogicConfigs: any = [jwtConfiguration];
// libs/api/config/features/src/index.ts
export * from './lib/configs/jwt.configuration';
This is base domain library which holds:
- decorators
- guards
- resolvers
- strategies
nx generate @nrwl/nest:library --name=feature --directory=api/domain/authentication --buildable --standaloneConfig --strict --tags=type:feature,scope:api --no-interactive
// libs/api/domain/authentication/feature/src/lib/api-domain-authentication-feature.module.ts
import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import {
JwtConfiguration,
jwtConfiguration,
} from '@wwwsolutions/api/config/features';
import { ApiDataAccessCoreModule } from '@wwwsolutions/api/data-access/core';
import { AuthenticationService } from '@wwwsolutions/api/domain/authentication/data-access';
import { UsersService } from '@wwwsolutions/api/domain/users/data-access';
import { AuthResolver } from './resolvers/auth.resolver';
import { JwtStrategy } from './strategies/jwt.strategy';
import { GqlAuthGuard } from './guards/gql-auth.guard';
@Module({
imports: [
ApiDataAccessCoreModule,
JwtModule.registerAsync({
useFactory: async (jwtConfiguration: JwtConfiguration) => ({
// api/config/features/src/lib/configs/jwt.configuration.ts
...jwtConfiguration.options,
}),
inject: [jwtConfiguration.KEY],
}),
],
providers: [
AuthenticationService,
UsersService,
JwtStrategy,
AuthResolver,
GqlAuthGuard,
],
})
export class ApiDomainAuthenticationFeatureModule {}
// libs/api/domain/authentication/feature/src/lib/decorators/ctx-user.decorator.ts
import { createParamDecorator } from '@nestjs/common';
import { GqlExecutionContext } from '@nestjs/graphql';
export const CtxUser = createParamDecorator(
(data, ctx) => GqlExecutionContext.create(ctx).getContext().req.user
);
// libs/api/domain/authentication/feature/src/lib/guards/gql-auth.guard.ts
import { ExecutionContext, Injectable } from '@nestjs/common';
import { GqlExecutionContext } from '@nestjs/graphql';
import { AuthGuard } from '@nestjs/passport';
@Injectable()
export class GqlAuthGuard extends AuthGuard('jwt') {
getRequest(context: ExecutionContext) {
const ctx = GqlExecutionContext.create(context);
return ctx.getContext().req;
}
}
// libs/api/domain/authentication/feature/src/lib/resolvers/auth.resolver.ts
import { Args, Mutation, Resolver } from '@nestjs/graphql';
import { AuthenticationService } from '@wwwsolutions/api/domain/authentication/data-access';
import {
UserToken,
UserLoginInput,
UserRegisterInput,
} from '@wwwsolutions/api/domain/users/data-access';
@Resolver()
export class AuthResolver {
constructor(private readonly auth: AuthenticationService) {}
// [PUBLIC API]
// LOGIN USER
@Mutation(() => UserToken)
async login(
@Args({ name: 'userLoginInput', type: () => UserLoginInput })
userLoginInput: UserLoginInput
): Promise<UserToken> {
return this.auth.login(userLoginInput);
}
// REGISTER USER
@Mutation(() => UserToken)
async register(
@Args({ name: 'userRegisterInput', type: () => UserRegisterInput })
userRegisterInput: UserRegisterInput
): Promise<UserToken> {
return this.auth.register(userRegisterInput);
}
}
// libs/api/domain/authentication/feature/src/lib/strategies/jwt.strategy.ts
import { UnauthorizedException, Injectable, Logger } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';
import {
AuthenticationService,
Jwt,
} from '@wwwsolutions/api/domain/authentication/data-access';
import {
InjectJwtConfig,
JwtConfiguration,
} from '@wwwsolutions/api/config/features';
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
private readonly logger = new Logger(JwtStrategy.name);
constructor(
@InjectJwtConfig()
readonly jwtConfiguration: JwtConfiguration,
private readonly auth: AuthenticationService
) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
// api/config/features/src/lib/configs/jwt.configuration.ts
secretOrKey: jwtConfiguration.secret,
});
}
async validate(payload: Jwt) {
// this.logger.log(payload);
const validated = await this.auth.validateUser(payload.userId);
if (!validated) {
throw new UnauthorizedException();
}
// this.logger.log(validated);
return validated;
}
}
// libs/api/domain/authentication/feature/src/lib/strategies/local.strategy.ts
import { UnauthorizedException, Injectable, Logger } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { Strategy } from 'passport-local';
import { AuthenticationService } from '@wwwsolutions/api/domain/authentication/data-access';
import { User } from '@prisma/client';
@Injectable()
export class LocalStrategy extends PassportStrategy(Strategy) {
private readonly logger = new Logger(LocalStrategy.name);
constructor(private readonly auth: AuthenticationService) {
super({
usernameField: 'email',
});
}
async validate(email: string, password: string): Promise<User> {
const validated = await this.auth.validateUserByEmailAndPassword(
email,
password
);
if (!validated) {
throw new UnauthorizedException();
}
this.logger.log(validated);
return validated;
}
}
// libs/api/domain/authentication/feature/src/index.ts
export * from './lib/api-domain-authentication-feature.module';
export * from './lib/decorators/ctx-user.decorator';
export * from './lib/guards/gql-auth.guard';
Contains domain's data layer and exposes:
- ApiGraphqlAuthenticationDataAccessModule
- AuthenticationService
- models
nx generate @nrwl/nest:library --name=data-access --directory=api/domain/authentication --buildable --standaloneConfig --strict --tags=type:data-access,scope:api --no-interactive
// libs/api/domain/authentication/data-access/src/lib/api-domain-authentication-data-access.module.ts
import { Module } from '@nestjs/common';
import { ApiDataAccessCoreModule } from '@wwwsolutions/api/data-access/core';
import { UsersService } from '@wwwsolutions/api/domain/users/data-access';
import { AuthenticationService } from './authentication.service';
@Module({
imports: [ApiDataAccessCoreModule],
providers: [AuthenticationService, UsersService],
exports: [AuthenticationService],
})
export class ApiDomainAuthenticationDataAccessModule {}
// libs/api/domain/authentication/data-access/src/lib/authentication.service.ts
import {
BadRequestException,
Injectable,
Logger,
NotFoundException,
} from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { PrismaDataService } from '@wwwsolutions/api/data-access/core';
import { AuthenticationUtils } from '@wwwsolutions/shared/utils';
import { User } from '@wwwsolutions/api/data-access/models';
import {
UserToken,
UserLoginInput,
UserRegisterInput,
} from '@wwwsolutions/api/domain/users/data-access';
import { UsersService } from '@wwwsolutions/api/domain/users/data-access';
import { Jwt } from './models/jwt.model';
@Injectable()
export class AuthenticationService {
private readonly logger = new Logger(AuthenticationService.name);
constructor(
private readonly data: PrismaDataService,
private readonly usersService: UsersService,
private readonly jwtService: JwtService
) {}
// LOGIN USER
async login({ email, password }: UserLoginInput): Promise<UserToken> {
// find user
const found = await this.data.findUserByEmail(email);
if (!found) {
throw new NotFoundException(`User with email ${email} does not exist`);
}
// validate password
const isPasswordValid = await AuthenticationUtils.validate(
password,
found.password
);
if (!isPasswordValid) {
throw new Error(`Invalid password`);
}
// sign token
const signedToken = this.signToken(found.id);
// compose return object
const userToken = { user: found, token: signedToken };
return userToken;
}
// REGISTER USER
async register({ email, password }: UserRegisterInput): Promise<UserToken> {
// make sure email is unique, check database
const found = await this.data.findUserByEmail(email);
if (found) {
throw new BadRequestException(`Cannot register with email ${email}`);
}
// hash password
const hashedPassword = await AuthenticationUtils.hash(password);
// create and return new user with hashed pwd
const created = await this.data.createUser({
data: { email, password: hashedPassword },
});
// sign token
const signedToken = this.signToken(created.id);
// compose return object
const userToken = { user: created, token: signedToken };
return userToken;
}
// [HELPER METHODS]
// VALIDATE USER
async validateUser(userId: number): Promise<User | null> {
// TODO: check if user credentials has expired
const validated = await this.usersService.user({ where: { id: userId } });
return validated;
}
async validateUserByEmailAndPassword(
email: string,
password: string
): Promise<User | null> {
throw new Error('Method not implemented.');
}
// SIGN TOKEN
signToken(userId: number): string {
const payload: Jwt = {
userId,
// TODO: extend token with metadata. example: role or isAdmin
};
const signed = this.jwtService.sign(payload);
return signed;
}
}
// libs/api/domain/authentication/data-access/src/lib/models/jwt.model.ts
export class Jwt {
userId!: number;
}
// libs/api/domain/authentication/data-access/src/index.ts
export * from './lib/api-domain-authentication-data-access.module';
export * from './lib/authentication.service';
export * from './lib/models/jwt.model;
Add feature module name to the domains array.
// libs/api/core/src/lib/registrations/domains.registration.ts
import { ApiDomainAuthenticationFeatureModule } from '@wwwsolutions/api/domain/authentication/feature';
export const domains: NestImportsType = [ApiDomainAuthenticationFeatureModule];
Utilize guards.
// libs/api/domain/users/feature/src/lib/users.resolver.ts
...
import { UseGuards } from '@nestjs/common';
import { GqlAuthGuard } from '@wwwsolutions/api/domain/authentication/feature';
...
...
// [PROTECTED/RESTRICTED API]
@UseGuards(GqlAuthGuard) // implement guard on class level | implement guard on method level
@Resolver()
...