@nodefony/security 10.0.0-beta.2 → 10.0.0-beta.4
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/dist/_virtual/{_@oxc-project_runtime@0.152.0 → _@oxc-project_runtime@0.155.0}/helpers/esm/decorate.js +1 -2
- package/dist/index.js +48 -16
- package/dist/nodefony/command/{security-token.js → security-mcp-token.js} +62 -24
- package/dist/nodefony/command/security-oauth-doctor.js +68 -0
- package/dist/nodefony/command/security-secrets.js +52 -29
- package/dist/nodefony/command/security-user-add.js +9 -3
- package/dist/nodefony/command/security-user-delete.js +10 -7
- package/dist/nodefony/command/security-user-list.js +5 -3
- package/dist/nodefony/command/security-user-password.js +9 -14
- package/dist/nodefony/config/config.js +99 -8
- package/dist/nodefony/config/defineModuleConfig.js +1 -2
- package/dist/nodefony/contracts/IAccessVoter.js +1 -2
- package/dist/nodefony/contracts/index.js +1 -2
- package/dist/nodefony/errors/AccessDeniedError.js +1 -2
- package/dist/nodefony/errors/ApiKeyError.js +3 -2
- package/dist/nodefony/errors/AuthenticationError.js +1 -2
- package/dist/nodefony/errors/CsrfError.js +1 -2
- package/dist/nodefony/errors/InsecureTransportError.js +23 -0
- package/dist/nodefony/errors/InvalidTargetError.js +1 -2
- package/dist/nodefony/errors/SsrfError.js +1 -2
- package/dist/nodefony/errors/ThrottledError.js +1 -2
- package/dist/nodefony/errors/UnverifiableTokenError.js +1 -2
- package/dist/nodefony/errors/WebAuthnError.js +1 -2
- package/dist/nodefony/errors/index.js +2 -1
- package/dist/nodefony/keycloak/keycloakAdmin.js +254 -0
- package/dist/nodefony/keycloak/keycloakRealm.js +259 -0
- package/dist/nodefony/keycloak/keycloakRealmInput.js +123 -0
- package/dist/nodefony/keycloak/scaffold.js +231 -0
- package/dist/nodefony/keycloak/security-keycloak-realm.js +130 -0
- package/dist/nodefony/scaffold/contribute.js +19 -0
- package/dist/nodefony/service/accessTokenVerifier.js +101 -34
- package/dist/nodefony/service/apiKeys.js +45 -5
- package/dist/nodefony/service/auditService.js +4 -5
- package/dist/nodefony/service/authFlow.js +118 -16
- package/dist/nodefony/service/authorization.js +1 -2
- package/dist/nodefony/service/cors.js +1 -2
- package/dist/nodefony/service/csrf.js +1 -2
- package/dist/nodefony/service/firewall.js +116 -25
- package/dist/nodefony/service/keycloak.js +208 -0
- package/dist/nodefony/service/oauth2.js +403 -28
- package/dist/nodefony/service/securityHeaders.js +1 -2
- package/dist/nodefony/service/tokenService.js +94 -27
- package/dist/nodefony/service/totp.js +55 -11
- package/dist/nodefony/service/webAuthn.js +24 -7
- package/dist/nodefony/service/webhooks.js +4 -5
- package/dist/nodefony/src/RoleHierarchyWalker.js +1 -2
- package/dist/nodefony/src/SecuredArea.js +15 -5
- package/dist/nodefony/src/accountBinding.js +96 -0
- package/dist/nodefony/src/admin/SecurityAdminApi.js +71 -42
- package/dist/nodefony/src/admin/WebhookAdminApi.js +23 -7
- package/dist/nodefony/src/admin/adminAudit.js +1 -3
- package/dist/nodefony/src/admin/userRevocationCascade.js +148 -20
- package/dist/nodefony/src/apikey/apiKeyFormat.js +1 -2
- package/dist/nodefony/src/audit/MemoryAuditStore.js +1 -2
- package/dist/nodefony/src/audit/auditBridge.js +1 -2
- package/dist/nodefony/src/audit/auditFilters.js +1 -2
- package/dist/nodefony/src/audit/auditStoreRegistry.js +1 -2
- package/dist/nodefony/src/audit/readAuditContext.js +1 -2
- package/dist/nodefony/src/audit/recordAudit.js +1 -2
- package/dist/nodefony/src/authenticator/AnonymousAuthenticator.js +1 -2
- package/dist/nodefony/src/authenticator/ApiKeyAuthenticator.js +3 -2
- package/dist/nodefony/src/authenticator/ExternalJwtAuthenticator.js +14 -59
- package/dist/nodefony/src/authenticator/ExternalSubjectResolver.js +180 -0
- package/dist/nodefony/src/authenticator/FirewallRealtimeAuthenticator.js +80 -19
- package/dist/nodefony/src/authenticator/JwtAuthenticator.js +92 -16
- package/dist/nodefony/src/authenticator/SessionAuthenticator.js +9 -5
- package/dist/nodefony/src/authenticator/UserPasswordAuthenticator.js +14 -5
- package/dist/nodefony/src/authenticator/authenticatorRegistry.js +5 -12
- package/dist/nodefony/src/authenticator/bearer.js +1 -2
- package/dist/nodefony/src/authenticator/externalSubject.js +1 -2
- package/dist/nodefony/src/authenticator/peekIssuer.js +1 -2
- package/dist/nodefony/src/boot/publicRoutesNotice.js +26 -0
- package/dist/nodefony/src/crypto/secretCipher.js +1 -2
- package/dist/nodefony/src/csp.js +1 -2
- package/dist/nodefony/src/csrfToken.js +1 -2
- package/dist/nodefony/src/entryPoint.js +38 -0
- package/dist/nodefony/src/net/ssrfGuard.js +1 -2
- package/dist/nodefony/src/net/transportFailure.js +57 -0
- package/dist/nodefony/src/oauth/clientCredentials.js +39 -0
- package/dist/nodefony/src/oauth/httpJson.js +3 -3
- package/dist/nodefony/src/oauth/metadata.js +28 -12
- package/dist/nodefony/src/oauth/oauth2Client.js +61 -35
- package/dist/nodefony/src/oauth/oauthProviderRegistry.js +40 -8
- package/dist/nodefony/src/oauth/providerDiagnosis.js +282 -0
- package/dist/nodefony/src/oauth/providerRoles.js +78 -0
- package/dist/nodefony/src/oauth/providers/github.js +48 -12
- package/dist/nodefony/src/oauth/providers/oidc.js +90 -8
- package/dist/nodefony/src/openToSignup.js +46 -0
- package/dist/nodefony/src/realtime/UserRealtimeToken.js +17 -3
- package/dist/nodefony/src/realtime/frameAuthorizer.js +1 -2
- package/dist/nodefony/src/rolesWithin.js +57 -0
- package/dist/nodefony/src/sessionAuthentication.js +25 -0
- package/dist/nodefony/src/sessionIdentity.js +29 -3
- package/dist/nodefony/src/throttle/LoginThrottler.js +1 -2
- package/dist/nodefony/src/token/AnonymousToken.js +1 -2
- package/dist/nodefony/src/token/JwtKeystore.js +89 -48
- package/dist/nodefony/src/token/MemoryTokenStore.js +1 -2
- package/dist/nodefony/src/token/RemoteJwtVerifier.js +46 -16
- package/dist/nodefony/src/token/UserToken.js +1 -2
- package/dist/nodefony/src/token/jwtRuntime.js +1 -2
- package/dist/nodefony/src/token/scopeGrants.js +109 -0
- package/dist/nodefony/src/token/secretFile.js +60 -4
- package/dist/nodefony/src/token/tokenCriteria.js +1 -2
- package/dist/nodefony/src/token/tokenFilters.js +1 -2
- package/dist/nodefony/src/token/tokenSort.js +1 -2
- package/dist/nodefony/src/token/tokenStatus.js +1 -2
- package/dist/nodefony/src/token/tokenStoreRegistry.js +1 -2
- package/dist/nodefony/src/totp/MemoryTotpSecretStore.js +1 -2
- package/dist/nodefony/src/totp/totpCipher.js +1 -2
- package/dist/nodefony/src/totp/totpCrypto.js +1 -2
- package/dist/nodefony/src/totp/totpOperations.js +1 -2
- package/dist/nodefony/src/totp/totpSecretStoreRegistry.js +1 -2
- package/dist/nodefony/src/transport/CredentialTransportPolicy.js +50 -0
- package/dist/nodefony/src/voter/RoleVoter.js +1 -2
- package/dist/nodefony/src/voter/ScopeVoter.js +1 -2
- package/dist/nodefony/src/voter/voterRegistry.js +1 -2
- package/dist/nodefony/src/webauthn/MemoryWebAuthnCredentialStore.js +1 -2
- package/dist/nodefony/src/webauthn/webAuthnCredentialStoreRegistry.js +1 -2
- package/dist/nodefony/src/webhook/MemoryWebhookStore.js +1 -2
- package/dist/nodefony/src/webhook/WebhookDispatcher.js +1 -2
- package/dist/nodefony/src/webhook/webhookCipher.js +1 -2
- package/dist/nodefony/src/webhook/webhookDelivery.js +1 -2
- package/dist/nodefony/src/webhook/webhookFilters.js +1 -2
- package/dist/nodefony/src/webhook/webhookSignature.js +1 -2
- package/dist/nodefony/src/webhook/webhookSort.js +1 -2
- package/dist/nodefony/src/webhook/webhookStoreRegistry.js +1 -2
- package/dist/types/index.d.ts +26 -6
- package/dist/types/nodefony/command/{security-token.d.ts → security-mcp-token.d.ts} +16 -6
- package/dist/types/nodefony/command/security-oauth-doctor.d.ts +23 -0
- package/dist/types/nodefony/command/security-secrets.d.ts +2 -0
- package/dist/types/nodefony/command/security-user-delete.d.ts +4 -3
- package/dist/types/nodefony/command/security-user-password.d.ts +4 -4
- package/dist/types/nodefony/config/config.d.ts +64 -1
- package/dist/types/nodefony/contracts/IAuthenticator.d.ts +1 -1
- package/dist/types/nodefony/contracts/IFirewallDescription.d.ts +42 -1
- package/dist/types/nodefony/contracts/IOAuthProvider.d.ts +48 -0
- package/dist/types/nodefony/contracts/ISecuredArea.d.ts +22 -0
- package/dist/types/nodefony/contracts/ITotpSecret.d.ts +4 -1
- package/dist/types/nodefony/contracts/ITotpSecretStore.d.ts +7 -1
- package/dist/types/nodefony/contracts/IWebAuthnCredential.d.ts +4 -1
- package/dist/types/nodefony/contracts/IWebAuthnCredentialStore.d.ts +6 -0
- package/dist/types/nodefony/errors/ApiKeyError.d.ts +3 -1
- package/dist/types/nodefony/errors/InsecureTransportError.d.ts +20 -0
- package/dist/types/nodefony/errors/index.d.ts +1 -0
- package/dist/types/nodefony/keycloak/keycloakAdmin.d.ts +154 -0
- package/dist/types/nodefony/keycloak/keycloakRealm.d.ts +138 -0
- package/dist/types/nodefony/keycloak/keycloakRealmInput.d.ts +92 -0
- package/dist/types/nodefony/keycloak/scaffold.d.ts +98 -0
- package/dist/types/nodefony/keycloak/security-keycloak-realm.d.ts +43 -0
- package/dist/types/nodefony/scaffold/contribute.d.ts +15 -0
- package/dist/types/nodefony/service/accessTokenVerifier.d.ts +35 -14
- package/dist/types/nodefony/service/apiKeys.d.ts +13 -2
- package/dist/types/nodefony/service/authFlow.d.ts +35 -3
- package/dist/types/nodefony/service/firewall.d.ts +17 -3
- package/dist/types/nodefony/service/keycloak.d.ts +115 -0
- package/dist/types/nodefony/service/oauth2.d.ts +145 -9
- package/dist/types/nodefony/service/tokenService.d.ts +7 -1
- package/dist/types/nodefony/service/totp.d.ts +28 -4
- package/dist/types/nodefony/service/webAuthn.d.ts +23 -1
- package/dist/types/nodefony/src/SecuredArea.d.ts +4 -0
- package/dist/types/nodefony/src/accountBinding.d.ts +82 -0
- package/dist/types/nodefony/src/admin/userRevocationCascade.d.ts +72 -12
- package/dist/types/nodefony/src/authenticator/ExternalJwtAuthenticator.d.ts +2 -26
- package/dist/types/nodefony/src/authenticator/ExternalSubjectResolver.d.ts +99 -0
- package/dist/types/nodefony/src/authenticator/FirewallRealtimeAuthenticator.d.ts +17 -1
- package/dist/types/nodefony/src/authenticator/JwtAuthenticator.d.ts +37 -1
- package/dist/types/nodefony/src/authenticator/UserPasswordAuthenticator.d.ts +11 -3
- package/dist/types/nodefony/src/boot/publicRoutesNotice.d.ts +16 -0
- package/dist/types/nodefony/src/entryPoint.d.ts +37 -0
- package/dist/types/nodefony/src/net/transportFailure.d.ts +22 -0
- package/dist/types/nodefony/src/oauth/clientCredentials.d.ts +54 -0
- package/dist/types/nodefony/src/oauth/httpJson.d.ts +0 -8
- package/dist/types/nodefony/src/oauth/metadata.d.ts +24 -1
- package/dist/types/nodefony/src/oauth/oauth2Client.d.ts +50 -1
- package/dist/types/nodefony/src/oauth/oauthProviderRegistry.d.ts +46 -2
- package/dist/types/nodefony/src/oauth/providerDiagnosis.d.ts +117 -0
- package/dist/types/nodefony/src/oauth/providerRoles.d.ts +58 -0
- package/dist/types/nodefony/src/oauth/providers/oidc.d.ts +34 -1
- package/dist/types/nodefony/src/openToSignup.d.ts +36 -0
- package/dist/types/nodefony/src/realtime/UserRealtimeToken.d.ts +7 -1
- package/dist/types/nodefony/src/realtime/realtimeContracts.d.ts +6 -0
- package/dist/types/nodefony/src/rolesWithin.d.ts +27 -0
- package/dist/types/nodefony/src/sessionAuthentication.d.ts +14 -0
- package/dist/types/nodefony/src/sessionIdentity.d.ts +22 -0
- package/dist/types/nodefony/src/token/JwtKeystore.d.ts +47 -5
- package/dist/types/nodefony/src/token/RemoteJwtVerifier.d.ts +29 -2
- package/dist/types/nodefony/src/token/scopeGrants.d.ts +77 -0
- package/dist/types/nodefony/src/token/secretFile.d.ts +22 -0
- package/dist/types/nodefony/src/transport/CredentialTransportPolicy.d.ts +47 -0
- package/docs/api-keys.md +182 -175
- package/docs/audit.md +141 -141
- package/docs/authenticators.md +129 -96
- package/docs/authorization.md +116 -69
- package/docs/cors.md +64 -64
- package/docs/csrf.md +77 -72
- package/docs/external-jwt.md +83 -11
- package/docs/firewall.md +119 -83
- package/docs/headers.md +82 -82
- package/docs/index.md +16 -8
- package/docs/keycloak.md +835 -0
- package/docs/login-page.md +361 -0
- package/docs/oauth2.md +291 -166
- package/docs/obtenir-un-jeton.md +131 -34
- package/docs/tokens.md +197 -140
- package/docs/totp.md +177 -136
- package/docs/webauthn.md +157 -154
- package/docs/webhooks.md +128 -125
- package/keycloak/themes/nodefony/account/resources/css/nodefony.css +66 -0
- package/keycloak/themes/nodefony/account/resources/img/favicon.ico +0 -0
- package/keycloak/themes/nodefony/account/resources/img/logo.png +0 -0
- package/keycloak/themes/nodefony/account/theme.properties +14 -0
- package/keycloak/themes/nodefony/admin/resources/css/nodefony.css +66 -0
- package/keycloak/themes/nodefony/admin/resources/img/favicon.ico +0 -0
- package/keycloak/themes/nodefony/admin/resources/img/logo.png +0 -0
- package/keycloak/themes/nodefony/admin/theme.properties +12 -0
- package/keycloak/themes/nodefony/email/html/template.ftl +73 -0
- package/keycloak/themes/nodefony/email/messages/messages_en.properties +2 -0
- package/keycloak/themes/nodefony/email/messages/messages_fr.properties +3 -0
- package/keycloak/themes/nodefony/email/resources/img/logo.png +0 -0
- package/keycloak/themes/nodefony/email/theme.properties +8 -0
- package/keycloak/themes/nodefony/login/messages/messages_en.properties +22 -0
- package/keycloak/themes/nodefony/login/messages/messages_fr.properties +25 -0
- package/keycloak/themes/nodefony/login/resources/css/login.css +1141 -0
- package/keycloak/themes/nodefony/login/resources/css/nodefony.css +688 -0
- package/keycloak/themes/nodefony/login/resources/css/skins/blueprint.css +70 -0
- package/keycloak/themes/nodefony/login/resources/css/skins/console.css +57 -0
- package/keycloak/themes/nodefony/login/resources/css/skins/dots.css +67 -0
- package/keycloak/themes/nodefony/login/resources/css/skins/enterprise.css +84 -0
- package/keycloak/themes/nodefony/login/resources/css/skins/horizon.css +158 -0
- package/keycloak/themes/nodefony/login/resources/css/skins/ledger.css +224 -0
- package/keycloak/themes/nodefony/login/resources/css/skins/minimal.css +63 -0
- package/keycloak/themes/nodefony/login/resources/css/skins/photo-card.css +72 -0
- package/keycloak/themes/nodefony/login/resources/img/favicon.ico +0 -0
- package/keycloak/themes/nodefony/login/resources/img/logo.png +0 -0
- package/keycloak/themes/nodefony/login/template.ftl +423 -0
- package/keycloak/themes/nodefony/login/theme.properties +17 -0
- package/keycloak/themes/nodefony/welcome/resources/css/nodefony.css +32 -0
- package/keycloak/themes/nodefony/welcome/resources/img/logo.png +0 -0
- package/keycloak/themes/nodefony/welcome/theme.properties +11 -0
- package/keycloak/themes/nodefony-blueprint/login/theme.properties +12 -0
- package/keycloak/themes/nodefony-console/login/theme.properties +12 -0
- package/keycloak/themes/nodefony-dots/login/theme.properties +12 -0
- package/keycloak/themes/nodefony-enterprise/login/theme.properties +12 -0
- package/keycloak/themes/nodefony-horizon/login/theme.properties +12 -0
- package/keycloak/themes/nodefony-ledger/login/theme.properties +12 -0
- package/keycloak/themes/nodefony-minimal/login/theme.properties +12 -0
- package/keycloak/themes/nodefony-photo-card/login/theme.properties +12 -0
- package/package.json +19 -17
- package/dist/_virtual/_@oxc-project_runtime@0.152.0/helpers/esm/decorateMetadata.js +0 -6
|
@@ -11,7 +11,16 @@ import { CliKernel, Command } from "nodefony";
|
|
|
11
11
|
*/
|
|
12
12
|
export declare function ttlSeconds(raw: string | undefined): number | undefined | Error;
|
|
13
13
|
/**
|
|
14
|
-
*
|
|
14
|
+
* Durée demandée à l'émetteur : `--ttl` explicite, sinon 8 h quand le jeton
|
|
15
|
+
* part chez les agents, sinon rien — la configuration décide.
|
|
16
|
+
*
|
|
17
|
+
* @param raw - la valeur de `--ttl` telle que tapée, ou rien
|
|
18
|
+
* @param write - le jeton sera posé dans la configuration des agents
|
|
19
|
+
* @returns les secondes, `undefined` pour le défaut de configuration, une `Error` sinon
|
|
20
|
+
*/
|
|
21
|
+
export declare function tokenTtlSeconds(raw: string | undefined, write: boolean): number | undefined | Error;
|
|
22
|
+
/**
|
|
23
|
+
* `nodefony security:mcp:token` — émet un jeton d'accès pour une porte de cette
|
|
15
24
|
* application (la porte MCP par défaut).
|
|
16
25
|
*
|
|
17
26
|
* **Pourquoi une commande, et pas un `curl`.** Le jeton s'obtenait par un appel
|
|
@@ -25,11 +34,12 @@ export declare function ttlSeconds(raw: string | undefined): number | undefined
|
|
|
25
34
|
* raison d'être de la liaison d'audience. La commande la vise d'elle-même,
|
|
26
35
|
* `--resource` ne sert qu'à en viser une autre.
|
|
27
36
|
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
37
|
+
* `--write` pose la valeur là où l'AGENT la lit (gitignoré), jamais dans le
|
|
38
|
+
* `.env` de l'application. Relancée, elle REMPLACE le jeton chez les agents qui
|
|
39
|
+
* le portent déjà — la rotation est le geste le plus fréquent — et n'en câble
|
|
40
|
+
* aucun nouveau sans le proposer.
|
|
31
41
|
*/
|
|
32
|
-
declare class
|
|
42
|
+
declare class SecurityMcpToken extends Command {
|
|
33
43
|
#private;
|
|
34
44
|
constructor(cli: CliKernel);
|
|
35
45
|
generate(identifierArg: string | undefined, opts: {
|
|
@@ -41,4 +51,4 @@ declare class SecurityToken extends Command {
|
|
|
41
51
|
json?: boolean;
|
|
42
52
|
}): Promise<this>;
|
|
43
53
|
}
|
|
44
|
-
export default
|
|
54
|
+
export default SecurityMcpToken;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { CliKernel, Command } from "nodefony";
|
|
2
|
+
/**
|
|
3
|
+
* `nodefony security:oauth:doctor` — dit POURQUOI un fournisseur OAuth refuse
|
|
4
|
+
* l'application, sans qu'un humain ait à se connecter.
|
|
5
|
+
*
|
|
6
|
+
* Trois verdicts distincts là où le journal ne montre qu'un 401 : l'émetteur
|
|
7
|
+
* se déclare autrement que la configuration ne l'écrit, l'URL de retour n'est
|
|
8
|
+
* pas enregistrée, le secret est faux. La même analyse alimente
|
|
9
|
+
* `nodefony doctor --live` (famille « Fournisseurs OAuth ») et l'endpoint
|
|
10
|
+
* d'administration `security/oauth/diagnosis` : une seule implémentation, celle
|
|
11
|
+
* du service `oauth2`.
|
|
12
|
+
*
|
|
13
|
+
* Valable pour tout fournisseur : les sondes passent par le fournisseur que le
|
|
14
|
+
* login construit, Keycloak n'y a rien de particulier.
|
|
15
|
+
*/
|
|
16
|
+
declare class SecurityOAuthDoctor extends Command {
|
|
17
|
+
constructor(cli: CliKernel);
|
|
18
|
+
generate(opts: {
|
|
19
|
+
provider?: string;
|
|
20
|
+
json?: boolean;
|
|
21
|
+
}): Promise<this>;
|
|
22
|
+
}
|
|
23
|
+
export default SecurityOAuthDoctor;
|
|
@@ -12,9 +12,10 @@ import { CliKernel, Command } from "nodefony";
|
|
|
12
12
|
* seul recours est une écriture directe en base. Le même garde-fou existe côté
|
|
13
13
|
* data plane (`UserAdminApi`) — ici il est constaté, pas supposé.
|
|
14
14
|
*
|
|
15
|
-
* La
|
|
16
|
-
*
|
|
17
|
-
*
|
|
15
|
+
* La suppression passe par `users.delete`, qui émet `onUserRevoked`
|
|
16
|
+
* (`reason: "deleted"`) et ATTEND la cascade de `@nodefony/security` : sessions
|
|
17
|
+
* et jetons éjectés, secret TOTP et passkeys retirés. La commande n'émet rien
|
|
18
|
+
* elle-même — le service est le seul émetteur du cycle de vie d'un compte.
|
|
18
19
|
*/
|
|
19
20
|
declare class SecurityUserDelete extends Command {
|
|
20
21
|
constructor(cli: CliKernel);
|
|
@@ -20,10 +20,10 @@ import { CliKernel, Command } from "nodefony";
|
|
|
20
20
|
*
|
|
21
21
|
* 🔒 **Les sessions et les jetons du compte sont RÉVOQUÉS**, et ce n'est pas une
|
|
22
22
|
* option : on change un mot de passe parce qu'il est compromis ou perdu, et
|
|
23
|
-
* laisser vivre les sessions ouvertes laisserait l'accès à qui l'a volé.
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
23
|
+
* laisser vivre les sessions ouvertes laisserait l'accès à qui l'a volé.
|
|
24
|
+
* `UserService.changePassword` émet la révocation lui-même — la même que pour
|
|
25
|
+
* le plan d'administration et la page « mon mot de passe » : un seul émetteur,
|
|
26
|
+
* aucun transport ne peut l'oublier.
|
|
27
27
|
*/
|
|
28
28
|
declare class SecurityUserPassword extends Command {
|
|
29
29
|
constructor(cli: CliKernel);
|
|
@@ -13,6 +13,11 @@ declare const areaSchema: z.ZodObject<{
|
|
|
13
13
|
host: z.ZodOptional<z.ZodString>;
|
|
14
14
|
resource: z.ZodOptional<z.ZodString>;
|
|
15
15
|
realtime: z.ZodDefault<z.ZodBoolean>;
|
|
16
|
+
subjectPolicy: z.ZodOptional<z.ZodEnum<{
|
|
17
|
+
ephemeral: "ephemeral";
|
|
18
|
+
require: "require";
|
|
19
|
+
}>>;
|
|
20
|
+
ephemeralRoles: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
16
21
|
}, z.core.$strict>;
|
|
17
22
|
export declare const securityConfigSchema: z.ZodObject<{
|
|
18
23
|
encoders: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
@@ -26,6 +31,7 @@ export declare const securityConfigSchema: z.ZodObject<{
|
|
|
26
31
|
rounds: z.ZodDefault<z.ZodNumber>;
|
|
27
32
|
}, z.core.$strict>>>;
|
|
28
33
|
roleHierarchy: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodArray<z.ZodString>>>;
|
|
34
|
+
grantableScopes: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodArray<z.ZodString>>>;
|
|
29
35
|
areas: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
30
36
|
pattern: z.ZodString;
|
|
31
37
|
security: z.ZodDefault<z.ZodBoolean>;
|
|
@@ -40,6 +46,11 @@ export declare const securityConfigSchema: z.ZodObject<{
|
|
|
40
46
|
host: z.ZodOptional<z.ZodString>;
|
|
41
47
|
resource: z.ZodOptional<z.ZodString>;
|
|
42
48
|
realtime: z.ZodDefault<z.ZodBoolean>;
|
|
49
|
+
subjectPolicy: z.ZodOptional<z.ZodEnum<{
|
|
50
|
+
ephemeral: "ephemeral";
|
|
51
|
+
require: "require";
|
|
52
|
+
}>>;
|
|
53
|
+
ephemeralRoles: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
43
54
|
}, z.core.$strict>>>;
|
|
44
55
|
realtimeChannels: z.ZodDefault<z.ZodArray<z.ZodObject<{
|
|
45
56
|
pattern: z.ZodString;
|
|
@@ -187,6 +198,7 @@ export declare const securityConfigSchema: z.ZodObject<{
|
|
|
187
198
|
resourceServer: z.ZodDefault<z.ZodObject<{
|
|
188
199
|
issuers: z.ZodDefault<z.ZodArray<z.ZodObject<{
|
|
189
200
|
issuer: z.ZodString;
|
|
201
|
+
discoveryUrl: z.ZodOptional<z.ZodString>;
|
|
190
202
|
jwksUri: z.ZodOptional<z.ZodString>;
|
|
191
203
|
algorithms: z.ZodDefault<z.ZodArray<z.ZodEnum<{
|
|
192
204
|
ES256: "ES256";
|
|
@@ -226,7 +238,7 @@ export declare const securityConfigSchema: z.ZodObject<{
|
|
|
226
238
|
defaultRoles: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
227
239
|
allowSignup: z.ZodDefault<z.ZodBoolean>;
|
|
228
240
|
successRedirect: z.ZodDefault<z.ZodString>;
|
|
229
|
-
failureRedirect: z.
|
|
241
|
+
failureRedirect: z.ZodOptional<z.ZodString>;
|
|
230
242
|
providers: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
231
243
|
clientId: z.ZodString;
|
|
232
244
|
clientSecret: z.ZodString;
|
|
@@ -236,14 +248,65 @@ export declare const securityConfigSchema: z.ZodObject<{
|
|
|
236
248
|
}>>;
|
|
237
249
|
redirectUri: z.ZodString;
|
|
238
250
|
issuer: z.ZodOptional<z.ZodString>;
|
|
251
|
+
discoveryUrl: z.ZodOptional<z.ZodString>;
|
|
239
252
|
scopes: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
240
253
|
successRedirect: z.ZodOptional<z.ZodString>;
|
|
241
254
|
failureRedirect: z.ZodOptional<z.ZodString>;
|
|
255
|
+
postLogoutRedirectUri: z.ZodOptional<z.ZodURL>;
|
|
242
256
|
defaultRoles: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
257
|
+
roleMapping: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
258
|
+
audiences: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
259
|
+
allowPlatformRoles: z.ZodDefault<z.ZodBoolean>;
|
|
260
|
+
rolesSource: z.ZodOptional<z.ZodArray<z.ZodEnum<{
|
|
261
|
+
client: "client";
|
|
262
|
+
groups: "groups";
|
|
263
|
+
realm: "realm";
|
|
264
|
+
}>>>;
|
|
243
265
|
label: z.ZodOptional<z.ZodString>;
|
|
266
|
+
icon: z.ZodOptional<z.ZodString>;
|
|
267
|
+
admin: z.ZodOptional<z.ZodObject<{
|
|
268
|
+
username: z.ZodString;
|
|
269
|
+
password: z.ZodString;
|
|
270
|
+
}, z.core.$strict>>;
|
|
244
271
|
hidden: z.ZodDefault<z.ZodBoolean>;
|
|
245
272
|
}, z.core.$strict>>>;
|
|
246
273
|
}, z.core.$strict>>;
|
|
274
|
+
loginPage: z.ZodDefault<z.ZodObject<{
|
|
275
|
+
enabled: z.ZodDefault<z.ZodBoolean>;
|
|
276
|
+
path: z.ZodDefault<z.ZodString>;
|
|
277
|
+
title: z.ZodOptional<z.ZodString>;
|
|
278
|
+
logo: z.ZodOptional<z.ZodString>;
|
|
279
|
+
template: z.ZodOptional<z.ZodString>;
|
|
280
|
+
stylesheet: z.ZodOptional<z.ZodString>;
|
|
281
|
+
heading: z.ZodOptional<z.ZodString>;
|
|
282
|
+
subtitle: z.ZodOptional<z.ZodString>;
|
|
283
|
+
providersFirst: z.ZodDefault<z.ZodBoolean>;
|
|
284
|
+
hero: z.ZodOptional<z.ZodUnion<readonly [z.ZodLiteral<false>, z.ZodObject<{
|
|
285
|
+
heading: z.ZodString;
|
|
286
|
+
text: z.ZodOptional<z.ZodString>;
|
|
287
|
+
}, z.core.$strict>]>>;
|
|
288
|
+
footer: z.ZodDefault<z.ZodBoolean>;
|
|
289
|
+
password: z.ZodDefault<z.ZodBoolean>;
|
|
290
|
+
skin: z.ZodDefault<z.ZodEnum<{
|
|
291
|
+
blueprint: "blueprint";
|
|
292
|
+
console: "console";
|
|
293
|
+
dots: "dots";
|
|
294
|
+
enterprise: "enterprise";
|
|
295
|
+
frontispiece: "frontispiece";
|
|
296
|
+
horizon: "horizon";
|
|
297
|
+
ledger: "ledger";
|
|
298
|
+
minimal: "minimal";
|
|
299
|
+
"photo-card": "photo-card";
|
|
300
|
+
}>>;
|
|
301
|
+
layout: z.ZodOptional<z.ZodEnum<{
|
|
302
|
+
bare: "bare";
|
|
303
|
+
card: "card";
|
|
304
|
+
split: "split";
|
|
305
|
+
}>>;
|
|
306
|
+
previewPicker: z.ZodDefault<z.ZodBoolean>;
|
|
307
|
+
previewHook: z.ZodOptional<z.ZodString>;
|
|
308
|
+
}, z.core.$strict>>;
|
|
309
|
+
allowInsecureCredentials: z.ZodDefault<z.ZodBoolean>;
|
|
247
310
|
apiKeys: z.ZodDefault<z.ZodObject<{
|
|
248
311
|
enabled: z.ZodDefault<z.ZodBoolean>;
|
|
249
312
|
prefix: z.ZodDefault<z.ZodString>;
|
|
@@ -33,7 +33,7 @@ export interface IAuthenticator {
|
|
|
33
33
|
/** Hook échec — log audit, throttling (J2)… Le 401 + challenge sont posés par le firewall. */
|
|
34
34
|
onFailure(context: ContextType, error: Error): Promise<void>;
|
|
35
35
|
/**
|
|
36
|
-
* Challenge `WWW-Authenticate` (RFC
|
|
36
|
+
* Challenge `WWW-Authenticate` (RFC 9110 §11 : tout 401 DOIT en porter un).
|
|
37
37
|
* Le firewall pose celui du premier authenticator de la zone qui en déclare.
|
|
38
38
|
* Ex. `'Basic realm="nodefony", charset="UTF-8"'`, `'Bearer'`.
|
|
39
39
|
*
|
|
@@ -29,6 +29,23 @@ export interface IFirewallZoneDescription {
|
|
|
29
29
|
host: string | null;
|
|
30
30
|
/** Zone valable aussi pour les frames WebSocket (api.request + subscribe). */
|
|
31
31
|
realtime: boolean;
|
|
32
|
+
/** Rôles exigés par défaut dans la zone (en OU) ; vide = toute identité authentifiée. */
|
|
33
|
+
roles?: readonly string[] | undefined;
|
|
34
|
+
/**
|
|
35
|
+
* Zone ouverte à tout compte que la connexion OAuth crée à la volée : sans
|
|
36
|
+
* rôle, avec `session`, et `oauth2.allowSignup` actif. Un constat, pas une
|
|
37
|
+
* faute — `roles: ["ROLE_USER"]` l'assume en une ligne.
|
|
38
|
+
*/
|
|
39
|
+
openToSignup?: boolean | undefined;
|
|
40
|
+
/**
|
|
41
|
+
* Le constat et ses deux gestes, rédigés par le pare-feu — `null` quand la
|
|
42
|
+
* zone n'est pas ouverte. Un lecteur (console, `doctor`) les rend TELS
|
|
43
|
+
* QUELS : deux rédactions du même fait finiraient par se contredire.
|
|
44
|
+
*/
|
|
45
|
+
openToSignupNotice?: {
|
|
46
|
+
message: string;
|
|
47
|
+
action: string;
|
|
48
|
+
} | null | undefined;
|
|
32
49
|
}
|
|
33
50
|
/** Un authenticator : disponible (registre de fabriques) et/ou monté (≥1 zone). */
|
|
34
51
|
export interface IFirewallAuthenticatorDescription {
|
|
@@ -38,7 +55,7 @@ export interface IFirewallAuthenticatorDescription {
|
|
|
38
55
|
mounted: boolean;
|
|
39
56
|
/** Présent dans le registre de fabriques (utilisable en config). */
|
|
40
57
|
available: boolean;
|
|
41
|
-
/** Déclare un challenge `WWW-Authenticate` (RFC
|
|
58
|
+
/** Déclare un challenge `WWW-Authenticate` (RFC 9110 §11). */
|
|
42
59
|
challenge: boolean;
|
|
43
60
|
}
|
|
44
61
|
/** Défenses transverses résolues (CSRF/CORS/headers/throttle) — sans secret. */
|
|
@@ -111,10 +128,34 @@ export interface IRoleDescription {
|
|
|
111
128
|
/** Tous les rôles atteignables transitivement (hors le rôle lui-même), triés. */
|
|
112
129
|
inherits: string[];
|
|
113
130
|
}
|
|
131
|
+
/**
|
|
132
|
+
* Table de correspondance des rôles d'UN fournisseur d'identité
|
|
133
|
+
* (`oauth2.providers.<nom>.roleMapping`) — des noms de rôles, rien d'autre de
|
|
134
|
+
* sa configuration.
|
|
135
|
+
*/
|
|
136
|
+
export interface IProviderRoleMappingDescription {
|
|
137
|
+
/** Nom du fournisseur (`keycloak`). */
|
|
138
|
+
provider: string;
|
|
139
|
+
/** Où ses rôles sont lus (`client`, `realm`, `groups`). */
|
|
140
|
+
sources: string[];
|
|
141
|
+
/**
|
|
142
|
+
* Rôle du fournisseur → rôle de l'application, triés par rôle du
|
|
143
|
+
* fournisseur ; `platform` = rôle de plateforme (`ROLE_NODEFONY_*`).
|
|
144
|
+
*/
|
|
145
|
+
mappings: {
|
|
146
|
+
from: string;
|
|
147
|
+
to: string;
|
|
148
|
+
platform: boolean;
|
|
149
|
+
}[];
|
|
150
|
+
/** `true` si la table peut accorder des rôles de plateforme. */
|
|
151
|
+
allowPlatformRoles: boolean;
|
|
152
|
+
}
|
|
114
153
|
/** Hiérarchie de rôles : déclaration brute + résolution aplatie. */
|
|
115
154
|
export interface IRoleHierarchyDescription {
|
|
116
155
|
/** Déclaration directe (`{ ROLE_ADMIN: ["ROLE_USER"] }`). */
|
|
117
156
|
hierarchy: Record<string, string[]>;
|
|
118
157
|
/** Résolution transitive précalculée par le {@link RoleHierarchyWalker}. */
|
|
119
158
|
roles: IRoleDescription[];
|
|
159
|
+
/** Rôles accordés par des fournisseurs d'identité — vide sans `roleMapping`. */
|
|
160
|
+
providerMappings: IProviderRoleMappingDescription[];
|
|
120
161
|
}
|
|
@@ -27,6 +27,19 @@ export interface IIssuerPolicy {
|
|
|
27
27
|
*/
|
|
28
28
|
readonly requireIssParameter: boolean;
|
|
29
29
|
}
|
|
30
|
+
/**
|
|
31
|
+
* Ce que porte une déconnexion initiée par l'application
|
|
32
|
+
* (OpenID Connect RP-Initiated Logout 1.0 §2).
|
|
33
|
+
*/
|
|
34
|
+
export interface ILogoutRequest {
|
|
35
|
+
/**
|
|
36
|
+
* ID token reçu au login (`id_token_hint`) : il dit au fournisseur QUELLE
|
|
37
|
+
* session fermer, et lui évite de demander une confirmation à l'utilisateur.
|
|
38
|
+
*/
|
|
39
|
+
readonly idTokenHint: string;
|
|
40
|
+
/** Adresse de retour, enregistrée chez le fournisseur (`post_logout_redirect_uri`). */
|
|
41
|
+
readonly postLogoutRedirectUri: string;
|
|
42
|
+
}
|
|
30
43
|
/**
|
|
31
44
|
* Adaptateur d'**un fournisseur OAuth/OIDC**, façade UNIFORME au-dessus d'un client
|
|
32
45
|
* OAuth 2.0 — masque les divergences entre fournisseurs derrière un contrat stable
|
|
@@ -79,4 +92,39 @@ export interface IOAuthProvider {
|
|
|
79
92
|
* @throws Si le fournisseur ne renvoie pas d'identifiant stable.
|
|
80
93
|
*/
|
|
81
94
|
fetchProfile(tokens: OAuth2Tokens): Promise<IOAuthProfile>;
|
|
95
|
+
/**
|
|
96
|
+
* Construit l'adresse de déconnexion chez le fournisseur (RP-Initiated
|
|
97
|
+
* Logout). ABSENTE quand le fournisseur n'en publie aucune (GitHub, OIDC sans
|
|
98
|
+
* `end_session_endpoint`) : la déconnexion reste alors locale, et l'ID token
|
|
99
|
+
* n'est pas conservé.
|
|
100
|
+
*/
|
|
101
|
+
createLogoutURL?(request: ILogoutRequest): URL;
|
|
102
|
+
/**
|
|
103
|
+
* Vérifie un jeton de déconnexion reçu sur le canal arrière (OpenID Connect
|
|
104
|
+
* Back-Channel Logout 1.0 §2.6) et rend ce qu'il désigne. ABSENTE quand le
|
|
105
|
+
* fournisseur ne sait pas en émettre (GitHub, OAuth sans OpenID Connect).
|
|
106
|
+
*
|
|
107
|
+
* @param logoutToken - le paramètre `logout_token` reçu, brut.
|
|
108
|
+
* @returns les claims qui désignent les sessions à fermer, ou `null` si le
|
|
109
|
+
* jeton est refusé (signature, émetteur, audience, expiration, forme).
|
|
110
|
+
* @throws Error si la vérification est IMPOSSIBLE (jeu de clés injoignable) —
|
|
111
|
+
* une panne n'est pas un jeton invalide.
|
|
112
|
+
*/
|
|
113
|
+
verifyLogoutToken?(logoutToken: string): Promise<ILogoutTokenClaims | null>;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Ce qu'un jeton de déconnexion VÉRIFIÉ désigne (OpenID Connect Back-Channel
|
|
117
|
+
* Logout 1.0 §2.4) : au moins l'un de `subject` et `sid`.
|
|
118
|
+
*/
|
|
119
|
+
export interface ILogoutTokenClaims {
|
|
120
|
+
/** Émetteur canonique — celui de la liste fermée qui a servi à vérifier. */
|
|
121
|
+
readonly issuer: string;
|
|
122
|
+
/** `sub` : l'utilisateur chez le fournisseur, ou `null`. */
|
|
123
|
+
readonly subject: string | null;
|
|
124
|
+
/** `sid` : LA session du fournisseur à fermer, ou `null` (= toutes celles de `subject`). */
|
|
125
|
+
readonly sid: string | null;
|
|
126
|
+
/** `jti` : identifiant unique du jeton, la clé de l'anti-rejeu. */
|
|
127
|
+
readonly tokenId: string;
|
|
128
|
+
/** `exp` : fin de validité, en secondes depuis l'époque. */
|
|
129
|
+
readonly expiresAt: number;
|
|
82
130
|
}
|
|
@@ -45,6 +45,28 @@ export interface ISecuredArea extends ISecurityZone {
|
|
|
45
45
|
* n'en ont pas besoin (session, mot de passe, jetons maison).
|
|
46
46
|
*/
|
|
47
47
|
readonly resource?: string | undefined;
|
|
48
|
+
/**
|
|
49
|
+
* Politique de rattachement du sujet d'un jeton TIERS propre à cette zone,
|
|
50
|
+
* ou `undefined` pour suivre `resourceServer.subjectPolicy`.
|
|
51
|
+
*
|
|
52
|
+
* C'est ce qui fait cohabiter une zone d'appelants machine (`ephemeral`) et
|
|
53
|
+
* une zone de personnes (`require`) dans la même application.
|
|
54
|
+
*/
|
|
55
|
+
readonly subjectPolicy?: "require" | "ephemeral" | undefined;
|
|
56
|
+
/**
|
|
57
|
+
* Rôles accordés par cette zone à un appelant `ephemeral` — définis dès que
|
|
58
|
+
* la zone déclare `subjectPolicy: "ephemeral"` (vide si elle n'en écrit
|
|
59
|
+
* aucun : un pouvoir local ne s'hérite jamais de la politique globale).
|
|
60
|
+
*/
|
|
61
|
+
readonly ephemeralRoles?: readonly string[] | undefined;
|
|
62
|
+
/**
|
|
63
|
+
* Page de connexion vers laquelle le pare-feu conduit un visiteur anonyme
|
|
64
|
+
* qui NAVIGUE (`302` vers `<entryPoint>?from=<chemin demandé>`), au lieu du
|
|
65
|
+
* `401`. Un appel d'API, un POST ou un WebSocket gardent leur `401` : un
|
|
66
|
+
* client qui attend du JSON casserait sur une page HTML. Omis = `401` pour
|
|
67
|
+
* tous. Chemin de l'origine, vérifié au boot.
|
|
68
|
+
*/
|
|
69
|
+
readonly entryPoint?: string | undefined;
|
|
48
70
|
/**
|
|
49
71
|
* Zone valable AUSSI pour le WebSocket (frames `api.request` + `subscribe`),
|
|
50
72
|
* pas seulement HTTP. Défaut `true` (Zero Trust : une zone protégée ferme TOUS
|
|
@@ -8,7 +8,10 @@ import type { TotpAlgorithm } from "../src/totp/totpCrypto.js";
|
|
|
8
8
|
* API). Les codes de récupération, eux, sont **hachés** (verify-only).
|
|
9
9
|
*/
|
|
10
10
|
export interface ITotpSecret {
|
|
11
|
-
/**
|
|
11
|
+
/**
|
|
12
|
+
* Propriétaire = `IUser.identifier` (clé naturelle), jamais `IUser.id` —
|
|
13
|
+
* cf `ITotpSecretStore`.
|
|
14
|
+
*/
|
|
12
15
|
readonly userId: string;
|
|
13
16
|
/**
|
|
14
17
|
* Secret partagé `K` **chiffré** (blob opaque : `iv.tag.ciphertext` base64url).
|
|
@@ -60,7 +60,13 @@ export interface TotpSecretUpdate {
|
|
|
60
60
|
* d'`IWebAuthnCredentialStore` / `ITokenStore` : le builtin `memory` est sans
|
|
61
61
|
* dépendance, les adapters lourds s'enregistrent depuis leur propre module.
|
|
62
62
|
*
|
|
63
|
-
* Modèle **1 secret / utilisateur**
|
|
63
|
+
* Modèle **1 secret / utilisateur** — `save` est un **upsert**.
|
|
64
|
+
*
|
|
65
|
+
* **Clé = `userId` = `IUser.identifier` — immuable, la même clé que la session et les jetons —,
|
|
66
|
+
* jamais `IUser.id`. Un appelant qui ne connaît que l'`id` (route admin
|
|
67
|
+
* `users/{id}/…`) résout d'abord le compte (`users.findById`), puis passe son
|
|
68
|
+
* `identifier`. Les deux sont des `string` : le compilateur ne voit pas l'erreur,
|
|
69
|
+
* et une clé fausse rend un succès qui n'a rien touché.
|
|
64
70
|
*/
|
|
65
71
|
export interface ITotpSecretStore {
|
|
66
72
|
/** Secret TOTP de l'utilisateur, ou `null` si non enrôlé. */
|
|
@@ -10,7 +10,10 @@
|
|
|
10
10
|
export interface IWebAuthnCredential {
|
|
11
11
|
/** Identifiant du credential (base64url) — unique, fourni par l'authenticator. */
|
|
12
12
|
readonly id: string;
|
|
13
|
-
/**
|
|
13
|
+
/**
|
|
14
|
+
* Propriétaire = `IUser.identifier` (sub / userHandle applicatif), jamais
|
|
15
|
+
* `IUser.id` — cf `IWebAuthnCredentialStore`.
|
|
16
|
+
*/
|
|
14
17
|
readonly userId: string;
|
|
15
18
|
/** Clé publique **COSE** encodée base64url — vérifie la signature des assertions. */
|
|
16
19
|
readonly publicKey: string;
|
|
@@ -67,6 +67,12 @@ export interface WebAuthnAuthUpdate {
|
|
|
67
67
|
* persistance (mémoire / ORM / Redis). Convention-frère d'`ITokenStore` :
|
|
68
68
|
* le builtin `memory` est sans dépendance, les adapters lourds s'enregistrent
|
|
69
69
|
* depuis leur propre module (inversion de dépendance).
|
|
70
|
+
*
|
|
71
|
+
* **Clé de propriétaire = `userId` = `IUser.identifier` — immuable, la même clé que la session et les jetons —,
|
|
72
|
+
* jamais `IUser.id`. Un appelant qui ne connaît que l'`id` (route admin
|
|
73
|
+
* `users/{id}/…`) résout d'abord le compte (`users.findById`), puis passe son
|
|
74
|
+
* `identifier`. Les deux sont des `string` : le compilateur ne voit pas l'erreur,
|
|
75
|
+
* et une clé fausse rend un succès qui n'a rien touché.
|
|
70
76
|
*/
|
|
71
77
|
export interface IWebAuthnCredentialStore {
|
|
72
78
|
/** Credential par son id (base64url), ou `null` — résolution à l'authentification. */
|
|
@@ -5,6 +5,8 @@ import { nodefonyError } from "nodefony";
|
|
|
5
5
|
* classes de `@nodefony/security`).
|
|
6
6
|
*
|
|
7
7
|
* - `400` — entrée invalide (nom vide, scope hors catalogue) ;
|
|
8
|
+
* - `403` — scope demandé que le porteur ne peut pas obtenir (`grantableScopes`,
|
|
9
|
+
* ou `admin:*` sans le rôle d'administration) ;
|
|
8
10
|
* - `409` — plafond `apiKeys.maxPerSubject` atteint ;
|
|
9
11
|
* - `503` — store de jetons indisponible (clés activées mais non provisionnées).
|
|
10
12
|
*
|
|
@@ -12,6 +14,6 @@ import { nodefonyError } from "nodefony";
|
|
|
12
14
|
* 401, message uniforme anti-énumération).
|
|
13
15
|
*/
|
|
14
16
|
export declare class ApiKeyError extends nodefonyError {
|
|
15
|
-
constructor(message: string, code: 400 | 409 | 503);
|
|
17
|
+
constructor(message: string, code: 400 | 403 | 409 | 503);
|
|
16
18
|
}
|
|
17
19
|
export default ApiKeyError;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { nodefonyError } from "nodefony";
|
|
2
|
+
/**
|
|
3
|
+
* Secret refusé parce qu'il arrive en clair — `code = 403` (RFC 9110 §15.5.4 :
|
|
4
|
+
* le serveur a compris la requête mais refuse de l'honorer).
|
|
5
|
+
*
|
|
6
|
+
* Message constant ({@link INSECURE_TRANSPORT_MESSAGE}, cœur) : le navigateur
|
|
7
|
+
* le reconnaît, et il ne dit rien de la topologie.
|
|
8
|
+
*
|
|
9
|
+
* Levée AVANT toute vérification, quand un mot de passe ou un code de second
|
|
10
|
+
* facteur arrive en production sur une requête dont le scheme effectif n'est
|
|
11
|
+
* pas `https` (cf {@link CredentialTransportPolicy}). Ce n'est pas un 401 :
|
|
12
|
+
* aucune preuve n'a été jugée, et rien de ce que le client corrigera dans son
|
|
13
|
+
* secret ne changera la réponse — c'est le canal qu'il doit changer. Pas de
|
|
14
|
+
* `WWW-Authenticate` non plus : un défi Basic ferait rouvrir au navigateur la
|
|
15
|
+
* fenêtre qui renverrait le mot de passe par le même canal.
|
|
16
|
+
*/
|
|
17
|
+
export declare class InsecureTransportError extends nodefonyError {
|
|
18
|
+
constructor();
|
|
19
|
+
}
|
|
20
|
+
export default InsecureTransportError;
|
|
@@ -6,3 +6,4 @@ export { InvalidTargetError } from "./InvalidTargetError.js";
|
|
|
6
6
|
export { CsrfError } from "./CsrfError.js";
|
|
7
7
|
export { SsrfError } from "./SsrfError.js";
|
|
8
8
|
export { WebAuthnError } from "./WebAuthnError.js";
|
|
9
|
+
export { InsecureTransportError } from "./InsecureTransportError.js";
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import type { LoginPageSkin } from "nodefony";
|
|
2
|
+
import type { IOAuthCheck } from "../src/oauth/providerDiagnosis.js";
|
|
3
|
+
import type { ISecurityConfig } from "../config/config.js";
|
|
4
|
+
/**
|
|
5
|
+
* Client de l'API d'administration de Keycloak — le point d'entrée UNIQUE de
|
|
6
|
+
* Nodefony vers cette API.
|
|
7
|
+
*
|
|
8
|
+
* Premier besoin : le thème de connexion d'un realm EXISTANT, que
|
|
9
|
+
* `security:keycloak:realm --write` ne peut pas changer (Keycloak ne relit le
|
|
10
|
+
* fichier d'import qu'à la création du realm). Les suivants — fermer une
|
|
11
|
+
* session Keycloak, lire un compte, exporter — s'ajoutent ici en méthodes, sur
|
|
12
|
+
* le même jeton et les mêmes gardes, au lieu de recopier l'appel.
|
|
13
|
+
*
|
|
14
|
+
* Il exige le compte d'administration du SERVEUR (realm `master`) : il est
|
|
15
|
+
* refusé hors développement, quoi que dise la configuration. Le service
|
|
16
|
+
* `keycloak` (`KeycloakService.admin()`) le rend, un par fournisseur.
|
|
17
|
+
*
|
|
18
|
+
* @module
|
|
19
|
+
*/
|
|
20
|
+
/** Le compte d'administration du serveur Keycloak (realm `master`). */
|
|
21
|
+
export interface IKeycloakAdminCredentials {
|
|
22
|
+
readonly username: string;
|
|
23
|
+
readonly password: string;
|
|
24
|
+
}
|
|
25
|
+
/** Le serveur à joindre et le compte qui l'administre. */
|
|
26
|
+
export interface IKeycloakAdminTarget {
|
|
27
|
+
/** Émetteur du fournisseur — l'adresse du realm (`https://…/realms/<nom>`). */
|
|
28
|
+
readonly issuer: string;
|
|
29
|
+
readonly admin: IKeycloakAdminCredentials;
|
|
30
|
+
}
|
|
31
|
+
/** Ce que le client doit savoir. */
|
|
32
|
+
export interface IKeycloakAdminClientOptions extends IKeycloakAdminTarget {
|
|
33
|
+
/** Environnement du kernel : tout autre que `development` est refusé. */
|
|
34
|
+
readonly environment: string;
|
|
35
|
+
/** Transport (défaut : `fetch` global) — injecté par les tests. */
|
|
36
|
+
readonly fetch?: typeof globalThis.fetch | undefined;
|
|
37
|
+
/** Délai de chaque appel, en millisecondes (défaut 10 s). */
|
|
38
|
+
readonly timeoutMs?: number | undefined;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Issue de {@link KeycloakAdminClient.applyLoginTheme} :
|
|
42
|
+
* - `applied` : le thème a changé ;
|
|
43
|
+
* - `unchanged` : le realm portait déjà ce thème ;
|
|
44
|
+
* - `unmanaged` : le realm porte un thème qui n'est pas de Nodefony (ou
|
|
45
|
+
* aucun) — il appartient à l'application, rien n'est touché.
|
|
46
|
+
*/
|
|
47
|
+
export type KeycloakLoginThemeOutcome = "applied" | "unchanged" | "unmanaged";
|
|
48
|
+
/** Ce que le geste a constaté et fait. */
|
|
49
|
+
export interface IKeycloakLoginThemeResult {
|
|
50
|
+
readonly realm: string;
|
|
51
|
+
/** Thème du realm AVANT le geste ; `null` s'il n'en déclarait aucun. */
|
|
52
|
+
readonly previous: string | null;
|
|
53
|
+
/** Thème que l'habillage demande. */
|
|
54
|
+
readonly theme: string;
|
|
55
|
+
readonly outcome: KeycloakLoginThemeOutcome;
|
|
56
|
+
}
|
|
57
|
+
/** Refus ou échec d'un appel ; le message nomme la cause et le geste suivant. */
|
|
58
|
+
export declare class KeycloakAdminError extends Error {
|
|
59
|
+
readonly status: number | null;
|
|
60
|
+
/**
|
|
61
|
+
* @param message - phrase destinée à l'opérateur
|
|
62
|
+
* @param status - statut HTTP de Keycloak, `null` s'il n'a pas répondu
|
|
63
|
+
*/
|
|
64
|
+
constructor(message: string, status?: number | null);
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Le realm et le compte d'administration d'un fournisseur, lus dans la
|
|
68
|
+
* configuration de sécurité VALIDÉE.
|
|
69
|
+
*
|
|
70
|
+
* @param config - configuration de sécurité (`defineSecurityConfig`)
|
|
71
|
+
* @param providerName - nom dans `oauth2.providers` (défaut `keycloak`)
|
|
72
|
+
* @returns l'émetteur et le compte
|
|
73
|
+
* @throws {KeycloakAdminError} Fournisseur absent, sans émetteur ou sans
|
|
74
|
+
* compte d'administration
|
|
75
|
+
*/
|
|
76
|
+
export declare function keycloakAdminTarget(config: ISecurityConfig, providerName?: string): IKeycloakAdminTarget;
|
|
77
|
+
/**
|
|
78
|
+
* Client de l'API d'administration d'UN realm Keycloak.
|
|
79
|
+
*
|
|
80
|
+
* Le jeton `admin-cli` est demandé au premier appel puis GARDÉ jusqu'à son
|
|
81
|
+
* expiration ; un 401 en cours de route le renouvelle une fois. Les gardes
|
|
82
|
+
* (développement seulement, https seulement) sont tenues au constructeur :
|
|
83
|
+
* un client qui existe a le droit de parler.
|
|
84
|
+
*/
|
|
85
|
+
export declare class KeycloakAdminClient {
|
|
86
|
+
#private;
|
|
87
|
+
/** Nom du realm administré. */
|
|
88
|
+
readonly realm: string;
|
|
89
|
+
/** Origine du serveur (`https://hôte:port`). */
|
|
90
|
+
readonly origin: string;
|
|
91
|
+
/**
|
|
92
|
+
* @param options - realm, compte, environnement, transport
|
|
93
|
+
* @throws {KeycloakAdminError} Hors développement, émetteur qui n'est pas un
|
|
94
|
+
* realm, ou émetteur en clair
|
|
95
|
+
*/
|
|
96
|
+
constructor(options: IKeycloakAdminClientOptions);
|
|
97
|
+
/**
|
|
98
|
+
* Un appel à l'API d'administration du realm, jeton joint.
|
|
99
|
+
*
|
|
100
|
+
* @param method - verbe HTTP
|
|
101
|
+
* @param path - chemin SOUS `/admin/realms/<realm>` (`""` = le realm, `/users?…`)
|
|
102
|
+
* @param body - corps JSON, ou rien
|
|
103
|
+
* @returns la réponse brute — au consommateur de lire son statut
|
|
104
|
+
* @throws {KeycloakAdminError} Serveur injoignable ou compte refusé
|
|
105
|
+
*/
|
|
106
|
+
request(method: string, path: string, body?: unknown): Promise<Response>;
|
|
107
|
+
/**
|
|
108
|
+
* La représentation du realm.
|
|
109
|
+
*
|
|
110
|
+
* @returns l'objet JSON rendu par Keycloak
|
|
111
|
+
* @throws {KeycloakAdminError} Realm introuvable ou lecture refusée
|
|
112
|
+
*/
|
|
113
|
+
getRealm(): Promise<Record<string, unknown>>;
|
|
114
|
+
/**
|
|
115
|
+
* Change des champs du realm — et eux seuls : Keycloak ne touche pas aux
|
|
116
|
+
* champs absents d'un `PUT` partiel.
|
|
117
|
+
*
|
|
118
|
+
* @param changes - les champs à écrire
|
|
119
|
+
* @throws {KeycloakAdminError} Écriture refusée
|
|
120
|
+
*/
|
|
121
|
+
updateRealm(changes: Record<string, unknown>): Promise<void>;
|
|
122
|
+
/**
|
|
123
|
+
* Pose sur le realm le thème de connexion qui porte un habillage de `/login`.
|
|
124
|
+
*
|
|
125
|
+
* Un realm habillé par l'application (thème ni `nodefony` ni `nodefony-*`,
|
|
126
|
+
* ou aucun) n'est JAMAIS modifié : même règle que la dérivation du fichier
|
|
127
|
+
* d'import.
|
|
128
|
+
*
|
|
129
|
+
* @param skin - habillage (`loginPage.skin`, ou celui d'un aperçu)
|
|
130
|
+
* @returns le thème d'avant, celui demandé, et ce qui a été fait
|
|
131
|
+
* @throws {KeycloakAdminError} Keycloak injoignable, compte refusé, realm
|
|
132
|
+
* introuvable
|
|
133
|
+
*/
|
|
134
|
+
applyLoginTheme(skin: LoginPageSkin): Promise<IKeycloakLoginThemeResult>;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Le thème de connexion qu'une page servie par Keycloak charge.
|
|
138
|
+
*
|
|
139
|
+
* Lu sans aucun identifiant : la page d'autorisation publique charge ses
|
|
140
|
+
* fichiers sous `/resources/<version>/login/<thème>/…`.
|
|
141
|
+
*
|
|
142
|
+
* @param html - début de la page d'autorisation
|
|
143
|
+
* @returns le nom du thème, ou `null` si la page n'est pas celle de Keycloak
|
|
144
|
+
*/
|
|
145
|
+
export declare function readKeycloakLoginTheme(html: string): string | null;
|
|
146
|
+
/**
|
|
147
|
+
* La sonde « thème » du diagnostic d'un fournisseur : la page de Keycloak
|
|
148
|
+
* suit-elle l'habillage de `/login` ?
|
|
149
|
+
*
|
|
150
|
+
* @param skin - habillage configuré (`loginPage.skin`)
|
|
151
|
+
* @returns une fonction qui juge la page d'autorisation ; elle rend `null` —
|
|
152
|
+
* aucune sonde — sur une page qui n'est pas de Keycloak
|
|
153
|
+
*/
|
|
154
|
+
export declare function keycloakLoginThemeCheck(skin: LoginPageSkin): (html: string) => IOAuthCheck | null;
|