@kourouma/express-authkit 0.0.0-stage → 0.1.0-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +78 -0
- package/LICENSE +21 -0
- package/README.md +218 -2
- package/SECURITY.md +30 -0
- package/bin/express-authkit.js +30 -0
- package/package.json +93 -4
- package/src/adapters/express/bearer-token.js +35 -0
- package/src/adapters/express/http-errors.js +89 -0
- package/src/adapters/express/index.js +405 -0
- package/src/config/assertions.js +80 -0
- package/src/config/auth-kit-options.js +61 -0
- package/src/config/defaults.js +66 -0
- package/src/config/jwt-options.js +86 -0
- package/src/config/password-options.js +38 -0
- package/src/config/refresh-token-options.js +49 -0
- package/src/config/secret-strength.js +112 -0
- package/src/core/authentication-service.js +278 -0
- package/src/core/authorization.js +156 -0
- package/src/core/contracts/assert-contract.js +26 -0
- package/src/core/contracts/auth-principal.js +36 -0
- package/src/core/contracts/authentication-service.js +44 -0
- package/src/core/contracts/password-provider.js +27 -0
- package/src/core/contracts/refresh-token-store.js +80 -0
- package/src/core/contracts/token-provider.js +45 -0
- package/src/core/contracts/user-repository.js +157 -0
- package/src/core/errors.js +258 -0
- package/src/core/input-limits.js +26 -0
- package/src/core/messages.js +239 -0
- package/src/core/refresh-token-service.js +149 -0
- package/src/factory/create-auth-kit.js +90 -0
- package/src/index.js +61 -0
- package/src/providers/argon2-password-provider.js +68 -0
- package/src/providers/in-memory-refresh-token-store.js +51 -0
- package/src/providers/jwt-token-provider.js +112 -0
- package/types/adapters/express/bearer-token.d.ts +11 -0
- package/types/adapters/express/http-errors.d.ts +30 -0
- package/types/adapters/express/index.d.ts +57 -0
- package/types/config/assertions.d.ts +40 -0
- package/types/config/auth-kit-options.d.ts +41 -0
- package/types/config/defaults.d.ts +55 -0
- package/types/config/jwt-options.d.ts +42 -0
- package/types/config/password-options.d.ts +23 -0
- package/types/config/refresh-token-options.d.ts +21 -0
- package/types/config/secret-strength.d.ts +7 -0
- package/types/core/authentication-service.d.ts +37 -0
- package/types/core/authorization.d.ts +40 -0
- package/types/core/contracts/assert-contract.d.ts +13 -0
- package/types/core/contracts/auth-principal.d.ts +38 -0
- package/types/core/contracts/authentication-service.d.ts +102 -0
- package/types/core/contracts/password-provider.d.ts +32 -0
- package/types/core/contracts/refresh-token-store.d.ts +105 -0
- package/types/core/contracts/token-provider.d.ts +79 -0
- package/types/core/contracts/user-repository.d.ts +112 -0
- package/types/core/errors.d.ts +213 -0
- package/types/core/input-limits.d.ts +20 -0
- package/types/core/messages.d.ts +141 -0
- package/types/core/refresh-token-service.d.ts +31 -0
- package/types/factory/create-auth-kit.d.ts +52 -0
- package/types/index.d.ts +60 -0
- package/types/providers/argon2-password-provider.d.ts +13 -0
- package/types/providers/in-memory-refresh-token-store.d.ts +11 -0
- package/types/providers/jwt-token-provider.d.ts +13 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# Historique des versions
|
|
2
|
+
|
|
3
|
+
Toutes les évolutions notables d'Express AuthKit sont consignées ici.
|
|
4
|
+
|
|
5
|
+
Le format s'inspire de [Keep a Changelog](https://keepachangelog.com/fr/1.1.0/)
|
|
6
|
+
et le projet suit le [Versionnage sémantique](https://semver.org/lang/fr/).
|
|
7
|
+
|
|
8
|
+
## Politique de versions
|
|
9
|
+
|
|
10
|
+
- **MAJEURE** (`1.0.0` → `2.0.0`) : changement incompatible de l'API publique
|
|
11
|
+
(exports, signatures, codes d'erreur, format de `req.auth`).
|
|
12
|
+
- **MINEURE** (`0.1.0` → `0.2.0`) : nouvelle fonctionnalité rétrocompatible.
|
|
13
|
+
- **CORRECTIF** (`0.1.0` → `0.1.1`) : correction de bug ou de sécurité rétrocompatible.
|
|
14
|
+
|
|
15
|
+
Tant que le projet est en `0.x`, une version mineure peut contenir des
|
|
16
|
+
changements incompatibles ; ils seront toujours signalés dans ce fichier
|
|
17
|
+
sous la rubrique **Incompatible**. L'API publique se limite aux exports de
|
|
18
|
+
`@kourouma/express-authkit` et `@kourouma/express-authkit/express`.
|
|
19
|
+
|
|
20
|
+
## [Non publié]
|
|
21
|
+
|
|
22
|
+
## [0.1.0] — candidate
|
|
23
|
+
|
|
24
|
+
Le paquet est publié sous le nom **`@kourouma/express-authkit`** : npm refuse le nom
|
|
25
|
+
`express-authkit`, jugé trop proche du paquet existant `express-auth-kit`. La commande
|
|
26
|
+
`npx express-authkit` garde son nom.
|
|
27
|
+
|
|
28
|
+
### Ajouté
|
|
29
|
+
|
|
30
|
+
- `createAuthKit()` : moteur d'authentification configurable par injection de dépendances.
|
|
31
|
+
- Hachage et vérification des mots de passe avec Argon2id (`auth.password.hash()`, `auth.password.verify()`).
|
|
32
|
+
- Émission de JWT d'accès signés HMAC (HS256 par défaut) avec `jose`, en-tête `typ: at+jwt`.
|
|
33
|
+
- Vérification stricte des JWT : signature, algorithme autorisé, `exp`, `iat`, `iss`, `aud`, `sub`, `jti`.
|
|
34
|
+
- `auth.login()` : authentification identifiant / mot de passe avec message d'erreur générique
|
|
35
|
+
et atténuation des écarts de temps de réponse pour les comptes inexistants.
|
|
36
|
+
- `auth.verifyAccessToken()` : contrôle de l'existence et du statut actif de l'utilisateur à chaque requête.
|
|
37
|
+
- Adaptateur Express `createExpressAuth()` : `authenticate()`, `login()` et `errorHandler()`.
|
|
38
|
+
- Contrats `UserRepository`, `PasswordProvider`, `TokenProvider` documentés en JSDoc et vérifiés au démarrage.
|
|
39
|
+
- Erreurs structurées avec codes stables (`AUTH_ERROR_CODES`).
|
|
40
|
+
- Validation de la configuration : secret absent, court ou manifestement faible refusé,
|
|
41
|
+
paramètres Argon2id sous le plancher OWASP refusés, options inconnues refusées.
|
|
42
|
+
- `expressAuth.login(options?)` : handler de connexion prêt à l'emploi
|
|
43
|
+
(`app.post("/login", express.json(), expressAuth.login())`), avec l'option `identifierField`.
|
|
44
|
+
Il ne crée aucune route : l'application choisit l'URL.
|
|
45
|
+
- Commande `npx express-authkit secret` pour générer un secret JWT.
|
|
46
|
+
- Les `id` renvoyés par le repository peuvent être des entiers, des `BigInt` ou des `ObjectId`
|
|
47
|
+
MongoDB : ils sont convertis automatiquement en chaîne.
|
|
48
|
+
- Tous les messages d'erreur sont en français, centralisés dans un seul module, et indiquent
|
|
49
|
+
comment corriger le problème. Les codes d'erreur restent stables (en anglais).
|
|
50
|
+
- `DependencyFailureError` (`DEPENDENCY_FAILURE`) : toute panne d'un repository ou d'un provider
|
|
51
|
+
est enveloppée, avec `dependency` (l'appel qui a échoué) et `cause` (l'erreur d'origine).
|
|
52
|
+
- `AuthKitError#toJSON()` : sérialisation sûre `{ name, code, message }`, sans pile ni cause.
|
|
53
|
+
- `expressAuth.errorHandler()` convertit aussi les erreurs du parseur `express.json()`
|
|
54
|
+
(JSON mal formé → 400, corps trop volumineux → 413) en réponses JSON.
|
|
55
|
+
- Oublier les parenthèses (`expressAuth.authenticate` au lieu de `expressAuth.authenticate()`)
|
|
56
|
+
lève une `ConfigurationError` explicite au lieu de bloquer la requête.
|
|
57
|
+
- `expressAuth.login({ identifierField })` cite le vrai nom du champ dans ses messages.
|
|
58
|
+
- **Refresh tokens** (option `refreshTokens: { store, expiresInSeconds }`, désactivée par défaut) :
|
|
59
|
+
`auth.refresh()`, `auth.logout()`, `auth.revokeAllSessions()`, handlers Express
|
|
60
|
+
`expressAuth.refresh()` et `expressAuth.logout()`. Jetons opaques de 256 bits, stockés sous forme
|
|
61
|
+
d'empreinte SHA-256, rotation à chaque usage, révocation de toute la session en cas de
|
|
62
|
+
réutilisation. Contrat `RefreshTokenStore` (`save`, `consume`, `revokeFamily`, `revokeUser`),
|
|
63
|
+
`createInMemoryRefreshTokenStore()` pour le développement, exemple PostgreSQL documenté.
|
|
64
|
+
- **Rôles et permissions** : `expressAuth.authorize(rule)` (rôle, `anyRole`, `allPermissions`,
|
|
65
|
+
règle `check` personnalisée), `isAuthorized()` hors d'Express, nouvelle erreur
|
|
66
|
+
`AccessDeniedError` (`ACCESS_DENIED`, 403). Le repository peut renvoyer `roles` et
|
|
67
|
+
`permissions` : ils sont relus à chaque requête (changement immédiat), exposés dans
|
|
68
|
+
`req.auth`, et jamais écrits dans le JWT.
|
|
69
|
+
- Nouvelle erreur `InvalidRefreshTokenError` (`REFRESH_TOKEN_INVALID`, 401).
|
|
70
|
+
- **Types TypeScript** générés depuis le JSDoc (`types/*.d.ts`, champ `types` et conditions
|
|
71
|
+
`types` dans `exports`), avec `req.auth` typé pour Express. Script `npm run typecheck` :
|
|
72
|
+
le code source passe la vérification TypeScript en mode strict.
|
|
73
|
+
- Le type de `jwt.secret` accepte `undefined` : `secret: process.env.AUTH_SECRET` compile en
|
|
74
|
+
TypeScript sans `!` (une valeur absente reste refusée au démarrage, avec un message clair).
|
|
75
|
+
- Démarrage rapide TypeScript dans `docs/typescript.md`, compilé et exécuté par
|
|
76
|
+
`npm run test:package`, avec les trois différences à connaître par rapport aux exemples JavaScript.
|
|
77
|
+
- `TokenProvider.issue()` est vérifié : un provider personnalisé qui renvoie un jeton mal formé
|
|
78
|
+
lève une `ContractViolationError`.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Express AuthKit contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,219 @@
|
|
|
1
|
-
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="docs/assets/banner.jpeg" alt="Express AuthKit — authentification pour Express.js : Argon2id, JWT, middleware de protection des routes" width="100%">
|
|
3
|
+
</p>
|
|
2
4
|
|
|
3
|
-
|
|
5
|
+
# Express AuthKit
|
|
6
|
+
|
|
7
|
+
**Ajoutez une connexion par e-mail et mot de passe à votre API Express, en quelques lignes, sans sacrifier la sécurité.**
|
|
8
|
+
|
|
9
|
+
> Version 0.1.0 (candidate), pas encore publiée sur npm. Voir les [limites connues](docs/securite.md#limites-connues).
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## En bref
|
|
14
|
+
|
|
15
|
+
Express AuthKit s'occupe pour vous de quatre choses :
|
|
16
|
+
|
|
17
|
+
1. **Stocker les mots de passe de façon sûre.** Vous ne gardez jamais le mot de passe, seulement une empreinte impossible à inverser.
|
|
18
|
+
2. **Connecter un utilisateur.** S'il donne le bon mot de passe, il reçoit un *token* (un jeton signé, comme un badge d'accès temporaire).
|
|
19
|
+
3. **Protéger vos routes.** Une route protégée refuse toute requête qui n'a pas de token valide.
|
|
20
|
+
4. **Contrôler les droits.** Une route peut être réservée à un rôle (« admin ») ou à des permissions précises.
|
|
21
|
+
|
|
22
|
+
Vous gardez votre base de données et votre modèle d'utilisateur. Vous écrivez seulement deux petites fonctions pour dire à la bibliothèque comment retrouver un utilisateur.
|
|
23
|
+
|
|
24
|
+
## Les mots à connaître
|
|
25
|
+
|
|
26
|
+
| Mot | En clair |
|
|
27
|
+
| --- | --- |
|
|
28
|
+
| **Hash** (empreinte) | Version brouillée du mot de passe, impossible à inverser. On stocke le hash, jamais le mot de passe. |
|
|
29
|
+
| **Token** (JWT) | Jeton signé remis après la connexion. Le client le renvoie à chaque requête pour prouver qui il est. Il expire après 15 minutes par défaut. |
|
|
30
|
+
| **Secret** | Longue chaîne aléatoire qui sert à signer les tokens. Elle doit rester secrète. |
|
|
31
|
+
| **Middleware** | Fonction Express qui s'exécute avant votre route, par exemple pour vérifier le token. |
|
|
32
|
+
| **Repository** | Petit objet qui sait retrouver un utilisateur dans **votre** base de données. |
|
|
33
|
+
|
|
34
|
+
## Prérequis
|
|
35
|
+
|
|
36
|
+
- **Node.js 22.12 ou plus récent** (la version 24 est recommandée).
|
|
37
|
+
- **Express 4.21+ ou 5.x**.
|
|
38
|
+
- Un projet en modules ES : `"type": "module"` dans `package.json`.
|
|
39
|
+
|
|
40
|
+
## Installation
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
npm install @kourouma/express-authkit express
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
> Tant que le package n'est pas publié sur npm, créez l'archive avec `npm pack` dans ce dépôt,
|
|
47
|
+
> puis installez-la dans votre projet : `npm install /chemin/vers/kourouma-express-authkit-0.1.0.tgz express`.
|
|
48
|
+
|
|
49
|
+
## Démarrage en cinq minutes
|
|
50
|
+
|
|
51
|
+
> **JavaScript ou TypeScript ?** Tous les exemples de cette documentation sont en **JavaScript**.
|
|
52
|
+
> Vous utilisez **TypeScript** ? Suivez plutôt le [démarrage TypeScript](docs/typescript.md) :
|
|
53
|
+
> même code, avec la configuration et les trois petites différences à connaître.
|
|
54
|
+
|
|
55
|
+
**Étape 1 — Créez un projet**
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
mkdir mon-api && cd mon-api
|
|
59
|
+
npm init -y
|
|
60
|
+
npm pkg set type=module
|
|
61
|
+
npm install @kourouma/express-authkit express
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
**Étape 2 — Créez un secret**
|
|
65
|
+
|
|
66
|
+
Cette commande génère un secret aléatoire et l'enregistre dans un fichier `.env` :
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
echo "AUTH_SECRET=$(npx express-authkit secret)" > .env
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Ne partagez jamais ce fichier et ne le mettez pas dans Git.
|
|
73
|
+
|
|
74
|
+
**Étape 3 — Créez `server.js`**
|
|
75
|
+
|
|
76
|
+
```javascript
|
|
77
|
+
import express from "express";
|
|
78
|
+
import { createAuthKit } from "@kourouma/express-authkit";
|
|
79
|
+
import { createExpressAuth } from "@kourouma/express-authkit/express";
|
|
80
|
+
|
|
81
|
+
// 1. Où trouver vos utilisateurs : ici une simple liste, pour la démonstration.
|
|
82
|
+
// En production, interrogez votre base (voir docs/repository-utilisateur.md).
|
|
83
|
+
const users = [];
|
|
84
|
+
const userRepository = {
|
|
85
|
+
findByIdentifier: async (email) => users.find((user) => user.email === email.toLowerCase()) ?? null,
|
|
86
|
+
findById: async (id) => users.find((user) => user.id === id) ?? null
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
// 2. Le moteur d'authentification (la configuration est vérifiée immédiatement).
|
|
90
|
+
const auth = createAuthKit({
|
|
91
|
+
users: userRepository,
|
|
92
|
+
jwt: {
|
|
93
|
+
secret: process.env.AUTH_SECRET,
|
|
94
|
+
issuer: "my-api",
|
|
95
|
+
audience: "my-client",
|
|
96
|
+
expiresInSeconds: 900
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
const expressAuth = createExpressAuth(auth);
|
|
100
|
+
|
|
101
|
+
// 3. Un utilisateur de démonstration (ne codez jamais de vrai mot de passe en dur).
|
|
102
|
+
users.push({
|
|
103
|
+
id: "1",
|
|
104
|
+
email: "alice@example.com",
|
|
105
|
+
passwordHash: await auth.password.hash("example-password"),
|
|
106
|
+
isActive: true
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// 4. L'application : une route de connexion et une route protégée.
|
|
110
|
+
const app = express();
|
|
111
|
+
|
|
112
|
+
app.post("/login", express.json(), expressAuth.login({ identifierField: "email" }));
|
|
113
|
+
|
|
114
|
+
app.get("/profile", expressAuth.authenticate(), (req, res) => {
|
|
115
|
+
res.json({ userId: req.auth.userId });
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
// 5. Des réponses JSON claires, en français, si le client envoie un JSON invalide.
|
|
119
|
+
app.use(expressAuth.errorHandler());
|
|
120
|
+
|
|
121
|
+
app.listen(3000, () => console.log("API sur http://localhost:3000"));
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Ce que fait ce fichier, en cinq points :
|
|
125
|
+
|
|
126
|
+
1. **`userRepository`** explique à la bibliothèque comment trouver un utilisateur : par son e-mail (à la connexion) et par son identifiant (à chaque requête).
|
|
127
|
+
2. **`createAuthKit()`** crée le moteur. Si le secret manque ou est trop faible, l'application refuse de démarrer et vous explique pourquoi.
|
|
128
|
+
3. **`auth.password.hash()`** transforme le mot de passe en empreinte avant de l'enregistrer.
|
|
129
|
+
4. **`expressAuth.login()`** crée la route de connexion, et **`expressAuth.authenticate()`** protège `/profile`. Dans la route protégée, `req.auth.userId` contient l'identifiant de l'utilisateur connecté.
|
|
130
|
+
5. **`expressAuth.errorHandler()`** transforme les erreurs (JSON mal formé, par exemple) en réponses JSON claires, avec des messages en français.
|
|
131
|
+
|
|
132
|
+
**Étape 4 — Lancez et testez**
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
node --env-file=.env server.js
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Dans un autre terminal :
|
|
139
|
+
|
|
140
|
+
```bash
|
|
141
|
+
# 1. Se connecter : la réponse contient "accessToken"
|
|
142
|
+
curl -s -X POST http://localhost:3000/login \
|
|
143
|
+
-H "content-type: application/json" \
|
|
144
|
+
-d '{"email":"alice@example.com","password":"example-password"}'
|
|
145
|
+
|
|
146
|
+
# 2. Sans token : refusé (401)
|
|
147
|
+
curl -i http://localhost:3000/profile
|
|
148
|
+
|
|
149
|
+
# 3. Avec le token (remplacez <accessToken>) : {"userId":"1"}
|
|
150
|
+
curl -s http://localhost:3000/profile -H "authorization: Bearer <accessToken>"
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Ça marche ? Votre API a maintenant une connexion sécurisée. 🎉
|
|
154
|
+
|
|
155
|
+
Une application plus complète, avec ses tests, se trouve dans [examples/basic-express](examples/basic-express).
|
|
156
|
+
|
|
157
|
+
## L'essentiel en un coup d'œil
|
|
158
|
+
|
|
159
|
+
| Je veux… | Code |
|
|
160
|
+
| --- | --- |
|
|
161
|
+
| Créer le moteur | `const auth = createAuthKit({ users, jwt: { secret, issuer, audience } })` |
|
|
162
|
+
| Le brancher sur Express | `const expressAuth = createExpressAuth(auth)` |
|
|
163
|
+
| Ajouter la route de connexion | `app.post("/login", express.json(), expressAuth.login())` |
|
|
164
|
+
| Protéger une route | `app.get("/me", expressAuth.authenticate(), handler)` puis `req.auth.userId` |
|
|
165
|
+
| Réserver une route à un rôle | `app.get("/admin", expressAuth.authorize("admin"), handler)` |
|
|
166
|
+
| Hacher un mot de passe (inscription) | `await auth.password.hash(password)` |
|
|
167
|
+
| Vérifier un mot de passe | `await auth.password.verify(password, hash)` |
|
|
168
|
+
| Rester connecté plus de 15 minutes | `refreshTokens: { store }` puis `app.post("/auth/refresh", express.json(), expressAuth.refresh())` |
|
|
169
|
+
| Déconnecter | `app.post("/auth/logout", express.json(), expressAuth.logout())` |
|
|
170
|
+
| Générer un secret | `npx express-authkit secret` |
|
|
171
|
+
|
|
172
|
+
**Votre repository** : deux fonctions, `findByIdentifier()` et `findById()`. Elles renvoient
|
|
173
|
+
`{ id, passwordHash, isActive }`, ou `null` si l'utilisateur n'existe pas.
|
|
174
|
+
`id` peut être un texte, un nombre ou un `ObjectId` MongoDB.
|
|
175
|
+
|
|
176
|
+
## Avant de passer en production
|
|
177
|
+
|
|
178
|
+
- [ ] Limitez le nombre de tentatives de connexion ([exemple prêt à copier](docs/securite.md#limitation-des-tentatives-de-connexion)).
|
|
179
|
+
- [ ] Servez l'API uniquement en **HTTPS**.
|
|
180
|
+
- [ ] Gardez le secret hors du code, dans une variable d'environnement.
|
|
181
|
+
|
|
182
|
+
La liste complète se trouve dans le [guide de sécurité](docs/securite.md).
|
|
183
|
+
|
|
184
|
+
## Documentation
|
|
185
|
+
|
|
186
|
+
**Pour démarrer**
|
|
187
|
+
|
|
188
|
+
- [Brancher ma base de données](docs/repository-utilisateur.md) (PostgreSQL, Prisma, MongoDB…)
|
|
189
|
+
- [Connecter les utilisateurs et protéger les routes](docs/connexion-et-routes.md)
|
|
190
|
+
- [Gérer les mots de passe](docs/mots-de-passe.md) (inscription, changement de mot de passe)
|
|
191
|
+
- [Rôles et permissions](docs/roles-et-permissions.md) (réserver une route aux administrateurs…)
|
|
192
|
+
- [Rester connecté : les refresh tokens](docs/refresh-tokens.md) (et la déconnexion)
|
|
193
|
+
- [Utiliser TypeScript](docs/typescript.md) (démarrage TypeScript, différences avec les exemples JavaScript)
|
|
194
|
+
- [Régler la configuration](docs/configuration.md)
|
|
195
|
+
|
|
196
|
+
**Quand quelque chose ne va pas**
|
|
197
|
+
|
|
198
|
+
- [Dépannage](docs/depannage.md)
|
|
199
|
+
- [Comprendre les erreurs](docs/erreurs.md)
|
|
200
|
+
|
|
201
|
+
**Pour aller plus loin**
|
|
202
|
+
|
|
203
|
+
- [Sécurité : bonnes pratiques et limites](docs/securite.md)
|
|
204
|
+
- [Référence complète de l'API](docs/reference-api.md)
|
|
205
|
+
- [Remplacer le hachage ou les tokens](docs/providers-personnalises.md)
|
|
206
|
+
- [Architecture du projet](docs/architecture.md)
|
|
207
|
+
|
|
208
|
+
**Le projet** : [contribuer](CONTRIBUTING.md) · [historique des versions](CHANGELOG.md) · [signaler une faille](SECURITY.md)
|
|
209
|
+
|
|
210
|
+
## Ce que la version 0.1.0 ne fait pas (encore)
|
|
211
|
+
|
|
212
|
+
Pas de connexion via Google/GitHub (OAuth), pas de double authentification (MFA), pas de
|
|
213
|
+
hiérarchie de rôles ni de droits par objet (ACL), pas de cookies `HttpOnly`, pas de réinitialisation de mot de passe
|
|
214
|
+
par e-mail.
|
|
215
|
+
Ces sujets sont [prévus pour plus tard](docs/architecture.md#points-dextension-futurs).
|
|
216
|
+
|
|
217
|
+
## Licence
|
|
218
|
+
|
|
219
|
+
[MIT](LICENSE)
|
package/SECURITY.md
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Politique de sécurité
|
|
2
|
+
|
|
3
|
+
## Versions prises en charge
|
|
4
|
+
|
|
5
|
+
| Version | Correctifs de sécurité |
|
|
6
|
+
| ------- | ---------------------- |
|
|
7
|
+
| 0.1.x | Oui |
|
|
8
|
+
|
|
9
|
+
## Signaler une vulnérabilité
|
|
10
|
+
|
|
11
|
+
**N'ouvrez pas d'issue publique pour une vulnérabilité.**
|
|
12
|
+
|
|
13
|
+
Utilisez la fonction « Report a vulnerability » (GitHub Security Advisories)
|
|
14
|
+
du dépôt, en indiquant :
|
|
15
|
+
|
|
16
|
+
- la version concernée ;
|
|
17
|
+
- une description du problème et de son impact ;
|
|
18
|
+
- les étapes ou le code permettant de le reproduire.
|
|
19
|
+
|
|
20
|
+
Nous visons un accusé de réception sous 5 jours ouvrés et une première
|
|
21
|
+
évaluation sous 14 jours. La divulgation est coordonnée avec la personne
|
|
22
|
+
qui a fait le signalement.
|
|
23
|
+
|
|
24
|
+
## Périmètre et limites connues
|
|
25
|
+
|
|
26
|
+
Express AuthKit s'appuie sur `jose` (JWT) et `argon2` (Argon2id) et
|
|
27
|
+
n'implémente aucun algorithme cryptographique. Les limites de sécurité
|
|
28
|
+
connues (révocation des JWT, rate limiting à la charge de l'application,
|
|
29
|
+
heuristique de détection des secrets faibles…) sont décrites dans
|
|
30
|
+
[docs/securite.md](docs/securite.md).
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Petit utilitaire en ligne de commande d'Express AuthKit.
|
|
4
|
+
*
|
|
5
|
+
* npx express-authkit secret → affiche un secret JWT aléatoire (48 octets, base64url)
|
|
6
|
+
*/
|
|
7
|
+
import { randomBytes } from "node:crypto";
|
|
8
|
+
|
|
9
|
+
const SECRET_BYTES = 48;
|
|
10
|
+
|
|
11
|
+
const USAGE = `Utilisation : express-authkit <commande>
|
|
12
|
+
|
|
13
|
+
Commandes :
|
|
14
|
+
secret Affiche un secret JWT aléatoire (${SECRET_BYTES} octets, base64url) pour AUTH_SECRET.
|
|
15
|
+
help Affiche cette aide.
|
|
16
|
+
|
|
17
|
+
Exemple :
|
|
18
|
+
echo "AUTH_SECRET=$(npx express-authkit secret)" >> .env
|
|
19
|
+
`;
|
|
20
|
+
|
|
21
|
+
const [command] = process.argv.slice(2);
|
|
22
|
+
|
|
23
|
+
if (command === "secret") {
|
|
24
|
+
process.stdout.write(`${randomBytes(SECRET_BYTES).toString("base64url")}\n`);
|
|
25
|
+
} else if (command === undefined || command === "help" || command === "--help" || command === "-h") {
|
|
26
|
+
process.stdout.write(USAGE);
|
|
27
|
+
} else {
|
|
28
|
+
process.stderr.write(`Commande inconnue : ${command}\n\n${USAGE}`);
|
|
29
|
+
process.exitCode = 1;
|
|
30
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,95 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kourouma/express-authkit",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0-rc.1",
|
|
4
|
+
"description": "Moteur d'authentification sécurisé et extensible pour Express.js : Argon2id, JWT (jose), repository utilisateur injectable.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"express",
|
|
7
|
+
"authentication",
|
|
8
|
+
"auth",
|
|
9
|
+
"jwt",
|
|
10
|
+
"jose",
|
|
11
|
+
"argon2",
|
|
12
|
+
"argon2id",
|
|
13
|
+
"middleware",
|
|
14
|
+
"security",
|
|
15
|
+
"hexagonal"
|
|
16
|
+
],
|
|
17
|
+
"license": "MIT",
|
|
18
|
+
"author": "Express AuthKit contributors",
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": "git+https://github.com/kourouma1/Express-AuthKit.git"
|
|
22
|
+
},
|
|
23
|
+
"homepage": "https://github.com/kourouma1/Express-AuthKit#readme",
|
|
24
|
+
"bugs": {
|
|
25
|
+
"url": "https://github.com/kourouma1/Express-AuthKit/issues"
|
|
26
|
+
},
|
|
27
|
+
"type": "module",
|
|
28
|
+
"sideEffects": false,
|
|
29
|
+
"exports": {
|
|
30
|
+
".": {
|
|
31
|
+
"types": "./types/index.d.ts",
|
|
32
|
+
"default": "./src/index.js"
|
|
33
|
+
},
|
|
34
|
+
"./express": {
|
|
35
|
+
"types": "./types/adapters/express/index.d.ts",
|
|
36
|
+
"default": "./src/adapters/express/index.js"
|
|
37
|
+
},
|
|
38
|
+
"./package.json": "./package.json"
|
|
39
|
+
},
|
|
40
|
+
"bin": {
|
|
41
|
+
"express-authkit": "bin/express-authkit.js"
|
|
42
|
+
},
|
|
43
|
+
"files": [
|
|
44
|
+
"bin/",
|
|
45
|
+
"src/",
|
|
46
|
+
"types/",
|
|
47
|
+
"README.md",
|
|
48
|
+
"CHANGELOG.md",
|
|
49
|
+
"SECURITY.md",
|
|
50
|
+
"LICENSE"
|
|
51
|
+
],
|
|
52
|
+
"engines": {
|
|
53
|
+
"node": ">=22.12.0"
|
|
54
|
+
},
|
|
55
|
+
"scripts": {
|
|
56
|
+
"test": "node --test \"tests/**/*.test.js\"",
|
|
57
|
+
"test:coverage": "node --test --experimental-test-coverage \"tests/**/*.test.js\"",
|
|
58
|
+
"test:package": "node scripts/verify-package.js",
|
|
59
|
+
"lint": "eslint .",
|
|
60
|
+
"check": "npm run lint && npm run typecheck && npm test",
|
|
61
|
+
"prepublishOnly": "npm run check",
|
|
62
|
+
"build:types": "node scripts/build-types.js",
|
|
63
|
+
"typecheck": "tsc -p tsconfig.types.json --noEmit --checkJs",
|
|
64
|
+
"prepack": "npm run build:types"
|
|
65
|
+
},
|
|
66
|
+
"dependencies": {
|
|
67
|
+
"argon2": "^0.45.1",
|
|
68
|
+
"jose": "^6.2.12"
|
|
69
|
+
},
|
|
70
|
+
"peerDependencies": {
|
|
71
|
+
"express": "^4.21.0 || ^5.0.0",
|
|
72
|
+
"@types/express": "^4.17.0 || ^5.0.0"
|
|
73
|
+
},
|
|
74
|
+
"peerDependenciesMeta": {
|
|
75
|
+
"express": {
|
|
76
|
+
"optional": true
|
|
77
|
+
},
|
|
78
|
+
"@types/express": {
|
|
79
|
+
"optional": true
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
"devDependencies": {
|
|
83
|
+
"@eslint/js": "^10.0.1",
|
|
84
|
+
"@types/express": "^5.0.6",
|
|
85
|
+
"@types/node": "^24.19.2",
|
|
86
|
+
"eslint": "^10.12.0",
|
|
87
|
+
"express": "^5.3.0",
|
|
88
|
+
"globals": "^17.13.0",
|
|
89
|
+
"typescript": "^7.0.2"
|
|
90
|
+
},
|
|
91
|
+
"types": "./types/index.d.ts",
|
|
92
|
+
"publishConfig": {
|
|
93
|
+
"access": "public"
|
|
94
|
+
}
|
|
95
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { InvalidTokenError, TokenMissingError } from "../../core/errors.js";
|
|
2
|
+
import { MAX_TOKEN_LENGTH } from "../../core/input-limits.js";
|
|
3
|
+
|
|
4
|
+
/** RFC 6750 §2.1 : `Bearer` suivi d’un jeton base64 ; le schéma ne distingue pas la casse. */
|
|
5
|
+
const BEARER_PATTERN = /^Bearer +([A-Za-z0-9\-._~+/]+=*)$/i;
|
|
6
|
+
const BEARER_SCHEME_PATTERN = /^Bearer(?: |$)/i;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Extrait le jeton d’accès d’un en-tête `Authorization`.
|
|
10
|
+
*
|
|
11
|
+
* Un en-tête utilisant un autre schéma (par exemple `Basic`) est traité comme
|
|
12
|
+
* une absence de jeton ; une valeur `Bearer` mal formée est invalide.
|
|
13
|
+
*
|
|
14
|
+
* @param {string | undefined} headerValue
|
|
15
|
+
* @returns {string}
|
|
16
|
+
* @throws {TokenMissingError | InvalidTokenError}
|
|
17
|
+
*/
|
|
18
|
+
export function extractBearerToken(headerValue) {
|
|
19
|
+
if (typeof headerValue !== "string" || headerValue.trim() === "") {
|
|
20
|
+
throw new TokenMissingError();
|
|
21
|
+
}
|
|
22
|
+
// La longueur de l’en-tête est limitée avant toute expression régulière.
|
|
23
|
+
if (headerValue.length > MAX_TOKEN_LENGTH + "Bearer ".length) {
|
|
24
|
+
throw new InvalidTokenError();
|
|
25
|
+
}
|
|
26
|
+
if (!BEARER_SCHEME_PATTERN.test(headerValue)) {
|
|
27
|
+
throw new TokenMissingError();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const match = BEARER_PATTERN.exec(headerValue);
|
|
31
|
+
if (match === null) {
|
|
32
|
+
throw new InvalidTokenError();
|
|
33
|
+
}
|
|
34
|
+
return match[1];
|
|
35
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { AUTH_ERROR_CODES, InvalidInputError, isAuthKitError } from "../../core/errors.js";
|
|
2
|
+
import { PUBLIC_MESSAGES } from "../../core/messages.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Statuts HTTP pour les erreurs causées par le client. Toute autre erreur
|
|
6
|
+
* (configuration, contrat, hash illisible, base de données…) est un problème
|
|
7
|
+
* serveur et est transmise à la chaîne d’erreurs Express.
|
|
8
|
+
*
|
|
9
|
+
* @type {Readonly<Record<string, number>>}
|
|
10
|
+
*/
|
|
11
|
+
const CLIENT_ERROR_STATUSES = Object.freeze({
|
|
12
|
+
[AUTH_ERROR_CODES.INVALID_INPUT]: 400,
|
|
13
|
+
[AUTH_ERROR_CODES.INVALID_CREDENTIALS]: 401,
|
|
14
|
+
[AUTH_ERROR_CODES.TOKEN_MISSING]: 401,
|
|
15
|
+
[AUTH_ERROR_CODES.TOKEN_INVALID]: 401,
|
|
16
|
+
[AUTH_ERROR_CODES.TOKEN_EXPIRED]: 401,
|
|
17
|
+
[AUTH_ERROR_CODES.REFRESH_TOKEN_INVALID]: 401,
|
|
18
|
+
[AUTH_ERROR_CODES.ACCESS_DENIED]: 403
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
/** Code d’erreur RFC 6750 §3.1 pour les jetons Bearer rejetés. */
|
|
22
|
+
/** @type {ReadonlySet<string>} */
|
|
23
|
+
const INVALID_TOKEN_CODES = new Set([AUTH_ERROR_CODES.TOKEN_INVALID, AUTH_ERROR_CODES.TOKEN_EXPIRED]);
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @param {unknown} error
|
|
27
|
+
* @returns {number | undefined} Statut à envoyer, ou `undefined` si ce n’est pas une erreur client.
|
|
28
|
+
*/
|
|
29
|
+
export function getClientErrorStatus(error) {
|
|
30
|
+
return isAuthKitError(error) ? CLIENT_ERROR_STATUSES[error.code] : undefined;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Comme {@link getClientErrorStatus}, mais reconnaît aussi les erreurs du
|
|
35
|
+
* parseur JSON d’Express (`express.json()`) : un JSON mal formé ou trop
|
|
36
|
+
* volumineux reçoit une réponse JSON propre au lieu de la page d’erreur HTML
|
|
37
|
+
* d’Express, qui peut afficher la pile d’appels en développement.
|
|
38
|
+
*
|
|
39
|
+
* @param {unknown} error
|
|
40
|
+
* @returns {{ error: import("../../core/errors.js").AuthKitError, status: number } | undefined}
|
|
41
|
+
*/
|
|
42
|
+
export function resolveClientError(error) {
|
|
43
|
+
const status = getClientErrorStatus(error);
|
|
44
|
+
if (status !== undefined) {
|
|
45
|
+
return { error: /** @type {import("../../core/errors.js").AuthKitError} */ (error), status };
|
|
46
|
+
}
|
|
47
|
+
const bodyParserErrorType =
|
|
48
|
+
error !== null && typeof error === "object" ? /** @type {{ type?: unknown }} */ (error).type : undefined;
|
|
49
|
+
if (bodyParserErrorType === "entity.parse.failed") {
|
|
50
|
+
return { error: new InvalidInputError(PUBLIC_MESSAGES.malformedJsonBody), status: 400 };
|
|
51
|
+
}
|
|
52
|
+
if (bodyParserErrorType === "entity.too.large") {
|
|
53
|
+
return { error: new InvalidInputError(PUBLIC_MESSAGES.payloadTooLarge), status: 413 };
|
|
54
|
+
}
|
|
55
|
+
return undefined;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Envoie une erreur d’authentification client au format JSON.
|
|
60
|
+
*
|
|
61
|
+
* Seuls le `code` stable et le `message` public sont envoyés : jamais la pile,
|
|
62
|
+
* la cause ou la raison interne de l’échec.
|
|
63
|
+
*
|
|
64
|
+
* @param {import("express").Response} res
|
|
65
|
+
* @param {import("../../core/errors.js").AuthKitError} error
|
|
66
|
+
* @param {number} status
|
|
67
|
+
* @param {string} realm
|
|
68
|
+
*/
|
|
69
|
+
export function sendClientError(res, error, status, realm) {
|
|
70
|
+
if (status === 401) {
|
|
71
|
+
res.set("WWW-Authenticate", buildChallenge(error.code, realm));
|
|
72
|
+
}
|
|
73
|
+
if (status === 403) {
|
|
74
|
+
// RFC 6750 §3.1 : droits insuffisants pour la ressource demandée.
|
|
75
|
+
res.set("WWW-Authenticate", `Bearer realm="${realm}", error="insufficient_scope"`);
|
|
76
|
+
}
|
|
77
|
+
res.set("Cache-Control", "no-store");
|
|
78
|
+
res.status(status).json({ error: { code: error.code, message: error.message } });
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* @param {string} code
|
|
83
|
+
* @param {string} realm
|
|
84
|
+
* @returns {string}
|
|
85
|
+
*/
|
|
86
|
+
function buildChallenge(code, realm) {
|
|
87
|
+
const challenge = `Bearer realm="${realm}"`;
|
|
88
|
+
return INVALID_TOKEN_CODES.has(code) ? `${challenge}, error="invalid_token"` : challenge;
|
|
89
|
+
}
|