@nodefony/security 10.0.0-alpha.2 → 10.0.0-alpha.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/dist/_virtual/{_@oxc-project_runtime@0.148.0 → _@oxc-project_runtime@0.149.0}/helpers/esm/decorate.js +1 -1
  2. package/dist/_virtual/{_@oxc-project_runtime@0.148.0 → _@oxc-project_runtime@0.149.0}/helpers/esm/decorateMetadata.js +1 -1
  3. package/dist/index.js +7 -3
  4. package/dist/nodefony/command/security-secrets.js +7 -3
  5. package/dist/nodefony/command/security-token.js +7 -6
  6. package/dist/nodefony/command/security-user-add.js +5 -4
  7. package/dist/nodefony/command/security-user-delete.js +2 -1
  8. package/dist/nodefony/command/security-user-list.js +2 -1
  9. package/dist/nodefony/config/config.js +6 -3
  10. package/dist/nodefony/service/auditService.js +3 -3
  11. package/dist/nodefony/service/oauth2.js +111 -25
  12. package/dist/nodefony/service/tokenService.js +3 -3
  13. package/dist/nodefony/service/totp.js +3 -3
  14. package/dist/nodefony/service/webAuthn.js +3 -3
  15. package/dist/nodefony/service/webhooks.js +3 -3
  16. package/dist/nodefony/src/oauth/httpJson.js +64 -0
  17. package/dist/nodefony/src/oauth/metadata.js +88 -0
  18. package/dist/nodefony/src/oauth/oauth2Client.js +266 -0
  19. package/dist/nodefony/src/oauth/oauthProviderRegistry.js +5 -16
  20. package/dist/nodefony/src/oauth/providers/github.js +34 -9
  21. package/dist/nodefony/src/oauth/providers/oidc.js +87 -13
  22. package/dist/types/index.d.ts +8 -1
  23. package/dist/types/nodefony/config/config.d.ts +6 -0
  24. package/dist/types/nodefony/contracts/IOAuthProvider.d.ts +45 -14
  25. package/dist/types/nodefony/contracts/ITokenStore.d.ts +1 -1
  26. package/dist/types/nodefony/service/oauth2.d.ts +47 -7
  27. package/dist/types/nodefony/src/oauth/httpJson.d.ts +28 -0
  28. package/dist/types/nodefony/src/oauth/metadata.d.ts +54 -0
  29. package/dist/types/nodefony/src/oauth/oauth2Client.d.ts +174 -0
  30. package/dist/types/nodefony/src/oauth/oauthProviderRegistry.d.ts +34 -17
  31. package/dist/types/nodefony/src/oauth/providers/github.d.ts +1 -1
  32. package/dist/types/nodefony/src/oauth/providers/oidc.d.ts +40 -15
  33. package/docs/oauth2.md +154 -70
  34. package/package.json +9 -10
@@ -1,9 +1,36 @@
1
- import type { OAuth2Tokens } from "arctic";
1
+ import type { IAuthorizationRequest, ITokenRequest, OAuth2Tokens } from "../src/oauth/oauth2Client.js";
2
2
  import type { IOAuthProfile } from "@nodefony/user";
3
3
  /**
4
- * Adaptateur d'**un fournisseur OAuth/OIDC**, façade UNIFORME au-dessus d'une
5
- * classe `arctic` — masque les divergences entre fournisseurs derrière un contrat
6
- * stable consommé par `OAuth2Service` :
4
+ * Ce qu'on exige du paramètre `iss` renvoyé par le serveur d'autorisation
5
+ * (RFC 9207).
6
+ *
7
+ * @remarks La règle a **trois** états, pas deux — et c'est ce que le booléen
8
+ * seul ne pouvait pas dire. La RFC §2.4 impose au client d'extraire `iss`
9
+ * « **if the parameter is present** », et son §2.3 fait annoncer le support par
10
+ * les métadonnées de l'émetteur. Refuser une réponse sans `iss` d'un serveur qui
11
+ * n'a jamais promis de l'émettre revient donc à refuser un serveur CONFORME —
12
+ * Microsoft Entra en est un.
13
+ *
14
+ * La défense anti-mix-up ne repose d'ailleurs pas sur ce seul paramètre : chaque
15
+ * fournisseur a son URL de redirection propre (`…/{provider}/callback`) et le
16
+ * flux vérifie que le fournisseur de retour est celui qui a démarré, ce que la
17
+ * RFC 9700 §4.4.2.2 donne comme défense principale. `iss` est la seconde ceinture.
18
+ */
19
+ export interface IIssuerPolicy {
20
+ /** Émetteur attendu, sous sa forme canonique. */
21
+ readonly issuer: string;
22
+ /**
23
+ * `true` si le serveur ANNONCE émettre `iss`
24
+ * (`authorization_response_iss_parameter_supported`) : son absence est alors
25
+ * une promesse non tenue, donc un refus. `false` : absent, on continue ;
26
+ * présent, il doit correspondre.
27
+ */
28
+ readonly requireIssParameter: boolean;
29
+ }
30
+ /**
31
+ * Adaptateur d'**un fournisseur OAuth/OIDC**, façade UNIFORME au-dessus d'un client
32
+ * OAuth 2.0 — masque les divergences entre fournisseurs derrière un contrat stable
33
+ * consommé par `OAuth2Service` :
7
34
  *
8
35
  * - **PKCE ou non** : Google attend `createAuthorizationURL(state, codeVerifier,
9
36
  * scopes)` ; GitHub `createAuthorizationURL(state, scopes)` (pas de
@@ -13,9 +40,9 @@ import type { IOAuthProfile } from "@nodefony/user";
13
40
  * l'API du fournisseur (GitHub `/user`). Le résultat est toujours normalisé en
14
41
  * {@link IOAuthProfile}.
15
42
  *
16
- * @remarks `arctic` n'est référencé ici qu'en **type** (`import type`, effacé à la
17
- * compilation) l'instance runtime est chargée paresseusement par le service et
18
- * injectée aux fabriques. Aucune dépendance runtime n'entre par ce contrat.
43
+ * @remarks Ce contrat n'introduit aucune dépendance : le client OAuth 2.0 sous-jacent
44
+ * est écrit dans le module même, et un fournisseur maison peut l'implémenter sans
45
+ * rien installer.
19
46
  */
