@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
|
@@ -1,11 +1,57 @@
|
|
|
1
|
+
import { compileProviderRoleMapping, isPlatformRole, mapProviderRoles } from "../src/oauth/providerRoles.js";
|
|
1
2
|
import { AuthenticationError } from "../errors/AuthenticationError.js";
|
|
2
3
|
import { defineSecurityConfig } from "../config/defineModuleConfig.js";
|
|
4
|
+
import { recordAudit } from "../src/audit/recordAudit.js";
|
|
5
|
+
import { keycloakLoginThemeCheck } from "../keycloak/keycloakAdmin.js";
|
|
3
6
|
import { generateCodeVerifier, generateState } from "../src/oauth/oauth2Client.js";
|
|
4
|
-
import { getOAuthProviderFactory, listOAuthProviders } from "../src/oauth/oauthProviderRegistry.js";
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
+
import { getOAuthProviderFactory, listOAuthProviders, oauthProviderRequiresIssuer } from "../src/oauth/oauthProviderRegistry.js";
|
|
8
|
+
import { diagnoseOAuthProvider } from "../src/oauth/providerDiagnosis.js";
|
|
9
|
+
import { BootConfigurationError, LOGIN_PAGE_PATH, Service, canonicalIssuer } from "nodefony";
|
|
10
|
+
// nodefony/service/oauth2.ts
|
|
7
11
|
const serviceName = "oauth2";
|
|
8
12
|
/**
|
|
13
|
+
* Clé de session de l'indice de déconnexion. Lue et écrite par CE service
|
|
14
|
+
* seulement ({@link OAuth2Service.rememberLogout},
|
|
15
|
+
* {@link OAuth2Service.logoutUrlFor}) : les contrôleurs ne la connaissent pas.
|
|
16
|
+
*/
|
|
17
|
+
const LOGOUT_HINT_KEY = "oauth2:logout";
|
|
18
|
+
/**
|
|
19
|
+
* Clé de MÉTADONNÉE de session (`metaBag`) qui indexe un login fédéré :
|
|
20
|
+
* fournisseur et `sid`, sans l'ID token. C'est elle que lit le canal arrière.
|
|
21
|
+
*
|
|
22
|
+
* @remarks Pas dans les attributs : l'énumération des sessions les REDACTE
|
|
23
|
+
* (aucun store ne les rend hors de la session elle-même), quand `metaBag` en
|
|
24
|
+
* sort — et le résumé d'administration ne l'expose que par liste blanche.
|
|
25
|
+
*/
|
|
26
|
+
const FEDERATION_KEY = "oauth2:federation";
|
|
27
|
+
/**
|
|
28
|
+
* Plafond de la mémoire anti-rejeu du canal arrière — au-delà, les plus
|
|
29
|
+
* anciens `jti` sont oubliés. Un fournisseur n'émet qu'un jeton par session
|
|
30
|
+
* fermée : ce plafond n'est atteint que sous un flot anormal.
|
|
31
|
+
*/
|
|
32
|
+
const MAX_SEEN_LOGOUT_TOKENS = 1e4;
|
|
33
|
+
/** Relit l'index fédéré d'une session énumérée — sa forme se vérifie. */
|
|
34
|
+
function readFederation(value) {
|
|
35
|
+
if (typeof value !== "object" || value === null) return null;
|
|
36
|
+
const { provider, sid } = value;
|
|
37
|
+
if (typeof provider !== "string") return null;
|
|
38
|
+
return {
|
|
39
|
+
provider,
|
|
40
|
+
sid: typeof sid === "string" ? sid : null
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
/** Relit un indice de session — il vient du stockage, sa forme se vérifie. */
|
|
44
|
+
function readLogoutHint(value) {
|
|
45
|
+
if (typeof value !== "object" || value === null) return null;
|
|
46
|
+
const { provider, idToken, sid } = value;
|
|
47
|
+
if (typeof provider !== "string" || typeof idToken !== "string") return null;
|
|
48
|
+
return {
|
|
49
|
+
provider,
|
|
50
|
+
idToken,
|
|
51
|
+
sid: typeof sid === "string" ? sid : null
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
9
55
|
* Sigles qui se lisent en capitales — les capitaliser mot à mot rendrait
|
|
10
56
|
* « Oidc », « Sso », qu'aucun utilisateur ne reconnaît comme la technologie.
|
|
11
57
|
*/
|
|
@@ -58,19 +104,97 @@ function oauthDisplayLabel(name) {
|
|
|
58
104
|
return name.split(/[-_.\s]+/).filter((word) => word.length > 0).map((word) => ACRONYMS.has(word.toLowerCase()) ? word.toUpperCase() : word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
|
|
59
105
|
}
|
|
60
106
|
/**
|
|
107
|
+
* Délai avant de retenter un fournisseur dont la construction a échoué — assez
|
|
108
|
+
* long pour ne pas marteler un émetteur en panne à chaque affichage de l'écran
|
|
109
|
+
* de connexion, assez court pour qu'un bouton revienne sans redémarrage.
|
|
110
|
+
*/
|
|
111
|
+
const UNREACHABLE_RETRY_MS = 3e4;
|
|
112
|
+
/**
|
|
113
|
+
* Confronte l'émetteur configuré d'un fournisseur à ce que sa fabrique exige.
|
|
114
|
+
*
|
|
115
|
+
* Une URL qui ne peut pas être un émetteur est refusée QUEL QUE SOIT le
|
|
116
|
+
* fournisseur : aucune valeur mal formée n'est une configuration voulue. Une
|
|
117
|
+
* absence ne l'est que si la fabrique a déclaré l'émetteur requis.
|
|
118
|
+
*
|
|
119
|
+
* @param name - nom du fournisseur (`oauth2.providers.<name>`).
|
|
120
|
+
* @param issuer - émetteur lu dans sa configuration.
|
|
121
|
+
* @throws BootConfigurationError - émetteur requis absent, ou mal formé.
|
|
122
|
+
*/
|
|
123
|
+
function checkProviderIssuer(name, issuer) {
|
|
124
|
+
const key = `security.oauth2.providers.${name}.issuer`;
|
|
125
|
+
if (issuer === void 0 || issuer === "") {
|
|
126
|
+
if (oauthProviderRequiresIssuer(name)) throw new BootConfigurationError(`[@nodefony/security] ${key} est requis : le fournisseur « ${name} » découvre ses points d'entrée à partir de son émetteur. Attendu : l'URL https de l'émetteur OpenID Connect (Keycloak : URL du realm, ex. « https://auth.example.com/realms/mon-royaume »).`);
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
try {
|
|
130
|
+
canonicalIssuer(issuer);
|
|
131
|
+
} catch (error) {
|
|
132
|
+
throw new BootConfigurationError(`[@nodefony/security] ${key} — ${error instanceof Error ? error.message : String(error)}`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Annonce qu'un annuaire accorde des rôles de PLATEFORME, à la sévérité de
|
|
137
|
+
* l'environnement — ou `null` quand il n'y a rien à dire.
|
|
138
|
+
*
|
|
139
|
+
* L'ouverture est ÉCRITE (`allowPlatformRoles`), donc voulue : l'annonce ne
|
|
140
|
+
* la conteste pas, elle la rappelle. Le risque — un annuaire compromis
|
|
141
|
+
* administre l'instance — n'est réel qu'en production : là, `WARNING` ;
|
|
142
|
+
* ailleurs le même fait est dit en `INFO`, sans sonner une alarme à chaque
|
|
143
|
+
* démarrage d'un décor de développement.
|
|
144
|
+
*
|
|
145
|
+
* @param name - nom du fournisseur (`oauth2.providers.<name>`).
|
|
146
|
+
* @param allowed - `allowPlatformRoles` tel que déclaré.
|
|
147
|
+
* @param granted - rôles de l'application que la table accorde.
|
|
148
|
+
* @param production - l'application tourne en production.
|
|
149
|
+
* @returns le message et sa sévérité, ou `null`.
|
|
150
|
+
*/
|
|
151
|
+
function platformRolesNotice(name, allowed, granted, production) {
|
|
152
|
+
if (allowed !== true) return null;
|
|
153
|
+
const platform = [...new Set(granted.filter(isPlatformRole))];
|
|
154
|
+
if (platform.length === 0) return null;
|
|
155
|
+
return {
|
|
156
|
+
message: `oauth2 "${name}" : l'annuaire accorde des rôles de PLATEFORME (${platform.join(", ")}) — son administrateur administre cette instance (allowPlatformRoles)`,
|
|
157
|
+
severity: production ? "WARNING" : "INFO"
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Claims du jeton d'ACCÈS, quand c'est un JWT — `null` sinon (jeton opaque).
|
|
162
|
+
*
|
|
163
|
+
* Keycloak pose les rôles dans le jeton d'accès, et seulement là par défaut
|
|
164
|
+
* (`realm_access`, `resource_access`) : ni le jeton d'identité ni userinfo ne
|
|
165
|
+
* les portent sans « mapper » ajouté. La signature n'est pas vérifiée, pour la
|
|
166
|
+
* raison qui vaut pour le jeton d'identité (OpenID Connect Core §3.1.3.7) : il
|
|
167
|
+
* vient d'être reçu du point de jeton, sur un canal TLS direct et authentifié.
|
|
168
|
+
*/
|
|
169
|
+
async function accessTokenClaims(tokens) {
|
|
170
|
+
try {
|
|
171
|
+
return (await import("jose")).decodeJwt(tokens.accessToken());
|
|
172
|
+
} catch {
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
61
177
|
* **Social login OAuth 2.0** (P6 J9) — orchestrateur du flux *Authorization Code*.
|
|
62
178
|
*
|
|
63
179
|
* Posture OAuth 2.1 (RFC 9700) : Authorization Code uniquement (jamais implicit /
|
|
64
180
|
* ROPC), **PKCE S256** quand le fournisseur le supporte (RFC 7636), **state**
|
|
65
|
-
* anti-CSRF, **iss** anti-mix-up (RFC 9207) ; aucun jeton n'atteint le
|
|
66
|
-
* (le login produit une **session BFF**, gérée hors de ce service par
|
|
67
|
-
* controller + `AuthFlow`)
|
|
181
|
+
* anti-CSRF, **iss** anti-mix-up (RFC 9207) ; aucun jeton d'accès n'atteint le
|
|
182
|
+
* navigateur (le login produit une **session BFF**, gérée hors de ce service par
|
|
183
|
+
* le controller + `AuthFlow`) — seul l'ID token y passe, une fois, à la
|
|
184
|
+
* déconnexion ({@link IFederatedLogoutHint}).
|
|
68
185
|
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
186
|
+
* Au boot (si `oauth2.enabled`), la configuration de chaque fournisseur est
|
|
187
|
+
* confrontée à ce que sa fabrique exige : un émetteur requis absent, ou qui n'est
|
|
188
|
+
* pas une URL d'émetteur valide, INTERROMPT le démarrage en nommant la clé — une
|
|
189
|
+
* faute d'écriture ne doit pas attendre le premier clic pour se montrer. Un nom
|
|
190
|
+
* inconnu du registre reste un WARNING.
|
|
191
|
+
*
|
|
192
|
+
* Les fournisseurs sont construits une fois puis mémoïsés : c'est là que les
|
|
193
|
+
* points d'entrée d'un émetteur OIDC sont découverts. Un run qui sert (ports
|
|
194
|
+
* ouverts) les construit DÈS le boot, sans l'attendre : un émetteur injoignable
|
|
195
|
+
* n'est pas une faute de configuration, il se journalise et son bouton quitte
|
|
196
|
+
* l'écran de connexion jusqu'à ce qu'une nouvelle tentative réussisse. Un run
|
|
197
|
+
* console ne touche jamais le réseau.
|
|
74
198
|
*
|
|
75
199
|
* Le service ne touche **ni HTTP ni session** : il rend à l'appelant les éléments
|
|
76
200
|
* (URL, state, verifier) que le controller persiste en session — testable sans
|
|
@@ -80,11 +204,15 @@ var OAuth2Service = class extends Service {
|
|
|
80
204
|
module;
|
|
81
205
|
#config = null;
|
|
82
206
|
#providers = null;
|
|
207
|
+
#unreachable = null;
|
|
208
|
+
#seenLogoutTokens = null;
|
|
209
|
+
#roleMappings = null;
|
|
83
210
|
#ready = false;
|
|
84
211
|
constructor(module) {
|
|
85
212
|
super(serviceName, module.container, module.notificationsCenter, module.options);
|
|
86
213
|
this.module = module;
|
|
87
|
-
|
|
214
|
+
const oauth2ConfigCheck = () => this.#build();
|
|
215
|
+
this.kernel?.once("onBoot", oauth2ConfigCheck);
|
|
88
216
|
}
|
|
89
217
|
#build() {
|
|
90
218
|
let config;
|
|
@@ -97,13 +225,32 @@ var OAuth2Service = class extends Service {
|
|
|
97
225
|
this.log("oauth2 idle — social login désactivé en config", "DEBUG");
|
|
98
226
|
return;
|
|
99
227
|
}
|
|
100
|
-
this.#config = config;
|
|
101
228
|
const known = new Set(listOAuthProviders());
|
|
102
229
|
const configured = Object.keys(config.oauth2.providers);
|
|
103
230
|
for (const name of configured) if (!known.has(name)) this.log(`oauth2 provider "${name}" inconnu du registre — ignoré (registerOAuthProvider manquant ?)`, "WARNING");
|
|
104
|
-
this.#ready = true;
|
|
105
231
|
const active = configured.filter((n) => known.has(n));
|
|
232
|
+
for (const name of active) {
|
|
233
|
+
const provider = config.oauth2.providers[name];
|
|
234
|
+
checkProviderIssuer(name, provider?.issuer);
|
|
235
|
+
const mapping = provider === void 0 ? null : compileProviderRoleMapping(provider);
|
|
236
|
+
if (mapping !== null) {
|
|
237
|
+
const notice = platformRolesNotice(name, provider?.allowPlatformRoles, Object.values(mapping.table), this.kernel?.environment === "production");
|
|
238
|
+
if (notice !== null) this.log(notice.message, notice.severity);
|
|
239
|
+
this.#roleMappings ??= Object.create(null);
|
|
240
|
+
this.#roleMappings[name] = mapping;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
this.#config = config;
|
|
244
|
+
this.#ready = true;
|
|
106
245
|
this.log(`oauth2 ready — providers: [${active.join(", ") || "aucun"}]`, "DEBUG");
|
|
246
|
+
if (this.kernel?.runProfile.servers === true) for (const name of active) this.#probe(name);
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Construit un fournisseur sans attendre le résultat — l'échec est consigné
|
|
250
|
+
* par {@link #resolveProvider}, rien n'est levé vers l'appelant.
|
|
251
|
+
*/
|
|
252
|
+
#probe(name) {
|
|
253
|
+
this.#resolveProvider(name).catch(() => void 0);
|
|
107
254
|
}
|
|
108
255
|
/** `true` si le social login est opérationnel (activé + boot OK). */
|
|
109
256
|
isEnabled() {
|
|
@@ -135,30 +282,43 @@ var OAuth2Service = class extends Service {
|
|
|
135
282
|
listDisplayProviders() {
|
|
136
283
|
if (this.#config === null) return [];
|
|
137
284
|
const configured = this.#config.oauth2.providers;
|
|
138
|
-
return this.listProviders().filter((name) => !configured[name]?.hidden).map((name) =>
|
|
139
|
-
name
|
|
140
|
-
|
|
141
|
-
|
|
285
|
+
return this.listProviders().filter((name) => !configured[name]?.hidden).filter((name) => this.#isReachable(name)).map((name) => {
|
|
286
|
+
const label = configured[name]?.label ?? oauthDisplayLabel(name);
|
|
287
|
+
const icon = configured[name]?.icon;
|
|
288
|
+
return icon === void 0 ? {
|
|
289
|
+
name,
|
|
290
|
+
label
|
|
291
|
+
} : {
|
|
292
|
+
name,
|
|
293
|
+
label,
|
|
294
|
+
icon
|
|
295
|
+
};
|
|
296
|
+
});
|
|
142
297
|
}
|
|
143
298
|
/**
|
|
144
299
|
* Redirections post-login (succès / échec) — lues par le controller.
|
|
145
300
|
* Surcharge PAR FOURNISSEUR si fournie, sinon valeur globale, sinon défaut.
|
|
301
|
+
*
|
|
302
|
+
* @remarks L'échec mène par défaut à la page de connexion (`loginPage.path`) :
|
|
303
|
+
* déplacer la page déplace la redirection, les deux ne peuvent pas diverger.
|
|
146
304
|
*/
|
|
147
305
|
getRedirects(provider) {
|
|
148
306
|
const o = this.#config?.oauth2;
|
|
149
307
|
const p = provider ? o?.providers[provider] : void 0;
|
|
150
308
|
return {
|
|
151
309
|
success: p?.successRedirect ?? o?.successRedirect ?? "/",
|
|
152
|
-
failure: p?.failureRedirect ?? o?.failureRedirect ??
|
|
310
|
+
failure: p?.failureRedirect ?? o?.failureRedirect ?? this.#config?.loginPage.path ?? LOGIN_PAGE_PATH
|
|
153
311
|
};
|
|
154
312
|
}
|
|
155
313
|
/**
|
|
156
314
|
* Étape 1 — prépare l'URL d'autorisation + les éléments anti-replay à stocker
|
|
157
315
|
* en session (`state`, et `code_verifier` si PKCE).
|
|
158
316
|
*
|
|
317
|
+
* @param options.theme - thème rendu par l'application, transmis au seul
|
|
318
|
+
* fournisseur qui sait l'afficher (Keycloak) ; les autres l'ignorent
|
|
159
319
|
* @throws AuthenticationError — fournisseur non configuré / inconnu du registre.
|
|
160
320
|
*/
|
|
161
|
-
async createAuthorization(provider) {
|
|
321
|
+
async createAuthorization(provider, options = {}) {
|
|
162
322
|
const resolved = await this.#resolveProvider(provider);
|
|
163
323
|
const state = generateState();
|
|
164
324
|
const codeVerifier = resolved.provider.usesPkce ? generateCodeVerifier() : null;
|
|
@@ -166,7 +326,8 @@ var OAuth2Service = class extends Service {
|
|
|
166
326
|
url: resolved.provider.createAuthorizationURL({
|
|
167
327
|
state,
|
|
168
328
|
codeVerifier,
|
|
169
|
-
scopes: resolved.scopes
|
|
329
|
+
scopes: resolved.scopes,
|
|
330
|
+
...options.theme ? { theme: options.theme } : {}
|
|
170
331
|
}).toString(),
|
|
171
332
|
state,
|
|
172
333
|
codeVerifier
|
|
@@ -194,10 +355,202 @@ var OAuth2Service = class extends Service {
|
|
|
194
355
|
const profile = await p.fetchProfile(tokens);
|
|
195
356
|
const cfg = this.#ensureReady().oauth2;
|
|
196
357
|
const defaultRoles = cfg.providers[provider]?.defaultRoles ?? cfg.defaultRoles;
|
|
197
|
-
|
|
358
|
+
const mapping = this.#roleMappings?.[provider];
|
|
359
|
+
const providerRoles = mapping === void 0 ? void 0 : mapProviderRoles(mapping, await accessTokenClaims(tokens), profile.raw);
|
|
360
|
+
const user = await this.#resolveProvisioner().provisionOAuthUser(profile, {
|
|
198
361
|
defaultRoles: [...defaultRoles],
|
|
199
|
-
allowSignup: cfg.allowSignup
|
|
200
|
-
|
|
362
|
+
allowSignup: cfg.allowSignup,
|
|
363
|
+
...providerRoles === void 0 ? {} : { providerRoles }
|
|
364
|
+
});
|
|
365
|
+
let logoutHint = null;
|
|
366
|
+
if (p.createLogoutURL !== void 0) {
|
|
367
|
+
const sid = profile.raw?.sid;
|
|
368
|
+
logoutHint = {
|
|
369
|
+
provider,
|
|
370
|
+
idToken: tokens.idToken(),
|
|
371
|
+
sid: typeof sid === "string" ? sid : null
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
return {
|
|
375
|
+
identifier: user.identifier,
|
|
376
|
+
logoutHint
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
/**
|
|
380
|
+
* Retient en session l'indice de déconnexion d'un login fédéré — à appeler
|
|
381
|
+
* APRÈS l'ouverture de la session authentifiée (l'ID de session est
|
|
382
|
+
* régénéré à l'ouverture).
|
|
383
|
+
*
|
|
384
|
+
* @param session - session authentifiée de la requête.
|
|
385
|
+
* @param hint - indice rendu par {@link exchangeAndProvision}, ou `null`.
|
|
386
|
+
*/
|
|
387
|
+
rememberLogout(session, hint) {
|
|
388
|
+
if (hint === null) return;
|
|
389
|
+
session.set(LOGOUT_HINT_KEY, hint);
|
|
390
|
+
session.setMetaBag?.(FEDERATION_KEY, {
|
|
391
|
+
provider: hint.provider,
|
|
392
|
+
sid: hint.sid
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
/**
|
|
396
|
+
* Adresse de déconnexion chez le fournisseur qui a ouvert la session, ou
|
|
397
|
+
* `null` : session locale, fournisseur sans point de déconnexion, ou
|
|
398
|
+
* fournisseur devenu indisponible. À lire AVANT de détruire la session.
|
|
399
|
+
*
|
|
400
|
+
* @param session - session courante, ou `null`.
|
|
401
|
+
* @returns l'URL vers laquelle envoyer le navigateur, ou `null`.
|
|
402
|
+
*/
|
|
403
|
+
async logoutUrlFor(session) {
|
|
404
|
+
const hint = readLogoutHint(session?.get(LOGOUT_HINT_KEY));
|
|
405
|
+
if (hint === null || !this.#ready) return null;
|
|
406
|
+
let p;
|
|
407
|
+
try {
|
|
408
|
+
({provider: p} = await this.#resolveProvider(hint.provider));
|
|
409
|
+
} catch {
|
|
410
|
+
return null;
|
|
411
|
+
}
|
|
412
|
+
if (p.createLogoutURL === void 0) return null;
|
|
413
|
+
return p.createLogoutURL({
|
|
414
|
+
idTokenHint: hint.idToken,
|
|
415
|
+
postLogoutRedirectUri: this.#postLogoutRedirectUri(hint.provider)
|
|
416
|
+
}).toString();
|
|
417
|
+
}
|
|
418
|
+
/**
|
|
419
|
+
* Déconnexion demandée PAR le fournisseur sur le canal arrière (OpenID
|
|
420
|
+
* Connect Back-Channel Logout 1.0) : vérifie le jeton, refuse un rejeu, puis
|
|
421
|
+
* détruit les sessions locales qu'il désigne.
|
|
422
|
+
*
|
|
423
|
+
* Une session est désignée si elle a été ouverte par CE fournisseur (index
|
|
424
|
+
* fédéré posé au login par {@link rememberLogout}) et, selon le jeton : porte son `sid` ; ou,
|
|
425
|
+
* sans `sid`, appartient à l'utilisateur lié à son `sub`. Une session ouverte
|
|
426
|
+
* par mot de passe ou par un autre fournisseur n'est jamais touchée — le
|
|
427
|
+
* fournisseur ne ferme que ce qu'il a ouvert.
|
|
428
|
+
*
|
|
429
|
+
* @remarks Les connexions WebSocket portées par une session détruite se
|
|
430
|
+
* ferment au tick de revalidation du hub temps réel (code `4001`).
|
|
431
|
+
*
|
|
432
|
+
* @param provider - fournisseur désigné par l'adresse appelée.
|
|
433
|
+
* @param logoutToken - paramètre `logout_token` reçu, brut.
|
|
434
|
+
* @returns `unsupported` si le fournisseur ne sait pas émettre de jeton de
|
|
435
|
+
* déconnexion ; `refused` si le jeton est invalide ou rejoué ; sinon le
|
|
436
|
+
* nombre de sessions détruites (0 : déjà déconnecté, c'est un succès §2.7).
|
|
437
|
+
* @throws Error - vérification impossible (jeu de clés injoignable) ou
|
|
438
|
+
* sessions impossibles à parcourir : la déconnexion a ÉCHOUÉ.
|
|
439
|
+
*/
|
|
440
|
+
async backchannelLogout(provider, logoutToken) {
|
|
441
|
+
const { provider: p } = await this.#resolveProvider(provider);
|
|
442
|
+
if (p.verifyLogoutToken === void 0) return { outcome: "unsupported" };
|
|
443
|
+
const claims = await p.verifyLogoutToken(logoutToken);
|
|
444
|
+
if (claims === null || !this.#firstSeen(provider, claims)) return { outcome: "refused" };
|
|
445
|
+
let owner = null;
|
|
446
|
+
if (claims.subject !== null) {
|
|
447
|
+
const users = this.get("users");
|
|
448
|
+
try {
|
|
449
|
+
owner = (await users?.loadUserByOAuth?.(provider, claims.subject))?.identifier ?? null;
|
|
450
|
+
} catch {
|
|
451
|
+
owner = null;
|
|
452
|
+
}
|
|
453
|
+
if (owner === null && claims.sid === null) return {
|
|
454
|
+
outcome: "done",
|
|
455
|
+
destroyed: 0
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
const sessions = this.get("sessions");
|
|
459
|
+
if (sessions?.destroyWhere === void 0) throw new Error(`aucun service "sessions" capable de révocation groupée (destroyWhere)`);
|
|
460
|
+
const destroyed = await sessions.destroyWhere(owner !== null ? { user: owner } : void 0, (data) => {
|
|
461
|
+
if (owner !== null && data.user !== owner) return false;
|
|
462
|
+
const federation = readFederation(data.metaBag?.[FEDERATION_KEY]);
|
|
463
|
+
if (federation === null || federation.provider !== provider) return false;
|
|
464
|
+
return claims.sid === null || federation.sid === claims.sid;
|
|
465
|
+
});
|
|
466
|
+
this.log(`oauth2 back-channel logout "${provider}" — ${destroyed} session(s) fermée(s)` + (owner !== null ? ` user=${owner}` : ""), "INFO");
|
|
467
|
+
if (destroyed > 0) recordAudit(this.container, {
|
|
468
|
+
category: "session",
|
|
469
|
+
action: "session.revoked",
|
|
470
|
+
outcome: "success",
|
|
471
|
+
actor: `oauth2:${provider}`,
|
|
472
|
+
resource: owner,
|
|
473
|
+
reason: "backchannel_logout",
|
|
474
|
+
metadata: {
|
|
475
|
+
count: destroyed,
|
|
476
|
+
bySid: claims.sid !== null
|
|
477
|
+
}
|
|
478
|
+
});
|
|
479
|
+
return {
|
|
480
|
+
outcome: "done",
|
|
481
|
+
destroyed
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
/**
|
|
485
|
+
* Anti-rejeu du canal arrière (§2.6, point 8) : `true` au premier passage
|
|
486
|
+
* d'un `jti`, `false` s'il a déjà été vu et n'a pas expiré.
|
|
487
|
+
*
|
|
488
|
+
* @remarks Mémoire PAR PROCESSUS, bornée : le même jeton rejoué sur un autre
|
|
489
|
+
* exemplaire passerait. Le rejeu n'y fermerait que des sessions que le
|
|
490
|
+
* fournisseur a déjà déclarées closes — la borne est la durée de vie du
|
|
491
|
+
* jeton, que le fournisseur garde courte (§4).
|
|
492
|
+
*/
|
|
493
|
+
#firstSeen(provider, claims) {
|
|
494
|
+
const now = Date.now();
|
|
495
|
+
const seen = this.#seenLogoutTokens ??= /* @__PURE__ */ new Map();
|
|
496
|
+
for (const [key, expiresAt] of seen) {
|
|
497
|
+
if (expiresAt > now && seen.size < MAX_SEEN_LOGOUT_TOKENS) break;
|
|
498
|
+
seen.delete(key);
|
|
499
|
+
}
|
|
500
|
+
const key = `${provider}\n${claims.tokenId}`;
|
|
501
|
+
const known = seen.get(key);
|
|
502
|
+
if (known !== void 0 && known > now) return false;
|
|
503
|
+
seen.set(key, claims.expiresAt * 1e3);
|
|
504
|
+
return true;
|
|
505
|
+
}
|
|
506
|
+
/**
|
|
507
|
+
* Retour après déconnexion chez le fournisseur : la valeur configurée, sinon
|
|
508
|
+
* l'écran d'échec (la page de connexion) résolu contre `redirectUri` — une
|
|
509
|
+
* adresse ABSOLUE, sur une origine déjà enregistrée chez le fournisseur.
|
|
510
|
+
*
|
|
511
|
+
* @remarks Query et fragment de l'écran d'échec sont RETIRÉS : ils signalent
|
|
512
|
+
* un échec (`?error=oauth`), et un fournisseur OpenID Connect refuse une
|
|
513
|
+
* adresse de retour qui porte un paramètre du protocole (`error`, `code`…) —
|
|
514
|
+
* Keycloak répond `invalid_redirect_uri`.
|
|
515
|
+
*/
|
|
516
|
+
#postLogoutRedirectUri(provider) {
|
|
517
|
+
const cfg = this.#ensureReady().oauth2.providers[provider];
|
|
518
|
+
if (cfg?.postLogoutRedirectUri !== void 0) return cfg.postLogoutRedirectUri;
|
|
519
|
+
const { failure } = this.getRedirects(provider);
|
|
520
|
+
const url = new URL(failure, cfg?.redirectUri);
|
|
521
|
+
url.search = "";
|
|
522
|
+
url.hash = "";
|
|
523
|
+
return url.toString();
|
|
524
|
+
}
|
|
525
|
+
/**
|
|
526
|
+
* Diagnostique les fournisseurs CONFIGURÉS sans connexion humaine : découverte
|
|
527
|
+
* et émetteur, URL de retour acceptée, secret accepté (cf
|
|
528
|
+
* `diagnoseOAuthProvider`).
|
|
529
|
+
*
|
|
530
|
+
* Le fournisseur est construit à NEUF, hors de la mémoïsation du login : un
|
|
531
|
+
* diagnostic doit voir le serveur tel qu'il est maintenant, et son échec ne
|
|
532
|
+
* doit pas retirer un bouton de l'écran de connexion.
|
|
533
|
+
*
|
|
534
|
+
* @param name - un seul fournisseur ; à défaut, tous ceux de la configuration.
|
|
535
|
+
* @returns un diagnostic par fournisseur ; vide si le social login est éteint.
|
|
536
|
+
*/
|
|
537
|
+
async diagnose(name) {
|
|
538
|
+
if (!this.#ready || this.#config === null) return [];
|
|
539
|
+
const providers = this.#config.oauth2.providers;
|
|
540
|
+
const names = name === void 0 ? Object.keys(providers) : [name];
|
|
541
|
+
const pageCheck = keycloakLoginThemeCheck(this.#config.loginPage.skin);
|
|
542
|
+
return Promise.all(names.map((n) => {
|
|
543
|
+
const cfg = Object.hasOwn(providers, n) ? providers[n] : void 0;
|
|
544
|
+
return diagnoseOAuthProvider({
|
|
545
|
+
name: n,
|
|
546
|
+
build: async () => (await this.#buildProvider(n)).provider,
|
|
547
|
+
clientId: cfg?.clientId ?? "",
|
|
548
|
+
redirectUri: cfg?.redirectUri ?? "",
|
|
549
|
+
clientAuthMethod: cfg?.clientAuthMethod,
|
|
550
|
+
scopes: cfg?.scopes,
|
|
551
|
+
pageCheck
|
|
552
|
+
});
|
|
553
|
+
}));
|
|
201
554
|
}
|
|
202
555
|
#resolveProvider(name) {
|
|
203
556
|
this.#ensureReady();
|
|
@@ -206,9 +559,31 @@ var OAuth2Service = class extends Service {
|
|
|
206
559
|
if (cached) return cached;
|
|
207
560
|
const pending = this.#buildProvider(name);
|
|
208
561
|
this.#providers.set(name, pending);
|
|
209
|
-
pending.
|
|
562
|
+
pending.then(() => {
|
|
563
|
+
if (this.#unreachable !== null && name in this.#unreachable) {
|
|
564
|
+
delete this.#unreachable[name];
|
|
565
|
+
this.log(`oauth2 provider "${name}" de nouveau joignable`, "INFO");
|
|
566
|
+
}
|
|
567
|
+
}, (error) => {
|
|
568
|
+
this.#providers?.delete(name);
|
|
569
|
+
this.#unreachable ??= Object.create(null);
|
|
570
|
+
this.#unreachable[name] = Date.now();
|
|
571
|
+
this.log(`oauth2 provider "${name}" indisponible — bouton retiré de l'écran de connexion : ${error instanceof Error ? error.message : String(error)}`, "WARNING");
|
|
572
|
+
});
|
|
210
573
|
return pending;
|
|
211
574
|
}
|
|
575
|
+
/**
|
|
576
|
+
* `true` si le fournisseur n'a pas échoué récemment. Un échec plus ancien que
|
|
577
|
+
* {@link UNREACHABLE_RETRY_MS} relance une construction en arrière-plan : sans
|
|
578
|
+
* elle, un bouton retiré ne reviendrait jamais, puisque plus personne ne
|
|
579
|
+
* pourrait cliquer pour déclencher la tentative suivante.
|
|
580
|
+
*/
|
|
581
|
+
#isReachable(name) {
|
|
582
|
+
const failedAt = this.#unreachable?.[name];
|
|
583
|
+
if (failedAt === void 0) return true;
|
|
584
|
+
if (Date.now() - failedAt >= UNREACHABLE_RETRY_MS && !this.#providers?.has(name)) this.#probe(name);
|
|
585
|
+
return false;
|
|
586
|
+
}
|
|
212
587
|
async #buildProvider(name) {
|
|
213
588
|
const providers = this.#ensureReady().oauth2.providers;
|
|
214
589
|
const cfg = Object.hasOwn(providers, name) ? providers[name] : void 0;
|
|
@@ -220,7 +595,8 @@ var OAuth2Service = class extends Service {
|
|
|
220
595
|
clientSecret: cfg.clientSecret,
|
|
221
596
|
clientAuthMethod: cfg.clientAuthMethod,
|
|
222
597
|
redirectUri: cfg.redirectUri,
|
|
223
|
-
issuer: cfg.issuer
|
|
598
|
+
issuer: cfg.issuer,
|
|
599
|
+
discoveryUrl: cfg.discoveryUrl
|
|
224
600
|
});
|
|
225
601
|
return {
|
|
226
602
|
provider,
|
|
@@ -237,5 +613,4 @@ var OAuth2Service = class extends Service {
|
|
|
237
613
|
return this.#config;
|
|
238
614
|
}
|
|
239
615
|
};
|
|
240
|
-
|
|
241
|
-
export { OAuth2Service, OAuth2Service as default, oauthDisplayLabel };
|
|
616
|
+
export { OAuth2Service, OAuth2Service as default, checkProviderIssuer, oauthDisplayLabel, platformRolesNotice };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { mergeCspFragments } from "../src/csp.js";
|
|
2
|
-
|
|
2
|
+
// nodefony/service/securityHeaders.ts
|
|
3
3
|
/** Marqueur substitué par le nonce CSP de la requête (cf `Context.cspNonce`). */
|
|
4
4
|
const NONCE_PLACEHOLDER = "{{nonce}}";
|
|
5
5
|
/**
|
|
@@ -87,5 +87,4 @@ var SecurityHeaders = class {
|
|
|
87
87
|
return this.#cspParts !== null ? merged.split(NONCE_PLACEHOLDER).join(nonce) : merged.replace(/\s*'nonce-\{\{nonce\}\}'/g, "");
|
|
88
88
|
}
|
|
89
89
|
};
|
|
90
|
-
//#endregion
|
|
91
90
|
export { SecurityHeaders, SecurityHeaders as default };
|