@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
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { SignJWT, errors as joseErrors, jwtVerify } from "jose";
|
|
2
|
+
|
|
3
|
+
import { resolveJwtOptions } from "../config/jwt-options.js";
|
|
4
|
+
import { InvalidInputError, InvalidTokenError, TokenExpiredError } from "../core/errors.js";
|
|
5
|
+
import { MAX_TOKEN_LENGTH } from "../core/input-limits.js";
|
|
6
|
+
import { subjectInvalid } from "../core/messages.js";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Type de média RFC 9068 pour les jetons d’accès JWT. L’exiger lors de la
|
|
10
|
+
* vérification empêche d’autres JWT signés avec la même clé (jetons d’identité,
|
|
11
|
+
* liens de confirmation d’e-mail…) d’être acceptés comme jetons d’accès.
|
|
12
|
+
*/
|
|
13
|
+
const ACCESS_TOKEN_TYPE = "at+jwt";
|
|
14
|
+
const REQUIRED_CLAIMS = Object.freeze(["sub", "iss", "aud", "exp", "iat", "jti"]);
|
|
15
|
+
const MAX_SUBJECT_LENGTH = 256;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Crée un {@link import("../core/contracts/token-provider.js").TokenProvider}
|
|
19
|
+
* qui émet et vérifie des jetons d’accès JWT signés en HMAC avec `jose`.
|
|
20
|
+
*
|
|
21
|
+
* Les jetons ne contiennent que des revendications standard (`sub`, `iss`,
|
|
22
|
+
* `aud`, `iat`, `exp`, `jti`) : aucune donnée personnelle ou sensible n’est
|
|
23
|
+
* placée dans la charge utile, qui est encodée mais non chiffrée.
|
|
24
|
+
*
|
|
25
|
+
* @param {import("../config/jwt-options.js").JwtOptions} options
|
|
26
|
+
* @returns {import("../core/contracts/token-provider.js").TokenProvider}
|
|
27
|
+
* @throws {import("../core/errors.js").ConfigurationError}
|
|
28
|
+
*/
|
|
29
|
+
export function createJwtTokenProvider(options) {
|
|
30
|
+
const config = resolveJwtOptions(options);
|
|
31
|
+
|
|
32
|
+
const verifyOptions = Object.freeze({
|
|
33
|
+
algorithms: [config.algorithm],
|
|
34
|
+
issuer: config.issuer,
|
|
35
|
+
audience: config.audience,
|
|
36
|
+
typ: ACCESS_TOKEN_TYPE,
|
|
37
|
+
clockTolerance: config.clockToleranceSeconds,
|
|
38
|
+
// Also makes jose reject tokens whose `iat` lies in the future, and tokens
|
|
39
|
+
// older than the configured lifetime (e.g. after shortening it).
|
|
40
|
+
maxTokenAge: config.expiresInSeconds,
|
|
41
|
+
requiredClaims: [...REQUIRED_CLAIMS]
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* @param {{ subject: string }} input
|
|
46
|
+
* @returns {Promise<import("../core/contracts/token-provider.js").IssuedToken>}
|
|
47
|
+
*/
|
|
48
|
+
async function issue(input) {
|
|
49
|
+
const subject = input?.subject;
|
|
50
|
+
if (typeof subject !== "string" || subject.length === 0 || subject.length > MAX_SUBJECT_LENGTH) {
|
|
51
|
+
throw new InvalidInputError(subjectInvalid(MAX_SUBJECT_LENGTH));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const issuedAt = Math.floor(Date.now() / 1000);
|
|
55
|
+
const expiresAt = issuedAt + config.expiresInSeconds;
|
|
56
|
+
const tokenId = globalThis.crypto.randomUUID();
|
|
57
|
+
|
|
58
|
+
const token = await new SignJWT({})
|
|
59
|
+
.setProtectedHeader({ alg: config.algorithm, typ: ACCESS_TOKEN_TYPE })
|
|
60
|
+
.setSubject(subject)
|
|
61
|
+
.setIssuer(config.issuer)
|
|
62
|
+
.setAudience(config.audience)
|
|
63
|
+
.setIssuedAt(issuedAt)
|
|
64
|
+
.setExpirationTime(expiresAt)
|
|
65
|
+
.setJti(tokenId)
|
|
66
|
+
.sign(config.key);
|
|
67
|
+
|
|
68
|
+
return { token, tokenId, issuedAt, expiresAt };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* @param {string} token
|
|
73
|
+
* @returns {Promise<import("../core/contracts/token-provider.js").VerifiedTokenClaims>}
|
|
74
|
+
*/
|
|
75
|
+
async function verify(token) {
|
|
76
|
+
if (typeof token !== "string" || token.length === 0 || token.length > MAX_TOKEN_LENGTH) {
|
|
77
|
+
throw new InvalidTokenError();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
let payload;
|
|
81
|
+
try {
|
|
82
|
+
({ payload } = await jwtVerify(token, config.key, verifyOptions));
|
|
83
|
+
} catch (error) {
|
|
84
|
+
throw toTokenError(error);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const { sub, jti, iat, exp } = payload;
|
|
88
|
+
if (typeof sub !== "string" || sub.length === 0 || sub.length > MAX_SUBJECT_LENGTH || typeof jti !== "string") {
|
|
89
|
+
throw new InvalidTokenError();
|
|
90
|
+
}
|
|
91
|
+
return { subject: sub, tokenId: jti, issuedAt: /** @type {number} */ (iat), expiresAt: /** @type {number} */ (exp) };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return Object.freeze({ issue, verify });
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Convertit les erreurs de `jose` en erreurs de la bibliothèque. Toute erreur
|
|
99
|
+
* qui ne vient pas de JOSE est inattendue et est propagée sans modification.
|
|
100
|
+
*
|
|
101
|
+
* @param {unknown} error
|
|
102
|
+
* @returns {unknown}
|
|
103
|
+
*/
|
|
104
|
+
function toTokenError(error) {
|
|
105
|
+
if (error instanceof joseErrors.JWTExpired) {
|
|
106
|
+
return new TokenExpiredError({ cause: error });
|
|
107
|
+
}
|
|
108
|
+
if (error instanceof joseErrors.JOSEError) {
|
|
109
|
+
return new InvalidTokenError({ cause: error });
|
|
110
|
+
}
|
|
111
|
+
return error;
|
|
112
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extrait le jeton d’accès d’un en-tête `Authorization`.
|
|
3
|
+
*
|
|
4
|
+
* Un en-tête utilisant un autre schéma (par exemple `Basic`) est traité comme
|
|
5
|
+
* une absence de jeton ; une valeur `Bearer` mal formée est invalide.
|
|
6
|
+
*
|
|
7
|
+
* @param {string | undefined} headerValue
|
|
8
|
+
* @returns {string}
|
|
9
|
+
* @throws {TokenMissingError | InvalidTokenError}
|
|
10
|
+
*/
|
|
11
|
+
export declare function extractBearerToken(headerValue: string | undefined): string;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @param {unknown} error
|
|
3
|
+
* @returns {number | undefined} Statut à envoyer, ou `undefined` si ce n’est pas une erreur client.
|
|
4
|
+
*/
|
|
5
|
+
export declare function getClientErrorStatus(error: unknown): number | undefined;
|
|
6
|
+
/**
|
|
7
|
+
* Comme {@link getClientErrorStatus}, mais reconnaît aussi les erreurs du
|
|
8
|
+
* parseur JSON d’Express (`express.json()`) : un JSON mal formé ou trop
|
|
9
|
+
* volumineux reçoit une réponse JSON propre au lieu de la page d’erreur HTML
|
|
10
|
+
* d’Express, qui peut afficher la pile d’appels en développement.
|
|
11
|
+
*
|
|
12
|
+
* @param {unknown} error
|
|
13
|
+
* @returns {{ error: import("../../core/errors.js").AuthKitError, status: number } | undefined}
|
|
14
|
+
*/
|
|
15
|
+
export declare function resolveClientError(error: unknown): {
|
|
16
|
+
error: import("../../core/errors.js").AuthKitError;
|
|
17
|
+
status: number;
|
|
18
|
+
} | undefined;
|
|
19
|
+
/**
|
|
20
|
+
* Envoie une erreur d’authentification client au format JSON.
|
|
21
|
+
*
|
|
22
|
+
* Seuls le `code` stable et le `message` public sont envoyés : jamais la pile,
|
|
23
|
+
* la cause ou la raison interne de l’échec.
|
|
24
|
+
*
|
|
25
|
+
* @param {import("express").Response} res
|
|
26
|
+
* @param {import("../../core/errors.js").AuthKitError} error
|
|
27
|
+
* @param {number} status
|
|
28
|
+
* @param {string} realm
|
|
29
|
+
*/
|
|
30
|
+
export declare function sendClientError(res: import("express").Response, error: import("../../core/errors.js").AuthKitError, status: number, realm: string): void;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Express AuthKit — adaptateur Express.js (`@kourouma/express-authkit/express`).
|
|
3
|
+
*
|
|
4
|
+
* Ce module n’importe pas Express : il produit uniquement des middlewares
|
|
5
|
+
* standard `(req, res, next)`, compatibles avec Express 4 et 5.
|
|
6
|
+
*
|
|
7
|
+
* @module @kourouma/express-authkit/express
|
|
8
|
+
*/
|
|
9
|
+
export type ExpressAuthOptions = {
|
|
10
|
+
/**
|
|
11
|
+
* Domaine annoncé dans l’en-tête `WWW-Authenticate`.
|
|
12
|
+
*/
|
|
13
|
+
realm?: string;
|
|
14
|
+
};
|
|
15
|
+
export type ExpressAuthorizationRequirement = string | {
|
|
16
|
+
anyRole?: string[];
|
|
17
|
+
allPermissions?: string[];
|
|
18
|
+
check?: (user: import("../../core/contracts/auth-principal.js").AuthPrincipal, req: import("express").Request) => boolean | Promise<boolean>;
|
|
19
|
+
};
|
|
20
|
+
export type LoginHandlerOptions = {
|
|
21
|
+
/**
|
|
22
|
+
* Champ du corps JSON contenant l’identifiant (par exemple `"email"`).
|
|
23
|
+
*/
|
|
24
|
+
identifierField?: string;
|
|
25
|
+
};
|
|
26
|
+
export type ExpressAuth = Readonly<{
|
|
27
|
+
authenticate: () => import("express").RequestHandler;
|
|
28
|
+
authorize: (requirement: ExpressAuthorizationRequirement) => import("express").RequestHandler;
|
|
29
|
+
login: (options?: LoginHandlerOptions) => import("express").RequestHandler;
|
|
30
|
+
refresh: () => import("express").RequestHandler;
|
|
31
|
+
logout: () => import("express").RequestHandler;
|
|
32
|
+
errorHandler: () => import("express").ErrorRequestHandler;
|
|
33
|
+
}>;
|
|
34
|
+
/**
|
|
35
|
+
* Crée l’intégration Express pour un moteur construit avec `createAuthKit()`.
|
|
36
|
+
*
|
|
37
|
+
* @example
|
|
38
|
+
* const expressAuth = createExpressAuth(auth);
|
|
39
|
+
* app.post("/login", express.json(), expressAuth.login());
|
|
40
|
+
* app.get("/profile", expressAuth.authenticate(), (req, res) => {
|
|
41
|
+
* res.json({ userId: req.auth.userId });
|
|
42
|
+
* });
|
|
43
|
+
*
|
|
44
|
+
* @param {import("../../factory/create-auth-kit.js").AuthKit} auth
|
|
45
|
+
* @param {ExpressAuthOptions} [options]
|
|
46
|
+
* @returns {ExpressAuth}
|
|
47
|
+
*/
|
|
48
|
+
export declare function createExpressAuth(auth: import("../../factory/create-auth-kit.js").AuthKit, options?: ExpressAuthOptions): ExpressAuth;
|
|
49
|
+
|
|
50
|
+
declare global {
|
|
51
|
+
namespace Express {
|
|
52
|
+
interface Request {
|
|
53
|
+
/** Utilisateur authentifié. Présent après `expressAuth.authenticate()`. */
|
|
54
|
+
auth?: import("../../core/contracts/auth-principal.js").AuthPrincipal;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Petites fonctions de vérification partagées par les validateurs de configuration.
|
|
3
|
+
*
|
|
4
|
+
* @module config/assertions
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* @param {unknown} value
|
|
8
|
+
* @returns {value is Record<string, unknown>}
|
|
9
|
+
*/
|
|
10
|
+
export declare function isPlainObject(value: unknown): value is Record<string, unknown>;
|
|
11
|
+
/**
|
|
12
|
+
* @param {unknown} value
|
|
13
|
+
* @param {string} name
|
|
14
|
+
* @returns {asserts value is Record<string, unknown>}
|
|
15
|
+
*/
|
|
16
|
+
export declare function assertPlainObject(value: unknown, name: string): asserts value is Record<string, unknown>;
|
|
17
|
+
/**
|
|
18
|
+
* Refuse les clés inconnues afin qu’une faute de frappe (par exemple
|
|
19
|
+
* `expiresIn` au lieu de `expiresInSeconds`) ne passe pas inaperçue.
|
|
20
|
+
*
|
|
21
|
+
* @param {Record<string, unknown>} object
|
|
22
|
+
* @param {readonly string[]} allowedKeys
|
|
23
|
+
* @param {string} name
|
|
24
|
+
*/
|
|
25
|
+
export declare function assertKnownKeys(object: Record<string, unknown>, allowedKeys: readonly string[], name: string): void;
|
|
26
|
+
/**
|
|
27
|
+
* @param {unknown} value
|
|
28
|
+
* @param {string} name
|
|
29
|
+
* @param {number} maxLength
|
|
30
|
+
* @returns {asserts value is string}
|
|
31
|
+
*/
|
|
32
|
+
export declare function assertNonEmptyString(value: unknown, name: string, maxLength: number): asserts value is string;
|
|
33
|
+
/**
|
|
34
|
+
* @param {unknown} value
|
|
35
|
+
* @param {string} name
|
|
36
|
+
* @param {number} min
|
|
37
|
+
* @param {number} max
|
|
38
|
+
* @returns {asserts value is number}
|
|
39
|
+
*/
|
|
40
|
+
export declare function assertIntegerInRange(value: unknown, name: string, min: number, max: number): asserts value is number;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export type ProviderOverrides = {
|
|
2
|
+
/**
|
|
3
|
+
* Remplace le fournisseur Argon2id. Les options `password` doivent alors être absentes.
|
|
4
|
+
*/
|
|
5
|
+
password?: import("../core/contracts/password-provider.js").PasswordProvider;
|
|
6
|
+
/**
|
|
7
|
+
* Remplace le fournisseur JWT. Les options `jwt` doivent alors être absentes.
|
|
8
|
+
*/
|
|
9
|
+
token?: import("../core/contracts/token-provider.js").TokenProvider;
|
|
10
|
+
};
|
|
11
|
+
export type AuthKitOptions = {
|
|
12
|
+
/**
|
|
13
|
+
* Dépôt d’utilisateurs de l’application.
|
|
14
|
+
*/
|
|
15
|
+
users: import("../core/contracts/user-repository.js").UserRepository;
|
|
16
|
+
/**
|
|
17
|
+
* Obligatoire sans `providers.token`.
|
|
18
|
+
*/
|
|
19
|
+
jwt?: import("./jwt-options.js").JwtOptions;
|
|
20
|
+
/**
|
|
21
|
+
* Paramètres de coût Argon2id.
|
|
22
|
+
*/
|
|
23
|
+
password?: import("./password-options.js").PasswordOptions;
|
|
24
|
+
/**
|
|
25
|
+
* Active les jetons de rafraîchissement (désactivés par défaut).
|
|
26
|
+
*/
|
|
27
|
+
refreshTokens?: import("./refresh-token-options.js").RefreshTokenOptions;
|
|
28
|
+
/**
|
|
29
|
+
* Implémentations personnalisées des fournisseurs.
|
|
30
|
+
*/
|
|
31
|
+
providers?: ProviderOverrides;
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* Valide la structure principale des options. Les valeurs imbriquées sont
|
|
35
|
+
* validées par leurs résolveurs et leurs vérifications de contrat dédiés.
|
|
36
|
+
*
|
|
37
|
+
* @param {unknown} options
|
|
38
|
+
* @returns {AuthKitOptions}
|
|
39
|
+
* @throws {ConfigurationError}
|
|
40
|
+
*/
|
|
41
|
+
export declare function validateAuthKitOptions(options: unknown): AuthKitOptions;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Valeurs sûres par défaut et limites acceptées.
|
|
3
|
+
*
|
|
4
|
+
* @module config/defaults
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Algorithmes JWT acceptés pour une signature par secret partagé, avec la taille
|
|
8
|
+
* minimale de clé exigée par la RFC 7518 §3.2.
|
|
9
|
+
*
|
|
10
|
+
* @type {Readonly<Record<string, number>>}
|
|
11
|
+
*/
|
|
12
|
+
export declare const JWT_ALGORITHM_MIN_KEY_BYTES: Readonly<Record<string, number>>;
|
|
13
|
+
export declare const JWT_DEFAULTS: Readonly<{
|
|
14
|
+
algorithm: "HS256";
|
|
15
|
+
/** 15 minutes : une courte durée réduit l’impact d’un jeton divulgué. */
|
|
16
|
+
expiresInSeconds: 900;
|
|
17
|
+
clockToleranceSeconds: 0;
|
|
18
|
+
}>;
|
|
19
|
+
export declare const JWT_LIMITS: Readonly<{
|
|
20
|
+
minExpiresInSeconds: 30;
|
|
21
|
+
/** Les jetons d’accès ne sont pas révocables : une durée de plus d’un jour est refusée. */
|
|
22
|
+
maxExpiresInSeconds: 86400;
|
|
23
|
+
maxClockToleranceSeconds: 60;
|
|
24
|
+
maxClaimLength: 256;
|
|
25
|
+
}>;
|
|
26
|
+
/**
|
|
27
|
+
* Valeurs Argon2id par défaut : « deuxième option recommandée » de la RFC 9106
|
|
28
|
+
* (64 Mio, t=3, p=4), plus forte que le minimum OWASP.
|
|
29
|
+
*/
|
|
30
|
+
export declare const ARGON2_DEFAULTS: Readonly<{
|
|
31
|
+
memoryCost: 65536;
|
|
32
|
+
timeCost: 3;
|
|
33
|
+
parallelism: 4;
|
|
34
|
+
}>;
|
|
35
|
+
/**
|
|
36
|
+
* Seuils en dessous desquels Argon2id est refusé : au moins 19 Mio de mémoire
|
|
37
|
+
* et 2 itérations, selon OWASP.
|
|
38
|
+
*/
|
|
39
|
+
export declare const ARGON2_LIMITS: Readonly<{
|
|
40
|
+
minMemoryCost: 19456;
|
|
41
|
+
maxMemoryCost: 4194304;
|
|
42
|
+
minTimeCost: 2;
|
|
43
|
+
maxTimeCost: 20;
|
|
44
|
+
minParallelism: 1;
|
|
45
|
+
maxParallelism: 16;
|
|
46
|
+
}>;
|
|
47
|
+
export declare const REFRESH_TOKEN_DEFAULTS: Readonly<{
|
|
48
|
+
/** 14 jours : l’utilisateur reste connecté s’il revient au moins toutes les deux semaines. */
|
|
49
|
+
expiresInSeconds: 1209600;
|
|
50
|
+
}>;
|
|
51
|
+
export declare const REFRESH_TOKEN_LIMITS: Readonly<{
|
|
52
|
+
minExpiresInSeconds: 300;
|
|
53
|
+
/** 90 jours au plus : au-delà, une session oubliée reste exploitable trop longtemps. */
|
|
54
|
+
maxExpiresInSeconds: 7776000;
|
|
55
|
+
}>;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export type JwtOptions = {
|
|
2
|
+
/**
|
|
3
|
+
* Clé de signature partagée produite par un générateur sûr (≥ 32 octets pour HS256). Une valeur absente (par exemple une variable d’environnement non définie) est acceptée par les types mais arrête createAuthKit() avec un message clair.
|
|
4
|
+
*/
|
|
5
|
+
secret: string | Uint8Array | undefined;
|
|
6
|
+
/**
|
|
7
|
+
* Valeur de la revendication `iss`, contrôlée à la vérification.
|
|
8
|
+
*/
|
|
9
|
+
issuer: string;
|
|
10
|
+
/**
|
|
11
|
+
* Valeur de la revendication `aud`, contrôlée à la vérification.
|
|
12
|
+
*/
|
|
13
|
+
audience: string;
|
|
14
|
+
/**
|
|
15
|
+
* Durée de vie du jeton (30 à 86 400 secondes).
|
|
16
|
+
*/
|
|
17
|
+
expiresInSeconds?: number;
|
|
18
|
+
/**
|
|
19
|
+
* Seul algorithme accepté à la vérification.
|
|
20
|
+
*/
|
|
21
|
+
algorithm?: "HS256" | "HS384" | "HS512";
|
|
22
|
+
/**
|
|
23
|
+
* Décalage d’horloge accepté entre instances (0 à 60 secondes).
|
|
24
|
+
*/
|
|
25
|
+
clockToleranceSeconds?: number;
|
|
26
|
+
};
|
|
27
|
+
export type ResolvedJwtOptions = Readonly<{
|
|
28
|
+
key: Uint8Array;
|
|
29
|
+
issuer: string;
|
|
30
|
+
audience: string;
|
|
31
|
+
expiresInSeconds: number;
|
|
32
|
+
algorithm: string;
|
|
33
|
+
clockToleranceSeconds: number;
|
|
34
|
+
}>;
|
|
35
|
+
/**
|
|
36
|
+
* Valide les options JWT, applique des valeurs sûres et refuse les valeurs dangereuses.
|
|
37
|
+
*
|
|
38
|
+
* @param {unknown} options
|
|
39
|
+
* @returns {ResolvedJwtOptions}
|
|
40
|
+
* @throws {ConfigurationError}
|
|
41
|
+
*/
|
|
42
|
+
export declare function resolveJwtOptions(options: unknown): ResolvedJwtOptions;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export type PasswordOptions = {
|
|
2
|
+
/**
|
|
3
|
+
* Mémoire en Kio (minimum 19 456, soit 19 Mio).
|
|
4
|
+
*/
|
|
5
|
+
memoryCost?: number;
|
|
6
|
+
/**
|
|
7
|
+
* Nombre d’itérations (minimum 2).
|
|
8
|
+
*/
|
|
9
|
+
timeCost?: number;
|
|
10
|
+
/**
|
|
11
|
+
* Niveau de parallélisme (1 à 16).
|
|
12
|
+
*/
|
|
13
|
+
parallelism?: number;
|
|
14
|
+
};
|
|
15
|
+
export type ResolvedPasswordOptions = Readonly<Required<PasswordOptions>>;
|
|
16
|
+
/**
|
|
17
|
+
* Valide les options Argon2id et refuse les paramètres sous le seuil OWASP.
|
|
18
|
+
*
|
|
19
|
+
* @param {unknown} [options]
|
|
20
|
+
* @returns {ResolvedPasswordOptions}
|
|
21
|
+
* @throws {import("../core/errors.js").ConfigurationError}
|
|
22
|
+
*/
|
|
23
|
+
export declare function resolvePasswordOptions(options?: unknown): ResolvedPasswordOptions;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export type RefreshTokenOptions = {
|
|
2
|
+
/**
|
|
3
|
+
* Stockage des jetons (votre base de données, ou `createInMemoryRefreshTokenStore()` en développement).
|
|
4
|
+
*/
|
|
5
|
+
store: import("../core/contracts/refresh-token-store.js").RefreshTokenStore;
|
|
6
|
+
/**
|
|
7
|
+
* Durée de vie d’un jeton de rafraîchissement
|
|
8
|
+
* (14 jours par défaut, de 5 minutes à 90 jours). Chaque rotation repart de zéro.
|
|
9
|
+
*/
|
|
10
|
+
expiresInSeconds?: number;
|
|
11
|
+
};
|
|
12
|
+
export type ResolvedRefreshTokenOptions = Readonly<{
|
|
13
|
+
store: import("../core/contracts/refresh-token-store.js").RefreshTokenStore;
|
|
14
|
+
expiresInSeconds: number;
|
|
15
|
+
}>;
|
|
16
|
+
/**
|
|
17
|
+
* @param {unknown} options
|
|
18
|
+
* @returns {ResolvedRefreshTokenOptions}
|
|
19
|
+
* @throws {ConfigurationError | import("../core/errors.js").ContractViolationError}
|
|
20
|
+
*/
|
|
21
|
+
export declare function resolveRefreshTokenOptions(options: unknown): ResolvedRefreshTokenOptions;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @param {unknown} secret
|
|
3
|
+
* @param {number} minBytes
|
|
4
|
+
* @returns {Uint8Array} Secret encodé en octets, prêt pour le fournisseur JWT.
|
|
5
|
+
* @throws {ConfigurationError}
|
|
6
|
+
*/
|
|
7
|
+
export declare function assertStrongSecret(secret: unknown, minBytes: number): Uint8Array;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export type UserRepository = import("./contracts/user-repository.js").UserRepository;
|
|
2
|
+
export type PasswordProvider = import("./contracts/password-provider.js").PasswordProvider;
|
|
3
|
+
export type TokenProvider = import("./contracts/token-provider.js").TokenProvider;
|
|
4
|
+
export type AuthenticationService = import("./contracts/authentication-service.js").AuthenticationService;
|
|
5
|
+
export type LoginCredentials = import("./contracts/authentication-service.js").LoginCredentials;
|
|
6
|
+
export type LoginResult = import("./contracts/authentication-service.js").LoginResult;
|
|
7
|
+
export type AuthPrincipal = import("./contracts/auth-principal.js").AuthPrincipal;
|
|
8
|
+
/**
|
|
9
|
+
* @typedef {import("./contracts/user-repository.js").UserRepository} UserRepository
|
|
10
|
+
* @typedef {import("./contracts/password-provider.js").PasswordProvider} PasswordProvider
|
|
11
|
+
* @typedef {import("./contracts/token-provider.js").TokenProvider} TokenProvider
|
|
12
|
+
* @typedef {import("./contracts/authentication-service.js").AuthenticationService} AuthenticationService
|
|
13
|
+
* @typedef {import("./contracts/authentication-service.js").LoginCredentials} LoginCredentials
|
|
14
|
+
* @typedef {import("./contracts/authentication-service.js").LoginResult} LoginResult
|
|
15
|
+
* @typedef {import("./contracts/auth-principal.js").AuthPrincipal} AuthPrincipal
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* Crée le service d’authentification indépendant du framework.
|
|
19
|
+
*
|
|
20
|
+
* La fabrique doit avoir validé les dépendances au préalable. Ce service ne
|
|
21
|
+
* conserve aucun état utilisateur : il peut être partagé entre les requêtes
|
|
22
|
+
* et entre plusieurs processus derrière un répartiteur de charge.
|
|
23
|
+
*
|
|
24
|
+
* @param {object} dependencies
|
|
25
|
+
* @param {UserRepository} dependencies.users
|
|
26
|
+
* @param {PasswordProvider} dependencies.passwordProvider
|
|
27
|
+
* @param {TokenProvider} dependencies.tokenProvider
|
|
28
|
+
* @param {ReturnType<typeof import("./refresh-token-service.js").createRefreshTokenService>} [dependencies.refreshTokens]
|
|
29
|
+
* Service des jetons de rafraîchissement ; absent si la fonctionnalité n’est pas activée.
|
|
30
|
+
* @returns {AuthenticationService}
|
|
31
|
+
*/
|
|
32
|
+
export declare function createAuthenticationService({ users, passwordProvider, tokenProvider, refreshTokens }: {
|
|
33
|
+
users: UserRepository;
|
|
34
|
+
passwordProvider: PasswordProvider;
|
|
35
|
+
tokenProvider: TokenProvider;
|
|
36
|
+
refreshTokens?: ReturnType<typeof import("./refresh-token-service.js").createRefreshTokenService>;
|
|
37
|
+
}): AuthenticationService;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export type AuthorizationRequirement = string | {
|
|
2
|
+
anyRole?: string[];
|
|
3
|
+
allPermissions?: string[];
|
|
4
|
+
check?: (principal: import("./contracts/auth-principal.js").AuthPrincipal, context?: unknown) => boolean | Promise<boolean>;
|
|
5
|
+
};
|
|
6
|
+
export type NormalizedAuthorizationRequirement = Readonly<{
|
|
7
|
+
anyRole: readonly string[];
|
|
8
|
+
allPermissions: readonly string[];
|
|
9
|
+
check: ((principal: import("./contracts/auth-principal.js").AuthPrincipal, context?: unknown) => boolean | Promise<boolean>) | undefined;
|
|
10
|
+
}>;
|
|
11
|
+
/**
|
|
12
|
+
* Valide une règle d’accès au démarrage, pour qu’une faute (rôle vide, clé
|
|
13
|
+
* mal orthographiée…) soit signalée avant la première requête.
|
|
14
|
+
*
|
|
15
|
+
* @param {unknown} requirement
|
|
16
|
+
* @returns {NormalizedAuthorizationRequirement}
|
|
17
|
+
* @throws {ConfigurationError}
|
|
18
|
+
*/
|
|
19
|
+
export declare function normalizeAuthorizationRequirement(requirement: unknown): NormalizedAuthorizationRequirement;
|
|
20
|
+
/**
|
|
21
|
+
* Indique si un utilisateur authentifié respecte une règle d’accès.
|
|
22
|
+
*
|
|
23
|
+
* - `anyRole` : l’utilisateur doit avoir **au moins un** de ces rôles ;
|
|
24
|
+
* - `allPermissions` : il doit avoir **toutes** ces permissions ;
|
|
25
|
+
* - `check` : fonction personnalisée, appelée en dernier, avec `context`
|
|
26
|
+
* (la requête HTTP avec l’adaptateur Express).
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* await isAuthorized(principal, "admin");
|
|
30
|
+
* await isAuthorized(principal, { allPermissions: ["orders:write"] });
|
|
31
|
+
* await isAuthorized(principal, { check: (user, req) => user.userId === req.params.id }, req);
|
|
32
|
+
*
|
|
33
|
+
* @param {import("./contracts/auth-principal.js").AuthPrincipal} principal
|
|
34
|
+
* @param {AuthorizationRequirement | NormalizedAuthorizationRequirement} requirement
|
|
35
|
+
* @param {unknown} [context] Information transmise à `check` (par exemple la requête).
|
|
36
|
+
* @returns {Promise<boolean>}
|
|
37
|
+
* @throws {ConfigurationError} Si la règle est invalide.
|
|
38
|
+
* @throws {ContractViolationError} Si `check` ne renvoie pas un booléen.
|
|
39
|
+
*/
|
|
40
|
+
export declare function isAuthorized(principal: import("./contracts/auth-principal.js").AuthPrincipal, requirement: AuthorizationRequirement | NormalizedAuthorizationRequirement, context?: unknown): Promise<boolean>;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vérifie que `candidate` expose toutes les méthodes de `methodNames`.
|
|
3
|
+
*
|
|
4
|
+
* JavaScript n’a pas d’interfaces : les contrats sont vérifiés par leur forme
|
|
5
|
+
* au démarrage afin de signaler clairement une dépendance mal configurée.
|
|
6
|
+
*
|
|
7
|
+
* @param {unknown} candidate Dépendance injectée.
|
|
8
|
+
* @param {string} contractName Nom utilisé dans les messages d’erreur.
|
|
9
|
+
* @param {readonly string[]} methodNames Méthodes exigées par le contrat.
|
|
10
|
+
* @returns {void}
|
|
11
|
+
* @throws {ContractViolationError}
|
|
12
|
+
*/
|
|
13
|
+
export declare function assertImplements(candidate: unknown, contractName: string, methodNames: readonly string[]): void;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Identité validée d’un utilisateur authentifié.
|
|
3
|
+
*
|
|
4
|
+
* Le principal ne contient que des données non sensibles issues d’un jeton
|
|
5
|
+
* vérifié. Il ne contient jamais le hash du mot de passe ni le jeton brut.
|
|
6
|
+
* Il est figé pour empêcher les middlewares suivants de le modifier.
|
|
7
|
+
*
|
|
8
|
+
* Les rôles et permissions viennent du repository, relu à chaque requête :
|
|
9
|
+
* ils ne sont jamais écrits dans le jeton, et un changement de rôle
|
|
10
|
+
* s’applique immédiatement.
|
|
11
|
+
*
|
|
12
|
+
* @typedef {Readonly<{
|
|
13
|
+
* userId: string,
|
|
14
|
+
* tokenId: string,
|
|
15
|
+
* issuedAt: number,
|
|
16
|
+
* expiresAt: number,
|
|
17
|
+
* roles: readonly string[],
|
|
18
|
+
* permissions: readonly string[]
|
|
19
|
+
* }>} AuthPrincipal
|
|
20
|
+
*/
|
|
21
|
+
export type AuthPrincipal = Readonly<{
|
|
22
|
+
userId: string;
|
|
23
|
+
tokenId: string;
|
|
24
|
+
issuedAt: number;
|
|
25
|
+
expiresAt: number;
|
|
26
|
+
roles: readonly string[];
|
|
27
|
+
permissions: readonly string[];
|
|
28
|
+
}>;
|
|
29
|
+
/**
|
|
30
|
+
* @param {import("./token-provider.js").VerifiedTokenClaims | import("./token-provider.js").IssuedToken} claims
|
|
31
|
+
* @param {{ id: string, roles: readonly string[], permissions: readonly string[] }} user
|
|
32
|
+
* @returns {AuthPrincipal}
|
|
33
|
+
*/
|
|
34
|
+
export declare function createAuthPrincipal(claims: import("./token-provider.js").VerifiedTokenClaims | import("./token-provider.js").IssuedToken, user: {
|
|
35
|
+
id: string;
|
|
36
|
+
roles: readonly string[];
|
|
37
|
+
permissions: readonly string[];
|
|
38
|
+
}): AuthPrincipal;
|