@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.
Files changed (62) hide show
  1. package/CHANGELOG.md +78 -0
  2. package/LICENSE +21 -0
  3. package/README.md +218 -2
  4. package/SECURITY.md +30 -0
  5. package/bin/express-authkit.js +30 -0
  6. package/package.json +93 -4
  7. package/src/adapters/express/bearer-token.js +35 -0
  8. package/src/adapters/express/http-errors.js +89 -0
  9. package/src/adapters/express/index.js +405 -0
  10. package/src/config/assertions.js +80 -0
  11. package/src/config/auth-kit-options.js +61 -0
  12. package/src/config/defaults.js +66 -0
  13. package/src/config/jwt-options.js +86 -0
  14. package/src/config/password-options.js +38 -0
  15. package/src/config/refresh-token-options.js +49 -0
  16. package/src/config/secret-strength.js +112 -0
  17. package/src/core/authentication-service.js +278 -0
  18. package/src/core/authorization.js +156 -0
  19. package/src/core/contracts/assert-contract.js +26 -0
  20. package/src/core/contracts/auth-principal.js +36 -0
  21. package/src/core/contracts/authentication-service.js +44 -0
  22. package/src/core/contracts/password-provider.js +27 -0
  23. package/src/core/contracts/refresh-token-store.js +80 -0
  24. package/src/core/contracts/token-provider.js +45 -0
  25. package/src/core/contracts/user-repository.js +157 -0
  26. package/src/core/errors.js +258 -0
  27. package/src/core/input-limits.js +26 -0
  28. package/src/core/messages.js +239 -0
  29. package/src/core/refresh-token-service.js +149 -0
  30. package/src/factory/create-auth-kit.js +90 -0
  31. package/src/index.js +61 -0
  32. package/src/providers/argon2-password-provider.js +68 -0
  33. package/src/providers/in-memory-refresh-token-store.js +51 -0
  34. package/src/providers/jwt-token-provider.js +112 -0
  35. package/types/adapters/express/bearer-token.d.ts +11 -0
  36. package/types/adapters/express/http-errors.d.ts +30 -0
  37. package/types/adapters/express/index.d.ts +57 -0
  38. package/types/config/assertions.d.ts +40 -0
  39. package/types/config/auth-kit-options.d.ts +41 -0
  40. package/types/config/defaults.d.ts +55 -0
  41. package/types/config/jwt-options.d.ts +42 -0
  42. package/types/config/password-options.d.ts +23 -0
  43. package/types/config/refresh-token-options.d.ts +21 -0
  44. package/types/config/secret-strength.d.ts +7 -0
  45. package/types/core/authentication-service.d.ts +37 -0
  46. package/types/core/authorization.d.ts +40 -0
  47. package/types/core/contracts/assert-contract.d.ts +13 -0
  48. package/types/core/contracts/auth-principal.d.ts +38 -0
  49. package/types/core/contracts/authentication-service.d.ts +102 -0
  50. package/types/core/contracts/password-provider.d.ts +32 -0
  51. package/types/core/contracts/refresh-token-store.d.ts +105 -0
  52. package/types/core/contracts/token-provider.d.ts +79 -0
  53. package/types/core/contracts/user-repository.d.ts +112 -0
  54. package/types/core/errors.d.ts +213 -0
  55. package/types/core/input-limits.d.ts +20 -0
  56. package/types/core/messages.d.ts +141 -0
  57. package/types/core/refresh-token-service.d.ts +31 -0
  58. package/types/factory/create-auth-kit.d.ts +52 -0
  59. package/types/index.d.ts +60 -0
  60. package/types/providers/argon2-password-provider.d.ts +13 -0
  61. package/types/providers/in-memory-refresh-token-store.d.ts +11 -0
  62. 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
- # Temporary Holding Version
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
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
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.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
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
+ }