20
47
  export interface IOAuthProvider {
21
48
  /**
@@ -24,24 +51,28 @@ export interface IOAuthProvider {
24
51
  */
25
52
  readonly usesPkce: boolean;
26
53
  /**
27
- * Identifiant d'émetteur attendu pour la défense anti-mix-up (RFC 9207), ou
28
- * `null` si le fournisseur n'émet pas le paramètre `iss` (ex. GitHub, non-OIDC).
29
- * Quand non-`null`, le service **rejette** une réponse dont l'`iss` diffère ou
30
- * manque.
54
+ * Politique de vérification du paramètre `iss` (anti-mix-up, RFC 9207), ou
55
+ * `null` pour un fournisseur qui ne relève pas de cette défense (GitHub,
56
+ * non-OIDC).
31
57
  */
32
- readonly expectedIssuer: string | null;
58
+ readonly issuerPolicy: IIssuerPolicy | null;
33
59
  /** Scopes appliqués quand la configuration n'en précise aucun. */
34
60
  readonly defaultScopes: string[];
35
61
  /**
36
62
  * Construit l'URL d'autorisation (étape 1). `codeVerifier` est non-`null`
37
63
  * lorsque {@link usesPkce} ; les fournisseurs sans PKCE l'ignorent.
64
+ *
65
+ * @remarks La forme est un OBJET pour que les paramètres normalisés encore
66
+ * absents — `resource` (RFC 8707), `nonce`, `prompt`… — s'ajoutent plus tard
67
+ * sans rupture. Ce contrat est EXPORTÉ, donc gelé à la publication : une
68
+ * signature positionnelle y aurait figé l'impossibilité de les accueillir.
38
69
  */
39
- createAuthorizationURL(state: string, codeVerifier: string | null, scopes: string[]): URL;
70
+ createAuthorizationURL(request: IAuthorizationRequest): URL;
40
71
  /**
41
72
  * Échange le `code` d'autorisation contre des jetons (étape 2, canal serveur).
42
73
  * `codeVerifier` doit correspondre à celui de l'étape 1 si {@link usesPkce}.
43
74
  */
44
- validateAuthorizationCode(code: string, codeVerifier: string | null): Promise<OAuth2Tokens>;
75
+ validateAuthorizationCode(request: ITokenRequest): Promise<OAuth2Tokens>;
45
76
  /**
46
77
  * Récupère et **normalise** le profil de l'utilisateur à partir des jetons.
47
78
  *
@@ -92,7 +92,7 @@ export interface IAccessTokenRecord {
92
92
  secretHash: string;
93
93
  /** Algorithme de hachage du secret (agilité crypto, migration future). Ex. `"sha256"`. */
94
94
  hashAlg: string;
95
- /** Client OAuth émetteur (slot OAuth2/arctic) ; `null` = non-OAuth. */
95
+ /** Client OAuth émetteur (slot OAuth2) ; `null` = non-OAuth. */
96
96
  clientId: string | null;
97
97
  /** Confirmation sender-constrained : `jkt` (DPoP RFC 9449) / `x5t#S256` (mTLS RFC 8705) ; `null` = bearer simple. */
98
98
  cnf: string | null;
@@ -1,4 +1,26 @@
1
1
  import { Service, Module } from "nodefony";
2
+ /**
3
+ * Libellé affichable d'un fournisseur, quand sa configuration n'en donne pas.
4
+ *
5
+ * Un écran de connexion ne doit JAMAIS montrer un identifiant technique brut :
6
+ * `mon-idp-interne` sur un bouton ne dit rien à qui doit cliquer. À défaut de
7
+ * marque connue, le nom de la clé de configuration est ce qui s'en rapproche le
8
+ * plus — mais rendu lisible : séparateurs en espaces, initiales en capitales,
9
+ * sigles préservés.
10
+ *
11
+ * Fonction PURE, donc éprouvable sans boot ni réseau.
12
+ *
13
+ * @param name - nom du fournisseur, tel qu'il est écrit dans la configuration
14
+ * @returns le libellé à afficher sur le bouton
15
+ */
16
+ export declare function oauthDisplayLabel(name: string): string;
17
+ /** Un fournisseur tel que l'écran de connexion doit le présenter. */
18
+ export interface IOAuthDisplayProvider {
19
+ /** Nom technique — celui que l'URL `/authorize` attend. */
20
+ readonly name: string;
21
+ /** Libellé du bouton : celui de la config, sinon dérivé du nom. */
22
+ readonly label: string;
23
+ }
2
24
  /** Données à porter en session entre `authorize` et `callback` (anti-replay). */
3
25
  export interface IOAuthAuthorization {
4
26
  /** URL d'autorisation vers laquelle rediriger l'utilisateur. */
@@ -9,8 +31,7 @@ export interface IOAuthAuthorization {
9
31
  readonly codeVerifier: string | null;
10
32
  }
11
33
  /**
12
- * **Social login OAuth 2.0** (P6 J9) — orchestrateur du flux *Authorization Code*
13
- * au-dessus d'`arctic`.
34
+ * **Social login OAuth 2.0** (P6 J9) — orchestrateur du flux *Authorization Code*.
14
35
  *
15
36
  * Posture OAuth 2.1 (RFC 9700) : Authorization Code uniquement (jamais implicit /
16
37
  * ROPC), **PKCE S256** quand le fournisseur le supporte (RFC 7636), **state**
@@ -18,10 +39,11 @@ export interface IOAuthAuthorization {
18
39
  * (le login produit une **session BFF**, gérée hors de ce service par le
19
40
  * controller + `AuthFlow`).
20
41
  *
21
- * `arctic` est **importé paresseusement** au premier login (cold path — jamais au
22
- * boot ni par requête), comme `@simplewebauthn`/`jose`. Au boot (si
23
- * `oauth2.enabled`) : seule la config est validée et les fournisseurs configurés
24
- * sont confrontés au registre (un nom inconnu = WARNING, pas fatal).
42
+ * Les fournisseurs sont construits **au premier login** (cold path — jamais au boot
43
+ * ni par requête) puis mémoïsés : c'est là que les points d'entrée d'un émetteur
44
+ * OIDC sont découverts, une seule fois par processus. Au boot (si `oauth2.enabled`)
45
+ * : seule la config est validée et les fournisseurs configurés sont confrontés au
46
+ * registre (un nom inconnu = WARNING, pas fatal).
25
47
  *
26
48
  * Le service ne touche **ni HTTP ni session** : il rend à l'appelant les éléments
27
49
  * (URL, state, verifier) que le controller persiste en session — testable sans
@@ -33,8 +55,26 @@ declare class OAuth2Service extends Service {
33
55
  constructor(module: Module);
34
56
  /** `true` si le social login est opérationnel (activé + boot OK). */
35
57
  isEnabled(): boolean;
36
- /** Noms des fournisseurs configurés ET connus du registre (UI : boutons à afficher). */
58
+ /**
59
+ * Noms des fournisseurs OPÉRATIONNELS — configurés ET connus du registre.
60
+ *
61
+ * 🔴 C'est la **garde d'autorisation** : `/authorize` refuse en 404 tout nom
62
+ * absent de cette liste. Elle répond donc à « ce flux peut-il s'ouvrir ? »,
63
+ * jamais à « ce bouton doit-il s'afficher ? » — pour l'écran, voir
64
+ * {@link listDisplayProviders}. Confondre les deux ferait d'un masquage une
65
+ * désactivation, et couperait les bancs qui exercent une fixture masquée.
66
+ */
37
67
  listProviders(): string[];
68
+ /**
69
+ * Fournisseurs à MONTRER sur l'écran de connexion, libellés compris.
70
+ *
71
+ * Rend TOUT fournisseur opérationnel — y compris ceux dont le framework ne
72
+ * connaît pas la marque, qui sont précisément ceux qu'une application
73
+ * enregistre elle-même. Le seul retrait possible est explicite et se lit dans
74
+ * la configuration du fournisseur (`hidden: true`), à côté de la raison qui
75
+ * l'a motivé ; il ne désactive rien.
76
+ */
77
+ listDisplayProviders(): IOAuthDisplayProvider[];
38
78
  /**
39
79
  * Redirections post-login (succès / échec) — lues par le controller.
40
80
  * Surcharge PAR FOURNISSEUR si fournie, sinon valeur globale, sinon défaut.
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Lecture BORNÉE d'un corps JSON — la brique de transport commune aux trois
3
+ * appels sortants du social login (découverte, point de jeton, API d'un
4
+ * fournisseur non-OIDC).
5
+ *
6
+ * Elle existe parce qu'une borne posée APRÈS `response.text()` ne protège plus
7
+ * rien : le corps est déjà entièrement en mémoire quand on mesure sa longueur.
8
+ */
9
+ /**
10
+ * Lit un corps JSON en refusant de dépasser une taille — la borne est vérifiée
11
+ * PENDANT la lecture, pas après : un corps déjà entièrement en mémoire ne se
12
+ * refuse plus.
13
+ *
14
+ * @param response - réponse dont le corps reste à lire.
15
+ * @param maxBytes - plafond, en octets réels du flux.
16
+ * @param subject - ce qu'on lisait, pour que l'erreur soit exploitable.
17
+ * @returns la valeur JSON telle quelle — un objet OU un tableau (l'API d'un
18
+ * fournisseur rend les deux ; c'est à l'appelant d'exiger la forme qu'il attend).
19
+ * @throws Error - corps trop gros ou illisible.
20
+ */
21
+ export declare function readJsonBounded(response: Response, maxBytes: number, subject: string): Promise<unknown>;
22
+ /**
23
+ * Comme {@link readJsonBounded}, mais exige un OBJET — la forme de toute réponse
24
+ * normalisée par une RFC (document de métadonnées, réponse d'un point de jeton).
25
+ *
26
+ * @throws Error - corps trop gros, illisible, ou qui n'est pas un objet JSON.
27
+ */
28
+ export declare function readJsonObjectBounded(response: Response, maxBytes: number, subject: string): Promise<Record<string, unknown>>;
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Points d'entrée retenus d'un serveur d'autorisation — le sous-ensemble dont le
3
+ * flux *Authorization Code* a besoin (RFC 8414 §2).
4
+ *
5
+ * @remarks Le nom dit **ce qu'on a découvert chez autrui**, à ne pas confondre
6
+ * avec `IAuthorizationServerMetadata` du cœur, qui décrit le document que Nodefony
7
+ * PUBLIE (champs bruts de la RFC).
8
+ */
9
+ export interface IDiscoveredAuthorizationServer {
10
+ /** Émetteur canonique, vérifié identique à celui interrogé (RFC 8414 §3.3). */
11
+ readonly issuer: string;
12
+ /** Point d'autorisation (RFC 6749 §3.1). */
13
+ readonly authorizationEndpoint: string;
14
+ /** Point de jeton (RFC 6749 §3.2). */
15
+ readonly tokenEndpoint: string;
16
+ /** Jeu de clés de signature — la porte d'une future vérification d'ID token. */
17
+ readonly jwksUri: string;
18
+ /** Méthodes PKCE annoncées (RFC 7636), ou `null` si le serveur n'en publie aucune. */
19
+ readonly codeChallengeMethodsSupported: string[] | null;
20
+ /**
21
+ * `true` si le serveur ANNONCE émettre le paramètre `iss` dans sa réponse
22
+ * d'autorisation (RFC 9207 §2.3). Absent du document ⇒ `false` : on ne peut
23
+ * alors pas exiger ce qu'il n'a pas promis.
24
+ */
25
+ readonly issParameterSupported: boolean;
26
+ }
27
+ /** Réglages de la découverte — l'injection de `fetch` est la voie pour éprouver sans TLS. */
28
+ export interface IDiscoveryOptions {
29
+ /**
30
+ * Implémentation de `fetch` à employer.
31
+ *
32
+ * @remarks C'est ce que prescrit le cœur pour éprouver le mécanisme sans TLS :
33
+ * un émetteur en clair est refusé (RFC 8414 §2), on injecte donc le transport
34
+ * plutôt que d'affaiblir la règle.
35
+ */
36
+ readonly fetch?: typeof globalThis.fetch;
37
+ /** Délai d'attente par URL candidate, en millisecondes. */
38
+ readonly timeoutMs?: number;
39
+ }
40
+ /**
41
+ * Interroge un serveur d'autorisation et rend ses points d'entrée.
42
+ *
43
+ * Les URL candidates sont celles du cœur (`issuerMetadataUrls`, ordre normatif
44
+ * RFC 8414 §3.1 : insertion oauth → insertion oidc → ajout oidc), et la réponse
45
+ * est CONFRONTÉE à l'émetteur demandé par `validateIssuerMetadata` (§3.3). C'est
46
+ * cette garde qui empêche un émetteur détourné d'imposer ses propres points
47
+ * d'entrée — la même attaque que le paramètre `iss` couvre au retour (RFC 9207).
48
+ *
49
+ * @param rawIssuer - identifiant d'émetteur tel qu'écrit en configuration.
50
+ * @param options - transport injectable et délai d'attente.
51
+ * @returns Les points d'entrée, prêts pour `OAuth2Client`.
52
+ * @throws Error - émetteur mal formé, document introuvable, incomplet, ou `issuer` discordant.
53
+ */
54
+ export declare function discoverAuthorizationServer(rawIssuer: string, options?: IDiscoveryOptions): Promise<IDiscoveredAuthorizationServer>;
@@ -0,0 +1,174 @@
1
+ /**
2
+ * Tire un `state` anti-CSRF (RFC 6749 §10.12, RFC 9700 §4.7) — 256 bits issus du
3
+ * générateur cryptographique du système.
4
+ */
5
+ export declare function generateState(): string;
6
+ /**
7
+ * Tire un `code_verifier` PKCE (RFC 7636 §4.1) — 43 caractères de l'alphabet
8
+ * `unreserved`, porteurs de 256 bits d'entropie.
9
+ */
10
+ export declare function generateCodeVerifier(): string;
11
+ /**
12
+ * Calcule le `code_challenge` de la méthode **S256** (RFC 7636 §4.2) :
13
+ * `BASE64URL(SHA256(ASCII(code_verifier)))`.
14
+ *
15
+ * @remarks La méthode `plain` n'est jamais proposée — OAuth 2.1 et la RFC 9700
16
+ * §2.1.1 l'excluent : elle ne protège pas d'un code intercepté.
17
+ */
18
+ export declare function createCodeChallenge(codeVerifier: string): string;
19
+ /**
20
+ * Refus du serveur d'autorisation au point de jeton (RFC 6749 §5.2). Porte le
21
+ * code `error` normalisé — la seule partie de la réponse sûre à journaliser.
22
+ */
23
+ export declare class OAuth2RequestError extends Error {
24
+ /** Code normalisé (`invalid_grant`, `invalid_client`, ...) — RFC 6749 §5.2. */
25
+ readonly code: string;
26
+ /** Description lisible fournie par le serveur, ou `null`. */
27
+ readonly description: string | null;
28
+ constructor(code: string, description: string | null);
29
+ }
30
+ /**
31
+ * Jetons rendus par le point de jeton (RFC 6749 §5.1), enveloppés pour qu'un champ
32
+ * attendu mais absent lève une erreur NOMMÉE au lieu de propager un `undefined`
33
+ * jusqu'au décodage du profil.
34
+ */
35
+ export declare class OAuth2Tokens {
36
+ #private;
37
+ /** Corps JSON brut de la réponse — donne accès aux extensions du fournisseur. */
38
+ readonly data: Record<string, unknown>;
39
+ constructor(data: Record<string, unknown>);
40
+ /** Jeton d'accès (RFC 6749 §5.1). */
41
+ accessToken(): string;
42
+ /** Type du jeton d'accès — `Bearer` en pratique (RFC 6750). */
43
+ tokenType(): string;
44
+ /** Jeton d'identité OIDC (OpenID Connect Core §3.1.3.3). */
45
+ idToken(): string;
46
+ /** `true` si le serveur a émis un jeton de rafraîchissement. */
47
+ hasRefreshToken(): boolean;
48
+ /** Jeton de rafraîchissement (RFC 6749 §1.5). */
49
+ refreshToken(): string;
50
+ /** Durée de vie restante du jeton d'accès, en secondes. */
51
+ accessTokenExpiresInSeconds(): number;
52
+ /** Instant d'expiration du jeton d'accès, dérivé de `expires_in`. */
53
+ accessTokenExpiresAt(): Date;
54
+ /** `true` si le serveur a annoncé les portées effectivement accordées. */
55
+ hasScopes(): boolean;
56
+ /** Portées accordées, telles que le serveur les a annoncées (RFC 6749 §3.3). */
57
+ scopes(): string[];
58
+ }
59
+ /**
60
+ * Comment le client s'authentifie au point de jeton (RFC 6749 §2.3, et le
61
+ * registre `token_endpoint_auth_method` d'OpenID Connect Discovery).
62
+ *
63
+ * 🔴 Elle se DÉCLARE, elle ne se déduit plus de la vacuité du secret. La
64
+ * convention implicite « secret non vide ⇒ Basic » avait deux défauts : elle ne
65
+ * pouvait pas exprimer `client_secret_post`, que certains serveurs exigent et
66
+ * annoncent dans leurs métadonnées (`token_endpoint_auth_methods_supported`,
67
+ * déjà découvert et lu par personne) ; et elle rendait un client public
68
+ * indiscernable d'un client dont le secret manque par erreur — deux situations
69
+ * qu'un serveur traite très différemment.
70
+ *
71
+ * L'union est ouverte par le bas : `private_key_jwt` (qu'Apple réclame)
72
+ * s'ajoutera ici sans toucher à la forme des appels.
73
+ */
74
+ export type OAuth2ClientAuthMethod = "client_secret_basic" | "client_secret_post" | "none";
75
+ /**
76
+ * Étape 1 — ce qu'on demande au point d'autorisation.
77
+ *
78
+ * La forme est un OBJET, et c'est tout l'enjeu : les paramètres normalisés qui
79
+ * manquent encore — `resource` (RFC 8707, exigé par le Model Context Protocol),
80
+ * `nonce`, `prompt`, `max_age`, `login_hint`, `access_type` — s'ajouteront en
81
+ * champs nommés sans toucher à un seul appelant. Une signature positionnelle
82
+ * aurait figé cette impossibilité à la publication de la 10.0.0.
83
+ */
84
+ export interface IAuthorizationRequest {
85
+ /** Valeur anti-CSRF à retrouver au retour (RFC 6749 §10.12). */
86
+ readonly state: string;
87
+ /** Secret PKCE (RFC 7636), ou `null` pour un fournisseur qui n'en veut pas. */
88
+ readonly codeVerifier: string | null;
89
+ /** Portées demandées ; aucune n'est ajoutée d'office. */
90
+ readonly scopes: readonly string[];
91
+ /**
92
+ * Paramètres versés TELS QUELS dans la requête d'autorisation.
93
+ *
94
+ * Un champ dédié plutôt qu'une signature d'index sur tout l'objet : celle-ci
95
+ * accepterait `stat` pour `state` sans rien dire. Ici, ce qui est nommé est
96
+ * vérifié par le compilateur, et ce qui passe en supplément est déclaré comme
97
+ * tel.
98
+ */
99
+ readonly additionalParameters?: Readonly<Record<string, string>>;
100
+ }
101
+ /**
102
+ * Étape 2 — ce qu'on présente au point de jeton.
103
+ *
104
+ * Même raison d'être qu'{@link IAuthorizationRequest} : `resource` doit être
105
+ * répété ici (RFC 8707 §2.2), et rien ne pouvait s'ajouter à une signature
106
+ * positionnelle.
107
+ */
108
+ export interface ITokenRequest {
109
+ /** Code reçu sur l'URL de redirection (RFC 6749 §4.1.2). */
110
+ readonly code: string;
111
+ /** Secret PKCE de l'étape 1, ou `null`. */
112
+ readonly codeVerifier: string | null;
113
+ /** Paramètres versés TELS QUELS dans le corps de la requête de jeton. */
114
+ readonly additionalParameters?: Readonly<Record<string, string>>;
115
+ }
116
+ /** Points d'entrée d'un serveur d'autorisation et identité du client. */
117
+ export interface IOAuth2ClientOptions {
118
+ /** Point d'autorisation (RFC 6749 §3.1) — où l'utilisateur est redirigé. */
119
+ readonly authorizationEndpoint: string;
120
+ /** Point de jeton (RFC 6749 §3.2) — où le code est échangé, de serveur à serveur. */
121
+ readonly tokenEndpoint: string;
122
+ /** Identifiant client délivré par le fournisseur. */
123
+ readonly clientId: string;
124
+ /** Secret client — vide, et seulement vide, quand {@link clientAuthMethod} vaut `"none"`. */
125
+ readonly clientSecret: string;
126
+ /**
127
+ * Comment ce client s'authentifie au point de jeton.
128
+ *
129
+ * REQUIS, et volontairement : c'est ce qui remplace la convention implicite
130
+ * « secret non vide ⇒ Basic ». Un fournisseur doit dire ce qu'il fait, pas le
131
+ * laisser déduire d'une longueur de chaîne.
132
+ */
133
+ readonly clientAuthMethod: OAuth2ClientAuthMethod;
134
+ /** URL de redirection exacte, telle qu'enregistrée chez le fournisseur (RFC 9700 §4.1). */
135
+ readonly redirectUri: string;
136
+ /**
137
+ * Implémentation de `fetch` à employer — la voie pour éprouver l'échange sans
138
+ * réseau, plutôt que de remplacer le `fetch` global du processus.
139
+ */
140
+ readonly fetch?: typeof globalThis.fetch;
141
+ /** Délai d'attente de l'échange, en millisecondes. */
142
+ readonly timeoutMs?: number;
143
+ }
144
+ /**
145
+ * Client d'un serveur d'autorisation donné : construit l'URL d'autorisation puis
146
+ * échange le code contre des jetons.
147
+ *
148
+ * Un exemplaire porte les endpoints DÉJÀ résolus — par découverte de métadonnées
149
+ * ({@link discoverAuthorizationServer}) ou en dur pour un fournisseur qui n'en
150
+ * publie pas (GitHub).
151
+ */
152
+ export declare class OAuth2Client {
153
+ #private;
154
+ constructor(options: IOAuth2ClientOptions);
155
+ /**
156
+ * Construit l'URL d'autorisation (RFC 6749 §4.1.1). Le `code_challenge` S256 est
157
+ * ajouté dès qu'un `codeVerifier` est fourni.
158
+ *
159
+ * @param request - ce qu'on demande au point d'autorisation.
160
+ * @returns l'URL vers laquelle rediriger l'utilisateur.
161
+ * @throws Error - un paramètre supplémentaire empiète sur le protocole.
162
+ */
163
+ createAuthorizationURL(request: IAuthorizationRequest): URL;
164
+ /**
165
+ * Échange le code d'autorisation contre des jetons (RFC 6749 §4.1.3), de serveur
166
+ * à serveur — le secret client ne quitte jamais ce canal.
167
+ *
168
+ * @param request - ce qu'on présente au point de jeton.
169
+ * @throws OAuth2RequestError - le serveur a refusé, en nommant la cause (RFC 6749 §5.2).
170
+ * @throws Error - réponse inintelligible, hors gabarit, serveur injoignable, ou
171
+ * paramètre supplémentaire empiétant sur le protocole.
172
+ */
173
+ validateAuthorizationCode(request: ITokenRequest): Promise<OAuth2Tokens>;
174
+ }
@@ -1,39 +1,56 @@
1
- import type * as Arctic from "arctic";
2
1
  import type { IOAuthProvider } from "../../contracts/IOAuthProvider.js";
2
+ import type { OAuth2ClientAuthMethod } from "./oauth2Client.js";
3
3
  /**
4
4
  * Registre de **fabriques de fournisseurs OAuth** — résout un nom configuré
5
5
  * (`oauth2.providers.<name>`) vers un {@link IOAuthProvider}, SANS coupler le
6
6
  * cœur à un fournisseur en dur.
7
7
  *
8
8
  * Convention-frère : `tokenStoreRegistry`, `authenticatorRegistry`,
9
- * `webAuthnCredentialStoreRegistry`. Les builtins (`google`, `github`) couvrent
10
- * les deux archétypes (OIDC+PKCE / OAuth simple) ; une application enregistre les
11
- * ~50 autres fournisseurs `arctic` (Microsoft, Apple, Discord...) ou un
12
- * fournisseur maison via {@link registerOAuthProvider}, sans éditer le core.
9
+ * `webAuthnCredentialStoreRegistry`.
13
10
  *
14
- * @remarks `arctic` n'est ici qu'un **type** : l'instance runtime, chargée
15
- * paresseusement par `OAuth2Service` au premier login, est passée à la fabrique
16
- * via {@link IOAuthProviderContext.arctic} zéro import runtime statique.
11
+ * @remarks Aucun fournisseur n'a de code propre : un serveur OpenID Connect publie
12
+ * ses points d'entrée (RFC 8414 / OpenID Connect Discovery), donc son seul émetteur
13
+ * suffit à le décrire. Enregistrer Microsoft Entra, Auth0, Okta ou Authentik tient
14
+ * en une ligne dans l'application :
15
+ *
16
+ * ```ts
17
+ * registerOAuthProvider("azure", (ctx) => createDiscoveredOidcProvider("azure", ctx));
18
+ * ```
17
19
  */
18
- /** Contexte de construction d'un fournisseur (lib arctic chargée + secrets de config). */
20
+ /** Contexte de construction d'un fournisseur (secrets et URL issus de la config). */
19
21
  export interface IOAuthProviderContext {
20
- /** Module `arctic` chargé paresseusement (les classes de fournisseurs). */
21
- readonly arctic: typeof Arctic;
22
22
  /** Identifiant client (config, issu de l'env de l'app). */
23
23
  readonly clientId: string;
24
- /** Secret client (config) — jamais loggé. */
24
+ /** Secret client (config) — jamais loggé ; vide pour un client public. */
25
25
  readonly clientSecret: string;
26
+ /**
27
+ * Comment le client s'authentifie au point de jeton, quand l'application le
28
+ * DÉCLARE ; `undefined` laisse le fournisseur poser son défaut.
29
+ *
30
+ * Elle remonte jusqu'ici parce qu'elle appartient au serveur d'autorisation,
31
+ * pas au code du fournisseur : un même Keycloak peut exiger `client_secret_post`
32
+ * là où un autre veut Basic, et ils l'annoncent dans leurs métadonnées. Sans ce
33
+ * champ, la forme serait ouverte dans le client et INATTEIGNABLE depuis une
34
+ * application — donc absente.
35
+ */
36
+ readonly clientAuthMethod?: OAuth2ClientAuthMethod;
26
37
  /** URL de callback exacte (RFC 9700). */
27
38
  readonly redirectUri: string;
28
39
  /**
29
- * Émetteur/realm des fournisseurs OIDC self-hosted (Keycloak : URL du realm,
30
- * ex. `https://kc.example/realms/app`) — `undefined` pour les fournisseurs à
31
- * endpoints fixes (Google, GitHub).
40
+ * Émetteur du fournisseur OIDC (Keycloak : URL du realm, ex.
41
+ * `https://kc.example/realms/app`) — `undefined` pour les fournisseurs dont
42
+ * l'émetteur est connu d'avance (Google) ou qui n'en publient pas (GitHub).
32
43
  */
33
44
  readonly issuer?: string;
34
45
  }
35
- /** Fabrique d'un fournisseur OAuth pour un nom donné. */
36
- export type OAuthProviderFactory = (ctx: IOAuthProviderContext) => IOAuthProvider;
46
+ /**
47
+ * Fabrique d'un fournisseur OAuth pour un nom donné.
48
+ *
49
+ * @remarks Elle peut être **asynchrone** : découvrir les points d'entrée d'un
50
+ * émetteur est une opération de construction, faite une seule fois par processus
51
+ * (le service mémoïse le fournisseur résolu).
52
+ */
53
+ export type OAuthProviderFactory = (ctx: IOAuthProviderContext) => IOAuthProvider | Promise<IOAuthProvider>;
37
54
  /**
38
55
  * Enregistre (ou remplace) la fabrique d'un fournisseur OAuth. Appelée par les
39
56
  * builtins au chargement, et par une application pour ses fournisseurs.
@@ -4,6 +4,6 @@ import type { IOAuthProviderContext } from "../oauthProviderRegistry.js";
4
4
  * Fournisseur **GitHub** (OAuth 2.0 simple, NON-OIDC). Pas de PKCE, pas d'ID
5
5
  * token : le profil est lu via l'API REST (`/user`), et l'email — souvent privé —
6
6
  * via `/user/emails` (scope `user:email`). GitHub n'émet pas de paramètre `iss`
7
- * (`expectedIssuer = null`) : la défense anti-CSRF repose sur le `state`.
7
+ * (`issuerPolicy = null`) : la défense anti-CSRF repose sur le `state`.
8
8
  */
9
9
  export declare function createGithubProvider(ctx: IOAuthProviderContext): IOAuthProvider;
@@ -1,35 +1,60 @@
1
- import type { OAuth2Tokens } from "arctic";
2
1
  import type { IOAuthProvider } from "../../../contracts/IOAuthProvider.js";
2
+ import type { IOAuthProviderContext } from "../oauthProviderRegistry.js";
3
+ import { type IAuthorizationRequest, type ITokenRequest, type OAuth2Tokens } from "../oauth2Client.js";
4
+ import { type IDiscoveryOptions } from "../metadata.js";
3
5
  /**
4
- * Client `arctic` minimal d'un fournisseur **OIDC avec PKCE** — surface
5
- * structurelle commune à `Google`, `MicrosoftEntraId`, `Auth0`, `Okta`,
6
- * `KeyCloak`... (une instance arctic de ces classes est assignable telle quelle).
6
+ * Client d'un fournisseur **OIDC avec PKCE** — surface structurelle que
7
+ * {@link OAuth2Client} remplit, et qu'un test peut remplacer par un double sans
8
+ * réseau.
7
9
  */
8
10
  export interface IOidcPkceClient {
9
- createAuthorizationURL(state: string, codeVerifier: string, scopes: string[]): URL;
10
- validateAuthorizationCode(code: string, codeVerifier: string): Promise<OAuth2Tokens>;
11
+ createAuthorizationURL(request: IAuthorizationRequest): URL;
12
+ validateAuthorizationCode(request: ITokenRequest): Promise<OAuth2Tokens>;
11
13
  }
12
14
  /** Paramètres d'un fournisseur OIDC générique. */
13
15
  export interface IOidcProviderOptions {
14
- /** Nom du fournisseur (`"google"`, `"microsoft"`...) — porté dans le profil. */
16
+ /** Nom du fournisseur (`"google"`, `"keycloak"`...) — porté dans le profil. */
15
17
  readonly name: string;
16
- /** Instance `arctic` (déjà construite avec les secrets). */
18
+ /** Client déjà construit sur les points d'entrée du fournisseur. */
17
19
  readonly client: IOidcPkceClient;
18
20
  /** Émetteur attendu (claim `iss`, anti-mix-up RFC 9207). */
19
21
  readonly issuer: string;
20
- /** `arctic.decodeIdToken` (injecté — arctic est chargé paresseusement). */
21
- readonly decodeIdToken: (idToken: string) => object;
22
+ /**
23
+ * `true` si l'émetteur ANNONCE le paramètre `iss` — son absence devient alors
24
+ * un refus. Défaut `false` : on ne peut pas exiger ce qui n'a pas été promis.
25
+ */
26
+ readonly issParameterSupported?: boolean;
27
+ /** Identifiant client — l'audience que l'ID token DOIT porter. */
28
+ readonly clientId: string;
29
+ /** Décodage des claims de l'ID token — synchrone ou non. */
30
+ readonly decodeIdToken: (idToken: string) => object | Promise<object>;
22
31
  /** Scopes par défaut si la config n'en précise aucun. */
23
32
  readonly defaultScopes?: string[];
24
33
  }
25
34
  /**
26
35
  * Fabrique un {@link IOAuthProvider} **générique OIDC** — couvre TOUT fournisseur
27
36
  * OpenID Connect sans code spécifique : le profil se lit toujours pareil (claims
28
- * standard `sub`/`email`/`email_verified`/`name` de l'ID token). Ajouter un
29
- * fournisseur OIDC = une entrée de quelques lignes (nom + classe arctic + issuer),
30
- * pas un fichier.
37
+ * standard `sub`/`email`/`email_verified`/`name` de l'ID token).
31
38
  *
32
- * PKCE S256 systématique (RFC 7636) ; le profil vient de l'ID token signé obtenu
33
- * du token endpoint via TLS (décodage suffisant en code flow, RFC 8725).
39
+ * PKCE S256 systématique (RFC 7636) ; le profil vient de l'ID token obtenu du
40
+ * point de jeton via TLS.
34
41
  */
35
42
  export declare function createOidcProvider(opts: IOidcProviderOptions): IOAuthProvider;
43
+ /** Réglages d'un fournisseur OIDC découvert. */
44
+ export interface IDiscoveredOidcOptions extends IDiscoveryOptions {
45
+ /** Émetteur ; à défaut, celui de la configuration de l'application. */
46
+ readonly issuer?: string;
47
+ }
48
+ /**
49
+ * Construit un fournisseur OIDC **en demandant ses points d'entrée à l'émetteur**
50
+ * (RFC 8414 / OpenID Connect Discovery) — aucune URL n'est écrite en dur.
51
+ *
52
+ * C'est ce qui permet d'enregistrer n'importe quel fournisseur OpenID Connect sans
53
+ * écrire une ligne de code : seul son émetteur le distingue.
54
+ *
55
+ * @param name - nom sous lequel le fournisseur est configuré.
56
+ * @param options - émetteur explicite, transport injectable, délai d'attente.
57
+ * @throws Error - émetteur absent, découverte impossible, ou serveur annonçant ne
58
+ * pas supporter PKCE S256 alors que ce fournisseur l'exige.
59
+ */
60
+ export declare function createDiscoveredOidcProvider(name: string, ctx: IOAuthProviderContext, options?: IDiscoveredOidcOptions): Promise<IOAuthProvider>;