Skip to content

Commit ea9774c

Browse files
Replace session auth by jwt (#47)
* Replace session authentication by JWT authentication * Remove wrong information in README * Remove resolutions in package lock * Upgraded packages minor version * Upgrade package bcrypt * Remove caret in package json
1 parent 478dd26 commit ea9774c

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+1231
-1175
lines changed

.env.example

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ DB_PORT=5432
99
DB_HOST_PORT=5433
1010
CORS_ORIGIN_ALLOWED=
1111
LOGGER_TYPE=console
12+
JWT_SECRET=secret
1213

1314
TEST_DB_HOST=db_test
1415
TEST_DB_NAME=test_db

.github/workflows/pull-request.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ env:
1919
TEST_DB_HOST: localhost
2020
TEST_DB_NAME: test_db
2121
TEST_DB_PORT: 5432
22+
JWT_SECRET: secret
2223

2324
jobs:
2425
type-check-lint-and-build:

README.md

Lines changed: 15 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
</p>
2020

2121
<p align="center">
22-
<b>A modern boilerplate for building scalable and maintainable REST APIs with authentication, written in TypeScript. It features Docker, Express, TypeORM, Passport, and integrates Clean Architecture principles with Dependency Injection powered by Inversify.</b></br>
22+
<b>A modern boilerplate for building scalable and maintainable REST APIs with authentication, written in TypeScript. It features Docker, Express, TypeORM, jsonwebtoken for authentication by JWT, and integrates Clean Architecture principles with Dependency Injection powered by Inversify.</b></br>
2323
<sub>Made with ❤️ by <a href="https://github.com/alexleboucher">Alex Le Boucher</a> and <a href="https://github.com/alexleboucher/docker-express-postgres-boilerplate/graphs/contributors">contributors</a></sub>
2424
</p>
2525

@@ -35,7 +35,7 @@ It integrates common features such as:
3535

3636
- Docker containerization
3737
- Database connection (PostgreSQL with TypeORM)
38-
- Authentication (using Passport)
38+
- Authentication (using jsonwebtoken)
3939
- Centralized error handling
4040
- Clean Architecture principles for better separation of concerns
4141
- Dependency Injection powered by Inversify for modular and testable code
@@ -54,8 +54,7 @@ Packages are frequently upgraded. You can easily see the packages version status
5454
## Features
5555

5656
- **Docker containerization** to easily run your code anywhere and avoid installing tools like PostgreSQL on your computer.
57-
- **Authentication** with [Passport](https://www.passportjs.org/).
58-
- **Authentication session** thanks to [express-session](https://github.com/expressjs/session) and [connect-pg-simple](https://github.com/voxpelli/node-connect-pg-simple).
57+
- **Authentication by JWT** with [jsonwebtoken](https://github.com/auth0/node-jsonwebtoken).
5958
- **Simplified Database Query** managed by [TypeORM](https://github.com/typeorm/typeorm).
6059
- **Object-oriented database model** with [TypeORM](https://github.com/typeorm/typeorm) entities.
6160
- **Integrated Testing Tools** with [Jest](https://jestjs.io/fr/docs/getting-started).
@@ -211,7 +210,6 @@ The project contains Github templates and workflows. If you don't want to keep t
211210
| GET | `/health` | Retures the server health status | None. |
212211
| POST | `/users` | Creates a new user. | `username` (min. 5 chars), `email` (valid), `password` (min. 8 chars). |
213212
| POST | `/auth/login` | Logs in a user. | `email` and `password`. |
214-
| POST | `/auth/logout` | Logs out the currently authenticated user. | None. |
215213
| GET | `/auth/authenticated` | Returns the user authentication status | None. |
216214

217215
---
@@ -237,7 +235,7 @@ The project contains Github templates and workflows. If you don't want to keep t
237235
| **src/domain/services/** | Interfaces for domain-level services (e.g., authentication, encryption). |
238236
| **src/domain/use-cases/** | Use cases implementing business logic. |
239237
| **src/infra/** | Infrastructure layer providing implementations for core and domain abstractions. |
240-
| **src/infra/auth/** | Authentication implementations using Passport.js and session management. |
238+
| **src/infra/auth/** | Authentication implementations |
241239
| **src/infra/database/** | Database configuration, models, and migrations. |
242240
| **src/infra/database/repositories/** | Concrete implementations of domain repository interfaces using TypeORM. |
243241
| **src/infra/id-generator/** | UUID-based ID generator. |
@@ -267,14 +265,9 @@ The project contains Github templates and workflows. If you don't want to keep t
267265
| TEST_DB_NAME | Test database name. || |
268266
| TEST_DB_PORT | Test database host port. || |
269267
| TEST_DB_HOST_PORT | Test database mapped port for accessing the test database in Docker. || |
268+
| JWT_SECRET | Secret used to encryot JSON web tokens. || |
269+
| JWT_EXPIRES_IN_SECONDS | Number of seconds before JWT tokens expire. | ✔️ | 86400 |
270270
| CORS_ORIGIN_ALLOWED | List of allowed origins for CORS. | ✔️ | * |
271-
| SESSION_SECRET | Secret key for signing the session ID cookie. | ✔️ | session-secret |
272-
| SESSION_RESAVE | Forces the session to be saved back to the session store, even if it was never modified. | ✔️ | false |
273-
| SESSION_SAVE_UNINITIALIZED | Forces an uninitialized session to be saved to the store. | ✔️ | false |
274-
| SESSION_COOKIE_SECURE | Ensures the cookie is only sent over HTTPS. | ✔️ | false |
275-
| SESSION_COOKIE_MAX_AGE | Lifetime of the session cookie in milliseconds. | ✔️ | 7776000000 (90 days) |
276-
| SESSION_COOKIE_HTTP_ONLY | Ensures the cookie is inaccessible to JavaScript (for XSS protection). | ✔️ | false |
277-
| SESSION_COOKIE_SAME_SITE | Controls whether the cookie is sent with cross-site requests. | ✔️ | lax |
278271
| DB_LOGGING | Enables or disables query logging in TypeORM. | ✔️ | false |
279272
| TYPEORM_ENTITIES | Path to TypeORM entity files. | ✔️ | src/infra/database/models/**/*.entity.ts |
280273
| TYPEORM_MIGRATIONS | Path to TypeORM migration files. | ✔️ | src/infra/database/migrations/**/*.ts |
@@ -284,16 +277,7 @@ The project contains Github templates and workflows. If you don't want to keep t
284277

285278
## Authentication
286279

287-
This boilerplate uses `Passport.js` to handle authentication. `Passport.js` is a powerful, flexible, and modular middleware that allows you to implement various authentication strategies, including social logins (e.g., Google, Facebook, GitHub, etc.).
288-
289-
### Configuration
290-
291-
The configuration for `Passport` is located in `src/infra/auth/authenticator/passport-authenticator.ts`. This class centralizes the setup of strategies and the implementation of required methods like `serializeUser` and `deserializeUser`.
292-
293-
- **`serializeUser`**: Defines what data should be stored in the session. By default, it stores the user ID.
294-
- **`deserializeUser`**: Fetches user information based on the session data and assigns it to `req.user`. This makes the authenticated user readily accessible via `req.user` without requiring additional calls.
295-
296-
You can find detailed documentation on `Passport.js` [here](https://www.passportjs.org/).
280+
This boilerplate uses JSON Web Tokens to handle authentication with `jsonwebtoken`.
297281

298282
### Route Protection
299283

@@ -303,22 +287,14 @@ To ensure route security and verify the user's authentication status, this boile
303287
This middleware ensures the user is authenticated before allowing access to the route. It integrates seamlessly with the controllers, as shown in the example below:
304288

305289
```typescript
306-
@httpPost('/logout', MIDDLEWARES_DI_TYPES.AuthenticatedMiddleware)
307-
public logout(): void {
308-
// Logout logic here
290+
@httpPost('/your-protected-route', MIDDLEWARES_DI_TYPES.AuthenticatedMiddleware)
291+
public yourProtectedRoute(): void {
292+
// yourProtectedRoute logic here
309293
}
310294
```
311295

312296
This pattern allows you to secure endpoints declaratively and keeps the authentication logic consistent throughout the project.
313297

314-
### Extending Authentication Strategies
315-
Adding new strategies is straightforward thanks to Passport's modular design. To include a new strategy:
316-
317-
1. Install the corresponding Passport strategy package (e.g., `passport-google-oauth20`).
318-
2. Configure the strategy in `passport-authenticator.ts` by adding it to the existing strategies.
319-
320-
This design simplifies the addition of new authentication methods and scales well as your application grows.
321-
322298
---
323299

324300
## Migrations
@@ -458,10 +434,10 @@ Use `test` to define individual tests within `describe` blocks:
458434
```
459435

460436
4. **Authenticated Requests:**
461-
For tests requiring user authentication, create an authenticated agent:
437+
For tests requiring user authentication, create an authenticated request:
462438
```typescript
463-
const { agent } = await testEnv.createAuthenticatedAgent();
464-
const res = await agent.get('/auth/authenticated');
439+
const request = await testEnv.authenticatedRequest();
440+
const res = await request.get('/auth/authenticated');
465441

466442
expect(res.body).toEqual({ authenticated: true });
467443
```
@@ -550,7 +526,7 @@ There are 3 workflows:
550526

551527
2. The workflow `main-tests` is triggered when code is merged or pushed on main. It runs the tests and sends the coverage to [Codecov](https://about.codecov.io/). It has coverage for the main branch. If you don't want to keep it, you can delete the file `main-tests.yml` in the folder `workflows`.
552528

553-
If you want to keep the tests on pull request but don't want to use Codecov, you can delete `main-tests` and only delete the last step `Upload coverage to Codecov` in `pull-request.yml`. You can also delete `codecov.yml`.<br>
529+
If you want to keep the tests on pull request but don't want to use Codecov, you can delete `main-tests` and only delete the last step `Upload coverage to Codecov` in `pull-request.yml`.<br>
554530
But if you want to use CodeCov, the only thing you need to do is set your `CODECOV_TOKEN` in your github secrets.
555531

556532
3. The workflow `main-build` is triggered when something is merged or pulled on main. It builds the project and its primary goal is to check if main is building. If you don't want to keep it, you can delete the file `main-build.yml` in the folder `workflows`.
@@ -573,7 +549,7 @@ You can see the upcoming or in progress features [here](https://github.com/users
573549
| --------------------------------- | --------------------------------- |
574550
| [Express](https://expressjs.com/) | Express is a minimal and flexible Node.js web application framework that provides a robust set of features for web and mobile applications. |
575551
| [TypeORM](http://typeorm.io/#/) | TypeORM is highly influenced by other ORMs, such as Hibernate, Doctrine and Entity Framework. |
576-
| [Passport](https://www.passportjs.org/) | Passport is authentication middleware for Node.js. Extremely flexible and modular, Passport can be unobtrusively dropped in to any Express-based web application. |
552+
| [jsonwebtoken](https://github.com/auth0/node-jsonwebtoken) | An implementation of JSON Web Tokens for Node.js that helps you securely transmit information between parties as a JSON object. |
577553
| [Docker](https://www.docker.com/) | Docker is a platform designed to help developers build, share, and run modern applications. We handle the tedious setup, so you can focus on the code. |
578554
| [PostgreSQL](https://www.postgresql.org/) | PostgreSQL is a powerful, open source object-relational database system with over 35 years of active development that has earned it a strong reputation for reliability, feature robustness, and performance. |
579555
| [TypeScript](https://www.typescriptlang.org/) | TypeScript is a strongly typed programming language that builds on JavaScript, giving you better tooling at any scale. |

package.json

Lines changed: 17 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -26,55 +26,46 @@
2626
"migration:run": "yarn typeorm migration:run -d src/infra/database/config/data-source.orm.ts"
2727
},
2828
"dependencies": {
29-
"bcryptjs": "2.4.3",
30-
"connect-pg-simple": "10.0.0",
29+
"bcryptjs": "3.0.2",
3130
"cors": "2.8.5",
3231
"dotenv": "16.4.7",
3332
"express": "4.21.2",
34-
"express-session": "1.18.1",
35-
"helmet": "8.0.0",
36-
"inversify": "6.2.1",
33+
"helmet": "8.1.0",
34+
"inversify": "6.2.2",
3735
"inversify-express-utils": "6.4.10",
36+
"jsonwebtoken": "9.0.2",
3837
"morgan": "1.10.0",
39-
"passport": "0.7.0",
40-
"passport-local": "1.0.0",
41-
"pg": "8.13.1",
38+
"pg": "8.14.1",
4239
"reflect-metadata": "0.2.2",
4340
"tslib": "2.8.1",
44-
"typeorm": "0.3.20",
45-
"uuid": "11.0.3",
46-
"zod": "3.24.1"
41+
"typeorm": "0.3.21",
42+
"uuid": "11.1.0",
43+
"zod": "3.24.2"
4744
},
4845
"devDependencies": {
4946
"@eslint-community/eslint-plugin-eslint-comments": "4.4.1",
50-
"@eslint/js": "9.17.0",
47+
"@eslint/js": "9.22.0",
5148
"@stylistic/eslint-plugin": "2.12.1",
52-
"@types/bcryptjs": "2.4.6",
53-
"@types/connect-pg-simple": "7.0.3",
49+
"@types/bcryptjs": "3.0.0",
5450
"@types/cors": "2.8.17",
5551
"@types/express": "4.17.21",
56-
"@types/express-session": "1.18.1",
5752
"@types/jest": "29.5.14",
53+
"@types/jsonwebtoken": "9.0.9",
5854
"@types/morgan": "1.9.9",
59-
"@types/node": "22.10.3",
60-
"@types/passport": "1.0.17",
61-
"@types/passport-local": "1.0.38",
55+
"@types/node": "22.13.10",
6256
"@types/supertest": "6.0.2",
63-
"eslint": "9.17.0",
57+
"eslint": "9.22.0",
6458
"eslint-import-resolver-typescript": "3.7.0",
6559
"eslint-plugin-import": "2.31.0",
6660
"jest": "29.7.0",
6761
"jest-mock-extended": "4.0.0-beta1",
6862
"nodemon": "3.1.9",
6963
"supertest": "7.0.0",
70-
"ts-jest": "29.2.5",
64+
"ts-jest": "29.2.6",
7165
"ts-node": "10.9.2",
72-
"tsc-alias": "1.8.10",
66+
"tsc-alias": "1.8.11",
7367
"tsconfig-paths": "4.2.0",
74-
"typescript": "5.7.2",
75-
"typescript-eslint": "8.19.0"
76-
},
77-
"resolutions": {
78-
"@types/express": "4.17.21"
68+
"typescript": "5.8.2",
69+
"typescript-eslint": "8.26.1"
7970
}
8071
}

src/app/controllers/auth/auth-controller.ts

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import type { NextFunction, Request, Response } from 'express';
44

55
import type { IRequestHandler } from '@/app/request-handlers/request-handler.interface';
66
import { REQUEST_HANDLERS_DI_TYPES } from '@/container/request-handlers/di-types';
7-
import { MIDDLEWARES_DI_TYPES } from '@/container/middlewares/di-types';
87

98
// The auth routes are a bit different from the other routes in the way that they don't call a use case
109
// but the authenticator directly because req, res and next are needed and it's not the responsibility
@@ -13,7 +12,6 @@ import { MIDDLEWARES_DI_TYPES } from '@/container/middlewares/di-types';
1312
export class AuthController extends BaseHttpController {
1413
constructor(
1514
@inject(REQUEST_HANDLERS_DI_TYPES.LoginRequestHandler) private readonly loginRequestHandler: IRequestHandler,
16-
@inject(REQUEST_HANDLERS_DI_TYPES.LogoutRequestHandler) private readonly logoutRequestHandler: IRequestHandler,
1715
@inject(REQUEST_HANDLERS_DI_TYPES.AuthenticatedRequestHandler) private readonly authenticatedRequestHandler: IRequestHandler,
1816
) {
1917
super();
@@ -28,9 +26,4 @@ export class AuthController extends BaseHttpController {
2826
public async login(req: Request, res: Response, next: NextFunction) {
2927
return this.loginRequestHandler.handle(req, res, next);
3028
}
31-
32-
@httpPost('/logout', MIDDLEWARES_DI_TYPES.AuthenticatedMiddleware)
33-
public async logout(req: Request, res: Response, next: NextFunction) {
34-
return this.logoutRequestHandler.handle(req, res, next);
35-
}
3629
}

src/app/middlewares/authenticated-middleware.ts

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,13 @@
1-
import { inject, injectable } from 'inversify';
1+
import { injectable } from 'inversify';
22
import { BaseMiddleware } from 'inversify-express-utils';
33
import type { NextFunction, Request, Response } from 'express';
44

5-
import { SERVICES_DI_TYPES } from '@/container/services/di-types';
6-
import type { IAuthenticator } from '@/domain/services/auth/authenticator.interface';
75
import { HttpError } from '@/app/http-error';
86

97
@injectable()
108
export class AuthenticatedMiddleware extends BaseMiddleware {
11-
constructor(
12-
@inject(SERVICES_DI_TYPES.Authenticator) private readonly authenticator: IAuthenticator,
13-
) {
14-
super();
15-
}
16-
179
handler(req: Request, res: Response, next: NextFunction) {
18-
if (!this.authenticator.isAuthenticated(req)) {
10+
if (!req.user) {
1911
throw HttpError.forbidden('User must be authenticated');
2012
}
2113

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import type { NextFunction, Request, Response } from 'express';
2+
import { inject, injectable } from 'inversify';
3+
4+
import { USE_CASES_DI_TYPES } from '@/container/use-cases/di-types';
5+
import type { IUseCase } from '@/core/use-case/use-case.interface';
6+
import type { GetCurrentUserUseCaseFailure, GetCurrentUserUseCasePayload, GetCurrentUserUseCaseSuccess } from '@/domain/use-cases/auth/get-current-user-use-case';
7+
8+
export interface ICurrentUserMiddleware {
9+
handler: (req: Request, res: Response, next: NextFunction) => Promise<void>;
10+
}
11+
12+
@injectable()
13+
export class CurrentUserMiddleware implements ICurrentUserMiddleware {
14+
constructor(
15+
@inject(USE_CASES_DI_TYPES.GetCurrentUserUseCase) private readonly getCurrentUserUseCase: IUseCase<GetCurrentUserUseCasePayload, GetCurrentUserUseCaseSuccess, GetCurrentUserUseCaseFailure>,
16+
) {}
17+
18+
async handler(req: Request, res: Response, next: NextFunction) {
19+
try {
20+
req.user = null;
21+
22+
const authHeader = req.headers.authorization;
23+
24+
if (authHeader) {
25+
const [type, token] = authHeader.split(' ');
26+
27+
if (type === 'Bearer' && token) {
28+
const result = await this.getCurrentUserUseCase.execute({ token });
29+
if (result.isSuccess()) {
30+
req.user = result.value.user;
31+
}
32+
}
33+
}
34+
35+
next();
36+
} catch (error) {
37+
next(error);
38+
}
39+
}
40+
}

0 commit comments

Comments
 (0)