@nodefony/security 10.0.0-alpha.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/LICENSE +544 -0
- package/README.md +182 -0
- package/dist/_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorate.js +9 -0
- package/dist/_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorateMetadata.js +6 -0
- package/dist/index.js +151 -0
- package/dist/nodefony/command/security-secrets.js +158 -0
- package/dist/nodefony/command/security-token.js +335 -0
- package/dist/nodefony/command/security-user-add.js +131 -0
- package/dist/nodefony/command/security-user-delete.js +102 -0
- package/dist/nodefony/command/security-user-list.js +77 -0
- package/dist/nodefony/config/config.js +366 -0
- package/dist/nodefony/config/defineModuleConfig.js +35 -0
- package/dist/nodefony/contracts/IAccessVoter.js +13 -0
- package/dist/nodefony/contracts/IApiKey.js +1 -0
- package/dist/nodefony/contracts/IAuditEvent.js +1 -0
- package/dist/nodefony/contracts/IAuditStore.js +1 -0
- package/dist/nodefony/contracts/IAuthenticator.js +1 -0
- package/dist/nodefony/contracts/IAuthorizationService.js +1 -0
- package/dist/nodefony/contracts/IFirewall.js +1 -0
- package/dist/nodefony/contracts/IFirewallDescription.js +1 -0
- package/dist/nodefony/contracts/IJwtKeystore.js +1 -0
- package/dist/nodefony/contracts/IOAuthProvider.js +1 -0
- package/dist/nodefony/contracts/ISecuredArea.js +1 -0
- package/dist/nodefony/contracts/IToken.js +1 -0
- package/dist/nodefony/contracts/ITokenStore.js +1 -0
- package/dist/nodefony/contracts/ITotpSecret.js +1 -0
- package/dist/nodefony/contracts/ITotpSecretStore.js +1 -0
- package/dist/nodefony/contracts/IWebAuthnCredential.js +1 -0
- package/dist/nodefony/contracts/IWebAuthnCredentialStore.js +1 -0
- package/dist/nodefony/contracts/IWebhookEndpoint.js +1 -0
- package/dist/nodefony/contracts/IWebhookStore.js +1 -0
- package/dist/nodefony/contracts/index.js +2 -0
- package/dist/nodefony/errors/AccessDeniedError.js +14 -0
- package/dist/nodefony/errors/ApiKeyError.js +21 -0
- package/dist/nodefony/errors/AuthenticationError.js +14 -0
- package/dist/nodefony/errors/CsrfError.js +23 -0
- package/dist/nodefony/errors/InvalidTargetError.js +39 -0
- package/dist/nodefony/errors/SsrfError.js +17 -0
- package/dist/nodefony/errors/ThrottledError.js +21 -0
- package/dist/nodefony/errors/UnverifiableTokenError.js +42 -0
- package/dist/nodefony/errors/WebAuthnError.js +21 -0
- package/dist/nodefony/errors/index.js +9 -0
- package/dist/nodefony/service/accessTokenVerifier.js +77 -0
- package/dist/nodefony/service/apiKeys.js +310 -0
- package/dist/nodefony/service/auditService.js +145 -0
- package/dist/nodefony/service/authFlow.js +332 -0
- package/dist/nodefony/service/authorization.js +95 -0
- package/dist/nodefony/service/cors.js +81 -0
- package/dist/nodefony/service/csrf.js +97 -0
- package/dist/nodefony/service/firewall.js +699 -0
- package/dist/nodefony/service/oauth2.js +153 -0
- package/dist/nodefony/service/securityHeaders.js +80 -0
- package/dist/nodefony/service/tokenService.js +486 -0
- package/dist/nodefony/service/totp.js +209 -0
- package/dist/nodefony/service/webAuthn.js +343 -0
- package/dist/nodefony/service/webhooks.js +539 -0
- package/dist/nodefony/src/RoleHierarchyWalker.js +77 -0
- package/dist/nodefony/src/SecuredArea.js +51 -0
- package/dist/nodefony/src/admin/SecurityAdminApi.js +495 -0
- package/dist/nodefony/src/admin/WebhookAdminApi.js +378 -0
- package/dist/nodefony/src/admin/adminAudit.js +37 -0
- package/dist/nodefony/src/admin/userRevocationCascade.js +40 -0
- package/dist/nodefony/src/apikey/apiKeyFormat.js +107 -0
- package/dist/nodefony/src/audit/MemoryAuditStore.js +121 -0
- package/dist/nodefony/src/audit/auditBridge.js +82 -0
- package/dist/nodefony/src/audit/auditFilters.js +60 -0
- package/dist/nodefony/src/audit/auditStoreRegistry.js +25 -0
- package/dist/nodefony/src/audit/readAuditContext.js +24 -0
- package/dist/nodefony/src/audit/recordAudit.js +16 -0
- package/dist/nodefony/src/authenticator/AnonymousAuthenticator.js +36 -0
- package/dist/nodefony/src/authenticator/ApiKeyAuthenticator.js +164 -0
- package/dist/nodefony/src/authenticator/ExternalJwtAuthenticator.js +224 -0
- package/dist/nodefony/src/authenticator/FirewallRealtimeAuthenticator.js +174 -0
- package/dist/nodefony/src/authenticator/JwtAuthenticator.js +176 -0
- package/dist/nodefony/src/authenticator/SessionAuthenticator.js +92 -0
- package/dist/nodefony/src/authenticator/UserPasswordAuthenticator.js +95 -0
- package/dist/nodefony/src/authenticator/authenticatorRegistry.js +63 -0
- package/dist/nodefony/src/authenticator/bearer.js +2 -0
- package/dist/nodefony/src/authenticator/externalSubject.js +36 -0
- package/dist/nodefony/src/authenticator/peekIssuer.js +56 -0
- package/dist/nodefony/src/crypto/secretCipher.js +79 -0
- package/dist/nodefony/src/csp.js +54 -0
- package/dist/nodefony/src/csrfToken.js +65 -0
- package/dist/nodefony/src/net/ssrfGuard.js +130 -0
- package/dist/nodefony/src/oauth/oauthProviderRegistry.js +37 -0
- package/dist/nodefony/src/oauth/providers/github.js +65 -0
- package/dist/nodefony/src/oauth/providers/oidc.js +48 -0
- package/dist/nodefony/src/realtime/UserRealtimeToken.js +94 -0
- package/dist/nodefony/src/realtime/frameAuthorizer.js +279 -0
- package/dist/nodefony/src/realtime/realtimeContracts.js +1 -0
- package/dist/nodefony/src/sessionIdentity.js +35 -0
- package/dist/nodefony/src/throttle/LoginThrottler.js +97 -0
- package/dist/nodefony/src/token/AnonymousToken.js +40 -0
- package/dist/nodefony/src/token/JwtKeystore.js +160 -0
- package/dist/nodefony/src/token/MemoryTokenStore.js +236 -0
- package/dist/nodefony/src/token/RemoteJwtVerifier.js +231 -0
- package/dist/nodefony/src/token/UserToken.js +67 -0
- package/dist/nodefony/src/token/jwtRuntime.js +19 -0
- package/dist/nodefony/src/token/secretFile.js +134 -0
- package/dist/nodefony/src/token/tokenCriteria.js +35 -0
- package/dist/nodefony/src/token/tokenFilters.js +72 -0
- package/dist/nodefony/src/token/tokenSort.js +40 -0
- package/dist/nodefony/src/token/tokenStatus.js +35 -0
- package/dist/nodefony/src/token/tokenStoreRegistry.js +25 -0
- package/dist/nodefony/src/totp/MemoryTotpSecretStore.js +97 -0
- package/dist/nodefony/src/totp/totpCipher.js +30 -0
- package/dist/nodefony/src/totp/totpCrypto.js +226 -0
- package/dist/nodefony/src/totp/totpOperations.js +129 -0
- package/dist/nodefony/src/totp/totpSecretStoreRegistry.js +18 -0
- package/dist/nodefony/src/voter/RoleVoter.js +32 -0
- package/dist/nodefony/src/voter/ScopeVoter.js +52 -0
- package/dist/nodefony/src/voter/voterRegistry.js +20 -0
- package/dist/nodefony/src/webauthn/MemoryWebAuthnCredentialStore.js +121 -0
- package/dist/nodefony/src/webauthn/webAuthnCredentialStoreRegistry.js +18 -0
- package/dist/nodefony/src/webhook/MemoryWebhookStore.js +87 -0
- package/dist/nodefony/src/webhook/WebhookDispatcher.js +208 -0
- package/dist/nodefony/src/webhook/webhookCipher.js +27 -0
- package/dist/nodefony/src/webhook/webhookDelivery.js +102 -0
- package/dist/nodefony/src/webhook/webhookFilters.js +56 -0
- package/dist/nodefony/src/webhook/webhookSignature.js +51 -0
- package/dist/nodefony/src/webhook/webhookSort.js +48 -0
- package/dist/nodefony/src/webhook/webhookStoreRegistry.js +18 -0
- package/dist/types/index.d.ts +157 -0
- package/dist/types/nodefony/command/security-secrets.d.ts +24 -0
- package/dist/types/nodefony/command/security-token.d.ts +44 -0
- package/dist/types/nodefony/command/security-user-add.d.ts +28 -0
- package/dist/types/nodefony/command/security-user-delete.d.ts +25 -0
- package/dist/types/nodefony/command/security-user-list.d.ts +28 -0
- package/dist/types/nodefony/config/config.d.ts +295 -0
- package/dist/types/nodefony/config/defineModuleConfig.d.ts +27 -0
- package/dist/types/nodefony/contracts/IAccessVoter.d.ts +23 -0
- package/dist/types/nodefony/contracts/IApiKey.d.ts +75 -0
- package/dist/types/nodefony/contracts/IAuditEvent.d.ts +94 -0
- package/dist/types/nodefony/contracts/IAuditStore.d.ts +80 -0
- package/dist/types/nodefony/contracts/IAuthenticator.d.ts +66 -0
- package/dist/types/nodefony/contracts/IAuthorizationService.d.ts +28 -0
- package/dist/types/nodefony/contracts/IFirewall.d.ts +64 -0
- package/dist/types/nodefony/contracts/IFirewallDescription.d.ts +120 -0
- package/dist/types/nodefony/contracts/IJwtKeystore.d.ts +40 -0
- package/dist/types/nodefony/contracts/IOAuthProvider.d.ts +51 -0
- package/dist/types/nodefony/contracts/ISecuredArea.d.ts +57 -0
- package/dist/types/nodefony/contracts/IToken.d.ts +41 -0
- package/dist/types/nodefony/contracts/ITokenStore.d.ts +240 -0
- package/dist/types/nodefony/contracts/ITotpSecret.d.ts +41 -0
- package/dist/types/nodefony/contracts/ITotpSecretStore.d.ts +88 -0
- package/dist/types/nodefony/contracts/IWebAuthnCredential.d.ts +56 -0
- package/dist/types/nodefony/contracts/IWebAuthnCredentialStore.d.ts +118 -0
- package/dist/types/nodefony/contracts/IWebhookEndpoint.d.ts +82 -0
- package/dist/types/nodefony/contracts/IWebhookStore.d.ts +85 -0
- package/dist/types/nodefony/contracts/index.d.ts +9 -0
- package/dist/types/nodefony/errors/AccessDeniedError.d.ts +10 -0
- package/dist/types/nodefony/errors/ApiKeyError.d.ts +17 -0
- package/dist/types/nodefony/errors/AuthenticationError.d.ts +10 -0
- package/dist/types/nodefony/errors/CsrfError.d.ts +19 -0
- package/dist/types/nodefony/errors/InvalidTargetError.d.ts +34 -0
- package/dist/types/nodefony/errors/SsrfError.d.ts +13 -0
- package/dist/types/nodefony/errors/ThrottledError.d.ts +16 -0
- package/dist/types/nodefony/errors/UnverifiableTokenError.d.ts +37 -0
- package/dist/types/nodefony/errors/WebAuthnError.d.ts +17 -0
- package/dist/types/nodefony/errors/index.d.ts +8 -0
- package/dist/types/nodefony/service/accessTokenVerifier.d.ts +29 -0
- package/dist/types/nodefony/service/apiKeys.d.ts +103 -0
- package/dist/types/nodefony/service/auditService.d.ts +30 -0
- package/dist/types/nodefony/service/authFlow.d.ts +123 -0
- package/dist/types/nodefony/service/authorization.d.ts +33 -0
- package/dist/types/nodefony/service/cors.d.ts +48 -0
- package/dist/types/nodefony/service/csrf.d.ts +57 -0
- package/dist/types/nodefony/service/firewall.d.ts +148 -0
- package/dist/types/nodefony/service/oauth2.d.ts +66 -0
- package/dist/types/nodefony/service/securityHeaders.d.ts +66 -0
- package/dist/types/nodefony/service/tokenService.d.ts +103 -0
- package/dist/types/nodefony/service/totp.d.ts +58 -0
- package/dist/types/nodefony/service/webAuthn.d.ts +123 -0
- package/dist/types/nodefony/service/webhooks.d.ts +160 -0
- package/dist/types/nodefony/src/RoleHierarchyWalker.d.ts +21 -0
- package/dist/types/nodefony/src/SecuredArea.d.ts +31 -0
- package/dist/types/nodefony/src/admin/SecurityAdminApi.d.ts +82 -0
- package/dist/types/nodefony/src/admin/WebhookAdminApi.d.ts +30 -0
- package/dist/types/nodefony/src/admin/adminAudit.d.ts +27 -0
- package/dist/types/nodefony/src/admin/userRevocationCascade.d.ts +31 -0
- package/dist/types/nodefony/src/apikey/apiKeyFormat.d.ts +43 -0
- package/dist/types/nodefony/src/audit/MemoryAuditStore.d.ts +33 -0
- package/dist/types/nodefony/src/audit/auditBridge.d.ts +49 -0
- package/dist/types/nodefony/src/audit/auditFilters.d.ts +56 -0
- package/dist/types/nodefony/src/audit/auditStoreRegistry.d.ts +37 -0
- package/dist/types/nodefony/src/audit/readAuditContext.d.ts +17 -0
- package/dist/types/nodefony/src/audit/recordAudit.d.ts +13 -0
- package/dist/types/nodefony/src/authenticator/AnonymousAuthenticator.d.ts +26 -0
- package/dist/types/nodefony/src/authenticator/ApiKeyAuthenticator.d.ts +74 -0
- package/dist/types/nodefony/src/authenticator/ExternalJwtAuthenticator.d.ts +132 -0
- package/dist/types/nodefony/src/authenticator/FirewallRealtimeAuthenticator.d.ts +78 -0
- package/dist/types/nodefony/src/authenticator/JwtAuthenticator.d.ts +69 -0
- package/dist/types/nodefony/src/authenticator/SessionAuthenticator.d.ts +70 -0
- package/dist/types/nodefony/src/authenticator/UserPasswordAuthenticator.d.ts +53 -0
- package/dist/types/nodefony/src/authenticator/authenticatorRegistry.d.ts +39 -0
- package/dist/types/nodefony/src/authenticator/bearer.d.ts +22 -0
- package/dist/types/nodefony/src/authenticator/externalSubject.d.ts +27 -0
- package/dist/types/nodefony/src/authenticator/peekIssuer.d.ts +31 -0
- package/dist/types/nodefony/src/crypto/secretCipher.d.ts +31 -0
- package/dist/types/nodefony/src/csp.d.ts +39 -0
- package/dist/types/nodefony/src/csrfToken.d.ts +36 -0
- package/dist/types/nodefony/src/net/ssrfGuard.d.ts +43 -0
- package/dist/types/nodefony/src/oauth/oauthProviderRegistry.d.ts +45 -0
- package/dist/types/nodefony/src/oauth/providers/github.d.ts +9 -0
- package/dist/types/nodefony/src/oauth/providers/oidc.d.ts +35 -0
- package/dist/types/nodefony/src/realtime/UserRealtimeToken.d.ts +62 -0
- package/dist/types/nodefony/src/realtime/frameAuthorizer.d.ts +171 -0
- package/dist/types/nodefony/src/realtime/realtimeContracts.d.ts +139 -0
- package/dist/types/nodefony/src/sessionIdentity.d.ts +20 -0
- package/dist/types/nodefony/src/throttle/LoginThrottler.d.ts +68 -0
- package/dist/types/nodefony/src/token/AnonymousToken.d.ts +23 -0
- package/dist/types/nodefony/src/token/JwtKeystore.d.ts +43 -0
- package/dist/types/nodefony/src/token/MemoryTokenStore.d.ts +66 -0
- package/dist/types/nodefony/src/token/RemoteJwtVerifier.d.ts +149 -0
- package/dist/types/nodefony/src/token/UserToken.d.ts +41 -0
- package/dist/types/nodefony/src/token/jwtRuntime.d.ts +28 -0
- package/dist/types/nodefony/src/token/secretFile.d.ts +70 -0
- package/dist/types/nodefony/src/token/tokenCriteria.d.ts +20 -0
- package/dist/types/nodefony/src/token/tokenFilters.d.ts +76 -0
- package/dist/types/nodefony/src/token/tokenSort.d.ts +33 -0
- package/dist/types/nodefony/src/token/tokenStatus.d.ts +38 -0
- package/dist/types/nodefony/src/token/tokenStoreRegistry.d.ts +38 -0
- package/dist/types/nodefony/src/totp/MemoryTotpSecretStore.d.ts +43 -0
- package/dist/types/nodefony/src/totp/totpCipher.d.ts +9 -0
- package/dist/types/nodefony/src/totp/totpCrypto.d.ts +164 -0
- package/dist/types/nodefony/src/totp/totpOperations.d.ts +73 -0
- package/dist/types/nodefony/src/totp/totpSecretStoreRegistry.d.ts +27 -0
- package/dist/types/nodefony/src/voter/RoleVoter.d.ts +25 -0
- package/dist/types/nodefony/src/voter/ScopeVoter.d.ts +30 -0
- package/dist/types/nodefony/src/voter/voterRegistry.d.ts +33 -0
- package/dist/types/nodefony/src/webauthn/MemoryWebAuthnCredentialStore.d.ts +39 -0
- package/dist/types/nodefony/src/webauthn/webAuthnCredentialStoreRegistry.d.ts +26 -0
- package/dist/types/nodefony/src/webhook/MemoryWebhookStore.d.ts +37 -0
- package/dist/types/nodefony/src/webhook/WebhookDispatcher.d.ts +69 -0
- package/dist/types/nodefony/src/webhook/webhookCipher.d.ts +8 -0
- package/dist/types/nodefony/src/webhook/webhookDelivery.d.ts +28 -0
- package/dist/types/nodefony/src/webhook/webhookFilters.d.ts +64 -0
- package/dist/types/nodefony/src/webhook/webhookSignature.d.ts +20 -0
- package/dist/types/nodefony/src/webhook/webhookSort.d.ts +39 -0
- package/dist/types/nodefony/src/webhook/webhookStoreRegistry.d.ts +31 -0
- package/docs/api-keys.md +691 -0
- package/docs/audit.md +751 -0
- package/docs/authenticators.md +487 -0
- package/docs/authorization.md +497 -0
- package/docs/cors.md +497 -0
- package/docs/csrf.md +392 -0
- package/docs/external-jwt.md +181 -0
- package/docs/firewall.md +546 -0
- package/docs/headers.md +616 -0
- package/docs/index.md +207 -0
- package/docs/lexique.md +190 -0
- package/docs/oauth2.md +575 -0
- package/docs/obtenir-un-jeton.md +225 -0
- package/docs/tokens.md +520 -0
- package/docs/totp.md +804 -0
- package/docs/webauthn.md +733 -0
- package/docs/webhooks.md +1016 -0
- package/package.json +83 -0
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
import { AuthenticationError } from "../errors/AuthenticationError.js";
|
|
2
|
+
import { defineSecurityConfig } from "../config/defineModuleConfig.js";
|
|
3
|
+
import { WebAuthnError } from "../errors/WebAuthnError.js";
|
|
4
|
+
import { getWebAuthnStoreFactory, listWebAuthnStores } from "../src/webauthn/webAuthnCredentialStoreRegistry.js";
|
|
5
|
+
import { AUTO_STORE, EMPTY_INFRA, Service, deriveStoreBackend, readStoreLocation, resolveAutoStore } from "nodefony";
|
|
6
|
+
import { Buffer } from "node:buffer";
|
|
7
|
+
//#region nodefony/service/webAuthn.ts
|
|
8
|
+
const serviceName = "webauthn";
|
|
9
|
+
function isFlushable(s) {
|
|
10
|
+
return s !== null && typeof s.flushNow === "function";
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* **WebAuthn / passkeys** (P6 J9) — orchestrateur des deux cérémonies FIDO2
|
|
14
|
+
* (WebAuthn L3 §7.1 enregistrement, §7.2 authentification).
|
|
15
|
+
*
|
|
16
|
+
* MFA **phishing-resistant** : la clé privée ne quitte jamais l'authenticator
|
|
17
|
+
* (Touch ID, Windows Hello, clé FIDO). Le serveur ne manipule QUE des clés
|
|
18
|
+
* publiques + vérifie des signatures. La vérification cryptographique (parsing
|
|
19
|
+
* CBOR/COSE, signatures ES256/RS256/EdDSA) est déléguée à `@simplewebauthn/server`
|
|
20
|
+
* (lib auditée de l'écosystème), **importée paresseusement** au 1ᵉʳ usage (cold
|
|
21
|
+
* path — l'enregistrement/login n'est pas le hot path par requête).
|
|
22
|
+
*
|
|
23
|
+
* Au boot (si `passkeys.enabled`) : résout le RP (rpID/rpName/origines depuis la
|
|
24
|
+
* config, sinon le domaine de l'app) + le store de credentials pluggable
|
|
25
|
+
* (`webAuthnCredentialStore` du container, sinon le builtin mémoire) et le pose
|
|
26
|
+
* au container — une application peut donc fournir le sien avant le boot.
|
|
27
|
+
*
|
|
28
|
+
* Il n'existe **pas** d'authenticator passkey : `WebAuthnController`
|
|
29
|
+
* (`@nodefony/framework`) mène les deux cérémonies via ce service, puis
|
|
30
|
+
* `AuthFlow.establishSessionFor()` ouvre la session BFF ; les requêtes
|
|
31
|
+
* suivantes sont ré-authentifiées par `SessionAuthenticator`.
|
|
32
|
+
*
|
|
33
|
+
* **Anti-rejeu** : le challenge serveur est porté HORS de ce service (en session
|
|
34
|
+
* BFF par le controller) ; chaque `verify*` reçoit le `expectedChallenge` qu'il
|
|
35
|
+
* a émis — un challenge n'est jamais réutilisable.
|
|
36
|
+
*/
|
|
37
|
+
var WebAuthnService = class extends Service {
|
|
38
|
+
module;
|
|
39
|
+
#config = null;
|
|
40
|
+
#lib = null;
|
|
41
|
+
#store = null;
|
|
42
|
+
#rpID = "localhost";
|
|
43
|
+
#rpName = "Nodefony";
|
|
44
|
+
#ready = false;
|
|
45
|
+
constructor(module) {
|
|
46
|
+
super(serviceName, module.container, module.notificationsCenter, module.options);
|
|
47
|
+
this.module = module;
|
|
48
|
+
this.kernel?.once("onBoot", () => this.#build());
|
|
49
|
+
this.kernel?.once("onTerminate", () => void this.#shutdown());
|
|
50
|
+
}
|
|
51
|
+
#build() {
|
|
52
|
+
let config;
|
|
53
|
+
try {
|
|
54
|
+
config = defineSecurityConfig(this.options);
|
|
55
|
+
} catch {
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
if (!config.passkeys.enabled) {
|
|
59
|
+
this.log("webauthn idle — passkeys désactivés en config", "DEBUG");
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
this.#config = config;
|
|
63
|
+
const rawDomain = config.passkeys.rpId ?? (this.kernel?.domain || "localhost");
|
|
64
|
+
this.#rpID = /^(\d{1,3}\.){3}\d{1,3}$|:/.test(rawDomain) ? "localhost" : rawDomain;
|
|
65
|
+
this.#rpName = config.passkeys.rpName ?? "Nodefony";
|
|
66
|
+
const existing = this.get("webAuthnCredentialStore");
|
|
67
|
+
let resolved;
|
|
68
|
+
let reason;
|
|
69
|
+
if (existing) {
|
|
70
|
+
this.#store = existing;
|
|
71
|
+
resolved = deriveStoreBackend(existing);
|
|
72
|
+
reason = "adapter posé au container (infra database déclarée)";
|
|
73
|
+
} else {
|
|
74
|
+
let driver = config.passkeys.store;
|
|
75
|
+
reason = `store explicitement configuré ("${driver}")`;
|
|
76
|
+
if (driver === AUTO_STORE) {
|
|
77
|
+
const auto = resolveAutoStore("durable", this.kernel?.infra ?? EMPTY_INFRA, listWebAuthnStores());
|
|
78
|
+
driver = auto.store;
|
|
79
|
+
reason = auto.reason;
|
|
80
|
+
this.log(`passkeys.store "auto" → "${driver}" (${auto.reason})`, "INFO");
|
|
81
|
+
}
|
|
82
|
+
const factory = getWebAuthnStoreFactory(driver);
|
|
83
|
+
if (!factory) {
|
|
84
|
+
const msg = `webauthn store "${driver}" inconnu (enregistrés : ${listWebAuthnStores().join(", ") || "aucun"})`;
|
|
85
|
+
if (this.kernel?.environment === "production") throw new Error(`${msg} — passkeys indisponibles : boot avorté.`);
|
|
86
|
+
this.log(`${msg} — passkeys indisponibles`, "CRITIC");
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
if (driver === "memory" && this.kernel?.environment === "production") this.log("passkeys.store \"memory\" en PRODUCTION — credentials WebAuthn volatils : tous les passkeys enregistrés sont perdus au redémarrage (utilisateurs verrouillés hors de leur compte). Déclarer une infra durable (NF_DATABASE_URL).", "WARNING");
|
|
90
|
+
this.#store = factory({
|
|
91
|
+
container: this.container,
|
|
92
|
+
config
|
|
93
|
+
});
|
|
94
|
+
this.container?.set("webAuthnCredentialStore", this.#store);
|
|
95
|
+
resolved = driver;
|
|
96
|
+
}
|
|
97
|
+
this.kernel?.registerStoreResolution({
|
|
98
|
+
brick: "passkeys",
|
|
99
|
+
nature: "durable",
|
|
100
|
+
configured: config.passkeys.store,
|
|
101
|
+
resolved,
|
|
102
|
+
available: listWebAuthnStores(),
|
|
103
|
+
reason,
|
|
104
|
+
configPath: "security.passkeys.store",
|
|
105
|
+
location: readStoreLocation(this.#store)
|
|
106
|
+
});
|
|
107
|
+
if (config.passkeys.attestation !== "none") this.log(`passkeys.attestation "${config.passkeys.attestation}" — l'attestation est demandée au navigateur mais N'EST PAS VÉRIFIÉE : ni métadonnées FIDO (MDS), ni certificats racines fabricant, et l'AAGUID n'est pas conservé. Ce réglage ne tient donc pas un AAL3 régulé. Repasser à "none" si la garantie n'est pas requise.`, "WARNING");
|
|
108
|
+
if (config.passkeys.residentKey === "discouraged") this.log("passkeys.residentKey \"discouraged\" — les credentials enrôlés ne sont pas découvrables, or le login par passkey est usernameless (aucun ciblage depuis la requête, anti-énumération). Ces porteurs ne pourront pas se connecter : repasser à \"preferred\" ou \"required\".", "WARNING");
|
|
109
|
+
this.#ready = true;
|
|
110
|
+
this.log(`webauthn ready — rpID "${this.#rpID}", store "${config.passkeys.store}"`, "DEBUG");
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Arrêt propre : écrit immédiatement le store sur disque s'il est persistant
|
|
114
|
+
* (driver `file`) → aucune écriture en attente de flush n'est perdue au
|
|
115
|
+
* redémarrage. No-op pour un store mémoire ou un adapter sans `flushNow`.
|
|
116
|
+
*/
|
|
117
|
+
async #shutdown() {
|
|
118
|
+
if (isFlushable(this.#store)) try {
|
|
119
|
+
await this.#store.flushNow();
|
|
120
|
+
} catch (e) {
|
|
121
|
+
this.log(e, "ERROR");
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
/** `true` si les cérémonies sont opérationnelles (passkeys activés, boot OK). */
|
|
125
|
+
isEnabled() {
|
|
126
|
+
return this.#ready;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Prépare les options de `navigator.credentials.create()` — le défi à signer +
|
|
130
|
+
* les contraintes (RP, type d'attestation, sélection d'authenticator). Le
|
|
131
|
+
* challenge renvoyé doit être stocké côté serveur (session) par l'appelant.
|
|
132
|
+
*
|
|
133
|
+
* `excludeCredentials` liste les passkeys déjà enregistrés du même utilisateur
|
|
134
|
+
* pour empêcher un double enregistrement sur le même authenticator (§7.1).
|
|
135
|
+
*/
|
|
136
|
+
async generateRegistrationOptions(user) {
|
|
137
|
+
this.#ensureReady();
|
|
138
|
+
const lib = await this.#ensureLib();
|
|
139
|
+
const pk = this.#config.passkeys;
|
|
140
|
+
const existing = await this.#store.findByUser(user.id);
|
|
141
|
+
return lib.generateRegistrationOptions({
|
|
142
|
+
rpName: this.#rpName,
|
|
143
|
+
rpID: this.#rpID,
|
|
144
|
+
userName: user.name,
|
|
145
|
+
userID: new Uint8Array(Buffer.from(user.id, "utf8")),
|
|
146
|
+
userDisplayName: user.displayName ?? user.name,
|
|
147
|
+
attestationType: pk.attestation,
|
|
148
|
+
timeout: pk.timeoutMs,
|
|
149
|
+
excludeCredentials: existing.map((c) => ({
|
|
150
|
+
id: c.id,
|
|
151
|
+
transports: [...c.transports]
|
|
152
|
+
})),
|
|
153
|
+
authenticatorSelection: {
|
|
154
|
+
residentKey: pk.residentKey,
|
|
155
|
+
userVerification: pk.userVerification,
|
|
156
|
+
...pk.authenticatorAttachment !== "any" ? { authenticatorAttachment: pk.authenticatorAttachment } : {}
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Vérifie la réponse d'enregistrement (challenge, origine, rpIdHash, flags,
|
|
162
|
+
* format d'attestation) et **persiste** le nouveau credential.
|
|
163
|
+
*
|
|
164
|
+
* @param expectedChallenge - le challenge émis par {@link generateRegistrationOptions} (session).
|
|
165
|
+
* @param userId - propriétaire du credential (utilisateur authentifié/en création).
|
|
166
|
+
* @param requestOrigin - origine HTTP de la requête (validée si aucune origine n'est configurée).
|
|
167
|
+
* @throws AuthenticationError (401) — vérification échouée.
|
|
168
|
+
* @throws WebAuthnError (409) — plafond `passkeys.maxPerUser` atteint.
|
|
169
|
+
*/
|
|
170
|
+
async verifyRegistration(response, expectedChallenge, userId, requestOrigin) {
|
|
171
|
+
this.#ensureReady();
|
|
172
|
+
const lib = await this.#ensureLib();
|
|
173
|
+
const pk = this.#config.passkeys;
|
|
174
|
+
let verification;
|
|
175
|
+
try {
|
|
176
|
+
verification = await lib.verifyRegistrationResponse({
|
|
177
|
+
response,
|
|
178
|
+
expectedChallenge,
|
|
179
|
+
expectedOrigin: this.#expectedOrigin(requestOrigin),
|
|
180
|
+
expectedRPID: this.#rpID,
|
|
181
|
+
requireUserVerification: pk.userVerification === "required"
|
|
182
|
+
});
|
|
183
|
+
} catch {
|
|
184
|
+
throw new AuthenticationError("WebAuthn registration failed");
|
|
185
|
+
}
|
|
186
|
+
if (!verification.verified || !verification.registrationInfo) throw new AuthenticationError("WebAuthn registration failed");
|
|
187
|
+
if (await this.#store.countByUser(userId) >= pk.maxPerUser) throw new WebAuthnError(`passkey limit reached (${pk.maxPerUser})`, 409);
|
|
188
|
+
const info = verification.registrationInfo;
|
|
189
|
+
const credential = {
|
|
190
|
+
id: info.credential.id,
|
|
191
|
+
userId,
|
|
192
|
+
publicKey: Buffer.from(info.credential.publicKey).toString("base64url"),
|
|
193
|
+
signCount: info.credential.counter,
|
|
194
|
+
transports: info.credential.transports ?? [],
|
|
195
|
+
backupEligible: info.credentialDeviceType === "multiDevice",
|
|
196
|
+
backupState: info.credentialBackedUp,
|
|
197
|
+
uvInitialized: info.userVerified,
|
|
198
|
+
createdAt: Date.now(),
|
|
199
|
+
lastUsedAt: null
|
|
200
|
+
};
|
|
201
|
+
await this.#store.save(credential);
|
|
202
|
+
return credential;
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Prépare les options de `navigator.credentials.get()`. Sans `userId`
|
|
206
|
+
* (usernameless) : `allowCredentials` est omis → l'authenticator propose ses
|
|
207
|
+
* passkeys découvrables (UX cible). Avec `userId` : ciblage des credentials
|
|
208
|
+
* connus de cet utilisateur.
|
|
209
|
+
*
|
|
210
|
+
* @param userId - identité **déjà prouvée** (session en cours) et jamais un
|
|
211
|
+
* identifiant reçu d'un appelant non authentifié : `allowCredentials`
|
|
212
|
+
* révélerait alors qu'un compte porte une passkey et **lesquelles**, or un
|
|
213
|
+
* `credentialId` est corrélable entre sites (W3C WebAuthn L3, « Privacy leak
|
|
214
|
+
* via credential IDs »). Le controller BFF applique cette règle —
|
|
215
|
+
* `WebAuthnController.loginOptions()` ne cible que depuis la session.
|
|
216
|
+
*/
|
|
217
|
+
async generateAuthenticationOptions(userId) {
|
|
218
|
+
this.#ensureReady();
|
|
219
|
+
const lib = await this.#ensureLib();
|
|
220
|
+
const pk = this.#config.passkeys;
|
|
221
|
+
let allowCredentials;
|
|
222
|
+
if (userId) allowCredentials = (await this.#store.findByUser(userId)).map((c) => ({
|
|
223
|
+
id: c.id,
|
|
224
|
+
transports: [...c.transports]
|
|
225
|
+
}));
|
|
226
|
+
return lib.generateAuthenticationOptions({
|
|
227
|
+
rpID: this.#rpID,
|
|
228
|
+
allowCredentials,
|
|
229
|
+
userVerification: pk.userVerification,
|
|
230
|
+
timeout: pk.timeoutMs
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Vérifie une assertion (signature sur `authData ‖ SHA-256(clientDataJSON)`
|
|
235
|
+
* avec la clé publique stockée, §7.2) + applique l'état (compteur anti-clone,
|
|
236
|
+
* sauvegarde, usage). Résout l'utilisateur propriétaire via le credentialId.
|
|
237
|
+
*
|
|
238
|
+
* @param expectedChallenge - le challenge émis par {@link generateAuthenticationOptions} (session).
|
|
239
|
+
* @throws AuthenticationError (401) — credential inconnu ou vérification échouée.
|
|
240
|
+
*/
|
|
241
|
+
async verifyAuthentication(response, expectedChallenge, requestOrigin) {
|
|
242
|
+
this.#ensureReady();
|
|
243
|
+
const lib = await this.#ensureLib();
|
|
244
|
+
const pk = this.#config.passkeys;
|
|
245
|
+
const stored = await this.#store.findById(response.id);
|
|
246
|
+
if (!stored) throw new AuthenticationError("WebAuthn authentication failed");
|
|
247
|
+
let verification;
|
|
248
|
+
try {
|
|
249
|
+
verification = await lib.verifyAuthenticationResponse({
|
|
250
|
+
response,
|
|
251
|
+
expectedChallenge,
|
|
252
|
+
expectedOrigin: this.#expectedOrigin(requestOrigin),
|
|
253
|
+
expectedRPID: this.#rpID,
|
|
254
|
+
credential: {
|
|
255
|
+
id: stored.id,
|
|
256
|
+
publicKey: new Uint8Array(Buffer.from(stored.publicKey, "base64url")),
|
|
257
|
+
counter: stored.signCount,
|
|
258
|
+
transports: [...stored.transports]
|
|
259
|
+
},
|
|
260
|
+
requireUserVerification: pk.userVerification === "required"
|
|
261
|
+
});
|
|
262
|
+
} catch {
|
|
263
|
+
throw new AuthenticationError("WebAuthn authentication failed");
|
|
264
|
+
}
|
|
265
|
+
if (!verification.verified) throw new AuthenticationError("WebAuthn authentication failed");
|
|
266
|
+
const info = verification.authenticationInfo;
|
|
267
|
+
await this.#store.update(stored.id, {
|
|
268
|
+
signCount: info.newCounter,
|
|
269
|
+
backupState: info.credentialBackedUp,
|
|
270
|
+
uvInitialized: stored.uvInitialized || info.userVerified,
|
|
271
|
+
lastUsedAt: Date.now()
|
|
272
|
+
});
|
|
273
|
+
return {
|
|
274
|
+
credential: stored,
|
|
275
|
+
userId: stored.userId
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
/** Liste les credentials d'un utilisateur (UX « mes appareils »). */
|
|
279
|
+
listUserCredentials(userId) {
|
|
280
|
+
this.#ensureReady();
|
|
281
|
+
return this.#store.findByUser(userId);
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* Page de passkeys pour le data plane admin (vue TRANSVERSE : « quels appareils
|
|
285
|
+
* portent des passkeys sur toute la plateforme »).
|
|
286
|
+
*
|
|
287
|
+
* ≠ {@link listUserCredentials}, qui sert la fiche d'UN utilisateur et le chemin
|
|
288
|
+
* chaud du login. Ici on ne matérialise jamais plus d'une page, et la projection
|
|
289
|
+
* du store exclut la clé publique.
|
|
290
|
+
*/
|
|
291
|
+
listCredentialsPage(query) {
|
|
292
|
+
this.#ensureReady();
|
|
293
|
+
return this.#store.listPage(query);
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* Nombre de passkeys correspondant aux filtres, ou `-1` si le backend ne sait
|
|
297
|
+
* pas compter à coût raisonnable (Redis).
|
|
298
|
+
*/
|
|
299
|
+
countCredentials(query) {
|
|
300
|
+
this.#ensureReady();
|
|
301
|
+
return this.#store.countCredentials(query);
|
|
302
|
+
}
|
|
303
|
+
/** Révoque un credential (retrait d'un appareil). */
|
|
304
|
+
removeCredential(credentialId) {
|
|
305
|
+
this.#ensureReady();
|
|
306
|
+
return this.#store.delete(credentialId);
|
|
307
|
+
}
|
|
308
|
+
/**
|
|
309
|
+
* Supprime un credential **du propriétaire** (self-service, anti-IDOR) : la
|
|
310
|
+
* suppression n'aboutit que si le credential appartient bien à `userId`, sinon
|
|
311
|
+
* `false` — 404 indiscernable côté client (on ne révèle pas l'existence d'un
|
|
312
|
+
* credential d'autrui).
|
|
313
|
+
*/
|
|
314
|
+
async removeUserCredential(userId, credentialId) {
|
|
315
|
+
this.#ensureReady();
|
|
316
|
+
const cred = await this.#store.findById(credentialId);
|
|
317
|
+
if (!cred || cred.userId !== userId) return false;
|
|
318
|
+
await this.#store.delete(credentialId);
|
|
319
|
+
return true;
|
|
320
|
+
}
|
|
321
|
+
/**
|
|
322
|
+
* Origine(s) attendue(s) (anti-phishing, §7.1/§7.2). Liste blanche de config en
|
|
323
|
+
* priorité (prod). À défaut : l'origine de la requête est acceptée **seulement
|
|
324
|
+
* si son hostname == rpID** (dev : `localhost:port` quel que soit le port, sans
|
|
325
|
+
* jamais ouvrir à un domaine tiers). Dernier recours : `https://{rpID}`.
|
|
326
|
+
*/
|
|
327
|
+
#expectedOrigin(requestOrigin) {
|
|
328
|
+
const origins = this.#config.passkeys.origins;
|
|
329
|
+
if (origins.length > 0) return [...origins];
|
|
330
|
+
if (requestOrigin) try {
|
|
331
|
+
if (new URL(requestOrigin).hostname === this.#rpID) return requestOrigin;
|
|
332
|
+
} catch {}
|
|
333
|
+
return `https://${this.#rpID}`;
|
|
334
|
+
}
|
|
335
|
+
async #ensureLib() {
|
|
336
|
+
return this.#lib ??= await import("@simplewebauthn/server");
|
|
337
|
+
}
|
|
338
|
+
#ensureReady() {
|
|
339
|
+
if (!this.#ready || !this.#store || !this.#config) throw new Error("WebAuthnService: non initialisé (passkeys désactivés ou boot échoué)");
|
|
340
|
+
}
|
|
341
|
+
};
|
|
342
|
+
//#endregion
|
|
343
|
+
export { WebAuthnService, WebAuthnService as default };
|