@nodefony/security 10.0.0-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +544 -0
- package/README.md +182 -0
- package/dist/_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorate.js +9 -0
- package/dist/_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorateMetadata.js +6 -0
- package/dist/index.js +151 -0
- package/dist/nodefony/command/security-secrets.js +158 -0
- package/dist/nodefony/command/security-token.js +335 -0
- package/dist/nodefony/command/security-user-add.js +131 -0
- package/dist/nodefony/command/security-user-delete.js +102 -0
- package/dist/nodefony/command/security-user-list.js +77 -0
- package/dist/nodefony/config/config.js +366 -0
- package/dist/nodefony/config/defineModuleConfig.js +35 -0
- package/dist/nodefony/contracts/IAccessVoter.js +13 -0
- package/dist/nodefony/contracts/IApiKey.js +1 -0
- package/dist/nodefony/contracts/IAuditEvent.js +1 -0
- package/dist/nodefony/contracts/IAuditStore.js +1 -0
- package/dist/nodefony/contracts/IAuthenticator.js +1 -0
- package/dist/nodefony/contracts/IAuthorizationService.js +1 -0
- package/dist/nodefony/contracts/IFirewall.js +1 -0
- package/dist/nodefony/contracts/IFirewallDescription.js +1 -0
- package/dist/nodefony/contracts/IJwtKeystore.js +1 -0
- package/dist/nodefony/contracts/IOAuthProvider.js +1 -0
- package/dist/nodefony/contracts/ISecuredArea.js +1 -0
- package/dist/nodefony/contracts/IToken.js +1 -0
- package/dist/nodefony/contracts/ITokenStore.js +1 -0
- package/dist/nodefony/contracts/ITotpSecret.js +1 -0
- package/dist/nodefony/contracts/ITotpSecretStore.js +1 -0
- package/dist/nodefony/contracts/IWebAuthnCredential.js +1 -0
- package/dist/nodefony/contracts/IWebAuthnCredentialStore.js +1 -0
- package/dist/nodefony/contracts/IWebhookEndpoint.js +1 -0
- package/dist/nodefony/contracts/IWebhookStore.js +1 -0
- package/dist/nodefony/contracts/index.js +2 -0
- package/dist/nodefony/errors/AccessDeniedError.js +14 -0
- package/dist/nodefony/errors/ApiKeyError.js +21 -0
- package/dist/nodefony/errors/AuthenticationError.js +14 -0
- package/dist/nodefony/errors/CsrfError.js +23 -0
- package/dist/nodefony/errors/InvalidTargetError.js +39 -0
- package/dist/nodefony/errors/SsrfError.js +17 -0
- package/dist/nodefony/errors/ThrottledError.js +21 -0
- package/dist/nodefony/errors/UnverifiableTokenError.js +42 -0
- package/dist/nodefony/errors/WebAuthnError.js +21 -0
- package/dist/nodefony/errors/index.js +9 -0
- package/dist/nodefony/service/accessTokenVerifier.js +77 -0
- package/dist/nodefony/service/apiKeys.js +310 -0
- package/dist/nodefony/service/auditService.js +145 -0
- package/dist/nodefony/service/authFlow.js +332 -0
- package/dist/nodefony/service/authorization.js +95 -0
- package/dist/nodefony/service/cors.js +81 -0
- package/dist/nodefony/service/csrf.js +97 -0
- package/dist/nodefony/service/firewall.js +699 -0
- package/dist/nodefony/service/oauth2.js +153 -0
- package/dist/nodefony/service/securityHeaders.js +80 -0
- package/dist/nodefony/service/tokenService.js +486 -0
- package/dist/nodefony/service/totp.js +209 -0
- package/dist/nodefony/service/webAuthn.js +343 -0
- package/dist/nodefony/service/webhooks.js +539 -0
- package/dist/nodefony/src/RoleHierarchyWalker.js +77 -0
- package/dist/nodefony/src/SecuredArea.js +51 -0
- package/dist/nodefony/src/admin/SecurityAdminApi.js +495 -0
- package/dist/nodefony/src/admin/WebhookAdminApi.js +378 -0
- package/dist/nodefony/src/admin/adminAudit.js +37 -0
- package/dist/nodefony/src/admin/userRevocationCascade.js +40 -0
- package/dist/nodefony/src/apikey/apiKeyFormat.js +107 -0
- package/dist/nodefony/src/audit/MemoryAuditStore.js +121 -0
- package/dist/nodefony/src/audit/auditBridge.js +82 -0
- package/dist/nodefony/src/audit/auditFilters.js +60 -0
- package/dist/nodefony/src/audit/auditStoreRegistry.js +25 -0
- package/dist/nodefony/src/audit/readAuditContext.js +24 -0
- package/dist/nodefony/src/audit/recordAudit.js +16 -0
- package/dist/nodefony/src/authenticator/AnonymousAuthenticator.js +36 -0
- package/dist/nodefony/src/authenticator/ApiKeyAuthenticator.js +164 -0
- package/dist/nodefony/src/authenticator/ExternalJwtAuthenticator.js +224 -0
- package/dist/nodefony/src/authenticator/FirewallRealtimeAuthenticator.js +174 -0
- package/dist/nodefony/src/authenticator/JwtAuthenticator.js +176 -0
- package/dist/nodefony/src/authenticator/SessionAuthenticator.js +92 -0
- package/dist/nodefony/src/authenticator/UserPasswordAuthenticator.js +95 -0
- package/dist/nodefony/src/authenticator/authenticatorRegistry.js +63 -0
- package/dist/nodefony/src/authenticator/bearer.js +2 -0
- package/dist/nodefony/src/authenticator/externalSubject.js +36 -0
- package/dist/nodefony/src/authenticator/peekIssuer.js +56 -0
- package/dist/nodefony/src/crypto/secretCipher.js +79 -0
- package/dist/nodefony/src/csp.js +54 -0
- package/dist/nodefony/src/csrfToken.js +65 -0
- package/dist/nodefony/src/net/ssrfGuard.js +130 -0
- package/dist/nodefony/src/oauth/oauthProviderRegistry.js +37 -0
- package/dist/nodefony/src/oauth/providers/github.js +65 -0
- package/dist/nodefony/src/oauth/providers/oidc.js +48 -0
- package/dist/nodefony/src/realtime/UserRealtimeToken.js +94 -0
- package/dist/nodefony/src/realtime/frameAuthorizer.js +279 -0
- package/dist/nodefony/src/realtime/realtimeContracts.js +1 -0
- package/dist/nodefony/src/sessionIdentity.js +35 -0
- package/dist/nodefony/src/throttle/LoginThrottler.js +97 -0
- package/dist/nodefony/src/token/AnonymousToken.js +40 -0
- package/dist/nodefony/src/token/JwtKeystore.js +160 -0
- package/dist/nodefony/src/token/MemoryTokenStore.js +236 -0
- package/dist/nodefony/src/token/RemoteJwtVerifier.js +231 -0
- package/dist/nodefony/src/token/UserToken.js +67 -0
- package/dist/nodefony/src/token/jwtRuntime.js +19 -0
- package/dist/nodefony/src/token/secretFile.js +134 -0
- package/dist/nodefony/src/token/tokenCriteria.js +35 -0
- package/dist/nodefony/src/token/tokenFilters.js +72 -0
- package/dist/nodefony/src/token/tokenSort.js +40 -0
- package/dist/nodefony/src/token/tokenStatus.js +35 -0
- package/dist/nodefony/src/token/tokenStoreRegistry.js +25 -0
- package/dist/nodefony/src/totp/MemoryTotpSecretStore.js +97 -0
- package/dist/nodefony/src/totp/totpCipher.js +30 -0
- package/dist/nodefony/src/totp/totpCrypto.js +226 -0
- package/dist/nodefony/src/totp/totpOperations.js +129 -0
- package/dist/nodefony/src/totp/totpSecretStoreRegistry.js +18 -0
- package/dist/nodefony/src/voter/RoleVoter.js +32 -0
- package/dist/nodefony/src/voter/ScopeVoter.js +52 -0
- package/dist/nodefony/src/voter/voterRegistry.js +20 -0
- package/dist/nodefony/src/webauthn/MemoryWebAuthnCredentialStore.js +121 -0
- package/dist/nodefony/src/webauthn/webAuthnCredentialStoreRegistry.js +18 -0
- package/dist/nodefony/src/webhook/MemoryWebhookStore.js +87 -0
- package/dist/nodefony/src/webhook/WebhookDispatcher.js +208 -0
- package/dist/nodefony/src/webhook/webhookCipher.js +27 -0
- package/dist/nodefony/src/webhook/webhookDelivery.js +102 -0
- package/dist/nodefony/src/webhook/webhookFilters.js +56 -0
- package/dist/nodefony/src/webhook/webhookSignature.js +51 -0
- package/dist/nodefony/src/webhook/webhookSort.js +48 -0
- package/dist/nodefony/src/webhook/webhookStoreRegistry.js +18 -0
- package/dist/types/index.d.ts +157 -0
- package/dist/types/nodefony/command/security-secrets.d.ts +24 -0
- package/dist/types/nodefony/command/security-token.d.ts +44 -0
- package/dist/types/nodefony/command/security-user-add.d.ts +28 -0
- package/dist/types/nodefony/command/security-user-delete.d.ts +25 -0
- package/dist/types/nodefony/command/security-user-list.d.ts +28 -0
- package/dist/types/nodefony/config/config.d.ts +295 -0
- package/dist/types/nodefony/config/defineModuleConfig.d.ts +27 -0
- package/dist/types/nodefony/contracts/IAccessVoter.d.ts +23 -0
- package/dist/types/nodefony/contracts/IApiKey.d.ts +75 -0
- package/dist/types/nodefony/contracts/IAuditEvent.d.ts +94 -0
- package/dist/types/nodefony/contracts/IAuditStore.d.ts +80 -0
- package/dist/types/nodefony/contracts/IAuthenticator.d.ts +66 -0
- package/dist/types/nodefony/contracts/IAuthorizationService.d.ts +28 -0
- package/dist/types/nodefony/contracts/IFirewall.d.ts +64 -0
- package/dist/types/nodefony/contracts/IFirewallDescription.d.ts +120 -0
- package/dist/types/nodefony/contracts/IJwtKeystore.d.ts +40 -0
- package/dist/types/nodefony/contracts/IOAuthProvider.d.ts +51 -0
- package/dist/types/nodefony/contracts/ISecuredArea.d.ts +57 -0
- package/dist/types/nodefony/contracts/IToken.d.ts +41 -0
- package/dist/types/nodefony/contracts/ITokenStore.d.ts +240 -0
- package/dist/types/nodefony/contracts/ITotpSecret.d.ts +41 -0
- package/dist/types/nodefony/contracts/ITotpSecretStore.d.ts +88 -0
- package/dist/types/nodefony/contracts/IWebAuthnCredential.d.ts +56 -0
- package/dist/types/nodefony/contracts/IWebAuthnCredentialStore.d.ts +118 -0
- package/dist/types/nodefony/contracts/IWebhookEndpoint.d.ts +82 -0
- package/dist/types/nodefony/contracts/IWebhookStore.d.ts +85 -0
- package/dist/types/nodefony/contracts/index.d.ts +9 -0
- package/dist/types/nodefony/errors/AccessDeniedError.d.ts +10 -0
- package/dist/types/nodefony/errors/ApiKeyError.d.ts +17 -0
- package/dist/types/nodefony/errors/AuthenticationError.d.ts +10 -0
- package/dist/types/nodefony/errors/CsrfError.d.ts +19 -0
- package/dist/types/nodefony/errors/InvalidTargetError.d.ts +34 -0
- package/dist/types/nodefony/errors/SsrfError.d.ts +13 -0
- package/dist/types/nodefony/errors/ThrottledError.d.ts +16 -0
- package/dist/types/nodefony/errors/UnverifiableTokenError.d.ts +37 -0
- package/dist/types/nodefony/errors/WebAuthnError.d.ts +17 -0
- package/dist/types/nodefony/errors/index.d.ts +8 -0
- package/dist/types/nodefony/service/accessTokenVerifier.d.ts +29 -0
- package/dist/types/nodefony/service/apiKeys.d.ts +103 -0
- package/dist/types/nodefony/service/auditService.d.ts +30 -0
- package/dist/types/nodefony/service/authFlow.d.ts +123 -0
- package/dist/types/nodefony/service/authorization.d.ts +33 -0
- package/dist/types/nodefony/service/cors.d.ts +48 -0
- package/dist/types/nodefony/service/csrf.d.ts +57 -0
- package/dist/types/nodefony/service/firewall.d.ts +148 -0
- package/dist/types/nodefony/service/oauth2.d.ts +66 -0
- package/dist/types/nodefony/service/securityHeaders.d.ts +66 -0
- package/dist/types/nodefony/service/tokenService.d.ts +103 -0
- package/dist/types/nodefony/service/totp.d.ts +58 -0
- package/dist/types/nodefony/service/webAuthn.d.ts +123 -0
- package/dist/types/nodefony/service/webhooks.d.ts +160 -0
- package/dist/types/nodefony/src/RoleHierarchyWalker.d.ts +21 -0
- package/dist/types/nodefony/src/SecuredArea.d.ts +31 -0
- package/dist/types/nodefony/src/admin/SecurityAdminApi.d.ts +82 -0
- package/dist/types/nodefony/src/admin/WebhookAdminApi.d.ts +30 -0
- package/dist/types/nodefony/src/admin/adminAudit.d.ts +27 -0
- package/dist/types/nodefony/src/admin/userRevocationCascade.d.ts +31 -0
- package/dist/types/nodefony/src/apikey/apiKeyFormat.d.ts +43 -0
- package/dist/types/nodefony/src/audit/MemoryAuditStore.d.ts +33 -0
- package/dist/types/nodefony/src/audit/auditBridge.d.ts +49 -0
- package/dist/types/nodefony/src/audit/auditFilters.d.ts +56 -0
- package/dist/types/nodefony/src/audit/auditStoreRegistry.d.ts +37 -0
- package/dist/types/nodefony/src/audit/readAuditContext.d.ts +17 -0
- package/dist/types/nodefony/src/audit/recordAudit.d.ts +13 -0
- package/dist/types/nodefony/src/authenticator/AnonymousAuthenticator.d.ts +26 -0
- package/dist/types/nodefony/src/authenticator/ApiKeyAuthenticator.d.ts +74 -0
- package/dist/types/nodefony/src/authenticator/ExternalJwtAuthenticator.d.ts +132 -0
- package/dist/types/nodefony/src/authenticator/FirewallRealtimeAuthenticator.d.ts +78 -0
- package/dist/types/nodefony/src/authenticator/JwtAuthenticator.d.ts +69 -0
- package/dist/types/nodefony/src/authenticator/SessionAuthenticator.d.ts +70 -0
- package/dist/types/nodefony/src/authenticator/UserPasswordAuthenticator.d.ts +53 -0
- package/dist/types/nodefony/src/authenticator/authenticatorRegistry.d.ts +39 -0
- package/dist/types/nodefony/src/authenticator/bearer.d.ts +22 -0
- package/dist/types/nodefony/src/authenticator/externalSubject.d.ts +27 -0
- package/dist/types/nodefony/src/authenticator/peekIssuer.d.ts +31 -0
- package/dist/types/nodefony/src/crypto/secretCipher.d.ts +31 -0
- package/dist/types/nodefony/src/csp.d.ts +39 -0
- package/dist/types/nodefony/src/csrfToken.d.ts +36 -0
- package/dist/types/nodefony/src/net/ssrfGuard.d.ts +43 -0
- package/dist/types/nodefony/src/oauth/oauthProviderRegistry.d.ts +45 -0
- package/dist/types/nodefony/src/oauth/providers/github.d.ts +9 -0
- package/dist/types/nodefony/src/oauth/providers/oidc.d.ts +35 -0
- package/dist/types/nodefony/src/realtime/UserRealtimeToken.d.ts +62 -0
- package/dist/types/nodefony/src/realtime/frameAuthorizer.d.ts +171 -0
- package/dist/types/nodefony/src/realtime/realtimeContracts.d.ts +139 -0
- package/dist/types/nodefony/src/sessionIdentity.d.ts +20 -0
- package/dist/types/nodefony/src/throttle/LoginThrottler.d.ts +68 -0
- package/dist/types/nodefony/src/token/AnonymousToken.d.ts +23 -0
- package/dist/types/nodefony/src/token/JwtKeystore.d.ts +43 -0
- package/dist/types/nodefony/src/token/MemoryTokenStore.d.ts +66 -0
- package/dist/types/nodefony/src/token/RemoteJwtVerifier.d.ts +149 -0
- package/dist/types/nodefony/src/token/UserToken.d.ts +41 -0
- package/dist/types/nodefony/src/token/jwtRuntime.d.ts +28 -0
- package/dist/types/nodefony/src/token/secretFile.d.ts +70 -0
- package/dist/types/nodefony/src/token/tokenCriteria.d.ts +20 -0
- package/dist/types/nodefony/src/token/tokenFilters.d.ts +76 -0
- package/dist/types/nodefony/src/token/tokenSort.d.ts +33 -0
- package/dist/types/nodefony/src/token/tokenStatus.d.ts +38 -0
- package/dist/types/nodefony/src/token/tokenStoreRegistry.d.ts +38 -0
- package/dist/types/nodefony/src/totp/MemoryTotpSecretStore.d.ts +43 -0
- package/dist/types/nodefony/src/totp/totpCipher.d.ts +9 -0
- package/dist/types/nodefony/src/totp/totpCrypto.d.ts +164 -0
- package/dist/types/nodefony/src/totp/totpOperations.d.ts +73 -0
- package/dist/types/nodefony/src/totp/totpSecretStoreRegistry.d.ts +27 -0
- package/dist/types/nodefony/src/voter/RoleVoter.d.ts +25 -0
- package/dist/types/nodefony/src/voter/ScopeVoter.d.ts +30 -0
- package/dist/types/nodefony/src/voter/voterRegistry.d.ts +33 -0
- package/dist/types/nodefony/src/webauthn/MemoryWebAuthnCredentialStore.d.ts +39 -0
- package/dist/types/nodefony/src/webauthn/webAuthnCredentialStoreRegistry.d.ts +26 -0
- package/dist/types/nodefony/src/webhook/MemoryWebhookStore.d.ts +37 -0
- package/dist/types/nodefony/src/webhook/WebhookDispatcher.d.ts +69 -0
- package/dist/types/nodefony/src/webhook/webhookCipher.d.ts +8 -0
- package/dist/types/nodefony/src/webhook/webhookDelivery.d.ts +28 -0
- package/dist/types/nodefony/src/webhook/webhookFilters.d.ts +64 -0
- package/dist/types/nodefony/src/webhook/webhookSignature.d.ts +20 -0
- package/dist/types/nodefony/src/webhook/webhookSort.d.ts +39 -0
- package/dist/types/nodefony/src/webhook/webhookStoreRegistry.d.ts +31 -0
- package/docs/api-keys.md +691 -0
- package/docs/audit.md +751 -0
- package/docs/authenticators.md +487 -0
- package/docs/authorization.md +497 -0
- package/docs/cors.md +497 -0
- package/docs/csrf.md +392 -0
- package/docs/external-jwt.md +181 -0
- package/docs/firewall.md +546 -0
- package/docs/headers.md +616 -0
- package/docs/index.md +207 -0
- package/docs/lexique.md +190 -0
- package/docs/oauth2.md +575 -0
- package/docs/obtenir-un-jeton.md +225 -0
- package/docs/tokens.md +520 -0
- package/docs/totp.md +804 -0
- package/docs/webauthn.md +733 -0
- package/docs/webhooks.md +1016 -0
- package/package.json +83 -0
package/docs/oauth2.md
ADDED
|
@@ -0,0 +1,575 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "OAuth 2.0 â social login (Authorization Code + PKCE, posture 2.1)"
|
|
3
|
+
navTitle: OAuth 2.0
|
|
4
|
+
lang: fr
|
|
5
|
+
module: "@nodefony/security"
|
|
6
|
+
topic: oauth2
|
|
7
|
+
coverageModule: security
|
|
8
|
+
coverageFiles: "oauth2.ts,oauthProviderRegistry"
|
|
9
|
+
section: "Sécurité"
|
|
10
|
+
audience: [developer]
|
|
11
|
+
tags:
|
|
12
|
+
[
|
|
13
|
+
security,
|
|
14
|
+
oauth2,
|
|
15
|
+
oidc,
|
|
16
|
+
pkce,
|
|
17
|
+
social-login,
|
|
18
|
+
shadow-user,
|
|
19
|
+
rfc9700,
|
|
20
|
+
rfc9207,
|
|
21
|
+
rfc7636,
|
|
22
|
+
bff,
|
|
23
|
+
]
|
|
24
|
+
version: "doc"
|
|
25
|
+
status: stable
|
|
26
|
+
updated: 2026-07-19
|
|
27
|
+
source: "src/packages/@nodefony/security/docs/oauth2.md"
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
# OAuth 2.0 â social login (« Se connecter avec GitHub »)
|
|
31
|
+
|
|
32
|
+
> Ton utilisateur clique « Se connecter avec GitHub », part chez GitHub, revient â et se retrouve
|
|
33
|
+
> connecté à **ton** application. Nodefony orchestre ce voyage avec la posture **OAuth 2.1**
|
|
34
|
+
> (RFC 9700) : Authorization Code, PKCE, `state` anti-CSRF, `iss` anti-mix-up. Point clé :
|
|
35
|
+
> **aucun jeton n'atteint le navigateur** â le retour produit une **session BFF**, exactement la mĂȘme
|
|
36
|
+
> qu'un login par mot de passe. Ancré sur `OAuth2Service` (`oauth2.ts:55`) et le controller BFF
|
|
37
|
+
> `OAuth2Controller` (`OAuth2Controller.ts:81`).
|
|
38
|
+
|
|
39
|
+
đ [Documentation](../../../../../docs/index.md) âș [SĂ©curitĂ©](index.md) âș **OAuth2**
|
|
40
|
+
|
|
41
|
+
## đ§ Le modĂšle mental â deux allers-retours, un secret qui ne bouge pas
|
|
42
|
+
|
|
43
|
+
Le social login n'est pas « GitHub nous donne l'utilisateur ». C'est **deux voyages** :
|
|
44
|
+
|
|
45
|
+
1. le navigateur va **demander un accord** chez le fournisseur et revient avec un **ticket Ă usage
|
|
46
|
+
unique** (le `code`) ;
|
|
47
|
+
2. **ton serveur seul** échange ce ticket contre des jetons, sur un canal serveur-à -serveur.
|
|
48
|
+
|
|
49
|
+
L'analogie : le `code` est un **ticket de vestiaire** confié au client. Le manteau ne s'échange qu'au
|
|
50
|
+
comptoir, sur présentation du ticket **et** du talon que le comptoir avait gardé (le `code_verifier`
|
|
51
|
+
PKCE). Voler le ticket dans la poche du client ne suffit pas.
|
|
52
|
+
|
|
53
|
+
```mermaid
|
|
54
|
+
sequenceDiagram
|
|
55
|
+
autonumber
|
|
56
|
+
participant U as Navigateur
|
|
57
|
+
participant C as OAuth2Controller (BFF)
|
|
58
|
+
participant S as OAuth2Service
|
|
59
|
+
participant P as Fournisseur (GitHubâŠ)
|
|
60
|
+
participant D as Provisioner (users)
|
|
61
|
+
U->>C: GET âŠ/oauth2/github/authorize
|
|
62
|
+
C->>S: createAuthorization("github")
|
|
63
|
+
S-->>C: { url, state, codeVerifier }
|
|
64
|
+
C->>C: state + verifier + provider EN SESSION
|
|
65
|
+
C-->>U: 302 vers le fournisseur (+ cookie de transit)
|
|
66
|
+
U->>P: consentement de l'utilisateur
|
|
67
|
+
P-->>U: 302 âŠ/callback?code&state&iss
|
|
68
|
+
U->>C: GET âŠ/oauth2/github/callback
|
|
69
|
+
C->>C: state reçu ⥠state en session ? (puis INVALIDĂ)
|
|
70
|
+
C->>S: exchangeAndProvision(code, verifier, iss)
|
|
71
|
+
S->>P: échange du code (canal serveur, PKCE)
|
|
72
|
+
P-->>S: jetons + profil
|
|
73
|
+
S->>D: provisionOAuthUser(profil, policy)
|
|
74
|
+
D-->>S: IUser local (Shadow User)
|
|
75
|
+
S-->>C: { identifier }
|
|
76
|
+
C-->>U: 302 successRedirect + session BFF (ID régénéré)
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Deux propriétés se lisent sur ce schéma. Le **secret d'échange** (`clientSecret`, `code_verifier`)
|
|
80
|
+
ne quitte jamais le serveur. Et le **résultat** n'est pas un jeton exposé au JavaScript : c'est un
|
|
81
|
+
cookie de session opaque, révocable cÎté serveur.
|
|
82
|
+
|
|
83
|
+
## đ Lexique
|
|
84
|
+
|
|
85
|
+
| Terme | Sens |
|
|
86
|
+
| ------------------ | ----------------------------------------------------------------------------------------------------- |
|
|
87
|
+
| OAuth 2.0 | Protocole de **délégation d'accÚs** (RFC 6749). Ici détourné pour prouver une identité. |
|
|
88
|
+
| OIDC | _OpenID Connect_ : couche d'**identité** au-dessus d'OAuth ; ajoute l'**ID token** signé. |
|
|
89
|
+
| IdP | _Identity Provider_ â le fournisseur qui authentifie (Google, GitHub, KeycloakâŠ). |
|
|
90
|
+
| Authorization Code | Le flux oĂč le serveur Ă©change un `code` Ă usage unique contre des jetons. Jamais cĂŽtĂ© client. |
|
|
91
|
+
| PKCE | _Proof Key for Code Exchange_ (RFC 7636) : lie la demande et l'échange (anti-interception du `code`). |
|
|
92
|
+
| `code_verifier` | Le secret aléatoire gardé en session ; son empreinte (`code_challenge`) part avec la demande. |
|
|
93
|
+
| `state` | Jeton anti-CSRF porté à l'aller et au retour, comparé cÎté serveur (RFC 9700). |
|
|
94
|
+
| `iss` | Ămetteur renvoyĂ© au callback ; doit correspondre Ă celui attendu (anti-mix-up, RFC 9207). |
|
|
95
|
+
| Mix-up | Attaque oĂč un `code` Ă©mis par un IdP est prĂ©sentĂ© au callback d'un **autre** IdP. |
|
|
96
|
+
| ID token | JWT signĂ© par l'IdP portant les _claims_ d'identitĂ© (`sub`, `email`, `name`âŠ). |
|
|
97
|
+
| `sub` | _Subject_ : identifiant **stable** du compte chez le fournisseur (jamais l'e-mail). |
|
|
98
|
+
| Claim | Une donnée d'identité attestée par l'IdP (couple clé/valeur dans l'ID token). |
|
|
99
|
+
| BFF | _Backend For Frontend_ : l'identité vit en **session serveur**, pas en jeton exposé au JS. |
|
|
100
|
+
| Shadow User | La ligne **locale** créée Ă l'image du compte externe â c'est elle qui porte les rĂŽles. |
|
|
101
|
+
| JIT | _Just In Time_ : le Shadow User est créé **au premier login**, pas par un import préalable. |
|
|
102
|
+
| `arctic` | La bibliothÚque OAuth utilisée (~50 fournisseurs), chargée **paresseusement** au premier login. |
|
|
103
|
+
|
|
104
|
+
## Qu'est-ce que c'est ? â et quelles failles ça ferme
|
|
105
|
+
|
|
106
|
+
Déléguer le login à Google ou GitHub, c'est ne plus stocker de mots de passe : plus de fuite de
|
|
107
|
+
hachages, plus de réinitialisation à gérer, un utilisateur qui n'invente pas un éniÚme secret.
|
|
108
|
+
|
|
109
|
+
Mais OAuth mal implémenté est une **fabrique à comptes usurpés**. Quatre failles classiques, et ce
|
|
110
|
+
qui les ferme ici :
|
|
111
|
+
|
|
112
|
+
- **Vol de jeton via XSS** â si un `access_token` transite par le navigateur, tout script injectĂ©
|
|
113
|
+
peut le lire. _Fermé par construction_ : aucun jeton ne sort du serveur, le résultat est un cookie
|
|
114
|
+
de session opaque (`OAuth2Controller.ts:155`).
|
|
115
|
+
- **Interception du `code`** â un `code` captĂ© (log de proxy, historique, redirection ouverte) est
|
|
116
|
+
échangeable par l'attaquant. _Fermé par **PKCE**_ : l'échange exige le `code_verifier` resté en
|
|
117
|
+
session (`OAuth2Service.createAuthorization()`, `oauth2.ts:143-145`).
|
|
118
|
+
- **CSRF de login** â un tiers force ta victime Ă terminer **son** flux Ă lui : elle se retrouve
|
|
119
|
+
connectée sur le compte de l'attaquant, qui lit ensuite ce qu'elle y dépose. _Fermé par le `state`_
|
|
120
|
+
comparé au retour (`OAuth2Controller.callback()`, `OAuth2Controller.ts:137-145`).
|
|
121
|
+
- **Mix-up d'IdP** â un `code` obtenu chez un fournisseur malveillant est prĂ©sentĂ© au callback d'un
|
|
122
|
+
fournisseur de confiance. _Fermé par la vérification de l'`iss`_ (`oauth2.ts:170-174`) **et** par
|
|
123
|
+
l'exigence « mĂȘme fournisseur qu'Ă l'aller » cĂŽtĂ© controller (`OAuth2Controller.ts:142`).
|
|
124
|
+
|
|
125
|
+
> [!IMPORTANT]
|
|
126
|
+
> Le fournisseur social te dit **qui** est la personne. Il ne te dit **rien** de ses droits.
|
|
127
|
+
> Se connecter avec le compte Google d'un administrateur de Google ne rend administrateur de rien
|
|
128
|
+
> chez toi. Les rĂŽles viennent de la ligne locale â voir la section Shadow User.
|
|
129
|
+
|
|
130
|
+
## La vision Nodefony â un service sans transport, une session BFF
|
|
131
|
+
|
|
132
|
+
Trois partis pris, tous vérifiables au code.
|
|
133
|
+
|
|
134
|
+
**Le service ne touche ni HTTP ni session.** `OAuth2Service` rend Ă l'appelant les Ă©lĂ©ments Ă
|
|
135
|
+
persister (`url`, `state`, `codeVerifier`) et un simple `{ identifier }` en sortie
|
|
136
|
+
(`IOAuthAuthorization`, `oauth2.ts:21`). Conséquence pratique : la logique OAuth se teste **sans
|
|
137
|
+
serveur**, comme `AuthFlow`. Le transport (cookies, redirections 302) vit dans le controller BFF.
|
|
138
|
+
|
|
139
|
+
**Le login social finit exactement comme un login classique.** Le callback appelle
|
|
140
|
+
`AuthFlow.establishSessionFor()` (`authFlow.ts:215`), qui re-résout l'identité, vérifie que le compte
|
|
141
|
+
est actif, **régénÚre l'ID de session** (anti-fixation, `session.regenerateId()`, `authFlow.ts:388`)
|
|
142
|
+
et journalise l'événement
|
|
143
|
+
d'audit. Il n'existe **aucun** authenticator `oauth2` dans la chaĂźne du firewall : aprĂšs le retour,
|
|
144
|
+
c'est l'authenticator `session` qui identifie chaque requĂȘte, comme aprĂšs un mot de passe.
|
|
145
|
+
|
|
146
|
+
**Coût nul quand on ne s'en sert pas.** `arctic` est importé **paresseusement** au premier login
|
|
147
|
+
(`OAuth2Service.#ensureLib()`, `oauth2.ts:234`) â jamais au boot, jamais par requĂȘte. Les
|
|
148
|
+
fournisseurs sont instanciés une fois puis mémoïsés (`oauth2.ts:191-220`). Les routes ne sont montées
|
|
149
|
+
que si le service existe (`framework/index.ts:379`) : sans social login configuré, la surface HTTP
|
|
150
|
+
est **404**, pas « désactivée ».
|
|
151
|
+
|
|
152
|
+
Au boot, la config est validée et les fournisseurs configurés sont confrontés au registre : un nom
|
|
153
|
+
inconnu produit un **WARNING, pas un Ă©chec fatal** â `OAuth2Service.#build()` confronte les noms
|
|
154
|
+
configurés à `listOAuthProviders()` (`oauth2.ts:86-95`) et le
|
|
155
|
+
reste de l'application démarre, le bouton correspondant n'apparaßt simplement pas.
|
|
156
|
+
|
|
157
|
+
## đ DĂ©marrage rapide
|
|
158
|
+
|
|
159
|
+
### Les secrets entrent par `env.ts`, la config les branche
|
|
160
|
+
|
|
161
|
+
Un fournisseur n'est monté **que si ses deux secrets sont présents** : pas de bouton mort sur l'écran
|
|
162
|
+
de login quand la variable manque.
|
|
163
|
+
|
|
164
|
+
```typescript
|
|
165
|
+
// env.ts â SEUL lecteur de process.env (catalogue typĂ©, validĂ© au boot).
|
|
166
|
+
// nodefony.config.ts â `ctx.env` EST ce catalogue (typĂ© par le paramĂštre gĂ©nĂ©rique).
|
|
167
|
+
import { defineConfig, defineEnv, envString, use } from "nodefony";
|
|
168
|
+
|
|
169
|
+
export const env = defineEnv({
|
|
170
|
+
GITHUB_CLIENT_ID: envString({ optional: true }),
|
|
171
|
+
GITHUB_CLIENT_SECRET: envString({ optional: true }),
|
|
172
|
+
// Base des callbacks : doit correspondre EXACTEMENT à l'URL enregistrée chez
|
|
173
|
+
// le fournisseur (RFC 9700 â comparaison de chaĂźnes, pas de prĂ©fixe).
|
|
174
|
+
OAUTH_REDIRECT_BASE: envString({ default: "https://localhost:5152" }),
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
export default defineConfig<typeof env>((ctx) => ({
|
|
178
|
+
modules: [
|
|
179
|
+
"@nodefony/http",
|
|
180
|
+
"@nodefony/framework",
|
|
181
|
+
use("@nodefony/security", {
|
|
182
|
+
oauth2: {
|
|
183
|
+
// RĂŽles posĂ©s Ă la CRĂATION du compte local, jamais réécrits ensuite.
|
|
184
|
+
defaultRoles: ["ROLE_USER"],
|
|
185
|
+
allowSignup: true, // false = un compte local déjà lié est exigé
|
|
186
|
+
successRedirect: "/",
|
|
187
|
+
failureRedirect: "/login?error=oauth",
|
|
188
|
+
providers: {
|
|
189
|
+
// Secrets absents â fournisseur non montĂ©, bouton non affichĂ©.
|
|
190
|
+
...(ctx.env.GITHUB_CLIENT_ID && ctx.env.GITHUB_CLIENT_SECRET
|
|
191
|
+
? {
|
|
192
|
+
github: {
|
|
193
|
+
clientId: ctx.env.GITHUB_CLIENT_ID,
|
|
194
|
+
clientSecret: ctx.env.GITHUB_CLIENT_SECRET,
|
|
195
|
+
redirectUri: `${ctx.env.OAUTH_REDIRECT_BASE}/nodefony/security/api/oauth2/github/callback`,
|
|
196
|
+
},
|
|
197
|
+
}
|
|
198
|
+
: {}),
|
|
199
|
+
},
|
|
200
|
+
},
|
|
201
|
+
}),
|
|
202
|
+
],
|
|
203
|
+
}));
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
### Les routes sont FOURNIES â tu n'Ă©cris aucun controller
|
|
207
|
+
|
|
208
|
+
`mountOAuth2Routes()` (`OAuth2Controller.ts:185`) monte trois routes sous
|
|
209
|
+
`/nodefony/security/api/oauth2` (`OAuth2Controller.ts:187`), et **seulement si** le service `oauth2`
|
|
210
|
+
est présent (`framework/index.ts:379`) :
|
|
211
|
+
|
|
212
|
+
| Route | RĂŽle |
|
|
213
|
+
| ---------------------------- | --------------------------------------------------------------------- |
|
|
214
|
+
| `GET âŠ/providers` | Noms des fournisseurs **opĂ©rationnels** â l'UI n'affiche que ceux-lĂ . |
|
|
215
|
+
| `GET âŠ/{provider}/authorize` | DĂ©marre le flux : pose l'Ă©tat en session, `302` vers le fournisseur. |
|
|
216
|
+
| `GET âŠ/{provider}/callback` | Valide, Ă©change, provisionne, ouvre la session BFF, `302`. |
|
|
217
|
+
|
|
218
|
+
Ton écran de login n'a donc qu'un lien à poser :
|
|
219
|
+
|
|
220
|
+
```html
|
|
221
|
+
<a href="/nodefony/security/api/oauth2/github/authorize"
|
|
222
|
+
>Se connecter avec GitHub</a
|
|
223
|
+
>
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
> [!WARNING]
|
|
227
|
+
> Ces routes portent `bypassFirewall: true` (`OAuth2Controller.ts:213`) â elles **sont** le mĂ©canisme
|
|
228
|
+
> d'authentification : l'utilisateur est anonyme pendant tout l'aller-retour. Les protéger créerait
|
|
229
|
+
> un interblocage (il faudrait ĂȘtre connectĂ© pour pouvoir se connecter). La session anonyme ne porte
|
|
230
|
+
> que `state`/`code_verifier`, et son ID est **régénéré** à la promotion.
|
|
231
|
+
|
|
232
|
+
### Ce qu'on observe
|
|
233
|
+
|
|
234
|
+
```bash
|
|
235
|
+
# 1) Démarrage : 302 vers le fournisseur + cookie de transit + state dans l'URL
|
|
236
|
+
curl -si -c /tmp/jar http://localhost:5151/nodefony/security/api/oauth2/github/authorize | head -3
|
|
237
|
+
# HTTP/1.1 302 Found
|
|
238
|
+
# Location: https://github.com/login/oauth/authorize?...&state=8f2câŠ
|
|
239
|
+
# Set-Cookie: nodefony-sessid=âŠ; HttpOnly; SameSite=Lax
|
|
240
|
+
|
|
241
|
+
# 2) Retour du fournisseur (c'est le NAVIGATEUR qui suit ce lien) â session BFF
|
|
242
|
+
curl -si -b /tmp/jar -c /tmp/jar \
|
|
243
|
+
"http://localhost:5151/nodefony/security/api/oauth2/github/callback?code=âŠ&state=8f2câŠ" | head -2
|
|
244
|
+
# HTTP/1.1 302 Found
|
|
245
|
+
# Location: /
|
|
246
|
+
|
|
247
|
+
# 3) L'identité est résolue comme aprÚs un login classique
|
|
248
|
+
curl -s -b /tmp/jar http://localhost:5151/nodefony/security/api/auth/me
|
|
249
|
+
# {"user":{"username":"jane@example.com","roles":["ROLE_USER"]}}
|
|
250
|
+
|
|
251
|
+
# 4) Ce que l'UI de login interroge pour n'afficher que des boutons vivants
|
|
252
|
+
curl -s http://localhost:5151/nodefony/security/api/oauth2/providers
|
|
253
|
+
# {"providers":["github"]}
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
Séquence identique prouvée de bout en bout sur serveur réel par `oauth2-flow.test.ts` (6 cas).
|
|
257
|
+
|
|
258
|
+
## đïž Le flux, Ă©tape par Ă©tape
|
|
259
|
+
|
|
260
|
+
### Ătape 1 â `createAuthorization(provider)`
|
|
261
|
+
|
|
262
|
+
`OAuth2Service.createAuthorization()` (`oauth2.ts:139`) fabrique trois choses :
|
|
263
|
+
|
|
264
|
+
1. un **`state`** aléatoire (anti-CSRF) ;
|
|
265
|
+
2. un **`code_verifier`** â **seulement si** le fournisseur pratique PKCE (`usesPkce`,
|
|
266
|
+
`oauth2.ts:143-145`) ; `null` sinon (GitHub) ;
|
|
267
|
+
3. l'**URL d'autorisation** construite par l'adaptateur du fournisseur, avec les scopes effectifs
|
|
268
|
+
(ceux de la config, sinon les scopes par défaut du fournisseur, `oauth2.ts:216`).
|
|
269
|
+
|
|
270
|
+
Le controller pose les trois valeurs en session, **persiste** (`session.save()` â pas seulement en
|
|
271
|
+
mémoire, `OAuth2Controller.ts:105-108`), puis redirige en 302.
|
|
272
|
+
|
|
273
|
+
### Ătape 2 â le retour, validĂ© avant tout appel rĂ©seau
|
|
274
|
+
|
|
275
|
+
`OAuth2Controller.callback()` (`OAuth2Controller.ts:113`) travaille dans cet ordre, et l'ordre est la
|
|
276
|
+
défense :
|
|
277
|
+
|
|
278
|
+
1. **lire l'Ă©tat de session, puis l'invalider immĂ©diatement** (`OAuth2Controller.ts:126-129`) â le
|
|
279
|
+
`state` est Ă **usage unique** : un rejeu du mĂȘme retour Ă©choue, mĂȘme avec le bon cookie ;
|
|
280
|
+
2. **comparer** : `code` et `state` présents, `state` reçu ⥠`state` attendu, **et** fournisseur du
|
|
281
|
+
callback ⥠fournisseur dĂ©marrĂ© (`OAuth2Controller.ts:137-145`). Un seul Ă©cart â `302` vers
|
|
282
|
+
`failureRedirect`, **sans jamais contacter le fournisseur** ;
|
|
283
|
+
3. seulement ensuite, `exchangeAndProvision()`.
|
|
284
|
+
|
|
285
|
+
### Ătape 3 â `exchangeAndProvision(provider, code, verifier, iss)`
|
|
286
|
+
|
|
287
|
+
`OAuth2Service.exchangeAndProvision()` (`oauth2.ts:162`) enchaĂźne :
|
|
288
|
+
|
|
289
|
+
1. **anti-mix-up** â si le fournisseur annonce un Ă©metteur attendu, l'`iss` reçu doit correspondre,
|
|
290
|
+
et un `iss` **absent** est un rejet, pas une tolérance (`oauth2.ts:170-174`) ;
|
|
291
|
+
2. **échange** du `code` sur le canal serveur, avec le `code_verifier`
|
|
292
|
+
(`validateAuthorizationCode`, `oauth2.ts:175`), puis lecture du profil (`fetchProfile`,
|
|
293
|
+
`oauth2.ts:176`) ;
|
|
294
|
+
3. **provisionnement** du Shadow User avec la politique effective â rĂŽles par dĂ©faut surchargeables
|
|
295
|
+
**par fournisseur** (`oauth2.ts:180-181`), `allowSignup` global (`oauth2.ts:182-185`).
|
|
296
|
+
|
|
297
|
+
Toute erreur de cette étape est convertie en **échec uniforme** par le controller (`302
|
|
298
|
+
failureRedirect`, `OAuth2Controller.ts:157-160`) : le client ne distingue pas un `iss` invalide d'un
|
|
299
|
+
échange refusé ou d'un signup interdit.
|
|
300
|
+
|
|
301
|
+
## đ§ââïž Le Shadow User â l'identitĂ© locale, et pourquoi OAuth n'accorde aucun droit
|
|
302
|
+
|
|
303
|
+
Nodefony ne « connecte pas un compte Google ». Il crée et retrouve une **ligne locale** liée au
|
|
304
|
+
compte externe : le _Shadow User_. C'est cette ligne qui porte l'identifiant, les rÎles, l'état
|
|
305
|
+
actif/verrouillĂ© â donc **tout** ce dont l'autorisation a besoin.
|
|
306
|
+
|
|
307
|
+
Le contrat s'appelle `IOAuthUserProvisioner` (`IOAuthUserProvisioner.ts:61`) ; l'implémentation par
|
|
308
|
+
défaut est `UserService.provisionOAuthUser()` (`UserService.ts:306`), en **find-or-create** :
|
|
309
|
+
|
|
310
|
+
| Situation au retour du fournisseur | Comportement |
|
|
311
|
+
| ---------------------------------------- | ------------------------------------------------------------------------------ |
|
|
312
|
+
| Lien social dĂ©jĂ connu | Le compte existant est rendu tel quel â rien n'est créé, rien n'est réécrit. |
|
|
313
|
+
| Lien inconnu, `allowSignup: true` | Création JIT : `password: null`, rÎles = `defaultRoles`, lien social persisté. |
|
|
314
|
+
| Lien inconnu, `allowSignup: false` | **Ăchec fail-closed** (`UserService.ts:363`) â un compte liĂ© est exigĂ©. |
|
|
315
|
+
| E-mail identique Ă un compte local | **Aucune liaison automatique** â un compte SĂPARĂ est créé. |
|
|
316
|
+
| MĂȘme `providerId` chez deux fournisseurs | Comptes sĂ©parĂ©s (le couple `provider` + `providerId` fait la clĂ©). |
|
|
317
|
+
|
|
318
|
+
### Pourquoi l'e-mail ne lie jamais automatiquement un compte
|
|
319
|
+
|
|
320
|
+
C'est le point le plus contre-intuitif, et c'est une décision de sécurité. Si un compte externe dont
|
|
321
|
+
l'e-mail vaut `admin@ton-domaine.fr` liait automatiquement l'administrateur local, il suffirait de
|
|
322
|
+
créer un compte chez un fournisseur laxiste avec cette adresse pour **prendre le compte admin**. La
|
|
323
|
+
liaison par e-mail est donc refusée y compris quand le fournisseur certifie l'adresse
|
|
324
|
+
(`emailVerified` reste informatif, `IOAuthUserProvisioner.ts:24`).
|
|
325
|
+
|
|
326
|
+
Conséquence assumée : l'utilisateur qui avait un mot de passe et clique « avec GitHub » obtient un
|
|
327
|
+
**second** compte. Le rattachement d'un compte externe Ă un compte existant est une action explicite,
|
|
328
|
+
faite **utilisateur dĂ©jĂ connectĂ©** â jamais un effet de bord du login.
|
|
329
|
+
|
|
330
|
+
L'identifiant du compte créé dérive de l'e-mail si le fournisseur en donne un, sinon d'une clé
|
|
331
|
+
prĂ©fixĂ©e `provider:providerId` â jamais de collision entre fournisseurs (`UserService.ts:325-326`).
|
|
332
|
+
|
|
333
|
+
### Les rÎles sont posés à la création, et plus jamais
|
|
334
|
+
|
|
335
|
+
`defaultRoles` s'applique **au moment du `create`** (`UserService.ts:348`). Un second login
|
|
336
|
+
n'écrase rien : promouvoir quelqu'un dans ta base reste effectif, et modifier `defaultRoles` en
|
|
337
|
+
config ne repeint pas les comptes existants. C'est la traduction de la rÚgle « OAuth =
|
|
338
|
+
authentification, pas autorisation » (`oauth2.ts:178-181`, `config.ts:822-827`).
|
|
339
|
+
|
|
340
|
+
> [!TIP]
|
|
341
|
+
> Un fournisseur social ne doit **jamais** figurer dans le chemin d'obtention d'un rÎle privilégié.
|
|
342
|
+
> Le schéma de rÎles de Nodefony distingue déjà `ROLE_NODEFONY_*` (plateforme) et `ROLE_*`
|
|
343
|
+
> (applicatif) â voir [authorization](./authorization.md).
|
|
344
|
+
|
|
345
|
+
### Brancher sa propre politique
|
|
346
|
+
|
|
347
|
+
Le provisioner est le service `users` **s'il implémente la capability**, détecté par duck-typing
|
|
348
|
+
(`OAuth2Service.#resolveProvisioner()`, `oauth2.ts:224-231`). S'il ne l'implémente pas, le login
|
|
349
|
+
**Ă©choue** â jamais de crĂ©ation silencieuse par dĂ©faut. Une application qui veut sa propre politique
|
|
350
|
+
(quota d'inscriptions, allowlist de domaines e-mail, rattachement à un tenant) implémente
|
|
351
|
+
`provisionOAuthUser()` sur son service `users` : le profil normalisé `IOAuthProfile`
|
|
352
|
+
(`IOAuthUserProvisioner.ts:12`) lui donne `provider`, `providerId`, `email`, `emailVerified`, `name`
|
|
353
|
+
et la charge brute `raw`.
|
|
354
|
+
|
|
355
|
+
## đ§© Fournisseurs â catalogue et extension
|
|
356
|
+
|
|
357
|
+
Un fournisseur est un adaptateur qui implémente `IOAuthProvider` (`IOAuthProvider.ts:21`) : il masque
|
|
358
|
+
les divergences (PKCE ou non, profil par ID token ou par appel d'API) derriĂšre un contrat unique.
|
|
359
|
+
Trois sont livrés, résolus par nom via le registre `oauthProviderRegistry.ts:45`.
|
|
360
|
+
|
|
361
|
+
| Nom | Famille | PKCE | `iss` vérifié | Profil lu depuis | Scopes par défaut |
|
|
362
|
+
| ---------- | ---------------- | :--: | --------------------- | ----------------- | ---------------------------- |
|
|
363
|
+
| `google` | OIDC | â
| `accounts.google.com` | ID token (claims) | `openid`, `profile`, `email` |
|
|
364
|
+
| `keycloak` | OIDC self-hosted | â
| URL du realm (config) | ID token (claims) | `openid`, `profile`, `email` |
|
|
365
|
+
| `github` | OAuth simple | â | â (non Ă©mis) | API REST `/user` | `read:user`, `user:email` |
|
|
366
|
+
|
|
367
|
+
### `google` â OIDC, le cas nominal
|
|
368
|
+
|
|
369
|
+
Construit par le helper générique `createOidcProvider()` (`oidc.ts:48`) : PKCE systématique
|
|
370
|
+
(`usesPkce: true`, `oidc.ts:56`), émetteur figé `https://accounts.google.com`
|
|
371
|
+
(`oauthProviderRegistry.ts:74`). Le profil se lit dans l'**ID token** â claims standard `sub`,
|
|
372
|
+
`email`, `email_verified`, `name` (`oidc.ts:72-89`). Un ID token sans `sub` est refusé : pas
|
|
373
|
+
d'identifiant stable, pas d'identité (`oidc.ts:77-80`).
|
|
374
|
+
|
|
375
|
+
### `keycloak` â OIDC self-hosted, l'Ă©metteur vient de ta config
|
|
376
|
+
|
|
377
|
+
MĂȘme helper, mais l'**issuer** (URL du realm) sert Ă la fois Ă construire le client et Ă valider
|
|
378
|
+
l'`iss` (`oauthProviderRegistry.ts:86-105`). Il est donc **obligatoire** : sans lui, la fabrique lĂšve
|
|
379
|
+
au premier login avec un message explicite (`oauthProviderRegistry.ts:89-93`).
|
|
380
|
+
|
|
381
|
+
### `github` â OAuth simple, l'archĂ©type non-OIDC
|
|
382
|
+
|
|
383
|
+
Pas de PKCE, pas d'ID token, pas d'`iss` (`usesPkce: false`, `expectedIssuer: null`,
|
|
384
|
+
`github.ts:43-44`) : ici, la défense anti-CSRF repose **entiÚrement** sur le `state`. Le profil vient
|
|
385
|
+
de l'API REST `/user` (`createGithubProvider()`, `github.ts:34`). Subtilité GitHub : l'e-mail
|
|
386
|
+
primaire est souvent privĂ© â l'adaptateur bascule alors sur `/user/emails` et n'accepte
|
|
387
|
+
`emailVerified` que si GitHub le certifie (`github.ts:68-77`).
|
|
388
|
+
|
|
389
|
+
### Enregistrer le sien â sans Ă©diter le cĆur
|
|
390
|
+
|
|
391
|
+
`arctic` couvre une cinquantaine de fournisseurs (Microsoft, Apple, Discord, Auth0, OktaâŠ). Ajouter
|
|
392
|
+
l'un d'eux â ou un IdP maison â se fait par `registerOAuthProvider()`
|
|
393
|
+
(`oauthProviderRegistry.ts:51`), au chargement de ton module (avant le `onBoot` du service) :
|
|
394
|
+
|
|
395
|
+
```typescript ignore
|
|
396
|
+
import { registerOAuthProvider } from "@nodefony/security";
|
|
397
|
+
|
|
398
|
+
// Tout fournisseur OIDC : nom + classe arctic + issuer. Rien d'autre à écrire.
|
|
399
|
+
registerOAuthProvider("microsoft", (ctx) =>
|
|
400
|
+
createOidcProvider({
|
|
401
|
+
name: "microsoft",
|
|
402
|
+
client: new ctx.arctic.MicrosoftEntraId(
|
|
403
|
+
tenantId,
|
|
404
|
+
ctx.clientId,
|
|
405
|
+
ctx.clientSecret,
|
|
406
|
+
ctx.redirectUri,
|
|
407
|
+
),
|
|
408
|
+
issuer: `https://login.microsoftonline.com/${tenantId}/v2.0`,
|
|
409
|
+
decodeIdToken: ctx.arctic.decodeIdToken,
|
|
410
|
+
}),
|
|
411
|
+
);
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
La fabrique reçoit `IOAuthProviderContext` (`oauthProviderRegistry.ts:23`) : la lib `arctic` dĂ©jĂ
|
|
415
|
+
chargée, plus les secrets issus de la config. Aucun import runtime d'`arctic` n'entre par ce chemin.
|
|
416
|
+
Exemple réel et sans réseau dans le dépÎt : `src/modules/test/nodefony/secure/oauthTestProvider.ts`.
|
|
417
|
+
|
|
418
|
+
## âïž Configuration
|
|
419
|
+
|
|
420
|
+
Section `oauth2` du schéma Zod (`config.ts:990`), branchée sur la config du module
|
|
421
|
+
(`config.ts:990`). Table dĂ©rivĂ©e du schĂ©ma â les dĂ©fauts sont ceux du code.
|
|
422
|
+
|
|
423
|
+
| Option | Type | Défaut | Effet |
|
|
424
|
+
| ----------------- | -------------------- | --------------- | ---------------------------------------------------------- |
|
|
425
|
+
| `enabled` | booléen | `true` | Coupe le social login ; les routes ne montent pas. |
|
|
426
|
+
| `defaultRoles` | liste de rÎles | `["ROLE_USER"]` | RÎles du Shadow User **à la création** (`config.ts:989`). |
|
|
427
|
+
| `allowSignup` | booléen | `true` | `false` = compte préexistant lié exigé (`config.ts:1015`). |
|
|
428
|
+
| `successRedirect` | chemin | `/` | OĂč revient l'utilisateur aprĂšs succĂšs. |
|
|
429
|
+
| `failureRedirect` | chemin | `/login` | OĂč il revient aprĂšs Ă©chec (uniforme, sans dĂ©tail). |
|
|
430
|
+
| `providers` | dictionnaire par nom | `{}` | Fournisseurs activés (`config.ts:1031`). |
|
|
431
|
+
|
|
432
|
+
Par fournisseur (`oauthProviderSchema`, `config.ts:948`) :
|
|
433
|
+
|
|
434
|
+
<!-- prettier-ignore -->
|
|
435
|
+
| Option | Requis | Effet |
|
|
436
|
+
| --- | :---: | --- |
|
|
437
|
+
| `clientId` / `clientSecret` | â
| Identifiants délivrés par l'IdP. Secrets : par `env.ts`, jamais journalisés. |
|
|
438
|
+
| `redirectUri` | â
| URL de callback **exacte** (`config.ts:958`). |
|
|
439
|
+
| `issuer` | OIDC self-hosted | Realm Keycloak ; ignoré par les IdP à endpoints fixes. |
|
|
440
|
+
| `scopes` | | Vide = scopes par défaut du fournisseur. |
|
|
441
|
+
| `successRedirect` / `failureRedirect` / `defaultRoles` | | Surchargent le global **pour ce fournisseur** (`oauth2.ts:124-131`). |
|
|
442
|
+
|
|
443
|
+
Les surcharges par fournisseur permettent la cohabitation : un IdP de recette garde ses redirections
|
|
444
|
+
et ses rĂŽles pendant qu'un IdP de production pointe ailleurs.
|
|
445
|
+
|
|
446
|
+
## đ SĂ©curitĂ© â jetons du fournisseur, rĂ©vocation, attaques couvertes
|
|
447
|
+
|
|
448
|
+
### Les jetons du fournisseur ne sont pas conservés
|
|
449
|
+
|
|
450
|
+
C'est un choix, et il a des conséquences à connaßtre. Les jetons obtenus à l'échange vivent dans la
|
|
451
|
+
portée locale de l'échange (`validateAuthorizationCode` puis `fetchProfile`, `oauth2.ts:175-176`) :
|
|
452
|
+
ils ne sont ni retournés, ni mis en
|
|
453
|
+
session, ni persistés. Le profil normalisé qui traverse le systÚme n'en contient aucun
|
|
454
|
+
(`IOAuthUserProvisioner.ts:8-10`).
|
|
455
|
+
|
|
456
|
+
- **ConsĂ©quence 1** â la surface d'exposition est minimale : pas de coffre de jetons Ă protĂ©ger, pas
|
|
457
|
+
de fuite possible par la base ni par la session.
|
|
458
|
+
- **ConsĂ©quence 2** â l'application **ne peut pas** appeler l'API du fournisseur au nom de
|
|
459
|
+
l'utilisateur plus tard (lire ses dépÎts, envoyer un mail). Nodefony fait de l'**authentification**,
|
|
460
|
+
pas de la **délégation d'accÚs**.
|
|
461
|
+
- **Si tu as besoin de cette dĂ©lĂ©gation** : le seul endroit oĂč les jetons sont visibles est le
|
|
462
|
+
`fetchProfile()` de ton adaptateur (`IOAuthProvider.ts:63`) â c'est lĂ que ton implĂ©mentation les
|
|
463
|
+
capture et les persiste, sous ta responsabilité (chiffrement au repos, rotation, révocation).
|
|
464
|
+
|
|
465
|
+
### Ce que « révoquer » veut dire ici
|
|
466
|
+
|
|
467
|
+
| Action | Effet sur ton application |
|
|
468
|
+
| --------------------------------------------- | ------------------------------------------------------------------------------- |
|
|
469
|
+
| DĂ©connexion (`AuthFlow.logout()`) | Session dĂ©truite cĂŽtĂ© serveur + cookie effacĂ© â immĂ©diat. |
|
|
470
|
+
| Compte local dĂ©sactivĂ©/verrouillĂ© | Rejet Ă la requĂȘte suivante : l'identitĂ© est **re-rĂ©solue** Ă chaque requĂȘte. |
|
|
471
|
+
| Autorisation rĂ©voquĂ©e **chez le fournisseur** | **Aucun effet automatique** â la session locale reste valide jusqu'Ă son terme. |
|
|
472
|
+
| `allowSignup: false` aprĂšs coup | Bloque les nouveaux comptes, pas les liens existants. |
|
|
473
|
+
|
|
474
|
+
La troisiĂšme ligne est le piĂšge courant : une fois la session BFF ouverte, ton application ne
|
|
475
|
+
redemande plus rien à GitHub. Pour couper l'accÚs, il faut agir **localement** (désactiver le compte
|
|
476
|
+
ou détruire les sessions), pas chez le fournisseur.
|
|
477
|
+
|
|
478
|
+
### Attaques couvertes, prouvées par les tests
|
|
479
|
+
|
|
480
|
+
| Vecteur | Défense | Preuve |
|
|
481
|
+
| -------------------------------------------------- | ------------------------------------------------------- | -------------------------------- |
|
|
482
|
+
| Rejeu du retour (mĂȘme `code`, mĂȘme `state`) | `state` consommĂ© + session rĂ©gĂ©nĂ©rĂ©e Ă la promotion | `oauth2-attack.test.ts:89` (S5) |
|
|
483
|
+
| `state` valide présenté au callback d'un autre IdP | Fournisseur attendu conservé en session et comparé | `oauth2-attack.test.ts:115` (S6) |
|
|
484
|
+
| `iss` falsifié ou absent | Comparaison stricte à `expectedIssuer` | `oauth2Service.test.ts:168` |
|
|
485
|
+
| Prise de compte par e-mail collidant un admin | Aucune liaison auto : compte séparé, admin intact | `oauth.attack.test.ts:71` (A1) |
|
|
486
|
+
| ĂlĂ©vation de privilĂšge par re-login | RĂŽles posĂ©s Ă la crĂ©ation, jamais réécrits | `oauth.attack.test.ts:123` (A2) |
|
|
487
|
+
| Collision d'identifiants entre fournisseurs | Clé = `provider` + `providerId` | `oauth.attack.test.ts:155` (A3) |
|
|
488
|
+
| Interception du `code` | PKCE : `code_verifier` exigé, refus si absent | `oauthProviders.test.ts:67` |
|
|
489
|
+
| CrĂ©ation de compte non voulue | Provisioner absent (`provisionOAuthUser`) â fail-closed | `oauth2Service.test.ts:192` |
|
|
490
|
+
|
|
491
|
+
## đ Normes appliquĂ©es
|
|
492
|
+
|
|
493
|
+
| Domaine | Norme | Ancrage |
|
|
494
|
+
| --------------------------------- | ------------------------ | --------------------------------------------------------------------- |
|
|
495
|
+
| Flux Authorization Code | RFC 6749 | `IOAuthProvider.validateAuthorizationCode()` (`IOAuthProvider.ts:53`) |
|
|
496
|
+
| PKCE | RFC 7636 | `usesPkce` (`IOAuthProvider.ts:26`) · `oidc.ts:49-54` |
|
|
497
|
+
| Sécurité OAuth (BCP 2.1) | RFC 9700 | `OAuth2Service` (`oauth2.ts:40`) · `oauth2Schema` (`config.ts:1001`) |
|
|
498
|
+
| Anti-mix-up (`iss`) | RFC 9207 | `expectedIssuer` (`IOAuthProvider.ts:34`) · `oauth2.ts:170-174` |
|
|
499
|
+
| Callback en correspondance exacte | RFC 9700 §4 | `redirectUri` (`config.ts:958`) |
|
|
500
|
+
| Claims d'identité OIDC | OpenID Connect Core | `fetchProfile()` du helper OIDC (`oidc.ts:72-89`) |
|
|
501
|
+
| ID token consommé en code flow | RFC 8725 | `createOidcProvider()` (`oidc.ts:46`) |
|
|
502
|
+
| Anti-fixation de session | OWASP Session Management | `session.regenerateId()` au login (`authFlow.ts:388`) |
|
|
503
|
+
|
|
504
|
+
Flux **exclus** par posture 2.1, et donc absents du code : `implicit` (jeton en fragment d'URL) et
|
|
505
|
+
`password` / ROPC (l'application verrait le mot de passe du fournisseur).
|
|
506
|
+
|
|
507
|
+
## đĄ ObservabilitĂ© â Studio
|
|
508
|
+
|
|
509
|
+
L'écran de connexion de Studio consomme directement le data plane : il interroge
|
|
510
|
+
`/nodefony/security/api/oauth2/providers` (`Login.tsx:341`) et n'affiche **que** les fournisseurs
|
|
511
|
+
opĂ©rationnels â zĂ©ro bouton mort. Le clic dĂ©clenche la redirection vers `authorize`
|
|
512
|
+
(`Login.tsx:84`).
|
|
513
|
+
|
|
514
|
+
CÎté suivi, chaque login réussi produit un événement d'audit `auth` / `login.success` via
|
|
515
|
+
`AuthFlow.establishSessionFor()` (`authFlow.ts:229-236`), consultable dans l'écran **Audit**. La
|
|
516
|
+
session ouverte apparaßt dans l'écran **Sessions** (IP et agent capturés à l'ouverture) ; le compte
|
|
517
|
+
provisionné dans l'écran **Users**, avec ses rÎles réels.
|
|
518
|
+
|
|
519
|
+
> [!NOTE]
|
|
520
|
+
> L'événement d'audit du login social porte la raison par défaut `federated`
|
|
521
|
+
> (`authFlow.ts:218`) : le controller n'affine pas le facteur. Pour distinguer OAuth de WebAuthn dans
|
|
522
|
+
> un filtre d'audit, s'appuyer sur le contexte de la requĂȘte plutĂŽt que sur cette seule valeur.
|
|
523
|
+
|
|
524
|
+
## â ïž PiĂšges (symptĂŽme â cause â correction)
|
|
525
|
+
|
|
526
|
+
| SymptĂŽme | Cause (dans le code) | Correction |
|
|
527
|
+
| ------------------------------------------------- | ----------------------------------------------------------------------------- | ------------------------------------------------------------------- |
|
|
528
|
+
| `404` sur `âŠ/oauth2/âŠ` | Service `oauth2` absent (module non chargĂ© / `enabled: false`) | Charger `@nodefony/security` et activer `oauth2` |
|
|
529
|
+
| WARNING « inconnu du registre » au boot | Nom configuré sans fabrique (`oauth2.ts:86-95`) | `registerOAuthProvider()` au chargement du module, ou builtin |
|
|
530
|
+
| `404` « Unknown provider » sur `authorize` | Le nom n'est pas dans `listProviders()` (`OAuth2Controller.ts:97`) | Vérifier le nom exact **et** la présence des secrets |
|
|
531
|
+
| Bouton absent de l'Ă©cran de login | Secrets manquants â fournisseur non montĂ© (spread conditionnel) | Renseigner `clientId`/`clientSecret` dans l'env |
|
|
532
|
+
| `redirect_uri_mismatch` chez le fournisseur | `redirectUri` â URL enregistrĂ©e, au caractĂšre prĂšs (`config.ts:958`) | Aligner schĂ©ma, hĂŽte, port et chemin `/âŠ/{provider}/callback` |
|
|
533
|
+
| Retour systĂ©matique sur `failureRedirect` | `state`/`verifier` absents (cookie perdu entre les deux requĂȘtes) | VĂ©rifier `SameSite`/domaine du cookie ; un seul hĂŽte en dev |
|
|
534
|
+
| Callback Ă©choue au **deuxiĂšme** essai | `state` Ă usage unique, consommĂ© (`OAuth2Controller.ts:126-129`) | Refaire le flux depuis `authorize` â comportement attendu |
|
|
535
|
+
| `OAuth issuer mismatch` | `iss` reçu â `expectedIssuer` (`oauth2.ts:170-174`) | Corriger `issuer` (Keycloak : URL exacte du realm) |
|
|
536
|
+
| Keycloak : erreur dĂšs le premier login | `issuer` absent en config (`oauthProviderRegistry.ts:89-93`) | Renseigner l'URL du realm |
|
|
537
|
+
| « provisioning indisponible » | `users` n'implémente pas la capability (`oauth2.ts:224-231`) | Implémenter `provisionOAuthUser()` sur le service `users` |
|
|
538
|
+
| Profil connu refusé | `allowSignup: false` sans lien préexistant (`UserService.ts:363`) | Activer `allowSignup` ou lier le compte au préalable |
|
|
539
|
+
| Doublon de compte pour un utilisateur existant | Aucune liaison auto par e-mail (choix de sécurité) | Rattacher explicitement, utilisateur connecté |
|
|
540
|
+
| RÎle attendu absent aprÚs re-login | RÎles posés à la **création** seulement (`UserService.ts:348`) | Modifier les rÎles en base ; `defaultRoles` ne réécrit rien |
|
|
541
|
+
| Jeton du fournisseur introuvable cĂŽtĂ© application | `IOAuthProfile` n'en porte aucun, par choix (`IOAuthUserProvisioner.ts:8-10`) | Le capturer dans son propre `fetchProfile()` et le stocker soi-mĂȘme |
|
|
542
|
+
|
|
543
|
+
## đ§Ș Tests & couverture
|
|
544
|
+
|
|
545
|
+
Trois familles couvrent le social login â les chiffres exacts vivent dans la carte de l'aperçu
|
|
546
|
+
(régénérée depuis vitest, jamais figée ici) :
|
|
547
|
+
|
|
548
|
+
- **unitaires** â `oauth2Service.test` (boot, introspection, les deux Ă©tapes, anti-mix-up,
|
|
549
|
+
provisioning fail-closed), `oauthProviders.test` (registre, helper OIDC, adaptateur GitHub avec
|
|
550
|
+
e-mail public et privé), `oauthProvisioner.test` (find-or-create, JIT, signup interdit, non-liaison
|
|
551
|
+
par e-mail) ;
|
|
552
|
+
- **intĂ©gration** â `oauth2-flow.test` : le flux complet sur **serveur rĂ©el**, du `302` d'`authorize`
|
|
553
|
+
à l'identité résolue par `/me`, avec un fournisseur de test déterministe et sans réseau ;
|
|
554
|
+
- **attaque** â `oauth2-attack.test` (rejeu du `state`, mix-up de fournisseur) et
|
|
555
|
+
`oauth.attack.test` (collision d'e-mail avec un admin, élévation par re-login, collision
|
|
556
|
+
d'identifiants entre fournisseurs).
|
|
557
|
+
|
|
558
|
+
Ce qui **manque** : aucun banc de charge dédié au social login (le flux est un chemin froid, deux
|
|
559
|
+
requĂȘtes par connexion), et aucun test contre un IdP rĂ©el (impossible Ă automatiser â le fournisseur
|
|
560
|
+
de test couvre la branche PKCE).
|
|
561
|
+
|
|
562
|
+
Couverture : `npm run coverage` dans `@nodefony/security`. Campagnes d'attaque : skill
|
|
563
|
+
`nodefony-security-review` (mode red/blue-team).
|
|
564
|
+
|
|
565
|
+
## đ Pour aller plus loin
|
|
566
|
+
|
|
567
|
+
- âŹïž **Retour au hub** : [SĂ©curitĂ© â vue d'ensemble](index.md) · [Toute la documentation](../../../../../docs/index.md)
|
|
568
|
+
- đ§ **Pages sĆurs** : [Authenticators](authenticators.md) · [Jetons](tokens.md)
|
|
569
|
+
|
|
570
|
+
- La session produite par le login â [session](../../http/docs/session.md) ·
|
|
571
|
+
[authenticators](./authenticators.md)
|
|
572
|
+
- Ce qui dĂ©cide des droits une fois connectĂ© â [authorization](./authorization.md)
|
|
573
|
+
- Les autres facteurs sans mot de passe â [webauthn](./webauthn.md) · [totp](./totp.md)
|
|
574
|
+
- Jetons d'API pour les machines (le pendant non-humain) â [tokens](./tokens.md)
|
|
575
|
+
- Vue d'ensemble du module â [index](./index.md) · Termes transverses â [lexique](./lexique.md)
|