Compare commits
22 Commits
Author | SHA1 | Date |
---|---|---|
|
3cc5097294 | |
|
a7cb98df56 | |
|
54006b9e2a | |
|
d3c14678e5 | |
|
ef1dbc3774 | |
|
6caa3d83cf | |
|
ee2c5c35c8 | |
|
754cc002c4 | |
|
11541e5935 | |
|
3f0bef812e | |
|
1026ca7eb9 | |
|
c0eb5dbaaf | |
|
08531919fa | |
|
7c3b852a04 | |
|
af7648ff5a | |
|
ca1cd171cc | |
|
de6e78b8b5 | |
|
d141a0ff83 | |
|
918ceae7a4 | |
|
9425bd1518 | |
|
4c7fe3b794 | |
|
f470987dc5 |
|
@ -7,10 +7,15 @@ HEALTH_SERVICE_PORT=6002
|
||||||
DATABASE_URL="postgresql://mobicoop:mobicoop@v3-db:5432/mobicoop?schema=auth"
|
DATABASE_URL="postgresql://mobicoop:mobicoop@v3-db:5432/mobicoop?schema=auth"
|
||||||
|
|
||||||
# MESSAGE BROKER
|
# MESSAGE BROKER
|
||||||
MESSAGE_BROKER_URI=amqp://v3-broker:5672
|
MESSAGE_BROKER_URI=amqp://mobicoop:mobicoop@v3-broker:5672
|
||||||
MESSAGE_BROKER_EXCHANGE=mobicoop
|
MESSAGE_BROKER_EXCHANGE=mobicoop
|
||||||
MESSAGE_BROKER_EXCHANGE_DURABILITY=true
|
MESSAGE_BROKER_EXCHANGE_DURABILITY=true
|
||||||
|
|
||||||
|
# REDIS
|
||||||
|
REDIS_HOST=v3-redis
|
||||||
|
REDIS_PASSWORD=redis
|
||||||
|
REDIS_PORT=6379
|
||||||
|
|
||||||
# OPA
|
# OPA
|
||||||
OPA_IMAGE=openpolicyagent/opa:0.57.0
|
OPA_IMAGE=docker.io/openpolicyagent/opa:0.58.0
|
||||||
OPA_URL=http://v3-auth-opa:8181/v1/data/
|
OPA_URL=http://v3-auth-opa:8181/v1/data/
|
||||||
|
|
|
@ -6,9 +6,9 @@ SERVICE_PORT=5002
|
||||||
DATABASE_URL="postgresql://mobicoop:mobicoop@localhost:5432/mobicoop-test?schema=auth"
|
DATABASE_URL="postgresql://mobicoop:mobicoop@localhost:5432/mobicoop-test?schema=auth"
|
||||||
|
|
||||||
# MESSAGE BROKER
|
# MESSAGE BROKER
|
||||||
MESSAGE_BROKER_URI=amqp://v3-broker:5672
|
MESSAGE_BROKER_URI=amqp://mobicoop:mobicoop@v3-broker:5672
|
||||||
MESSAGE_BROKER_EXCHANGE=mobicoop
|
MESSAGE_BROKER_EXCHANGE=mobicoop
|
||||||
|
|
||||||
# OPA
|
# OPA
|
||||||
OPA_IMAGE=openpolicyagent/opa:0.54.0
|
OPA_IMAGE=docker.io/openpolicyagent/opa:0.54.0
|
||||||
OPA_URL=http://v3-auth-opa:8181/v1/data/
|
OPA_URL=http://v3-auth-opa:8181/v1/data/
|
||||||
|
|
|
@ -4,6 +4,10 @@ stages:
|
||||||
- test
|
- test
|
||||||
- build
|
- build
|
||||||
|
|
||||||
|
include:
|
||||||
|
- template: Security/SAST.gitlab-ci.yml
|
||||||
|
- template: Security/Secret-Detection.gitlab-ci.yml
|
||||||
|
|
||||||
##############
|
##############
|
||||||
# TEST STAGE #
|
# TEST STAGE #
|
||||||
##############
|
##############
|
||||||
|
|
|
@ -0,0 +1,55 @@
|
||||||
|
_Replace italic text by your own description_
|
||||||
|
|
||||||
|
## Feature Merge Request
|
||||||
|
|
||||||
|
### Why this Merge Request
|
||||||
|
|
||||||
|
_This merge request addresses, and describe the problem or user story being addressed._
|
||||||
|
|
||||||
|
### What is implemented, what is the chosen solution
|
||||||
|
|
||||||
|
_Explain the fix or solution implemented. Which other solution have been envisaged._
|
||||||
|
|
||||||
|
### Related issues and impact on other project in codebase
|
||||||
|
|
||||||
|
_Provide links to the related issues, feature requests and merge request (from Gitlab and Redmine)._
|
||||||
|
|
||||||
|
_And Link to other project Impacted._
|
||||||
|
|
||||||
|
### Other Information
|
||||||
|
|
||||||
|
_Include any extra information or considerations for reviewers._
|
||||||
|
|
||||||
|
## Checklists
|
||||||
|
|
||||||
|
### Merge Request
|
||||||
|
|
||||||
|
- [ ] Target branch identified.
|
||||||
|
- [ ] Code based on last version of target branch.
|
||||||
|
- [ ] Description filled.
|
||||||
|
- [ ] Impact on other project codebase identified.
|
||||||
|
- [ ] Documentation reflects the changes made.
|
||||||
|
- [ ] Test run in gitlab pipeline and locally.
|
||||||
|
- [ ] One or more reviewer is defined
|
||||||
|
|
||||||
|
### Code Review
|
||||||
|
|
||||||
|
- [ ] Code follows project coding guidelines.
|
||||||
|
- [ ] Code follows project designed architecture.
|
||||||
|
- [ ] Code is easily readable.
|
||||||
|
- [ ] Everything new have an explicit and pertinent name (variable, method, file ...)
|
||||||
|
- [ ] No redundant/duplicate code (unless explain by architecture choice)
|
||||||
|
- [ ] Commit are all related to MR and well written (Atomic commit).
|
||||||
|
- [ ] New code is tested and covered by automated test.
|
||||||
|
- [ ] No useless logging or debugging code.
|
||||||
|
- [ ] No code can be replaced by library or framework code.
|
||||||
|
|
||||||
|
### TODO before merge
|
||||||
|
|
||||||
|
- [ ] _add any task here_
|
||||||
|
- [ ] ...
|
||||||
|
|
||||||
|
### TODO after merge
|
||||||
|
|
||||||
|
- [ ] _add any task here_
|
||||||
|
- [ ] ...
|
|
@ -0,0 +1,62 @@
|
||||||
|
_Replace italic text by your own description_
|
||||||
|
|
||||||
|
## Release Merge Request
|
||||||
|
|
||||||
|
### Why this Merge Request
|
||||||
|
|
||||||
|
_This merge request addresses, and describe the problem or user story being addressed._
|
||||||
|
|
||||||
|
### What is implemented, what is the chosen solution
|
||||||
|
|
||||||
|
_Explain the fix or solution implemented. Which other solution have been envisaged._
|
||||||
|
|
||||||
|
### Related issues and impact on other project in codebase
|
||||||
|
|
||||||
|
_Provide links to the related issues, feature requests and merge request (from Gitlab and Redmine)._
|
||||||
|
|
||||||
|
_And Link to other project Impacted._
|
||||||
|
|
||||||
|
### Other Information
|
||||||
|
|
||||||
|
_Include any extra information or considerations for reviewers._
|
||||||
|
|
||||||
|
## Checklists
|
||||||
|
|
||||||
|
### Merge Request
|
||||||
|
|
||||||
|
- [ ] Target branch identified.
|
||||||
|
- [ ] Code based on last version of target branch.
|
||||||
|
- [ ] Description filled.
|
||||||
|
- [ ] Impact on other project codebase identified.
|
||||||
|
- [ ] Documentation reflects the changes made.
|
||||||
|
- [ ] Test run in gitlab pipeline and locally.
|
||||||
|
- [ ] One or more reviewer is defined
|
||||||
|
|
||||||
|
### Code Review
|
||||||
|
|
||||||
|
- [ ] Code follows project coding guidelines.
|
||||||
|
- [ ] Code follows project designed architecture.
|
||||||
|
- [ ] Code is easily readable.
|
||||||
|
- [ ] Everything new have an explicit and pertinent name (variable, method, file ...)
|
||||||
|
- [ ] No redundant/duplicate code (unless explain by architecture choice)
|
||||||
|
- [ ] Commit are all related to MR and well written (Atomic commit).
|
||||||
|
- [ ] New code is tested and covered by automated test.
|
||||||
|
- [ ] No useless logging or debugging code.
|
||||||
|
- [ ] No code can be replaced by library or framework code.
|
||||||
|
|
||||||
|
### Change Management
|
||||||
|
|
||||||
|
- [ ] Release is planned
|
||||||
|
- [ ] Merge Request to be included are identified
|
||||||
|
- [ ] Concerned Team are aware of the change
|
||||||
|
- [ ] No other change on the same day (if possible)
|
||||||
|
|
||||||
|
### TODO before merge
|
||||||
|
|
||||||
|
- [ ] _add any task here_
|
||||||
|
- [ ] ...
|
||||||
|
|
||||||
|
### TODO after merge
|
||||||
|
|
||||||
|
- [ ] _add any task here_
|
||||||
|
- [ ] ...
|
|
@ -0,0 +1,37 @@
|
||||||
|
_Replace italic text by your own description_
|
||||||
|
|
||||||
|
## Small Fix Merge Request
|
||||||
|
|
||||||
|
### Why this Merge Request
|
||||||
|
|
||||||
|
_This merge request addresses, and describe the problem or user story being addressed._
|
||||||
|
|
||||||
|
### What is implemented, what is the chosen solution
|
||||||
|
|
||||||
|
_Explain the fix or solution implemented. Which other solution have been envisaged._
|
||||||
|
|
||||||
|
### Related issues and impact on other project in codebase
|
||||||
|
|
||||||
|
_Provide links to the related issues, feature requests and merge request (from Gitlab and Redmine)._
|
||||||
|
|
||||||
|
_And Link to other project Impacted._
|
||||||
|
|
||||||
|
### Other Information
|
||||||
|
|
||||||
|
_Include any extra information or considerations for reviewers._
|
||||||
|
|
||||||
|
## Checklists
|
||||||
|
|
||||||
|
### Merge Request
|
||||||
|
|
||||||
|
- [ ] Target branch identified.
|
||||||
|
- [ ] Code based on last version of target branch.
|
||||||
|
- [ ] Description filled.
|
||||||
|
- [ ] Impact on other project codebase identified.
|
||||||
|
- [ ] Test run in gitlab pipeline and locally.
|
||||||
|
|
||||||
|
### Code Review
|
||||||
|
|
||||||
|
- [ ] Code is easily readable.
|
||||||
|
- [ ] Commit are all related to MR and well written (Atomic commit).
|
||||||
|
- [ ] No useless logging or debugging code.
|
|
@ -4,3 +4,4 @@ node_modules
|
||||||
dist
|
dist
|
||||||
coverage
|
coverage
|
||||||
.prettierrc.json
|
.prettierrc.json
|
||||||
|
.gitlab
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
|
ARG NODE_VERSION=20.9.0
|
||||||
|
|
||||||
###################
|
###################
|
||||||
# BUILD FOR LOCAL DEVELOPMENT
|
# BUILD FOR LOCAL DEVELOPMENT
|
||||||
###################
|
###################
|
||||||
|
|
||||||
FROM node:18-alpine3.16 As development
|
FROM docker.io/node:${NODE_VERSION} As development
|
||||||
|
|
||||||
# Create app directory
|
# Create app directory
|
||||||
WORKDIR /usr/src/app
|
WORKDIR /usr/src/app
|
||||||
|
@ -29,7 +31,7 @@ USER node
|
||||||
# BUILD FOR PRODUCTION
|
# BUILD FOR PRODUCTION
|
||||||
###################
|
###################
|
||||||
|
|
||||||
FROM node:18-alpine3.16 As build
|
FROM docker.io/node:${NODE_VERSION} As build
|
||||||
|
|
||||||
WORKDIR /usr/src/app
|
WORKDIR /usr/src/app
|
||||||
|
|
||||||
|
@ -66,7 +68,7 @@ USER node
|
||||||
# PRODUCTION
|
# PRODUCTION
|
||||||
###################
|
###################
|
||||||
|
|
||||||
FROM node:18-alpine3.16 As production
|
FROM docker.io/node:${NODE_VERSION} As production
|
||||||
|
|
||||||
# Copy package.json to be able to execute migration command
|
# Copy package.json to be able to execute migration command
|
||||||
COPY --chown=node:node package*.json ./
|
COPY --chown=node:node package*.json ./
|
||||||
|
|
|
@ -5,11 +5,8 @@ SERVICE_PORT=5002
|
||||||
# PRISMA
|
# PRISMA
|
||||||
DATABASE_URL="postgresql://mobicoop:mobicoop@v3-db:5432/mobicoop?schema=public"
|
DATABASE_URL="postgresql://mobicoop:mobicoop@v3-db:5432/mobicoop?schema=public"
|
||||||
|
|
||||||
# RABBIT MQ
|
|
||||||
RMQ_URI=amqp://v3-auth-broker:5672
|
|
||||||
|
|
||||||
# MESSAGE BROKER
|
# MESSAGE BROKER
|
||||||
BROKER_IMAGE=rabbitmq:3-alpine
|
MESSAGE_BROKER_IMAGE=docker.io/rabbitmq:3-alpine
|
||||||
|
|
||||||
# POSTGRES
|
# POSTGRES
|
||||||
POSTGRES_IMAGE=postgres:15.0
|
POSTGRES_IMAGE=docker.io/postgres:15.0
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
|
ARG NODE_VERSION=20.9.0
|
||||||
|
|
||||||
###################
|
###################
|
||||||
# BUILD FOR CI TESTING
|
# BUILD FOR CI TESTING
|
||||||
###################
|
###################
|
||||||
|
|
||||||
FROM node:18-alpine3.16
|
FROM docker.io/node:${NODE_VERSION}
|
||||||
|
|
||||||
# Create app directory
|
# Create app directory
|
||||||
WORKDIR /usr/src/app
|
WORKDIR /usr/src/app
|
||||||
|
|
|
@ -15,7 +15,7 @@ services:
|
||||||
|
|
||||||
broker:
|
broker:
|
||||||
container_name: v3-broker
|
container_name: v3-broker
|
||||||
image: ${BROKER_IMAGE}
|
image: ${MESSAGE_BROKER_IMAGE}
|
||||||
ports:
|
ports:
|
||||||
- 5672:5672
|
- 5672:5672
|
||||||
networks:
|
networks:
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
77
package.json
77
package.json
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@mobicoop/auth",
|
"name": "@mobicoop/auth",
|
||||||
"version": "0.6.5",
|
"version": "0.8.2",
|
||||||
"description": "Mobicoop V3 Auth Service",
|
"description": "Mobicoop V3 Auth Service",
|
||||||
"author": "sbriat",
|
"author": "sbriat",
|
||||||
"private": true,
|
"private": true,
|
||||||
|
@ -24,62 +24,65 @@
|
||||||
"test:integration:ci": "npm run migrate:test:ci && dotenv -e ci/.env.ci -- jest --testPathPattern 'tests/integration/' --runInBand",
|
"test:integration:ci": "npm run migrate:test:ci && dotenv -e ci/.env.ci -- jest --testPathPattern 'tests/integration/' --runInBand",
|
||||||
"test:cov": "jest --testPathPattern 'tests/unit/' --coverage",
|
"test:cov": "jest --testPathPattern 'tests/unit/' --coverage",
|
||||||
"test:e2e": "jest --config ./test/jest-e2e.json",
|
"test:e2e": "jest --config ./test/jest-e2e.json",
|
||||||
"migrate": "docker exec v3-auth-api sh -c 'npx prisma migrate dev'",
|
"repl": "docker exec -it v3-auth-api npm run start -- --entryFile repl",
|
||||||
|
"migrate": "docker exec v3-auth-api sh -c 'npx prisma migrate deploy'",
|
||||||
|
"migrate:dev": "docker exec v3-auth-api sh -c 'npx prisma migrate dev'",
|
||||||
"migrate:test": "dotenv -e .env.test -- npx prisma migrate deploy",
|
"migrate:test": "dotenv -e .env.test -- npx prisma migrate deploy",
|
||||||
"migrate:test:ci": "dotenv -e ci/.env.ci -- npx prisma migrate deploy",
|
"migrate:test:ci": "dotenv -e ci/.env.ci -- npx prisma migrate deploy",
|
||||||
"migrate:deploy": "npx prisma migrate deploy"
|
"migrate:deploy": "npx prisma migrate deploy"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@golevelup/nestjs-rabbitmq": "^4.0.0",
|
"@golevelup/nestjs-rabbitmq": "^4.1.0",
|
||||||
"@grpc/grpc-js": "^1.9.9",
|
"@grpc/grpc-js": "^1.9.13",
|
||||||
"@grpc/proto-loader": "^0.7.10",
|
"@grpc/proto-loader": "^0.7.10",
|
||||||
"@mobicoop/ddd-library": "^2.1.1",
|
"@mobicoop/configuration-module": "^8.0.0",
|
||||||
"@mobicoop/health-module": "^2.3.1",
|
"@mobicoop/ddd-library": "^2.4.3",
|
||||||
"@mobicoop/message-broker-module": "^2.1.1",
|
"@mobicoop/health-module": "^2.3.2",
|
||||||
|
"@mobicoop/message-broker-module": "^2.1.2",
|
||||||
"@nestjs/axios": "^3.0.1",
|
"@nestjs/axios": "^3.0.1",
|
||||||
"@nestjs/common": "^10.2.8",
|
"@nestjs/common": "^10.3.0",
|
||||||
"@nestjs/config": "^3.1.1",
|
"@nestjs/config": "^3.1.1",
|
||||||
"@nestjs/core": "^10.2.8",
|
"@nestjs/core": "^10.3.0",
|
||||||
"@nestjs/cqrs": "^10.2.6",
|
"@nestjs/cqrs": "^10.2.6",
|
||||||
"@nestjs/event-emitter": "^2.0.2",
|
"@nestjs/event-emitter": "^2.0.3",
|
||||||
"@nestjs/microservices": "^10.2.8",
|
"@nestjs/microservices": "^10.3.0",
|
||||||
"@nestjs/platform-express": "^10.2.8",
|
"@nestjs/platform-express": "^10.3.0",
|
||||||
"@nestjs/terminus": "^10.1.1",
|
"@nestjs/terminus": "^10.2.0",
|
||||||
"@prisma/client": "^5.5.2",
|
"@prisma/client": "^5.8.1",
|
||||||
"axios": "^1.6.0",
|
"argon2": "^0.31.2",
|
||||||
|
"axios": "^1.6.5",
|
||||||
"bcrypt": "5.1.0",
|
"bcrypt": "5.1.0",
|
||||||
"class-transformer": "^0.5.1",
|
"class-transformer": "^0.5.1",
|
||||||
"class-validator": "^0.14.0",
|
"class-validator": "^0.14.1",
|
||||||
"reflect-metadata": "^0.1.13",
|
|
||||||
"rimraf": "^5.0.5",
|
"rimraf": "^5.0.5",
|
||||||
"rxjs": "^7.8.1"
|
"rxjs": "^7.8.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@nestjs/cli": "^10.2.1",
|
"@nestjs/cli": "^10.3.0",
|
||||||
"@nestjs/schematics": "^10.0.3",
|
"@nestjs/schematics": "^10.1.0",
|
||||||
"@nestjs/testing": "^10.2.8",
|
"@nestjs/testing": "^10.3.0",
|
||||||
"@types/bcrypt": "^5.0.1",
|
"@types/bcrypt": "^5.0.2",
|
||||||
"@types/express": "^4.17.20",
|
"@types/express": "^4.17.21",
|
||||||
"@types/jest": "29.5.7",
|
"@types/jest": "29.5.11",
|
||||||
"@types/node": "^20.8.10",
|
"@types/node": "^20.11.4",
|
||||||
"@types/supertest": "^2.0.15",
|
"@types/supertest": "^6.0.2",
|
||||||
"@types/uuid": "^9.0.6",
|
"@types/uuid": "^9.0.7",
|
||||||
"@typescript-eslint/eslint-plugin": "^6.9.1",
|
"@typescript-eslint/eslint-plugin": "^6.19.0",
|
||||||
"@typescript-eslint/parser": "^6.9.1",
|
"@typescript-eslint/parser": "^6.19.0",
|
||||||
"dotenv-cli": "^7.3.0",
|
"dotenv-cli": "^7.3.0",
|
||||||
"eslint": "^8.52.0",
|
"eslint": "^8.56.0",
|
||||||
"eslint-config-prettier": "^9.0.0",
|
"eslint-config-prettier": "^9.1.0",
|
||||||
"eslint-plugin-prettier": "^5.0.1",
|
"eslint-plugin-prettier": "^5.1.3",
|
||||||
"jest": "29.7.0",
|
"jest": "29.7.0",
|
||||||
"prettier": "^3.0.3",
|
"prettier": "^3.2.2",
|
||||||
"prisma": "^5.5.2",
|
"prisma": "^5.8.1",
|
||||||
"source-map-support": "^0.5.21",
|
"source-map-support": "^0.5.21",
|
||||||
"supertest": "^6.3.3",
|
"supertest": "^6.3.4",
|
||||||
"ts-jest": "29.1.1",
|
"ts-jest": "29.1.1",
|
||||||
"ts-loader": "^9.5.0",
|
"ts-loader": "^9.5.1",
|
||||||
"ts-node": "^10.9.1",
|
"ts-node": "^10.9.2",
|
||||||
"tsconfig-paths": "4.2.0",
|
"tsconfig-paths": "4.2.0",
|
||||||
"typescript": "^5.2.2",
|
"typescript": "^5.3.3",
|
||||||
"uuid": "^9.0.1"
|
"uuid": "^9.0.1"
|
||||||
},
|
},
|
||||||
"jest": {
|
"jest": {
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
|
|
||||||
generator client {
|
generator client {
|
||||||
provider = "prisma-client-js"
|
provider = "prisma-client-js"
|
||||||
binaryTargets = ["linux-musl", "debian-openssl-3.0.x"]
|
binaryTargets = ["linux-musl", "debian-openssl-3.0.x", "linux-musl-openssl-3.0.x"]
|
||||||
}
|
}
|
||||||
|
|
||||||
datasource db {
|
datasource db {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { ConfigModule } from '@nestjs/config';
|
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||||
import { AuthenticationModule } from '@modules/authentication/authentication.module';
|
import { AuthenticationModule } from '@modules/authentication/authentication.module';
|
||||||
import { EventEmitterModule } from '@nestjs/event-emitter';
|
import { EventEmitterModule } from '@nestjs/event-emitter';
|
||||||
import {
|
import {
|
||||||
|
@ -21,6 +21,10 @@ import {
|
||||||
HEALTH_USERNAME_REPOSITORY,
|
HEALTH_USERNAME_REPOSITORY,
|
||||||
SERVICE_NAME,
|
SERVICE_NAME,
|
||||||
} from './app.constants';
|
} from './app.constants';
|
||||||
|
import {
|
||||||
|
ConfigurationModule,
|
||||||
|
ConfigurationModuleOptions,
|
||||||
|
} from '@mobicoop/configuration-module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
|
@ -53,6 +57,19 @@ import {
|
||||||
messagePublisher,
|
messagePublisher,
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
|
ConfigurationModule.forRootAsync({
|
||||||
|
imports: [ConfigModule],
|
||||||
|
inject: [ConfigService],
|
||||||
|
useFactory: async (
|
||||||
|
configService: ConfigService,
|
||||||
|
): Promise<ConfigurationModuleOptions> => {
|
||||||
|
return {
|
||||||
|
host: configService.get<string>('REDIS_HOST') as string,
|
||||||
|
port: configService.get<number>('REDIS_PORT') as number,
|
||||||
|
password: configService.get<string>('REDIS_PASSWORD'),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
}),
|
||||||
AuthenticationModule,
|
AuthenticationModule,
|
||||||
AuthorizationModule,
|
AuthorizationModule,
|
||||||
MessagerModule,
|
MessagerModule,
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { KeyType, Type } from '@mobicoop/configuration-module';
|
||||||
import { IsStrongPasswordOptions } from 'class-validator';
|
import { IsStrongPasswordOptions } from 'class-validator';
|
||||||
|
|
||||||
export const STRONG_PASSWORD_OPTIONS: IsStrongPasswordOptions = {
|
export const STRONG_PASSWORD_OPTIONS: IsStrongPasswordOptions = {
|
||||||
|
@ -7,3 +8,11 @@ export const STRONG_PASSWORD_OPTIONS: IsStrongPasswordOptions = {
|
||||||
minSymbols: 1,
|
minSymbols: 1,
|
||||||
minUppercase: 1,
|
minUppercase: 1,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const AUTH_CONFIG_ENCRYPTION_ALGORITHM = 'encryptionAlgorithm';
|
||||||
|
export const AuthKeyTypes: KeyType[] = [
|
||||||
|
{
|
||||||
|
key: AUTH_CONFIG_ENCRYPTION_ALGORITHM,
|
||||||
|
type: Type.STRING,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
|
@ -1,3 +1,7 @@
|
||||||
export const AUTH_MESSAGE_PUBLISHER = Symbol('AUTH_MESSAGE_PUBLISHER');
|
export const AUTH_MESSAGE_PUBLISHER = Symbol('AUTH_MESSAGE_PUBLISHER');
|
||||||
export const AUTHENTICATION_REPOSITORY = Symbol('AUTHENTICATION_REPOSITORY');
|
export const AUTHENTICATION_REPOSITORY = Symbol('AUTHENTICATION_REPOSITORY');
|
||||||
export const USERNAME_REPOSITORY = Symbol('USERNAME_REPOSITORY');
|
export const USERNAME_REPOSITORY = Symbol('USERNAME_REPOSITORY');
|
||||||
|
export const AUTHENTICATION_CONFIGURATION_REPOSITORY = Symbol(
|
||||||
|
'AUTHENTICATION_CONFIGURATION_REPOSITORY',
|
||||||
|
);
|
||||||
|
export const PASSWORD_VERIFIER = Symbol('PASSWORD_VERIFIER');
|
||||||
|
|
|
@ -4,7 +4,9 @@ import { CreateAuthenticationService } from './core/application/commands/create-
|
||||||
import { AuthenticationMapper } from './authentication.mapper';
|
import { AuthenticationMapper } from './authentication.mapper';
|
||||||
import {
|
import {
|
||||||
AUTH_MESSAGE_PUBLISHER,
|
AUTH_MESSAGE_PUBLISHER,
|
||||||
|
AUTHENTICATION_CONFIGURATION_REPOSITORY,
|
||||||
AUTHENTICATION_REPOSITORY,
|
AUTHENTICATION_REPOSITORY,
|
||||||
|
PASSWORD_VERIFIER,
|
||||||
USERNAME_REPOSITORY,
|
USERNAME_REPOSITORY,
|
||||||
} from './authentication.di-tokens';
|
} from './authentication.di-tokens';
|
||||||
import { AuthenticationRepository } from './infrastructure/authentication.repository';
|
import { AuthenticationRepository } from './infrastructure/authentication.repository';
|
||||||
|
@ -27,6 +29,8 @@ import { ValidateAuthenticationGrpcController } from './interface/grpc-controlle
|
||||||
import { ValidateAuthenticationQueryHandler } from './core/application/queries/validate-authentication/validate-authentication.query-handler';
|
import { ValidateAuthenticationQueryHandler } from './core/application/queries/validate-authentication/validate-authentication.query-handler';
|
||||||
import { UserUpdatedMessageHandler } from './interface/message-handlers/user-updated.message-handler';
|
import { UserUpdatedMessageHandler } from './interface/message-handlers/user-updated.message-handler';
|
||||||
import { UserDeletedMessageHandler } from './interface/message-handlers/user-deleted.message-handler';
|
import { UserDeletedMessageHandler } from './interface/message-handlers/user-deleted.message-handler';
|
||||||
|
import { ConfigurationRepository } from '@mobicoop/configuration-module';
|
||||||
|
import { PasswordVerifier } from './infrastructure/password-verifier';
|
||||||
|
|
||||||
const grpcControllers = [
|
const grpcControllers = [
|
||||||
CreateAuthenticationGrpcController,
|
CreateAuthenticationGrpcController,
|
||||||
|
@ -62,6 +66,10 @@ const repositories: Provider[] = [
|
||||||
provide: USERNAME_REPOSITORY,
|
provide: USERNAME_REPOSITORY,
|
||||||
useClass: UsernameRepository,
|
useClass: UsernameRepository,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
provide: AUTHENTICATION_CONFIGURATION_REPOSITORY,
|
||||||
|
useClass: ConfigurationRepository,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const messagePublishers: Provider[] = [
|
const messagePublishers: Provider[] = [
|
||||||
|
@ -71,7 +79,13 @@ const messagePublishers: Provider[] = [
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const orms: Provider[] = [PrismaService];
|
const orms: Provider[] = [
|
||||||
|
PrismaService,
|
||||||
|
{
|
||||||
|
provide: PASSWORD_VERIFIER,
|
||||||
|
useClass: PasswordVerifier,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [CqrsModule],
|
imports: [CqrsModule],
|
||||||
|
|
|
@ -6,26 +6,50 @@ import {
|
||||||
UniqueConstraintException,
|
UniqueConstraintException,
|
||||||
} from '@mobicoop/ddd-library';
|
} from '@mobicoop/ddd-library';
|
||||||
import { CreateAuthenticationCommand } from './create-authentication.command';
|
import { CreateAuthenticationCommand } from './create-authentication.command';
|
||||||
import { AUTHENTICATION_REPOSITORY } from '@modules/authentication/authentication.di-tokens';
|
import {
|
||||||
|
AUTHENTICATION_CONFIGURATION_REPOSITORY,
|
||||||
|
AUTHENTICATION_REPOSITORY,
|
||||||
|
} from '@modules/authentication/authentication.di-tokens';
|
||||||
import { AuthenticationRepositoryPort } from '../../ports/authentication.repository.port';
|
import { AuthenticationRepositoryPort } from '../../ports/authentication.repository.port';
|
||||||
import { AuthenticationEntity } from '@modules/authentication/core/domain/authentication.entity';
|
import { AuthenticationEntity } from '@modules/authentication/core/domain/authentication.entity';
|
||||||
import {
|
import {
|
||||||
AuthenticationAlreadyExistsException,
|
AuthenticationAlreadyExistsException,
|
||||||
UsernameAlreadyExistsException,
|
UsernameAlreadyExistsException,
|
||||||
} from '@modules/authentication/core/domain/authentication.errors';
|
} from '@modules/authentication/core/domain/authentication.errors';
|
||||||
|
import {
|
||||||
|
Configurator,
|
||||||
|
Domain,
|
||||||
|
GetConfigurationRepositoryPort,
|
||||||
|
} from '@mobicoop/configuration-module';
|
||||||
|
import {
|
||||||
|
AUTH_CONFIG_ENCRYPTION_ALGORITHM,
|
||||||
|
AuthKeyTypes,
|
||||||
|
} from '@modules/authentication/authentication.constants';
|
||||||
|
import { EncryptionAlgorithm } from '@modules/authentication/core/domain/username.types';
|
||||||
|
import { PasswordEncrypter } from '@modules/authentication/infrastructure/password-encrypter';
|
||||||
|
import { PasswordEncrypterPort } from '../../ports/password-encrypter.port';
|
||||||
|
|
||||||
@CommandHandler(CreateAuthenticationCommand)
|
@CommandHandler(CreateAuthenticationCommand)
|
||||||
export class CreateAuthenticationService implements ICommandHandler {
|
export class CreateAuthenticationService implements ICommandHandler {
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(AUTHENTICATION_REPOSITORY)
|
@Inject(AUTHENTICATION_REPOSITORY)
|
||||||
private readonly authenticationRepository: AuthenticationRepositoryPort,
|
private readonly authenticationRepository: AuthenticationRepositoryPort,
|
||||||
|
@Inject(AUTHENTICATION_CONFIGURATION_REPOSITORY)
|
||||||
|
private readonly configurationRepository: GetConfigurationRepositoryPort,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async execute(command: CreateAuthenticationCommand): Promise<AggregateID> {
|
async execute(command: CreateAuthenticationCommand): Promise<AggregateID> {
|
||||||
|
const authConfigurator: Configurator =
|
||||||
|
await this.configurationRepository.mget(Domain.AUTH, AuthKeyTypes);
|
||||||
|
const passwordEncrypter: PasswordEncrypterPort = new PasswordEncrypter(
|
||||||
|
authConfigurator.get<EncryptionAlgorithm>(
|
||||||
|
AUTH_CONFIG_ENCRYPTION_ALGORITHM,
|
||||||
|
),
|
||||||
|
);
|
||||||
const authentication: AuthenticationEntity =
|
const authentication: AuthenticationEntity =
|
||||||
await AuthenticationEntity.create({
|
await AuthenticationEntity.create({
|
||||||
userId: command.userId,
|
userId: command.userId,
|
||||||
password: command.password,
|
password: await passwordEncrypter.encrypt(command.password),
|
||||||
usernames: command.usernames,
|
usernames: command.usernames,
|
||||||
});
|
});
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -1,22 +1,51 @@
|
||||||
import { CommandHandler, ICommandHandler } from '@nestjs/cqrs';
|
import { CommandHandler, ICommandHandler } from '@nestjs/cqrs';
|
||||||
import { Inject } from '@nestjs/common';
|
import { Inject } from '@nestjs/common';
|
||||||
import { AggregateID } from '@mobicoop/ddd-library';
|
import { AggregateID } from '@mobicoop/ddd-library';
|
||||||
import { AUTHENTICATION_REPOSITORY } from '@modules/authentication/authentication.di-tokens';
|
import {
|
||||||
|
AUTHENTICATION_CONFIGURATION_REPOSITORY,
|
||||||
|
AUTHENTICATION_REPOSITORY,
|
||||||
|
} from '@modules/authentication/authentication.di-tokens';
|
||||||
import { UpdatePasswordCommand } from './update-password.command';
|
import { UpdatePasswordCommand } from './update-password.command';
|
||||||
import { AuthenticationRepositoryPort } from '../../ports/authentication.repository.port';
|
import { AuthenticationRepositoryPort } from '../../ports/authentication.repository.port';
|
||||||
import { AuthenticationEntity } from '@modules/authentication/core/domain/authentication.entity';
|
import { AuthenticationEntity } from '@modules/authentication/core/domain/authentication.entity';
|
||||||
|
import {
|
||||||
|
Domain,
|
||||||
|
Configurator,
|
||||||
|
GetConfigurationRepositoryPort,
|
||||||
|
} from '@mobicoop/configuration-module';
|
||||||
|
import {
|
||||||
|
AUTH_CONFIG_ENCRYPTION_ALGORITHM,
|
||||||
|
AuthKeyTypes,
|
||||||
|
} from '@modules/authentication/authentication.constants';
|
||||||
|
import { EncryptionAlgorithm } from '@modules/authentication/core/domain/username.types';
|
||||||
|
import { PasswordEncrypterPort } from '../../ports/password-encrypter.port';
|
||||||
|
import { PasswordEncrypter } from '@modules/authentication/infrastructure/password-encrypter';
|
||||||
|
|
||||||
@CommandHandler(UpdatePasswordCommand)
|
@CommandHandler(UpdatePasswordCommand)
|
||||||
export class UpdatePasswordService implements ICommandHandler {
|
export class UpdatePasswordService implements ICommandHandler {
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(AUTHENTICATION_REPOSITORY)
|
@Inject(AUTHENTICATION_REPOSITORY)
|
||||||
private readonly authenticationRepository: AuthenticationRepositoryPort,
|
private readonly authenticationRepository: AuthenticationRepositoryPort,
|
||||||
|
@Inject(AUTHENTICATION_CONFIGURATION_REPOSITORY)
|
||||||
|
private readonly configurationRepository: GetConfigurationRepositoryPort,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async execute(command: UpdatePasswordCommand): Promise<AggregateID> {
|
async execute(command: UpdatePasswordCommand): Promise<AggregateID> {
|
||||||
|
const authConfigurator: Configurator =
|
||||||
|
await this.configurationRepository.mget(Domain.AUTH, AuthKeyTypes);
|
||||||
|
const encryptionAlgorithm: EncryptionAlgorithm =
|
||||||
|
authConfigurator.get<EncryptionAlgorithm>(
|
||||||
|
AUTH_CONFIG_ENCRYPTION_ALGORITHM,
|
||||||
|
);
|
||||||
|
const passwordEncrypter: PasswordEncrypterPort = new PasswordEncrypter(
|
||||||
|
encryptionAlgorithm,
|
||||||
|
);
|
||||||
const authentication: AuthenticationEntity =
|
const authentication: AuthenticationEntity =
|
||||||
await this.authenticationRepository.findOneById(command.userId);
|
await this.authenticationRepository.findOneById(command.userId);
|
||||||
await authentication.updatePassword(command.password);
|
const encryptedPassword: string = await passwordEncrypter.encrypt(
|
||||||
|
command.password,
|
||||||
|
);
|
||||||
|
await authentication.updatePassword(encryptedPassword);
|
||||||
await this.authenticationRepository.update(command.userId, authentication);
|
await this.authenticationRepository.update(command.userId, authentication);
|
||||||
return authentication.id;
|
return authentication.id;
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
import { EncryptionAlgorithm } from '../../domain/username.types';
|
||||||
|
|
||||||
|
export abstract class PasswordEncrypterPort {
|
||||||
|
constructor(protected readonly encryptionAlgorithm: EncryptionAlgorithm) {}
|
||||||
|
abstract encrypt(plainPassword: string): Promise<string>;
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
export interface PasswordVerifierPort {
|
||||||
|
verify(passwordToVerify: string, encryptedPassword: string): Promise<boolean>;
|
||||||
|
}
|
|
@ -3,6 +3,7 @@ import { Inject, UnauthorizedException } from '@nestjs/common';
|
||||||
import { ValidateAuthenticationQuery } from './validate-authentication.query';
|
import { ValidateAuthenticationQuery } from './validate-authentication.query';
|
||||||
import {
|
import {
|
||||||
AUTHENTICATION_REPOSITORY,
|
AUTHENTICATION_REPOSITORY,
|
||||||
|
PASSWORD_VERIFIER,
|
||||||
USERNAME_REPOSITORY,
|
USERNAME_REPOSITORY,
|
||||||
} from '@modules/authentication/authentication.di-tokens';
|
} from '@modules/authentication/authentication.di-tokens';
|
||||||
import { AuthenticationRepositoryPort } from '../../ports/authentication.repository.port';
|
import { AuthenticationRepositoryPort } from '../../ports/authentication.repository.port';
|
||||||
|
@ -10,6 +11,7 @@ import { UsernameRepositoryPort } from '../../ports/username.repository.port';
|
||||||
import { AuthenticationEntity } from '@modules/authentication/core/domain/authentication.entity';
|
import { AuthenticationEntity } from '@modules/authentication/core/domain/authentication.entity';
|
||||||
import { UsernameEntity } from '@modules/authentication/core/domain/username.entity';
|
import { UsernameEntity } from '@modules/authentication/core/domain/username.entity';
|
||||||
import { AggregateID, NotFoundException } from '@mobicoop/ddd-library';
|
import { AggregateID, NotFoundException } from '@mobicoop/ddd-library';
|
||||||
|
import { PasswordVerifierPort } from '../../ports/password-verifier.port';
|
||||||
|
|
||||||
@QueryHandler(ValidateAuthenticationQuery)
|
@QueryHandler(ValidateAuthenticationQuery)
|
||||||
export class ValidateAuthenticationQueryHandler implements IQueryHandler {
|
export class ValidateAuthenticationQueryHandler implements IQueryHandler {
|
||||||
|
@ -18,6 +20,8 @@ export class ValidateAuthenticationQueryHandler implements IQueryHandler {
|
||||||
private readonly authenticationRepository: AuthenticationRepositoryPort,
|
private readonly authenticationRepository: AuthenticationRepositoryPort,
|
||||||
@Inject(USERNAME_REPOSITORY)
|
@Inject(USERNAME_REPOSITORY)
|
||||||
private readonly usernameRepository: UsernameRepositoryPort,
|
private readonly usernameRepository: UsernameRepositoryPort,
|
||||||
|
@Inject(PASSWORD_VERIFIER)
|
||||||
|
private readonly passwordVerifier: PasswordVerifierPort,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
execute = async (
|
execute = async (
|
||||||
|
@ -40,6 +44,7 @@ export class ValidateAuthenticationQueryHandler implements IQueryHandler {
|
||||||
try {
|
try {
|
||||||
const isAuthenticated = await authenticationEntity.authenticate(
|
const isAuthenticated = await authenticationEntity.authenticate(
|
||||||
query.password,
|
query.password,
|
||||||
|
this.passwordVerifier,
|
||||||
);
|
);
|
||||||
if (isAuthenticated) return authenticationEntity.id;
|
if (isAuthenticated) return authenticationEntity.id;
|
||||||
throw new UnauthorizedException();
|
throw new UnauthorizedException();
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import { AggregateRoot, AggregateID } from '@mobicoop/ddd-library';
|
import { AggregateRoot, AggregateID } from '@mobicoop/ddd-library';
|
||||||
import * as bcrypt from 'bcrypt';
|
|
||||||
import {
|
import {
|
||||||
AuthenticationProps,
|
AuthenticationProps,
|
||||||
CreateAuthenticationProps,
|
CreateAuthenticationProps,
|
||||||
|
@ -7,6 +6,7 @@ import {
|
||||||
import { AuthenticationCreatedDomainEvent } from './events/authentication-created.domain-event';
|
import { AuthenticationCreatedDomainEvent } from './events/authentication-created.domain-event';
|
||||||
import { AuthenticationDeletedDomainEvent } from './events/authentication-deleted.domain-event';
|
import { AuthenticationDeletedDomainEvent } from './events/authentication-deleted.domain-event';
|
||||||
import { PasswordUpdatedDomainEvent } from './events/password-updated.domain-event';
|
import { PasswordUpdatedDomainEvent } from './events/password-updated.domain-event';
|
||||||
|
import { PasswordVerifierPort } from '../application/ports/password-verifier.port';
|
||||||
|
|
||||||
export class AuthenticationEntity extends AggregateRoot<AuthenticationProps> {
|
export class AuthenticationEntity extends AggregateRoot<AuthenticationProps> {
|
||||||
protected readonly _id: AggregateID;
|
protected readonly _id: AggregateID;
|
||||||
|
@ -15,12 +15,11 @@ export class AuthenticationEntity extends AggregateRoot<AuthenticationProps> {
|
||||||
create: CreateAuthenticationProps,
|
create: CreateAuthenticationProps,
|
||||||
): Promise<AuthenticationEntity> => {
|
): Promise<AuthenticationEntity> => {
|
||||||
const props: AuthenticationProps = { ...create };
|
const props: AuthenticationProps = { ...create };
|
||||||
const hash = await AuthenticationEntity.encryptPassword(props.password);
|
|
||||||
const authentication = new AuthenticationEntity({
|
const authentication = new AuthenticationEntity({
|
||||||
id: props.userId,
|
id: props.userId,
|
||||||
props: {
|
props: {
|
||||||
userId: props.userId,
|
userId: props.userId,
|
||||||
password: hash,
|
password: props.password,
|
||||||
usernames: props.usernames,
|
usernames: props.usernames,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -31,7 +30,7 @@ export class AuthenticationEntity extends AggregateRoot<AuthenticationProps> {
|
||||||
};
|
};
|
||||||
|
|
||||||
updatePassword = async (password: string): Promise<void> => {
|
updatePassword = async (password: string): Promise<void> => {
|
||||||
this.props.password = await AuthenticationEntity.encryptPassword(password);
|
this.props.password = password;
|
||||||
this.addEvent(
|
this.addEvent(
|
||||||
new PasswordUpdatedDomainEvent({
|
new PasswordUpdatedDomainEvent({
|
||||||
aggregateId: this.id,
|
aggregateId: this.id,
|
||||||
|
@ -47,13 +46,13 @@ export class AuthenticationEntity extends AggregateRoot<AuthenticationProps> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
authenticate = async (password: string): Promise<boolean> =>
|
authenticate = async (
|
||||||
await bcrypt.compare(password, this.props.password);
|
password: string,
|
||||||
|
passwordVerifier: PasswordVerifierPort,
|
||||||
|
): Promise<boolean> =>
|
||||||
|
await passwordVerifier.verify(password, this.props.password);
|
||||||
|
|
||||||
validate(): void {
|
validate(): void {
|
||||||
// entity business rules validation to protect it's invariant before saving entity to a database
|
// entity business rules validation to protect it's invariant before saving entity to a database
|
||||||
}
|
}
|
||||||
|
|
||||||
private static encryptPassword = async (password: string): Promise<string> =>
|
|
||||||
await bcrypt.hash(password, 10);
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,3 +19,10 @@ export enum Type {
|
||||||
EMAIL = 'EMAIL',
|
EMAIL = 'EMAIL',
|
||||||
PHONE = 'PHONE',
|
PHONE = 'PHONE',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum EncryptionAlgorithm {
|
||||||
|
BCRYPT = 'BCRYPT',
|
||||||
|
ARGON2I = 'ARGON2I',
|
||||||
|
ARGON2D = 'ARGON2D',
|
||||||
|
ARGON2ID = 'ARGON2ID',
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,35 @@
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { EncryptionAlgorithm } from '../core/domain/username.types';
|
||||||
|
import * as bcrypt from 'bcrypt';
|
||||||
|
import * as argon2 from 'argon2';
|
||||||
|
import { PasswordEncrypterPort } from '../core/application/ports/password-encrypter.port';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class PasswordEncrypter extends PasswordEncrypterPort {
|
||||||
|
encrypt = async (plainPassword: string): Promise<string> => {
|
||||||
|
switch (this.encryptionAlgorithm) {
|
||||||
|
case EncryptionAlgorithm.BCRYPT:
|
||||||
|
return await bcrypt.hash(plainPassword, 10);
|
||||||
|
case EncryptionAlgorithm.ARGON2D:
|
||||||
|
case EncryptionAlgorithm.ARGON2I:
|
||||||
|
case EncryptionAlgorithm.ARGON2ID:
|
||||||
|
return await argon2.hash(plainPassword, {
|
||||||
|
type: this.argonType(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private argonType = ():
|
||||||
|
| typeof argon2.argon2d
|
||||||
|
| typeof argon2.argon2i
|
||||||
|
| typeof argon2.argon2id => {
|
||||||
|
switch (this.encryptionAlgorithm) {
|
||||||
|
case EncryptionAlgorithm.ARGON2D:
|
||||||
|
return argon2.argon2d;
|
||||||
|
case EncryptionAlgorithm.ARGON2I:
|
||||||
|
return argon2.argon2i;
|
||||||
|
default:
|
||||||
|
return argon2.argon2id;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
|
@ -0,0 +1,51 @@
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { EncryptionAlgorithm } from '../core/domain/username.types';
|
||||||
|
import * as bcrypt from 'bcrypt';
|
||||||
|
import * as argon2 from 'argon2';
|
||||||
|
import { PasswordVerifierPort } from '../core/application/ports/password-verifier.port';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class PasswordVerifier implements PasswordVerifierPort {
|
||||||
|
verify = async (
|
||||||
|
passwordToVerify: string,
|
||||||
|
encryptedPassword: string,
|
||||||
|
): Promise<boolean> => {
|
||||||
|
const encryptionAlgorithm: EncryptionAlgorithm =
|
||||||
|
this.guessEncryptionAlgorithm(encryptedPassword);
|
||||||
|
switch (encryptionAlgorithm) {
|
||||||
|
case EncryptionAlgorithm.BCRYPT:
|
||||||
|
return await bcrypt.compare(passwordToVerify, encryptedPassword);
|
||||||
|
case EncryptionAlgorithm.ARGON2D:
|
||||||
|
case EncryptionAlgorithm.ARGON2I:
|
||||||
|
case EncryptionAlgorithm.ARGON2ID:
|
||||||
|
return await argon2.verify(encryptedPassword, passwordToVerify, {
|
||||||
|
type: this.argonType(encryptionAlgorithm),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private guessEncryptionAlgorithm = (
|
||||||
|
password: string,
|
||||||
|
): EncryptionAlgorithm => {
|
||||||
|
if (password.substring(1, 9) === 'argon2id')
|
||||||
|
return EncryptionAlgorithm.ARGON2ID;
|
||||||
|
if (password.substring(1, 8) === 'argon2i')
|
||||||
|
return EncryptionAlgorithm.ARGON2I;
|
||||||
|
if (password.substring(1, 8) === 'argon2d')
|
||||||
|
return EncryptionAlgorithm.ARGON2D;
|
||||||
|
return EncryptionAlgorithm.BCRYPT;
|
||||||
|
};
|
||||||
|
|
||||||
|
private argonType = (
|
||||||
|
encryptionAlgorithm: EncryptionAlgorithm,
|
||||||
|
): typeof argon2.argon2d | typeof argon2.argon2i | typeof argon2.argon2id => {
|
||||||
|
switch (encryptionAlgorithm) {
|
||||||
|
case EncryptionAlgorithm.ARGON2D:
|
||||||
|
return argon2.argon2d;
|
||||||
|
case EncryptionAlgorithm.ARGON2I:
|
||||||
|
return argon2.argon2i;
|
||||||
|
default:
|
||||||
|
return argon2.argon2id;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { PasswordVerifierPort } from '@modules/authentication/core/application/ports/password-verifier.port';
|
||||||
import { AuthenticationEntity } from '@modules/authentication/core/domain/authentication.entity';
|
import { AuthenticationEntity } from '@modules/authentication/core/domain/authentication.entity';
|
||||||
import { CreateAuthenticationProps } from '@modules/authentication/core/domain/authentication.types';
|
import { CreateAuthenticationProps } from '@modules/authentication/core/domain/authentication.types';
|
||||||
import { AuthenticationDeletedDomainEvent } from '@modules/authentication/core/domain/events/authentication-deleted.domain-event';
|
import { AuthenticationDeletedDomainEvent } from '@modules/authentication/core/domain/events/authentication-deleted.domain-event';
|
||||||
|
@ -30,6 +31,10 @@ const createAuthenticationPropsWith2Usernames: CreateAuthenticationProps = {
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const mockPasswordVerifier: PasswordVerifierPort = {
|
||||||
|
verify: jest.fn().mockResolvedValueOnce(true).mockResolvedValueOnce(false),
|
||||||
|
};
|
||||||
|
|
||||||
describe('Authentication entity create', () => {
|
describe('Authentication entity create', () => {
|
||||||
it('should create a new authentication entity', async () => {
|
it('should create a new authentication entity', async () => {
|
||||||
const authenticationEntity: AuthenticationEntity =
|
const authenticationEntity: AuthenticationEntity =
|
||||||
|
@ -37,7 +42,6 @@ describe('Authentication entity create', () => {
|
||||||
expect(authenticationEntity.id).toBe(
|
expect(authenticationEntity.id).toBe(
|
||||||
'165192d4-398a-4469-a16b-98c02cc6f531',
|
'165192d4-398a-4469-a16b-98c02cc6f531',
|
||||||
);
|
);
|
||||||
expect(authenticationEntity.getProps().password.length).toBe(60);
|
|
||||||
expect(authenticationEntity.domainEvents.length).toBe(1);
|
expect(authenticationEntity.domainEvents.length).toBe(1);
|
||||||
});
|
});
|
||||||
it('should create a new authentication entity with 2 usernames', async () => {
|
it('should create a new authentication entity with 2 usernames', async () => {
|
||||||
|
@ -80,15 +84,19 @@ describe('Authentication password validation', () => {
|
||||||
it('should validate a valid password', async () => {
|
it('should validate a valid password', async () => {
|
||||||
const authenticationEntity: AuthenticationEntity =
|
const authenticationEntity: AuthenticationEntity =
|
||||||
await AuthenticationEntity.create(createAuthenticationProps);
|
await AuthenticationEntity.create(createAuthenticationProps);
|
||||||
const result: boolean =
|
const result: boolean = await authenticationEntity.authenticate(
|
||||||
await authenticationEntity.authenticate('somePassword');
|
'somePassword',
|
||||||
|
mockPasswordVerifier,
|
||||||
|
);
|
||||||
expect(result).toBeTruthy();
|
expect(result).toBeTruthy();
|
||||||
});
|
});
|
||||||
it('should not validate an invalid password', async () => {
|
it('should not validate an invalid password', async () => {
|
||||||
const authenticationEntity: AuthenticationEntity =
|
const authenticationEntity: AuthenticationEntity =
|
||||||
await AuthenticationEntity.create(createAuthenticationProps);
|
await AuthenticationEntity.create(createAuthenticationProps);
|
||||||
const result: boolean =
|
const result: boolean = await authenticationEntity.authenticate(
|
||||||
await authenticationEntity.authenticate('someWrongPassword');
|
'someWrongPassword',
|
||||||
|
mockPasswordVerifier,
|
||||||
|
);
|
||||||
expect(result).toBeFalsy();
|
expect(result).toBeFalsy();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,9 +1,19 @@
|
||||||
|
import {
|
||||||
|
Domain,
|
||||||
|
Configurator,
|
||||||
|
GetConfigurationRepositoryPort,
|
||||||
|
KeyType,
|
||||||
|
} from '@mobicoop/configuration-module';
|
||||||
import {
|
import {
|
||||||
AggregateID,
|
AggregateID,
|
||||||
ConflictException,
|
ConflictException,
|
||||||
UniqueConstraintException,
|
UniqueConstraintException,
|
||||||
} from '@mobicoop/ddd-library';
|
} from '@mobicoop/ddd-library';
|
||||||
import { AUTHENTICATION_REPOSITORY } from '@modules/authentication/authentication.di-tokens';
|
import { AUTH_CONFIG_ENCRYPTION_ALGORITHM } from '@modules/authentication/authentication.constants';
|
||||||
|
import {
|
||||||
|
AUTHENTICATION_CONFIGURATION_REPOSITORY,
|
||||||
|
AUTHENTICATION_REPOSITORY,
|
||||||
|
} from '@modules/authentication/authentication.di-tokens';
|
||||||
import { CreateAuthenticationCommand } from '@modules/authentication/core/application/commands/create-authentication/create-authentication.command';
|
import { CreateAuthenticationCommand } from '@modules/authentication/core/application/commands/create-authentication/create-authentication.command';
|
||||||
import { CreateAuthenticationService } from '@modules/authentication/core/application/commands/create-authentication/create-authentication.service';
|
import { CreateAuthenticationService } from '@modules/authentication/core/application/commands/create-authentication/create-authentication.service';
|
||||||
import { AuthenticationEntity } from '@modules/authentication/core/domain/authentication.entity';
|
import { AuthenticationEntity } from '@modules/authentication/core/domain/authentication.entity';
|
||||||
|
@ -44,6 +54,25 @@ const mockAuthenticationRepository = {
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const mockConfigurationRepository: GetConfigurationRepositoryPort = {
|
||||||
|
get: jest.fn(),
|
||||||
|
mget: jest.fn().mockImplementation(
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
(domain: Domain, keyTypes: KeyType[]) => {
|
||||||
|
switch (domain) {
|
||||||
|
case Domain.AUTH:
|
||||||
|
return new Configurator(Domain.AUTH, [
|
||||||
|
{
|
||||||
|
domain: Domain.AUTH,
|
||||||
|
key: AUTH_CONFIG_ENCRYPTION_ALGORITHM,
|
||||||
|
value: 'BCRYPT',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
describe('Create Authentication Service', () => {
|
describe('Create Authentication Service', () => {
|
||||||
let createAuthenticationService: CreateAuthenticationService;
|
let createAuthenticationService: CreateAuthenticationService;
|
||||||
|
|
||||||
|
@ -54,6 +83,10 @@ describe('Create Authentication Service', () => {
|
||||||
provide: AUTHENTICATION_REPOSITORY,
|
provide: AUTHENTICATION_REPOSITORY,
|
||||||
useValue: mockAuthenticationRepository,
|
useValue: mockAuthenticationRepository,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
provide: AUTHENTICATION_CONFIGURATION_REPOSITORY,
|
||||||
|
useValue: mockConfigurationRepository,
|
||||||
|
},
|
||||||
CreateAuthenticationService,
|
CreateAuthenticationService,
|
||||||
],
|
],
|
||||||
}).compile();
|
}).compile();
|
||||||
|
|
|
@ -1,10 +1,20 @@
|
||||||
import { AggregateID } from '@mobicoop/ddd-library';
|
import { AggregateID } from '@mobicoop/ddd-library';
|
||||||
import { AUTHENTICATION_REPOSITORY } from '@modules/authentication/authentication.di-tokens';
|
import {
|
||||||
|
AUTHENTICATION_CONFIGURATION_REPOSITORY,
|
||||||
|
AUTHENTICATION_REPOSITORY,
|
||||||
|
} from '@modules/authentication/authentication.di-tokens';
|
||||||
import { UpdatePasswordService } from '@modules/authentication/core/application/commands/update-password/update-password.service';
|
import { UpdatePasswordService } from '@modules/authentication/core/application/commands/update-password/update-password.service';
|
||||||
import { Type } from '@modules/authentication/core/domain/username.types';
|
import { Type } from '@modules/authentication/core/domain/username.types';
|
||||||
import { UpdatePasswordRequestDto } from '@modules/authentication/interface/grpc-controllers/dtos/update-password.request.dto';
|
import { UpdatePasswordRequestDto } from '@modules/authentication/interface/grpc-controllers/dtos/update-password.request.dto';
|
||||||
import { Test, TestingModule } from '@nestjs/testing';
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
import { UpdatePasswordCommand } from '@modules/authentication/core/application/commands/update-password/update-password.command';
|
import { UpdatePasswordCommand } from '@modules/authentication/core/application/commands/update-password/update-password.command';
|
||||||
|
import {
|
||||||
|
Domain,
|
||||||
|
Configurator,
|
||||||
|
GetConfigurationRepositoryPort,
|
||||||
|
KeyType,
|
||||||
|
} from '@mobicoop/configuration-module';
|
||||||
|
import { AUTH_CONFIG_ENCRYPTION_ALGORITHM } from '@modules/authentication/authentication.constants';
|
||||||
|
|
||||||
const updatePasswordRequest: UpdatePasswordRequestDto = {
|
const updatePasswordRequest: UpdatePasswordRequestDto = {
|
||||||
userId: '165192d4-398a-4469-a16b-98c02cc6f531',
|
userId: '165192d4-398a-4469-a16b-98c02cc6f531',
|
||||||
|
@ -24,6 +34,25 @@ const mockAuthenticationRepository = {
|
||||||
update: jest.fn().mockImplementation(),
|
update: jest.fn().mockImplementation(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const mockConfigurationRepository: GetConfigurationRepositoryPort = {
|
||||||
|
get: jest.fn(),
|
||||||
|
mget: jest.fn().mockImplementation(
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
(domain: Domain, keyTypes: KeyType[]) => {
|
||||||
|
switch (domain) {
|
||||||
|
case Domain.AUTH:
|
||||||
|
return new Configurator(Domain.AUTH, [
|
||||||
|
{
|
||||||
|
domain: Domain.AUTH,
|
||||||
|
key: AUTH_CONFIG_ENCRYPTION_ALGORITHM,
|
||||||
|
value: 'BCRYPT',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
describe('Update Password Service', () => {
|
describe('Update Password Service', () => {
|
||||||
let updatePasswordService: UpdatePasswordService;
|
let updatePasswordService: UpdatePasswordService;
|
||||||
|
|
||||||
|
@ -34,6 +63,10 @@ describe('Update Password Service', () => {
|
||||||
provide: AUTHENTICATION_REPOSITORY,
|
provide: AUTHENTICATION_REPOSITORY,
|
||||||
useValue: mockAuthenticationRepository,
|
useValue: mockAuthenticationRepository,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
provide: AUTHENTICATION_CONFIGURATION_REPOSITORY,
|
||||||
|
useValue: mockConfigurationRepository,
|
||||||
|
},
|
||||||
UpdatePasswordService,
|
UpdatePasswordService,
|
||||||
],
|
],
|
||||||
}).compile();
|
}).compile();
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
import { AggregateID, NotFoundException } from '@mobicoop/ddd-library';
|
import { AggregateID, NotFoundException } from '@mobicoop/ddd-library';
|
||||||
import {
|
import {
|
||||||
AUTHENTICATION_REPOSITORY,
|
AUTHENTICATION_REPOSITORY,
|
||||||
|
PASSWORD_VERIFIER,
|
||||||
USERNAME_REPOSITORY,
|
USERNAME_REPOSITORY,
|
||||||
} from '@modules/authentication/authentication.di-tokens';
|
} from '@modules/authentication/authentication.di-tokens';
|
||||||
|
import { PasswordVerifierPort } from '@modules/authentication/core/application/ports/password-verifier.port';
|
||||||
import { ValidateAuthenticationQuery } from '@modules/authentication/core/application/queries/validate-authentication/validate-authentication.query';
|
import { ValidateAuthenticationQuery } from '@modules/authentication/core/application/queries/validate-authentication/validate-authentication.query';
|
||||||
import { ValidateAuthenticationQueryHandler } from '@modules/authentication/core/application/queries/validate-authentication/validate-authentication.query-handler';
|
import { ValidateAuthenticationQueryHandler } from '@modules/authentication/core/application/queries/validate-authentication/validate-authentication.query-handler';
|
||||||
import { UnauthorizedException } from '@nestjs/common';
|
import { UnauthorizedException } from '@nestjs/common';
|
||||||
|
@ -50,6 +52,10 @@ const mockAuthenticationRepository = {
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const mockPasswordVerifier: PasswordVerifierPort = {
|
||||||
|
verify: jest.fn().mockResolvedValueOnce(true).mockResolvedValueOnce(false),
|
||||||
|
};
|
||||||
|
|
||||||
describe('Validate Authentication Query Handler', () => {
|
describe('Validate Authentication Query Handler', () => {
|
||||||
let validateAuthenticationQueryHandler: ValidateAuthenticationQueryHandler;
|
let validateAuthenticationQueryHandler: ValidateAuthenticationQueryHandler;
|
||||||
|
|
||||||
|
@ -64,6 +70,10 @@ describe('Validate Authentication Query Handler', () => {
|
||||||
provide: USERNAME_REPOSITORY,
|
provide: USERNAME_REPOSITORY,
|
||||||
useValue: mockUsernameRepository,
|
useValue: mockUsernameRepository,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
provide: PASSWORD_VERIFIER,
|
||||||
|
useValue: mockPasswordVerifier,
|
||||||
|
},
|
||||||
ValidateAuthenticationQueryHandler,
|
ValidateAuthenticationQueryHandler,
|
||||||
],
|
],
|
||||||
}).compile();
|
}).compile();
|
||||||
|
|
|
@ -0,0 +1,41 @@
|
||||||
|
import { PasswordEncrypterPort } from '@modules/authentication/core/application/ports/password-encrypter.port';
|
||||||
|
import { EncryptionAlgorithm } from '@modules/authentication/core/domain/username.types';
|
||||||
|
import { PasswordEncrypter } from '@modules/authentication/infrastructure/password-encrypter';
|
||||||
|
|
||||||
|
describe('Password encrypter', () => {
|
||||||
|
it('should encrypt a password in bcrypt', async () => {
|
||||||
|
const passwordEncrypter: PasswordEncrypterPort = new PasswordEncrypter(
|
||||||
|
EncryptionAlgorithm.BCRYPT,
|
||||||
|
);
|
||||||
|
const encryptedPassword: string =
|
||||||
|
await passwordEncrypter.encrypt('somePassword');
|
||||||
|
expect(encryptedPassword.substring(0, 2)).toBe('$2');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should encrypt a password in argon2i', async () => {
|
||||||
|
const passwordEncrypter: PasswordEncrypterPort = new PasswordEncrypter(
|
||||||
|
EncryptionAlgorithm.ARGON2I,
|
||||||
|
);
|
||||||
|
const encryptedPassword: string =
|
||||||
|
await passwordEncrypter.encrypt('somePassword');
|
||||||
|
expect(encryptedPassword.substring(0, 9)).toBe('$argon2i$');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should encrypt a password in argon2d', async () => {
|
||||||
|
const passwordEncrypter: PasswordEncrypterPort = new PasswordEncrypter(
|
||||||
|
EncryptionAlgorithm.ARGON2D,
|
||||||
|
);
|
||||||
|
const encryptedPassword: string =
|
||||||
|
await passwordEncrypter.encrypt('somePassword');
|
||||||
|
expect(encryptedPassword.substring(0, 9)).toBe('$argon2d$');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should encrypt a password in argon2id', async () => {
|
||||||
|
const passwordEncrypter: PasswordEncrypterPort = new PasswordEncrypter(
|
||||||
|
EncryptionAlgorithm.ARGON2ID,
|
||||||
|
);
|
||||||
|
const encryptedPassword: string =
|
||||||
|
await passwordEncrypter.encrypt('somePassword');
|
||||||
|
expect(encryptedPassword.substring(0, 10)).toBe('$argon2id$');
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,46 @@
|
||||||
|
import { PasswordVerifierPort } from '@modules/authentication/core/application/ports/password-verifier.port';
|
||||||
|
import { PasswordVerifier } from '@modules/authentication/infrastructure/password-verifier';
|
||||||
|
|
||||||
|
describe('Password verifier', () => {
|
||||||
|
const passwordVerifier: PasswordVerifierPort = new PasswordVerifier();
|
||||||
|
|
||||||
|
it('should verify a bcrypt password', async () => {
|
||||||
|
const bcryptEncryptedPassword: string =
|
||||||
|
'$2b$10$IGMj7/tH66kzO8rqwdogK.tgY2nFOdtMC.dzMAYUVfbSTPwWk7sk.';
|
||||||
|
const verified: boolean = await passwordVerifier.verify(
|
||||||
|
'somePassword',
|
||||||
|
bcryptEncryptedPassword,
|
||||||
|
);
|
||||||
|
expect(verified).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should verify an argon2i password', async () => {
|
||||||
|
const argon2iEncryptedPassword: string =
|
||||||
|
'$argon2i$v=19$m=16,t=2,p=1$c2lERkU0UmpMYkhSa1h3eQ$hSyr7TZZDzvq9XJgA+D/pw';
|
||||||
|
const verified: boolean = await passwordVerifier.verify(
|
||||||
|
'somePassword',
|
||||||
|
argon2iEncryptedPassword,
|
||||||
|
);
|
||||||
|
expect(verified).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should verify an argon2d password', async () => {
|
||||||
|
const argon2dEncryptedPassword: string =
|
||||||
|
'$argon2d$v=19$m=16,t=2,p=1$c2lERkU0UmpMYkhSa1h3eQ$YBopyKamOUl7ZPhu+KPASw';
|
||||||
|
const verified: boolean = await passwordVerifier.verify(
|
||||||
|
'somePassword',
|
||||||
|
argon2dEncryptedPassword,
|
||||||
|
);
|
||||||
|
expect(verified).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should verify an argon2id password', async () => {
|
||||||
|
const argon2idEncryptedPassword: string =
|
||||||
|
'$argon2id$v=19$m=16,t=2,p=1$c2lERkU0UmpMYkhSa1h3eQ$Heokt0Lk6mtBmRE07q94lw';
|
||||||
|
const verified: boolean = await passwordVerifier.verify(
|
||||||
|
'somePassword',
|
||||||
|
argon2idEncryptedPassword,
|
||||||
|
);
|
||||||
|
expect(verified).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,7 @@
|
||||||
|
import { repl } from '@nestjs/core';
|
||||||
|
import { AppModule } from './app.module';
|
||||||
|
|
||||||
|
async function bootstrap() {
|
||||||
|
await repl(AppModule);
|
||||||
|
}
|
||||||
|
bootstrap();
|
Loading…
Reference in New Issue