@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.
- package/dist/_virtual/{_@oxc-project_runtime@0.148.0 → _@oxc-project_runtime@0.149.0}/helpers/esm/decorate.js +1 -1
- package/dist/_virtual/{_@oxc-project_runtime@0.148.0 → _@oxc-project_runtime@0.149.0}/helpers/esm/decorateMetadata.js +1 -1
- package/dist/index.js +7 -3
- package/dist/nodefony/command/security-secrets.js +7 -3
- package/dist/nodefony/command/security-token.js +7 -6
- package/dist/nodefony/command/security-user-add.js +5 -4
- package/dist/nodefony/command/security-user-delete.js +2 -1
- package/dist/nodefony/command/security-user-list.js +2 -1
- package/dist/nodefony/config/config.js +6 -3
- package/dist/nodefony/service/auditService.js +3 -3
- package/dist/nodefony/service/oauth2.js +111 -25
- package/dist/nodefony/service/tokenService.js +3 -3
- package/dist/nodefony/service/totp.js +3 -3
- package/dist/nodefony/service/webAuthn.js +3 -3
- package/dist/nodefony/service/webhooks.js +3 -3
- package/dist/nodefony/src/oauth/httpJson.js +64 -0
- package/dist/nodefony/src/oauth/metadata.js +88 -0
- package/dist/nodefony/src/oauth/oauth2Client.js +266 -0
- package/dist/nodefony/src/oauth/oauthProviderRegistry.js +5 -16
- package/dist/nodefony/src/oauth/providers/github.js +34 -9
- package/dist/nodefony/src/oauth/providers/oidc.js +87 -13
- package/dist/types/index.d.ts +8 -1
- package/dist/types/nodefony/config/config.d.ts +6 -0
- package/dist/types/nodefony/contracts/IOAuthProvider.d.ts +45 -14
- package/dist/types/nodefony/contracts/ITokenStore.d.ts +1 -1
- package/dist/types/nodefony/service/oauth2.d.ts +47 -7
- package/dist/types/nodefony/src/oauth/httpJson.d.ts +28 -0
- package/dist/types/nodefony/src/oauth/metadata.d.ts +54 -0
- package/dist/types/nodefony/src/oauth/oauth2Client.d.ts +174 -0
- package/dist/types/nodefony/src/oauth/oauthProviderRegistry.d.ts +34 -17
- package/dist/types/nodefony/src/oauth/providers/github.d.ts +1 -1
- package/dist/types/nodefony/src/oauth/providers/oidc.d.ts +40 -15
- package/docs/oauth2.md +154 -70
- package/package.json +9 -10
|
@@ -1,9 +1,36 @@
|
|
|
1
|
-
import type { OAuth2Tokens } from "
|
|
1
|
+
import type { IAuthorizationRequest, ITokenRequest, OAuth2Tokens } from "../src/oauth/oauth2Client.js";
|
|
2
2
|
import type { IOAuthProfile } from "@nodefony/user";
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
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
|
|
17
|
-
*
|
|
18
|
-
*
|
|
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
|
-
*
|
|
28
|
-
* `null`
|
|
29
|
-
*
|
|
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
|
|
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(
|
|
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(
|
|
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
|
|
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
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
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
|
-
/**
|
|
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`.
|
|
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
|
|
15
|
-
*
|
|
16
|
-
*
|
|
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 (
|
|
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
|
|
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
|
|
30
|
-
*
|
|
31
|
-
*
|
|
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
|
-
/**
|
|
36
|
-
|
|
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
|
-
* (`
|
|
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
|
|
5
|
-
*
|
|
6
|
-
*
|
|
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(
|
|
10
|
-
validateAuthorizationCode(
|
|
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"`, `"
|
|
16
|
+
/** Nom du fournisseur (`"google"`, `"keycloak"`...) — porté dans le profil. */
|
|
15
17
|
readonly name: string;
|
|
16
|
-
/**
|
|
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
|
-
/**
|
|
21
|
-
|
|
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).
|
|
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
|
|
33
|
-
*
|
|
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>;
|