@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.
Files changed (258) hide show
  1. package/LICENSE +544 -0
  2. package/README.md +182 -0
  3. package/dist/_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorate.js +9 -0
  4. package/dist/_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorateMetadata.js +6 -0
  5. package/dist/index.js +151 -0
  6. package/dist/nodefony/command/security-secrets.js +158 -0
  7. package/dist/nodefony/command/security-token.js +335 -0
  8. package/dist/nodefony/command/security-user-add.js +131 -0
  9. package/dist/nodefony/command/security-user-delete.js +102 -0
  10. package/dist/nodefony/command/security-user-list.js +77 -0
  11. package/dist/nodefony/config/config.js +366 -0
  12. package/dist/nodefony/config/defineModuleConfig.js +35 -0
  13. package/dist/nodefony/contracts/IAccessVoter.js +13 -0
  14. package/dist/nodefony/contracts/IApiKey.js +1 -0
  15. package/dist/nodefony/contracts/IAuditEvent.js +1 -0
  16. package/dist/nodefony/contracts/IAuditStore.js +1 -0
  17. package/dist/nodefony/contracts/IAuthenticator.js +1 -0
  18. package/dist/nodefony/contracts/IAuthorizationService.js +1 -0
  19. package/dist/nodefony/contracts/IFirewall.js +1 -0
  20. package/dist/nodefony/contracts/IFirewallDescription.js +1 -0
  21. package/dist/nodefony/contracts/IJwtKeystore.js +1 -0
  22. package/dist/nodefony/contracts/IOAuthProvider.js +1 -0
  23. package/dist/nodefony/contracts/ISecuredArea.js +1 -0
  24. package/dist/nodefony/contracts/IToken.js +1 -0
  25. package/dist/nodefony/contracts/ITokenStore.js +1 -0
  26. package/dist/nodefony/contracts/ITotpSecret.js +1 -0
  27. package/dist/nodefony/contracts/ITotpSecretStore.js +1 -0
  28. package/dist/nodefony/contracts/IWebAuthnCredential.js +1 -0
  29. package/dist/nodefony/contracts/IWebAuthnCredentialStore.js +1 -0
  30. package/dist/nodefony/contracts/IWebhookEndpoint.js +1 -0
  31. package/dist/nodefony/contracts/IWebhookStore.js +1 -0
  32. package/dist/nodefony/contracts/index.js +2 -0
  33. package/dist/nodefony/errors/AccessDeniedError.js +14 -0
  34. package/dist/nodefony/errors/ApiKeyError.js +21 -0
  35. package/dist/nodefony/errors/AuthenticationError.js +14 -0
  36. package/dist/nodefony/errors/CsrfError.js +23 -0
  37. package/dist/nodefony/errors/InvalidTargetError.js +39 -0
  38. package/dist/nodefony/errors/SsrfError.js +17 -0
  39. package/dist/nodefony/errors/ThrottledError.js +21 -0
  40. package/dist/nodefony/errors/UnverifiableTokenError.js +42 -0
  41. package/dist/nodefony/errors/WebAuthnError.js +21 -0
  42. package/dist/nodefony/errors/index.js +9 -0
  43. package/dist/nodefony/service/accessTokenVerifier.js +77 -0
  44. package/dist/nodefony/service/apiKeys.js +310 -0
  45. package/dist/nodefony/service/auditService.js +145 -0
  46. package/dist/nodefony/service/authFlow.js +332 -0
  47. package/dist/nodefony/service/authorization.js +95 -0
  48. package/dist/nodefony/service/cors.js +81 -0
  49. package/dist/nodefony/service/csrf.js +97 -0
  50. package/dist/nodefony/service/firewall.js +699 -0
  51. package/dist/nodefony/service/oauth2.js +153 -0
  52. package/dist/nodefony/service/securityHeaders.js +80 -0
  53. package/dist/nodefony/service/tokenService.js +486 -0
  54. package/dist/nodefony/service/totp.js +209 -0
  55. package/dist/nodefony/service/webAuthn.js +343 -0
  56. package/dist/nodefony/service/webhooks.js +539 -0
  57. package/dist/nodefony/src/RoleHierarchyWalker.js +77 -0
  58. package/dist/nodefony/src/SecuredArea.js +51 -0
  59. package/dist/nodefony/src/admin/SecurityAdminApi.js +495 -0
  60. package/dist/nodefony/src/admin/WebhookAdminApi.js +378 -0
  61. package/dist/nodefony/src/admin/adminAudit.js +37 -0
  62. package/dist/nodefony/src/admin/userRevocationCascade.js +40 -0
  63. package/dist/nodefony/src/apikey/apiKeyFormat.js +107 -0
  64. package/dist/nodefony/src/audit/MemoryAuditStore.js +121 -0
  65. package/dist/nodefony/src/audit/auditBridge.js +82 -0
  66. package/dist/nodefony/src/audit/auditFilters.js +60 -0
  67. package/dist/nodefony/src/audit/auditStoreRegistry.js +25 -0
  68. package/dist/nodefony/src/audit/readAuditContext.js +24 -0
  69. package/dist/nodefony/src/audit/recordAudit.js +16 -0
  70. package/dist/nodefony/src/authenticator/AnonymousAuthenticator.js +36 -0
  71. package/dist/nodefony/src/authenticator/ApiKeyAuthenticator.js +164 -0
  72. package/dist/nodefony/src/authenticator/ExternalJwtAuthenticator.js +224 -0
  73. package/dist/nodefony/src/authenticator/FirewallRealtimeAuthenticator.js +174 -0
  74. package/dist/nodefony/src/authenticator/JwtAuthenticator.js +176 -0
  75. package/dist/nodefony/src/authenticator/SessionAuthenticator.js +92 -0
  76. package/dist/nodefony/src/authenticator/UserPasswordAuthenticator.js +95 -0
  77. package/dist/nodefony/src/authenticator/authenticatorRegistry.js +63 -0
  78. package/dist/nodefony/src/authenticator/bearer.js +2 -0
  79. package/dist/nodefony/src/authenticator/externalSubject.js +36 -0
  80. package/dist/nodefony/src/authenticator/peekIssuer.js +56 -0
  81. package/dist/nodefony/src/crypto/secretCipher.js +79 -0
  82. package/dist/nodefony/src/csp.js +54 -0
  83. package/dist/nodefony/src/csrfToken.js +65 -0
  84. package/dist/nodefony/src/net/ssrfGuard.js +130 -0
  85. package/dist/nodefony/src/oauth/oauthProviderRegistry.js +37 -0
  86. package/dist/nodefony/src/oauth/providers/github.js +65 -0
  87. package/dist/nodefony/src/oauth/providers/oidc.js +48 -0
  88. package/dist/nodefony/src/realtime/UserRealtimeToken.js +94 -0
  89. package/dist/nodefony/src/realtime/frameAuthorizer.js +279 -0
  90. package/dist/nodefony/src/realtime/realtimeContracts.js +1 -0
  91. package/dist/nodefony/src/sessionIdentity.js +35 -0
  92. package/dist/nodefony/src/throttle/LoginThrottler.js +97 -0
  93. package/dist/nodefony/src/token/AnonymousToken.js +40 -0
  94. package/dist/nodefony/src/token/JwtKeystore.js +160 -0
  95. package/dist/nodefony/src/token/MemoryTokenStore.js +236 -0
  96. package/dist/nodefony/src/token/RemoteJwtVerifier.js +231 -0
  97. package/dist/nodefony/src/token/UserToken.js +67 -0
  98. package/dist/nodefony/src/token/jwtRuntime.js +19 -0
  99. package/dist/nodefony/src/token/secretFile.js +134 -0
  100. package/dist/nodefony/src/token/tokenCriteria.js +35 -0
  101. package/dist/nodefony/src/token/tokenFilters.js +72 -0
  102. package/dist/nodefony/src/token/tokenSort.js +40 -0
  103. package/dist/nodefony/src/token/tokenStatus.js +35 -0
  104. package/dist/nodefony/src/token/tokenStoreRegistry.js +25 -0
  105. package/dist/nodefony/src/totp/MemoryTotpSecretStore.js +97 -0
  106. package/dist/nodefony/src/totp/totpCipher.js +30 -0
  107. package/dist/nodefony/src/totp/totpCrypto.js +226 -0
  108. package/dist/nodefony/src/totp/totpOperations.js +129 -0
  109. package/dist/nodefony/src/totp/totpSecretStoreRegistry.js +18 -0
  110. package/dist/nodefony/src/voter/RoleVoter.js +32 -0
  111. package/dist/nodefony/src/voter/ScopeVoter.js +52 -0
  112. package/dist/nodefony/src/voter/voterRegistry.js +20 -0
  113. package/dist/nodefony/src/webauthn/MemoryWebAuthnCredentialStore.js +121 -0
  114. package/dist/nodefony/src/webauthn/webAuthnCredentialStoreRegistry.js +18 -0
  115. package/dist/nodefony/src/webhook/MemoryWebhookStore.js +87 -0
  116. package/dist/nodefony/src/webhook/WebhookDispatcher.js +208 -0
  117. package/dist/nodefony/src/webhook/webhookCipher.js +27 -0
  118. package/dist/nodefony/src/webhook/webhookDelivery.js +102 -0
  119. package/dist/nodefony/src/webhook/webhookFilters.js +56 -0
  120. package/dist/nodefony/src/webhook/webhookSignature.js +51 -0
  121. package/dist/nodefony/src/webhook/webhookSort.js +48 -0
  122. package/dist/nodefony/src/webhook/webhookStoreRegistry.js +18 -0
  123. package/dist/types/index.d.ts +157 -0
  124. package/dist/types/nodefony/command/security-secrets.d.ts +24 -0
  125. package/dist/types/nodefony/command/security-token.d.ts +44 -0
  126. package/dist/types/nodefony/command/security-user-add.d.ts +28 -0
  127. package/dist/types/nodefony/command/security-user-delete.d.ts +25 -0
  128. package/dist/types/nodefony/command/security-user-list.d.ts +28 -0
  129. package/dist/types/nodefony/config/config.d.ts +295 -0
  130. package/dist/types/nodefony/config/defineModuleConfig.d.ts +27 -0
  131. package/dist/types/nodefony/contracts/IAccessVoter.d.ts +23 -0
  132. package/dist/types/nodefony/contracts/IApiKey.d.ts +75 -0
  133. package/dist/types/nodefony/contracts/IAuditEvent.d.ts +94 -0
  134. package/dist/types/nodefony/contracts/IAuditStore.d.ts +80 -0
  135. package/dist/types/nodefony/contracts/IAuthenticator.d.ts +66 -0
  136. package/dist/types/nodefony/contracts/IAuthorizationService.d.ts +28 -0
  137. package/dist/types/nodefony/contracts/IFirewall.d.ts +64 -0
  138. package/dist/types/nodefony/contracts/IFirewallDescription.d.ts +120 -0
  139. package/dist/types/nodefony/contracts/IJwtKeystore.d.ts +40 -0
  140. package/dist/types/nodefony/contracts/IOAuthProvider.d.ts +51 -0
  141. package/dist/types/nodefony/contracts/ISecuredArea.d.ts +57 -0
  142. package/dist/types/nodefony/contracts/IToken.d.ts +41 -0
  143. package/dist/types/nodefony/contracts/ITokenStore.d.ts +240 -0
  144. package/dist/types/nodefony/contracts/ITotpSecret.d.ts +41 -0
  145. package/dist/types/nodefony/contracts/ITotpSecretStore.d.ts +88 -0
  146. package/dist/types/nodefony/contracts/IWebAuthnCredential.d.ts +56 -0
  147. package/dist/types/nodefony/contracts/IWebAuthnCredentialStore.d.ts +118 -0
  148. package/dist/types/nodefony/contracts/IWebhookEndpoint.d.ts +82 -0
  149. package/dist/types/nodefony/contracts/IWebhookStore.d.ts +85 -0
  150. package/dist/types/nodefony/contracts/index.d.ts +9 -0
  151. package/dist/types/nodefony/errors/AccessDeniedError.d.ts +10 -0
  152. package/dist/types/nodefony/errors/ApiKeyError.d.ts +17 -0
  153. package/dist/types/nodefony/errors/AuthenticationError.d.ts +10 -0
  154. package/dist/types/nodefony/errors/CsrfError.d.ts +19 -0
  155. package/dist/types/nodefony/errors/InvalidTargetError.d.ts +34 -0
  156. package/dist/types/nodefony/errors/SsrfError.d.ts +13 -0
  157. package/dist/types/nodefony/errors/ThrottledError.d.ts +16 -0
  158. package/dist/types/nodefony/errors/UnverifiableTokenError.d.ts +37 -0
  159. package/dist/types/nodefony/errors/WebAuthnError.d.ts +17 -0
  160. package/dist/types/nodefony/errors/index.d.ts +8 -0
  161. package/dist/types/nodefony/service/accessTokenVerifier.d.ts +29 -0
  162. package/dist/types/nodefony/service/apiKeys.d.ts +103 -0
  163. package/dist/types/nodefony/service/auditService.d.ts +30 -0
  164. package/dist/types/nodefony/service/authFlow.d.ts +123 -0
  165. package/dist/types/nodefony/service/authorization.d.ts +33 -0
  166. package/dist/types/nodefony/service/cors.d.ts +48 -0
  167. package/dist/types/nodefony/service/csrf.d.ts +57 -0
  168. package/dist/types/nodefony/service/firewall.d.ts +148 -0
  169. package/dist/types/nodefony/service/oauth2.d.ts +66 -0
  170. package/dist/types/nodefony/service/securityHeaders.d.ts +66 -0
  171. package/dist/types/nodefony/service/tokenService.d.ts +103 -0
  172. package/dist/types/nodefony/service/totp.d.ts +58 -0
  173. package/dist/types/nodefony/service/webAuthn.d.ts +123 -0
  174. package/dist/types/nodefony/service/webhooks.d.ts +160 -0
  175. package/dist/types/nodefony/src/RoleHierarchyWalker.d.ts +21 -0
  176. package/dist/types/nodefony/src/SecuredArea.d.ts +31 -0
  177. package/dist/types/nodefony/src/admin/SecurityAdminApi.d.ts +82 -0
  178. package/dist/types/nodefony/src/admin/WebhookAdminApi.d.ts +30 -0
  179. package/dist/types/nodefony/src/admin/adminAudit.d.ts +27 -0
  180. package/dist/types/nodefony/src/admin/userRevocationCascade.d.ts +31 -0
  181. package/dist/types/nodefony/src/apikey/apiKeyFormat.d.ts +43 -0
  182. package/dist/types/nodefony/src/audit/MemoryAuditStore.d.ts +33 -0
  183. package/dist/types/nodefony/src/audit/auditBridge.d.ts +49 -0
  184. package/dist/types/nodefony/src/audit/auditFilters.d.ts +56 -0
  185. package/dist/types/nodefony/src/audit/auditStoreRegistry.d.ts +37 -0
  186. package/dist/types/nodefony/src/audit/readAuditContext.d.ts +17 -0
  187. package/dist/types/nodefony/src/audit/recordAudit.d.ts +13 -0
  188. package/dist/types/nodefony/src/authenticator/AnonymousAuthenticator.d.ts +26 -0
  189. package/dist/types/nodefony/src/authenticator/ApiKeyAuthenticator.d.ts +74 -0
  190. package/dist/types/nodefony/src/authenticator/ExternalJwtAuthenticator.d.ts +132 -0
  191. package/dist/types/nodefony/src/authenticator/FirewallRealtimeAuthenticator.d.ts +78 -0
  192. package/dist/types/nodefony/src/authenticator/JwtAuthenticator.d.ts +69 -0
  193. package/dist/types/nodefony/src/authenticator/SessionAuthenticator.d.ts +70 -0
  194. package/dist/types/nodefony/src/authenticator/UserPasswordAuthenticator.d.ts +53 -0
  195. package/dist/types/nodefony/src/authenticator/authenticatorRegistry.d.ts +39 -0
  196. package/dist/types/nodefony/src/authenticator/bearer.d.ts +22 -0
  197. package/dist/types/nodefony/src/authenticator/externalSubject.d.ts +27 -0
  198. package/dist/types/nodefony/src/authenticator/peekIssuer.d.ts +31 -0
  199. package/dist/types/nodefony/src/crypto/secretCipher.d.ts +31 -0
  200. package/dist/types/nodefony/src/csp.d.ts +39 -0
  201. package/dist/types/nodefony/src/csrfToken.d.ts +36 -0
  202. package/dist/types/nodefony/src/net/ssrfGuard.d.ts +43 -0
  203. package/dist/types/nodefony/src/oauth/oauthProviderRegistry.d.ts +45 -0
  204. package/dist/types/nodefony/src/oauth/providers/github.d.ts +9 -0
  205. package/dist/types/nodefony/src/oauth/providers/oidc.d.ts +35 -0
  206. package/dist/types/nodefony/src/realtime/UserRealtimeToken.d.ts +62 -0
  207. package/dist/types/nodefony/src/realtime/frameAuthorizer.d.ts +171 -0
  208. package/dist/types/nodefony/src/realtime/realtimeContracts.d.ts +139 -0
  209. package/dist/types/nodefony/src/sessionIdentity.d.ts +20 -0
  210. package/dist/types/nodefony/src/throttle/LoginThrottler.d.ts +68 -0
  211. package/dist/types/nodefony/src/token/AnonymousToken.d.ts +23 -0
  212. package/dist/types/nodefony/src/token/JwtKeystore.d.ts +43 -0
  213. package/dist/types/nodefony/src/token/MemoryTokenStore.d.ts +66 -0
  214. package/dist/types/nodefony/src/token/RemoteJwtVerifier.d.ts +149 -0
  215. package/dist/types/nodefony/src/token/UserToken.d.ts +41 -0
  216. package/dist/types/nodefony/src/token/jwtRuntime.d.ts +28 -0
  217. package/dist/types/nodefony/src/token/secretFile.d.ts +70 -0
  218. package/dist/types/nodefony/src/token/tokenCriteria.d.ts +20 -0
  219. package/dist/types/nodefony/src/token/tokenFilters.d.ts +76 -0
  220. package/dist/types/nodefony/src/token/tokenSort.d.ts +33 -0
  221. package/dist/types/nodefony/src/token/tokenStatus.d.ts +38 -0
  222. package/dist/types/nodefony/src/token/tokenStoreRegistry.d.ts +38 -0
  223. package/dist/types/nodefony/src/totp/MemoryTotpSecretStore.d.ts +43 -0
  224. package/dist/types/nodefony/src/totp/totpCipher.d.ts +9 -0
  225. package/dist/types/nodefony/src/totp/totpCrypto.d.ts +164 -0
  226. package/dist/types/nodefony/src/totp/totpOperations.d.ts +73 -0
  227. package/dist/types/nodefony/src/totp/totpSecretStoreRegistry.d.ts +27 -0
  228. package/dist/types/nodefony/src/voter/RoleVoter.d.ts +25 -0
  229. package/dist/types/nodefony/src/voter/ScopeVoter.d.ts +30 -0
  230. package/dist/types/nodefony/src/voter/voterRegistry.d.ts +33 -0
  231. package/dist/types/nodefony/src/webauthn/MemoryWebAuthnCredentialStore.d.ts +39 -0
  232. package/dist/types/nodefony/src/webauthn/webAuthnCredentialStoreRegistry.d.ts +26 -0
  233. package/dist/types/nodefony/src/webhook/MemoryWebhookStore.d.ts +37 -0
  234. package/dist/types/nodefony/src/webhook/WebhookDispatcher.d.ts +69 -0
  235. package/dist/types/nodefony/src/webhook/webhookCipher.d.ts +8 -0
  236. package/dist/types/nodefony/src/webhook/webhookDelivery.d.ts +28 -0
  237. package/dist/types/nodefony/src/webhook/webhookFilters.d.ts +64 -0
  238. package/dist/types/nodefony/src/webhook/webhookSignature.d.ts +20 -0
  239. package/dist/types/nodefony/src/webhook/webhookSort.d.ts +39 -0
  240. package/dist/types/nodefony/src/webhook/webhookStoreRegistry.d.ts +31 -0
  241. package/docs/api-keys.md +691 -0
  242. package/docs/audit.md +751 -0
  243. package/docs/authenticators.md +487 -0
  244. package/docs/authorization.md +497 -0
  245. package/docs/cors.md +497 -0
  246. package/docs/csrf.md +392 -0
  247. package/docs/external-jwt.md +181 -0
  248. package/docs/firewall.md +546 -0
  249. package/docs/headers.md +616 -0
  250. package/docs/index.md +207 -0
  251. package/docs/lexique.md +190 -0
  252. package/docs/oauth2.md +575 -0
  253. package/docs/obtenir-un-jeton.md +225 -0
  254. package/docs/tokens.md +520 -0
  255. package/docs/totp.md +804 -0
  256. package/docs/webauthn.md +733 -0
  257. package/docs/webhooks.md +1016 -0
  258. 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)