@nodefony/security 10.0.0-beta.3 → 10.0.0-beta.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/_virtual/{_@oxc-project_runtime@0.153.0 → _@oxc-project_runtime@0.155.0}/helpers/esm/decorate.js +1 -2
- package/dist/index.js +6 -8
- package/dist/nodefony/command/{security-token.js → security-mcp-token.js} +18 -11
- package/dist/nodefony/command/security-oauth-doctor.js +5 -3
- package/dist/nodefony/command/security-secrets.js +10 -4
- package/dist/nodefony/command/security-user-add.js +9 -3
- package/dist/nodefony/command/security-user-delete.js +5 -3
- package/dist/nodefony/command/security-user-list.js +5 -3
- package/dist/nodefony/command/security-user-password.js +5 -3
- package/dist/nodefony/config/config.js +20 -4
- package/dist/nodefony/config/defineModuleConfig.js +1 -2
- package/dist/nodefony/contracts/IAccessVoter.js +1 -2
- package/dist/nodefony/errors/AccessDeniedError.js +1 -2
- package/dist/nodefony/errors/ApiKeyError.js +3 -2
- package/dist/nodefony/errors/AuthenticationError.js +1 -2
- package/dist/nodefony/errors/CsrfError.js +1 -2
- package/dist/nodefony/errors/InsecureTransportError.js +1 -2
- package/dist/nodefony/errors/InvalidTargetError.js +1 -2
- package/dist/nodefony/errors/SsrfError.js +1 -2
- package/dist/nodefony/errors/ThrottledError.js +1 -2
- package/dist/nodefony/errors/UnverifiableTokenError.js +1 -2
- package/dist/nodefony/errors/WebAuthnError.js +1 -2
- package/dist/nodefony/keycloak/keycloakAdmin.js +1 -2
- package/dist/nodefony/keycloak/keycloakRealm.js +1 -2
- package/dist/nodefony/keycloak/keycloakRealmInput.js +1 -2
- package/dist/nodefony/keycloak/scaffold.js +3 -3
- package/dist/nodefony/keycloak/security-keycloak-realm.js +5 -3
- package/dist/nodefony/scaffold/contribute.js +1 -2
- package/dist/nodefony/service/accessTokenVerifier.js +101 -34
- package/dist/nodefony/service/apiKeys.js +45 -5
- package/dist/nodefony/service/auditService.js +4 -5
- package/dist/nodefony/service/authFlow.js +10 -8
- package/dist/nodefony/service/authorization.js +1 -2
- package/dist/nodefony/service/cors.js +1 -2
- package/dist/nodefony/service/csrf.js +1 -2
- package/dist/nodefony/service/firewall.js +5 -4
- package/dist/nodefony/service/keycloak.js +1 -2
- package/dist/nodefony/service/oauth2.js +29 -7
- package/dist/nodefony/service/securityHeaders.js +1 -2
- package/dist/nodefony/service/tokenService.js +38 -22
- package/dist/nodefony/service/totp.js +4 -5
- package/dist/nodefony/service/webAuthn.js +4 -5
- package/dist/nodefony/service/webhooks.js +4 -5
- package/dist/nodefony/src/RoleHierarchyWalker.js +1 -2
- package/dist/nodefony/src/SecuredArea.js +1 -2
- package/dist/nodefony/src/accountBinding.js +96 -0
- package/dist/nodefony/src/admin/SecurityAdminApi.js +1 -2
- package/dist/nodefony/src/admin/WebhookAdminApi.js +23 -7
- package/dist/nodefony/src/admin/adminAudit.js +1 -2
- package/dist/nodefony/src/admin/userRevocationCascade.js +79 -15
- package/dist/nodefony/src/apikey/apiKeyFormat.js +1 -2
- package/dist/nodefony/src/audit/MemoryAuditStore.js +1 -2
- package/dist/nodefony/src/audit/auditBridge.js +1 -2
- package/dist/nodefony/src/audit/auditFilters.js +1 -2
- package/dist/nodefony/src/audit/auditStoreRegistry.js +1 -2
- package/dist/nodefony/src/audit/readAuditContext.js +1 -2
- package/dist/nodefony/src/audit/recordAudit.js +1 -2
- package/dist/nodefony/src/authenticator/AnonymousAuthenticator.js +1 -2
- package/dist/nodefony/src/authenticator/ApiKeyAuthenticator.js +3 -2
- package/dist/nodefony/src/authenticator/ExternalJwtAuthenticator.js +8 -113
- package/dist/nodefony/src/authenticator/ExternalSubjectResolver.js +180 -0
- package/dist/nodefony/src/authenticator/FirewallRealtimeAuthenticator.js +42 -4
- package/dist/nodefony/src/authenticator/JwtAuthenticator.js +91 -16
- package/dist/nodefony/src/authenticator/SessionAuthenticator.js +9 -5
- package/dist/nodefony/src/authenticator/UserPasswordAuthenticator.js +1 -2
- package/dist/nodefony/src/authenticator/authenticatorRegistry.js +3 -17
- package/dist/nodefony/src/authenticator/externalSubject.js +1 -2
- package/dist/nodefony/src/authenticator/peekIssuer.js +1 -2
- package/dist/nodefony/src/boot/publicRoutesNotice.js +1 -2
- package/dist/nodefony/src/crypto/secretCipher.js +1 -2
- package/dist/nodefony/src/csp.js +1 -2
- package/dist/nodefony/src/csrfToken.js +1 -2
- package/dist/nodefony/src/entryPoint.js +1 -2
- package/dist/nodefony/src/net/ssrfGuard.js +1 -2
- package/dist/nodefony/src/net/transportFailure.js +57 -0
- package/dist/nodefony/src/oauth/clientCredentials.js +1 -2
- package/dist/nodefony/src/oauth/httpJson.js +3 -3
- package/dist/nodefony/src/oauth/metadata.js +5 -16
- package/dist/nodefony/src/oauth/oauth2Client.js +1 -2
- package/dist/nodefony/src/oauth/oauthProviderRegistry.js +1 -2
- package/dist/nodefony/src/oauth/providerDiagnosis.js +5 -6
- package/dist/nodefony/src/oauth/providerRoles.js +1 -2
- package/dist/nodefony/src/oauth/providers/github.js +16 -6
- package/dist/nodefony/src/oauth/providers/oidc.js +9 -6
- package/dist/nodefony/src/openToSignup.js +1 -2
- package/dist/nodefony/src/realtime/UserRealtimeToken.js +17 -3
- package/dist/nodefony/src/realtime/frameAuthorizer.js +1 -2
- package/dist/nodefony/src/rolesWithin.js +57 -0
- package/dist/nodefony/src/sessionAuthentication.js +1 -2
- package/dist/nodefony/src/sessionIdentity.js +29 -3
- package/dist/nodefony/src/throttle/LoginThrottler.js +1 -2
- package/dist/nodefony/src/token/AnonymousToken.js +1 -2
- package/dist/nodefony/src/token/JwtKeystore.js +14 -5
- package/dist/nodefony/src/token/MemoryTokenStore.js +1 -2
- package/dist/nodefony/src/token/RemoteJwtVerifier.js +6 -3
- package/dist/nodefony/src/token/UserToken.js +1 -2
- package/dist/nodefony/src/token/jwtRuntime.js +1 -2
- package/dist/nodefony/src/token/scopeGrants.js +109 -0
- package/dist/nodefony/src/token/secretFile.js +1 -2
- package/dist/nodefony/src/token/tokenCriteria.js +1 -2
- package/dist/nodefony/src/token/tokenFilters.js +1 -2
- package/dist/nodefony/src/token/tokenSort.js +1 -2
- package/dist/nodefony/src/token/tokenStatus.js +1 -2
- package/dist/nodefony/src/token/tokenStoreRegistry.js +1 -2
- package/dist/nodefony/src/totp/MemoryTotpSecretStore.js +1 -2
- package/dist/nodefony/src/totp/totpCipher.js +1 -2
- package/dist/nodefony/src/totp/totpCrypto.js +1 -2
- package/dist/nodefony/src/totp/totpOperations.js +1 -2
- package/dist/nodefony/src/totp/totpSecretStoreRegistry.js +1 -2
- package/dist/nodefony/src/transport/CredentialTransportPolicy.js +1 -2
- package/dist/nodefony/src/voter/RoleVoter.js +1 -2
- package/dist/nodefony/src/voter/ScopeVoter.js +1 -2
- package/dist/nodefony/src/voter/voterRegistry.js +1 -2
- package/dist/nodefony/src/webauthn/MemoryWebAuthnCredentialStore.js +1 -2
- package/dist/nodefony/src/webauthn/webAuthnCredentialStoreRegistry.js +1 -2
- package/dist/nodefony/src/webhook/MemoryWebhookStore.js +1 -2
- package/dist/nodefony/src/webhook/WebhookDispatcher.js +1 -2
- package/dist/nodefony/src/webhook/webhookCipher.js +1 -2
- package/dist/nodefony/src/webhook/webhookDelivery.js +1 -2
- package/dist/nodefony/src/webhook/webhookFilters.js +1 -2
- package/dist/nodefony/src/webhook/webhookSignature.js +1 -2
- package/dist/nodefony/src/webhook/webhookSort.js +1 -2
- package/dist/nodefony/src/webhook/webhookStoreRegistry.js +1 -2
- package/dist/types/nodefony/command/{security-token.d.ts → security-mcp-token.d.ts} +7 -6
- package/dist/types/nodefony/config/config.d.ts +1 -0
- package/dist/types/nodefony/errors/ApiKeyError.d.ts +3 -1
- package/dist/types/nodefony/service/accessTokenVerifier.d.ts +35 -14
- package/dist/types/nodefony/service/apiKeys.d.ts +13 -2
- package/dist/types/nodefony/service/oauth2.d.ts +20 -0
- package/dist/types/nodefony/src/accountBinding.d.ts +82 -0
- package/dist/types/nodefony/src/admin/userRevocationCascade.d.ts +23 -5
- package/dist/types/nodefony/src/authenticator/ExternalJwtAuthenticator.d.ts +2 -44
- package/dist/types/nodefony/src/authenticator/ExternalSubjectResolver.d.ts +99 -0
- package/dist/types/nodefony/src/authenticator/FirewallRealtimeAuthenticator.d.ts +4 -1
- package/dist/types/nodefony/src/authenticator/JwtAuthenticator.d.ts +37 -1
- package/dist/types/nodefony/src/net/transportFailure.d.ts +22 -0
- package/dist/types/nodefony/src/oauth/httpJson.d.ts +0 -8
- package/dist/types/nodefony/src/realtime/UserRealtimeToken.d.ts +7 -1
- package/dist/types/nodefony/src/realtime/realtimeContracts.d.ts +6 -0
- package/dist/types/nodefony/src/rolesWithin.d.ts +27 -0
- package/dist/types/nodefony/src/sessionIdentity.d.ts +22 -0
- package/dist/types/nodefony/src/token/scopeGrants.d.ts +77 -0
- package/docs/api-keys.md +181 -174
- package/docs/audit.md +140 -140
- package/docs/authenticators.md +101 -101
- package/docs/authorization.md +116 -69
- package/docs/cors.md +64 -64
- package/docs/csrf.md +74 -71
- package/docs/external-jwt.md +22 -12
- package/docs/firewall.md +85 -82
- package/docs/headers.md +81 -81
- package/docs/index.md +4 -4
- package/docs/keycloak.md +95 -78
- package/docs/login-page.md +23 -23
- package/docs/oauth2.md +157 -146
- package/docs/obtenir-un-jeton.md +116 -26
- package/docs/tokens.md +128 -122
- package/docs/totp.md +136 -136
- package/docs/webauthn.md +153 -153
- package/docs/webhooks.md +127 -124
- package/package.json +13 -13
- package/dist/_virtual/_@oxc-project_runtime@0.153.0/helpers/esm/decorateMetadata.js +0 -6
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { AuthenticationError } from "../../errors/AuthenticationError.js";
|
|
2
|
+
import { loadBoundAccount } from "../accountBinding.js";
|
|
2
3
|
import { UserToken } from "../token/UserToken.js";
|
|
3
4
|
import { bearerToken } from "./bearer.js";
|
|
4
5
|
import peekIssuer from "./peekIssuer.js";
|
|
5
|
-
|
|
6
|
+
import { canonicalIssuer } from "nodefony";
|
|
7
|
+
// nodefony/src/authenticator/JwtAuthenticator.ts
|
|
6
8
|
const COMPACT_JWS = /^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]*$/;
|
|
7
9
|
const INVALID_TOKEN = "Invalid token";
|
|
8
10
|
/**
|
|
@@ -82,6 +84,57 @@ var JwtAuthenticator = class {
|
|
|
82
84
|
async authenticate(token) {
|
|
83
85
|
const raw = token.getCredentials();
|
|
84
86
|
if (typeof raw !== "string" || raw.length === 0) throw new AuthenticationError(INVALID_TOKEN);
|
|
87
|
+
const { user, payload } = await this.#verify(raw, this.#runtime.audiences);
|
|
88
|
+
return this.#promote(token, user, payload);
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Vérifie un jeton émis par CETTE application pour une ressource protégée
|
|
92
|
+
* nommée par l'appelant — la porte d'une ressource (RFC 8707), pas une zone.
|
|
93
|
+
*
|
|
94
|
+
* Les MÊMES règles que {@link authenticate}, appliquées par la même fonction :
|
|
95
|
+
* signature par le keyset local, `iss`, `typ: at+jwt`, révocation ciblée
|
|
96
|
+
* (`jti`) et en masse (`invalidBefore`), compte encore actif. Seule
|
|
97
|
+
* l'audience diffère : c'est celle de la ressource qui appelle, et elle est
|
|
98
|
+
* exigée. Un jeton révoqué, ou celui d'un compte désactivé, n'ouvre donc pas
|
|
99
|
+
* plus une porte MCP qu'une route — ce que la vérification par le seul jeu de
|
|
100
|
+
* clés publiques laissait passer jusqu'à l'expiration.
|
|
101
|
+
*
|
|
102
|
+
* @param raw - le jeton présenté, brut
|
|
103
|
+
* @param audience - l'URI canonique de la ressource qui le reçoit
|
|
104
|
+
* @returns le principal établi, ou `null` pour tout refus (contrat
|
|
105
|
+
* `IAccessTokenVerifier` : un refus n'est jamais une exception)
|
|
106
|
+
* @throws Error si le câblage manque (keystore, store, annuaire) — la porte
|
|
107
|
+
* le traduit en `503`, jamais en jeton accepté
|
|
108
|
+
*/
|
|
109
|
+
async verifyForResource(raw, audience) {
|
|
110
|
+
let verified;
|
|
111
|
+
try {
|
|
112
|
+
verified = await this.#verify(raw, audience);
|
|
113
|
+
} catch (error) {
|
|
114
|
+
if (error instanceof AuthenticationError) return null;
|
|
115
|
+
throw error;
|
|
116
|
+
}
|
|
117
|
+
const { payload } = verified;
|
|
118
|
+
const scope = payload.scope;
|
|
119
|
+
return {
|
|
120
|
+
issuer: this.#resourceIssuer(),
|
|
121
|
+
subject: payload.sub,
|
|
122
|
+
scopes: typeof scope === "string" && scope.length > 0 ? scope.split(" ") : [],
|
|
123
|
+
expiresAt: payload.exp,
|
|
124
|
+
issuedAt: payload.iat,
|
|
125
|
+
tokenId: payload.jti,
|
|
126
|
+
claims: payload
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* La vérification entière d'un jeton maison — UNE implémentation, pour la
|
|
131
|
+
* zone ({@link authenticate}) comme pour la ressource
|
|
132
|
+
* ({@link verifyForResource}).
|
|
133
|
+
*
|
|
134
|
+
* @throws AuthenticationError pour tout refus (message uniforme)
|
|
135
|
+
* @throws Error si le câblage manque
|
|
136
|
+
*/
|
|
137
|
+
async #verify(raw, audience) {
|
|
85
138
|
const jose = this.#jose ??= await import("jose");
|
|
86
139
|
const getKey = await this.#ensureGetKey(jose);
|
|
87
140
|
let payload;
|
|
@@ -89,7 +142,7 @@ var JwtAuthenticator = class {
|
|
|
89
142
|
payload = (await jose.jwtVerify(raw, getKey, {
|
|
90
143
|
algorithms: ["EdDSA"],
|
|
91
144
|
issuer: this.#runtime.issuer,
|
|
92
|
-
audience
|
|
145
|
+
audience,
|
|
93
146
|
typ: "at+jwt"
|
|
94
147
|
})).payload;
|
|
95
148
|
} catch {
|
|
@@ -104,8 +157,31 @@ var JwtAuthenticator = class {
|
|
|
104
157
|
if (invalidBefore !== null) {
|
|
105
158
|
if ((typeof payload.iat === "number" ? payload.iat * 1e3 : 0) < invalidBefore) throw new AuthenticationError(INVALID_TOKEN);
|
|
106
159
|
}
|
|
107
|
-
const user = await this.#
|
|
108
|
-
|
|
160
|
+
const user = await loadBoundAccount(this.#resolveUserProvider(), sub, payload["uid"], payload["cv"]);
|
|
161
|
+
if (user === null) throw new AuthenticationError(INVALID_TOKEN);
|
|
162
|
+
return {
|
|
163
|
+
user,
|
|
164
|
+
payload
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Recharge le compte que désigne un principal établi par
|
|
169
|
+
* {@link verifyForResource} — pour une ressource protégée qui fait agir un
|
|
170
|
+
* outil au nom du propriétaire du jeton.
|
|
171
|
+
*
|
|
172
|
+
* La MÊME règle que la zone ({@link loadBoundAccount}), rejouée sur les
|
|
173
|
+
* revendications du principal : le vérificateur ne garde pas le compte qu'il
|
|
174
|
+
* a lu, et une seconde lecture de l'annuaire est le coût accepté d'un outil
|
|
175
|
+
* qui la demande — les autres ne la paient pas.
|
|
176
|
+
*
|
|
177
|
+
* @param principal - principal d'un jeton émis par CETTE application
|
|
178
|
+
* @returns le compte, ou `null` s'il n'est pas (ou plus) celui du jeton
|
|
179
|
+
* @throws Error si l'annuaire des comptes n'est pas câblé
|
|
180
|
+
*/
|
|
181
|
+
resolveAccount(principal) {
|
|
182
|
+
const subject = principal.subject;
|
|
183
|
+
if (principal.issuer !== this.#resourceIssuer() || typeof subject !== "string" || subject.length === 0) return Promise.resolve(null);
|
|
184
|
+
return loadBoundAccount(this.#resolveUserProvider(), subject, principal.claims?.["uid"], principal.claims?.["cv"]);
|
|
109
185
|
}
|
|
110
186
|
/** Slot audit (J4b). */
|
|
111
187
|
onSuccess(_context, _token) {
|
|
@@ -119,6 +195,17 @@ var JwtAuthenticator = class {
|
|
|
119
195
|
challenge() {
|
|
120
196
|
return "Bearer";
|
|
121
197
|
}
|
|
198
|
+
/**
|
|
199
|
+
* L'émetteur que portent les principaux de {@link verifyForResource} — et
|
|
200
|
+
* donc celui qu'un principal doit porter pour être un jeton maison.
|
|
201
|
+
*/
|
|
202
|
+
#resourceIssuer() {
|
|
203
|
+
try {
|
|
204
|
+
return canonicalIssuer(this.#runtime.issuer);
|
|
205
|
+
} catch {
|
|
206
|
+
return this.#runtime.issuer;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
122
209
|
/** Construit (une fois) le résolveur de clé par `kid` depuis le JWKS public. */
|
|
123
210
|
async #ensureGetKey(jose) {
|
|
124
211
|
if (this.#getKey !== null) return this.#getKey;
|
|
@@ -137,17 +224,6 @@ var JwtAuthenticator = class {
|
|
|
137
224
|
ut.setAttribute("issuer", payload.iss);
|
|
138
225
|
return ut;
|
|
139
226
|
}
|
|
140
|
-
async #resolveUserOrReject(sub) {
|
|
141
|
-
const provider = this.#resolveUserProvider();
|
|
142
|
-
let user;
|
|
143
|
-
try {
|
|
144
|
-
user = await provider.loadUserByIdentifier(sub);
|
|
145
|
-
} catch {
|
|
146
|
-
throw new AuthenticationError(INVALID_TOKEN);
|
|
147
|
-
}
|
|
148
|
-
if (!user.isActive() || user.isLocked()) throw new AuthenticationError(INVALID_TOKEN);
|
|
149
|
-
return user;
|
|
150
|
-
}
|
|
151
227
|
#resolveKeystore() {
|
|
152
228
|
if (this.#keystore === null) {
|
|
153
229
|
const ks = this.#container.get("jwtKeystore");
|
|
@@ -173,5 +249,4 @@ var JwtAuthenticator = class {
|
|
|
173
249
|
return this.#userProvider;
|
|
174
250
|
}
|
|
175
251
|
};
|
|
176
|
-
//#endregion
|
|
177
252
|
export { JwtAuthenticator, JwtAuthenticator as default };
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { AuthenticationError } from "../../errors/AuthenticationError.js";
|
|
2
|
-
import {
|
|
2
|
+
import { ACCOUNT_ID_KEY, CREDENTIALS_VERSION_KEY } from "../accountBinding.js";
|
|
3
|
+
import { resolveBoundSessionIdentity } from "../sessionIdentity.js";
|
|
3
4
|
import { UserToken } from "../token/UserToken.js";
|
|
4
|
-
|
|
5
|
+
// nodefony/src/authenticator/SessionAuthenticator.ts
|
|
5
6
|
/**
|
|
6
7
|
* Authentification par **session serveur** (cookie opaque, modèle BFF) — la
|
|
7
8
|
* preuve des requêtes qui SUIVENT le login (`AuthFlow.login`, qui a déjà posé
|
|
@@ -59,7 +60,11 @@ var SessionAuthenticator = class {
|
|
|
59
60
|
}
|
|
60
61
|
/** Extrait l'identifiant du blob de session (jamais de secret en jeu). */
|
|
61
62
|
createToken(context) {
|
|
62
|
-
const credentials = {
|
|
63
|
+
const credentials = {
|
|
64
|
+
identifier: context.session?.user ?? "",
|
|
65
|
+
accountId: context.session?.getMetaBag(ACCOUNT_ID_KEY),
|
|
66
|
+
credentialsVersion: context.session?.getMetaBag(CREDENTIALS_VERSION_KEY)
|
|
67
|
+
};
|
|
63
68
|
return Promise.resolve(new UserToken(this.name, credentials));
|
|
64
69
|
}
|
|
65
70
|
/**
|
|
@@ -74,7 +79,7 @@ var SessionAuthenticator = class {
|
|
|
74
79
|
const credentials = token.getCredentials();
|
|
75
80
|
if (!credentials?.identifier) throw new AuthenticationError("Invalid session");
|
|
76
81
|
const provider = this.#provider ??= this.#resolveProvider();
|
|
77
|
-
const user = await
|
|
82
|
+
const user = await resolveBoundSessionIdentity(provider, credentials.identifier, credentials.accountId, credentials.credentialsVersion);
|
|
78
83
|
return token.promote(user);
|
|
79
84
|
}
|
|
80
85
|
/**
|
|
@@ -90,5 +95,4 @@ var SessionAuthenticator = class {
|
|
|
90
95
|
return Promise.resolve();
|
|
91
96
|
}
|
|
92
97
|
};
|
|
93
|
-
//#endregion
|
|
94
98
|
export { SessionAuthenticator, SessionAuthenticator as default };
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { AuthenticationError } from "../../errors/AuthenticationError.js";
|
|
2
2
|
import { ThrottledError } from "../../errors/ThrottledError.js";
|
|
3
3
|
import { UserToken } from "../token/UserToken.js";
|
|
4
|
-
|
|
4
|
+
// nodefony/src/authenticator/UserPasswordAuthenticator.ts
|
|
5
5
|
const BASIC_SCHEME = /^basic\s+/i;
|
|
6
6
|
const INVALID_CREDENTIALS = "Invalid credentials";
|
|
7
7
|
/**
|
|
@@ -101,5 +101,4 @@ var UserPasswordAuthenticator = class {
|
|
|
101
101
|
return "Basic realm=\"nodefony\", charset=\"UTF-8\"";
|
|
102
102
|
}
|
|
103
103
|
};
|
|
104
|
-
//#endregion
|
|
105
104
|
export { UserPasswordAuthenticator, UserPasswordAuthenticator as default };
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import { compileProviderRoleMapping } from "../oauth/providerRoles.js";
|
|
2
1
|
import { AnonymousAuthenticator } from "./AnonymousAuthenticator.js";
|
|
3
2
|
import { SessionAuthenticator } from "./SessionAuthenticator.js";
|
|
4
3
|
import { UserPasswordAuthenticator } from "./UserPasswordAuthenticator.js";
|
|
5
4
|
import { JwtAuthenticator } from "./JwtAuthenticator.js";
|
|
6
5
|
import { ApiKeyAuthenticator } from "./ApiKeyAuthenticator.js";
|
|
6
|
+
import { externalSubjectOptions } from "./ExternalSubjectResolver.js";
|
|
7
7
|
import { ExternalJwtAuthenticator } from "./ExternalJwtAuthenticator.js";
|
|
8
8
|
import { resolveJwtRuntime } from "../token/jwtRuntime.js";
|
|
9
|
-
|
|
9
|
+
// nodefony/src/authenticator/authenticatorRegistry.ts
|
|
10
10
|
const factories = /* @__PURE__ */ new Map();
|
|
11
11
|
/**
|
|
12
12
|
* Enregistre (ou remplace) la fabrique d'un authenticator. Appelé par les
|
|
@@ -45,20 +45,7 @@ registerAuthenticatorFactory("jwt", ({ container, config }) => {
|
|
|
45
45
|
return new JwtAuthenticator(container, resolveJwtRuntime(config.jwt));
|
|
46
46
|
});
|
|
47
47
|
registerAuthenticatorFactory("external-jwt", ({ container, config }) => {
|
|
48
|
-
|
|
49
|
-
return new ExternalJwtAuthenticator(container, {
|
|
50
|
-
issuers: rs.issuers.map((i) => ({
|
|
51
|
-
issuer: i.issuer,
|
|
52
|
-
subjectMapping: i.subjectMapping
|
|
53
|
-
})),
|
|
54
|
-
subjectPolicy: rs.subjectPolicy,
|
|
55
|
-
ephemeralRoles: rs.ephemeralRoles,
|
|
56
|
-
oauthProviders: Object.entries(config.oauth2.providers).flatMap(([name, p]) => p.issuer ? [{
|
|
57
|
-
name,
|
|
58
|
-
issuer: p.issuer,
|
|
59
|
-
roleMapping: compileProviderRoleMapping(p)
|
|
60
|
-
}] : [])
|
|
61
|
-
});
|
|
48
|
+
return new ExternalJwtAuthenticator(container, externalSubjectOptions(config));
|
|
62
49
|
});
|
|
63
50
|
registerAuthenticatorFactory("apikey", ({ container, config }) => {
|
|
64
51
|
return new ApiKeyAuthenticator(container, {
|
|
@@ -66,5 +53,4 @@ registerAuthenticatorFactory("apikey", ({ container, config }) => {
|
|
|
66
53
|
lastUsedThrottleS: config.apiKeys.lastUsedThrottleS
|
|
67
54
|
});
|
|
68
55
|
});
|
|
69
|
-
//#endregion
|
|
70
56
|
export { getAuthenticatorFactory, listAuthenticatorFactories, registerAuthenticatorFactory };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
// nodefony/src/authenticator/externalSubject.ts
|
|
2
2
|
/**
|
|
3
3
|
* Séparateur entre l'émetteur et le sujet.
|
|
4
4
|
*
|
|
@@ -32,5 +32,4 @@ function localIdentifierFor(issuer, subject, mapping) {
|
|
|
32
32
|
if (mapping === "subject") return subject;
|
|
33
33
|
return `${issuer}${SEPARATOR}${subject}`;
|
|
34
34
|
}
|
|
35
|
-
//#endregion
|
|
36
35
|
export { localIdentifierFor };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
// nodefony/src/authenticator/peekIssuer.ts
|
|
2
2
|
/**
|
|
3
3
|
* Lecture NON VÉRIFIÉE de l'émetteur d'un JWS compact.
|
|
4
4
|
*
|
|
@@ -52,5 +52,4 @@ function peekIssuer(raw) {
|
|
|
52
52
|
return null;
|
|
53
53
|
}
|
|
54
54
|
}
|
|
55
|
-
//#endregion
|
|
56
55
|
export { peekIssuer as default, peekIssuer };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { isFrameworkZone } from "nodefony";
|
|
2
|
-
|
|
2
|
+
// nodefony/src/boot/publicRoutesNotice.ts
|
|
3
3
|
/**
|
|
4
4
|
* Le point d'attention « routes métier publiques » du bilan de démarrage.
|
|
5
5
|
*
|
|
@@ -23,5 +23,4 @@ function publicRoutesNotice(zones) {
|
|
|
23
23
|
fix: "use(\"@nodefony/security\", { areas: { main: { pattern: \"^/api\", authenticators: [\"session\"] } } })"
|
|
24
24
|
};
|
|
25
25
|
}
|
|
26
|
-
//#endregion
|
|
27
26
|
export { publicRoutesNotice };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createCipheriv, createDecipheriv, hkdfSync, randomBytes } from "node:crypto";
|
|
2
2
|
import { Buffer } from "node:buffer";
|
|
3
|
-
|
|
3
|
+
// nodefony/src/crypto/secretCipher.ts
|
|
4
4
|
/**
|
|
5
5
|
* Chiffrement **réversible** de secrets applicatifs au repos — AES-256-GCM
|
|
6
6
|
* (authentifié). Brique générique partagée par tous les domaines qui doivent
|
|
@@ -75,5 +75,4 @@ function decryptSecret(blob, key) {
|
|
|
75
75
|
decipher.setAuthTag(tag);
|
|
76
76
|
return Buffer.concat([decipher.update(ct), decipher.final()]);
|
|
77
77
|
}
|
|
78
|
-
//#endregion
|
|
79
78
|
export { decryptSecret, deriveKey, encryptSecret, generateEphemeralKey };
|
package/dist/nodefony/src/csp.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
// nodefony/src/csp.ts
|
|
2
2
|
/**
|
|
3
3
|
* Parse une chaîne CSP en directives ordonnées `[nom, sources[]]`. L'ordre des
|
|
4
4
|
* directives et des sources est préservé (déterminisme : header stable, tests
|
|
@@ -49,5 +49,4 @@ function mergeCspFragments(base, fragments) {
|
|
|
49
49
|
}
|
|
50
50
|
return serializeCsp(directives);
|
|
51
51
|
}
|
|
52
|
-
//#endregion
|
|
53
52
|
export { mergeCspFragments, parseCsp, serializeCsp };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createHmac, randomBytes, timingSafeEqual } from "node:crypto";
|
|
2
|
-
|
|
2
|
+
// nodefony/src/csrfToken.ts
|
|
3
3
|
/**
|
|
4
4
|
* Jeton synchronizer CSRF — modèle **double-submit signé** (OWASP CSRF Prevention
|
|
5
5
|
* Cheat Sheet, « Signed Double-Submit Cookie »). Le token est
|
|
@@ -61,5 +61,4 @@ var CsrfTokenManager = class CsrfTokenManager {
|
|
|
61
61
|
return timingSafeEqual(ba, bb);
|
|
62
62
|
}
|
|
63
63
|
};
|
|
64
|
-
//#endregion
|
|
65
64
|
export { CsrfTokenManager, CsrfTokenManager as default };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { loginPagePath, safeRedirectPath } from "nodefony";
|
|
2
|
-
|
|
2
|
+
// nodefony/src/entryPoint.ts
|
|
3
3
|
/**
|
|
4
4
|
* Dit si une requête refusée est la NAVIGATION d'un navigateur — le seul cas
|
|
5
5
|
* où conduire à la page de connexion aide, au lieu de casser un client.
|
|
@@ -35,5 +35,4 @@ function entryPointLocation(entryPoint, request) {
|
|
|
35
35
|
if (request.path.split("?", 1)[0] === entryPoint.split("?", 1)[0]) return null;
|
|
36
36
|
return loginPagePath(safeRedirectPath(request.path), entryPoint);
|
|
37
37
|
}
|
|
38
|
-
//#endregion
|
|
39
38
|
export { entryPointLocation, isBrowserNavigation };
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { SsrfError } from "../../errors/SsrfError.js";
|
|
2
2
|
import { BlockList, isIP } from "node:net";
|
|
3
3
|
import { lookup } from "node:dns/promises";
|
|
4
|
-
|
|
4
|
+
// nodefony/src/net/ssrfGuard.ts
|
|
5
5
|
/**
|
|
6
6
|
* Protection **SSRF** (Server-Side Request Forgery) générique pour les URL
|
|
7
7
|
* sortantes — endpoints webhook, fetch applicatif, futurs agents/RAG.
|
|
@@ -126,5 +126,4 @@ async function assertPublicUrl(rawUrl, options = {}) {
|
|
|
126
126
|
addresses
|
|
127
127
|
};
|
|
128
128
|
}
|
|
129
|
-
//#endregion
|
|
130
129
|
export { assertPublicUrl, isBlockedAddress };
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// nodefony/src/net/transportFailure.ts
|
|
2
|
+
/**
|
|
3
|
+
* Codes TLS d'une chaîne de certificats qu'aucune autorité CONNUE ne signe.
|
|
4
|
+
*
|
|
5
|
+
* Leur remède est le même : faire reconnaître l'autorité. Un certificat expiré
|
|
6
|
+
* (`CERT_HAS_EXPIRED`) ou émis pour un autre nom (`ERR_TLS_CERT_ALTNAME_INVALID`)
|
|
7
|
+
* n'en fait pas partie : ajouter une autorité n'y changerait rien.
|
|
8
|
+
*/
|
|
9
|
+
const UNTRUSTED_AUTHORITY_CODES = /* @__PURE__ */ new Set([
|
|
10
|
+
"SELF_SIGNED_CERT_IN_CHAIN",
|
|
11
|
+
"DEPTH_ZERO_SELF_SIGNED_CERT",
|
|
12
|
+
"UNABLE_TO_VERIFY_LEAF_SIGNATURE",
|
|
13
|
+
"UNABLE_TO_GET_ISSUER_CERT",
|
|
14
|
+
"UNABLE_TO_GET_ISSUER_CERT_LOCALLY",
|
|
15
|
+
"CERT_UNTRUSTED"
|
|
16
|
+
]);
|
|
17
|
+
/** Le code porté par la `cause` d'un échec de `fetch`, s'il y en a un. */
|
|
18
|
+
function causeCode(error) {
|
|
19
|
+
if (!(error instanceof Error)) return null;
|
|
20
|
+
const cause = error.cause;
|
|
21
|
+
if (!(cause instanceof Error)) return null;
|
|
22
|
+
const code = cause.code;
|
|
23
|
+
return typeof code === "string" ? code : null;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Rend un échec de transport LISIBLE : `fetch` (undici) ne dit que
|
|
27
|
+
* `fetch failed` et range la vraie raison dans `cause` — certificat refusé,
|
|
28
|
+
* connexion refusée, nom introuvable.
|
|
29
|
+
*
|
|
30
|
+
* @param error - ce que `fetch` (ou une bibliothèque qui l'appelle) a levé
|
|
31
|
+
* @returns `message (CODE : raison)`, ou le message seul faute de cause
|
|
32
|
+
*/
|
|
33
|
+
function describeTransportFailure(error) {
|
|
34
|
+
if (!(error instanceof Error)) return String(error);
|
|
35
|
+
const cause = error.cause;
|
|
36
|
+
if (!(cause instanceof Error)) return error.message;
|
|
37
|
+
const code = causeCode(error);
|
|
38
|
+
return code !== null ? `${error.message} (${code} : ${cause.message})` : `${error.message} (${cause.message})`;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Le remède d'un certificat signé par une autorité que le processus ne
|
|
42
|
+
* reconnaît pas — `null` pour tout autre échec.
|
|
43
|
+
*
|
|
44
|
+
* En développement, Nodefony reconnaît d'office l'autorité qu'il a générée ; en
|
|
45
|
+
* production jamais, et c'est voulu. Sans ce remède, le journal ne montrait que
|
|
46
|
+
* `SELF_SIGNED_CERT_IN_CHAIN` : la variable qui le règle n'était écrite que dans
|
|
47
|
+
* un commentaire du `.env`, fichier qu'un serveur de production n'a pas.
|
|
48
|
+
*
|
|
49
|
+
* @param error - ce que `fetch` (ou une bibliothèque qui l'appelle) a levé
|
|
50
|
+
* @returns une phrase d'action, ou `null`
|
|
51
|
+
*/
|
|
52
|
+
function untrustedAuthorityHint(error) {
|
|
53
|
+
const code = causeCode(error);
|
|
54
|
+
if (code === null || !UNTRUSTED_AUTHORITY_CODES.has(code)) return null;
|
|
55
|
+
return "le certificat de ce serveur est signé par une autorité que l'application ne reconnaît pas : déclare-la dans NF_TLS_CA_FILE (fichier PEM, `http.certificates.extraCaFiles`)";
|
|
56
|
+
}
|
|
57
|
+
export { describeTransportFailure, untrustedAuthorityHint };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { postTokenRequest } from "./oauth2Client.js";
|
|
2
2
|
import { discoverAuthorizationServer } from "./metadata.js";
|
|
3
|
-
|
|
3
|
+
// nodefony/src/oauth/clientCredentials.ts
|
|
4
4
|
/**
|
|
5
5
|
* Obtient un jeton d'accès pour un programme, par le flux `client_credentials`
|
|
6
6
|
* (RFC 6749 §4.4), auprès d'un émetteur découvert (RFC 8414).
|
|
@@ -36,5 +36,4 @@ async function requestClientCredentialsToken(request) {
|
|
|
36
36
|
timeoutMs: request.timeoutMs
|
|
37
37
|
}, body);
|
|
38
38
|
}
|
|
39
|
-
//#endregion
|
|
40
39
|
export { requestClientCredentialsToken };
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
|
|
1
|
+
import { isPlainObject } from "nodefony";
|
|
2
|
+
// nodefony/src/oauth/httpJson.ts
|
|
2
3
|
/**
|
|
3
4
|
* Lecture BORNÉE d'un corps JSON — la brique de transport commune aux trois
|
|
4
5
|
* appels sortants du social login (découverte, point de jeton, API d'un
|
|
@@ -57,8 +58,7 @@ async function readJsonBounded(response, maxBytes, subject) {
|
|
|
57
58
|
*/
|
|
58
59
|
async function readJsonObjectBounded(response, maxBytes, subject) {
|
|
59
60
|
const payload = await readJsonBounded(response, maxBytes, subject);
|
|
60
|
-
if (
|
|
61
|
+
if (!isPlainObject(payload)) throw new Error(`${subject} → corps inattendu (objet JSON attendu)`);
|
|
61
62
|
return payload;
|
|
62
63
|
}
|
|
63
|
-
//#endregion
|
|
64
64
|
export { readJsonBounded, readJsonObjectBounded };
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import { describeTransportFailure, untrustedAuthorityHint } from "../net/transportFailure.js";
|
|
1
2
|
import { readJsonObjectBounded } from "./httpJson.js";
|
|
2
3
|
import { canonicalIssuer, isBackChannelTransportAllowed, metadataCandidateUrls, validateIssuerMetadata } from "nodefony";
|
|
3
|
-
|
|
4
|
+
// nodefony/src/oauth/metadata.ts
|
|
4
5
|
/**
|
|
5
6
|
* **Découverte des métadonnées d'un serveur d'autorisation** (RFC 8414) — la face
|
|
6
7
|
* CLIENTE de la règle que le cœur porte déjà.
|
|
@@ -22,19 +23,6 @@ import { canonicalIssuer, isBackChannelTransportAllowed, metadataCandidateUrls,
|
|
|
22
23
|
const DISCOVERY_TIMEOUT_MS = 1e4;
|
|
23
24
|
/** Au-delà, le document n'est plus un document de métadonnées — on refuse de lire. */
|
|
24
25
|
const MAX_METADATA_BYTES = 1048576;
|
|
25
|
-
/**
|
|
26
|
-
* Rend un échec de transport LISIBLE : `fetch` (undici) ne dit que
|
|
27
|
-
* `fetch failed` et range la vraie raison dans `cause` — certificat refusé,
|
|
28
|
-
* connexion refusée, nom introuvable. Sans elle, « métadonnées introuvables »
|
|
29
|
-
* envoie chercher un émetteur absent quand c'est la confiance TLS qui manque.
|
|
30
|
-
*/
|
|
31
|
-
function describeTransportFailure(error) {
|
|
32
|
-
if (!(error instanceof Error)) return String(error);
|
|
33
|
-
const cause = error.cause;
|
|
34
|
-
if (!(cause instanceof Error)) return error.message;
|
|
35
|
-
const code = cause.code;
|
|
36
|
-
return typeof code === "string" ? `${error.message} (${code} : ${cause.message})` : `${error.message} (${cause.message})`;
|
|
37
|
-
}
|
|
38
26
|
async function fetchMetadataDocument(url, options) {
|
|
39
27
|
const response = await (options.fetch ?? globalThis.fetch)(url, {
|
|
40
28
|
headers: {
|
|
@@ -86,6 +74,7 @@ function optionalEndpoint(document, field, issuer) {
|
|
|
86
74
|
async function discoverAuthorizationServer(rawIssuer, options = {}) {
|
|
87
75
|
const issuer = canonicalIssuer(rawIssuer);
|
|
88
76
|
const failures = [];
|
|
77
|
+
let hint = null;
|
|
89
78
|
const discoveryUrl = options.discoveryUrl;
|
|
90
79
|
for (const candidate of metadataCandidateUrls(issuer, discoveryUrl)) {
|
|
91
80
|
let document;
|
|
@@ -93,6 +82,7 @@ async function discoverAuthorizationServer(rawIssuer, options = {}) {
|
|
|
93
82
|
document = await fetchMetadataDocument(candidate, options);
|
|
94
83
|
} catch (error) {
|
|
95
84
|
failures.push(describeTransportFailure(error));
|
|
85
|
+
hint ??= untrustedAuthorityHint(error);
|
|
96
86
|
continue;
|
|
97
87
|
}
|
|
98
88
|
const identity = validateIssuerMetadata(document, issuer, discoveryUrl);
|
|
@@ -109,7 +99,6 @@ async function discoverAuthorizationServer(rawIssuer, options = {}) {
|
|
|
109
99
|
idTokenSigningAlgValuesSupported: Array.isArray(idTokenAlgs) ? idTokenAlgs.filter((a) => typeof a === "string") : null
|
|
110
100
|
};
|
|
111
101
|
}
|
|
112
|
-
throw new Error(`métadonnées introuvables pour « ${issuer} » — ${failures.join(" ; ")}`);
|
|
102
|
+
throw new Error(`métadonnées introuvables pour « ${issuer} » — ${failures.join(" ; ")}` + (hint === null ? "" : ` — ${hint}`));
|
|
113
103
|
}
|
|
114
|
-
//#endregion
|
|
115
104
|
export { discoverAuthorizationServer };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { readJsonObjectBounded } from "./httpJson.js";
|
|
2
2
|
import { createHash, randomBytes } from "node:crypto";
|
|
3
|
-
|
|
3
|
+
// nodefony/src/oauth/oauth2Client.ts
|
|
4
4
|
/**
|
|
5
5
|
* Client **OAuth 2.0 / Authorization Code** minimal — la face cliente du protocole
|
|
6
6
|
* dont Nodefony écrit déjà la face serveur (émetteur de jetons, métadonnées,
|
|
@@ -289,5 +289,4 @@ function basicCredentials(client) {
|
|
|
289
289
|
const secret = formUrlencode(client.clientSecret);
|
|
290
290
|
return Buffer.from(`${id}:${secret}`, "utf8").toString("base64");
|
|
291
291
|
}
|
|
292
|
-
//#endregion
|
|
293
292
|
export { OAuth2Client, OAuth2RequestError, OAuth2Tokens, createCodeChallenge, generateCodeVerifier, generateState, postTokenRequest };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createDiscoveredOidcProvider } from "./providers/oidc.js";
|
|
2
2
|
import { createGithubProvider } from "./providers/github.js";
|
|
3
|
-
|
|
3
|
+
// nodefony/src/oauth/oauthProviderRegistry.ts
|
|
4
4
|
const factories = /* @__PURE__ */ new Map();
|
|
5
5
|
/**
|
|
6
6
|
* Enregistre (ou remplace) la fabrique d'un fournisseur OAuth. Appelée par les
|
|
@@ -55,5 +55,4 @@ registerOAuthProvider("google", (ctx) => createDiscoveredOidcProvider("google",
|
|
|
55
55
|
registerOAuthProvider("keycloak", async (ctx) => withKeycloakThemeHint(await createDiscoveredOidcProvider("keycloak", ctx)), { requiresIssuer: true });
|
|
56
56
|
registerOAuthProvider("oidc", (ctx) => createDiscoveredOidcProvider("oidc", ctx), { requiresIssuer: true });
|
|
57
57
|
registerOAuthProvider("github", createGithubProvider);
|
|
58
|
-
//#endregion
|
|
59
58
|
export { KEYCLOAK_THEME_PARAMETER, getOAuthProviderFactory, listOAuthProviders, oauthProviderRequiresIssuer, registerOAuthProvider, withKeycloakThemeHint };
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import { describeTransportFailure, untrustedAuthorityHint } from "../net/transportFailure.js";
|
|
1
2
|
import { OAuth2RequestError, generateCodeVerifier, generateState } from "./oauth2Client.js";
|
|
2
3
|
import { IssuerMismatchError } from "nodefony";
|
|
3
|
-
|
|
4
|
+
// nodefony/src/oauth/providerDiagnosis.ts
|
|
4
5
|
/** Le code présenté au point de jeton : il n'a jamais été émis, il sera refusé. */
|
|
5
6
|
const PROBE_CODE = "nodefony-doctor-probe";
|
|
6
7
|
const AUTHORIZATION_TIMEOUT_MS = 1e4;
|
|
@@ -10,10 +11,9 @@ const AUTHORIZATION_TIMEOUT_MS = 1e4;
|
|
|
10
11
|
*/
|
|
11
12
|
const MAX_ERROR_PAGE_BYTES = 65536;
|
|
12
13
|
function errorMessage(error) {
|
|
13
|
-
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
return error.message;
|
|
14
|
+
const hint = untrustedAuthorityHint(error);
|
|
15
|
+
const text = describeTransportFailure(error);
|
|
16
|
+
return hint === null ? text : `${text} — ${hint}`;
|
|
17
17
|
}
|
|
18
18
|
/** Les premiers octets d'une page, en texte — le reste est abandonné. */
|
|
19
19
|
async function readHead(response, maxBytes) {
|
|
@@ -279,5 +279,4 @@ async function diagnoseOAuthProvider(input) {
|
|
|
279
279
|
checks
|
|
280
280
|
};
|
|
281
281
|
}
|
|
282
|
-
//#endregion
|
|
283
282
|
export { diagnoseOAuthProvider };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
// nodefony/src/oauth/providerRoles.ts
|
|
2
2
|
/** Source par défaut : les rôles du CLIENT — ceux que l'annuaire attribue à CETTE application. */
|
|
3
3
|
const DEFAULT_ROLE_SOURCES = ["client"];
|
|
4
4
|
/**
|
|
@@ -75,5 +75,4 @@ function strings(value) {
|
|
|
75
75
|
if (!Array.isArray(value)) return [];
|
|
76
76
|
return value.filter((v) => typeof v === "string");
|
|
77
77
|
}
|
|
78
|
-
//#endregion
|
|
79
78
|
export { DEFAULT_ROLE_SOURCES, PLATFORM_ROLE_PREFIX, compileProviderRoleMapping, isPlatformRole, mapProviderRoles };
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { readJsonBounded } from "../httpJson.js";
|
|
1
|
+
import { readJsonBounded, readJsonObjectBounded } from "../httpJson.js";
|
|
2
2
|
import { OAuth2Client, OAuth2RequestError } from "../oauth2Client.js";
|
|
3
|
-
|
|
3
|
+
import { isPlainObject } from "nodefony";
|
|
4
|
+
// nodefony/src/oauth/providers/github.ts
|
|
4
5
|
/** Scopes minimaux : profil public + emails (l'email primaire peut être privé). */
|
|
5
6
|
const DEFAULT_SCOPES = ["read:user", "user:email"];
|
|
6
7
|
const API = "https://api.github.com";
|
|
@@ -25,7 +26,17 @@ async function ghGet(url, accessToken) {
|
|
|
25
26
|
signal: AbortSignal.timeout(API_TIMEOUT_MS)
|
|
26
27
|
});
|
|
27
28
|
if (!res.ok) throw new Error(`GitHub API ${url} → ${res.status}`);
|
|
28
|
-
return
|
|
29
|
+
return res;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Les adresses que rend `GET /user/emails` — un tableau, sinon une erreur qui
|
|
33
|
+
* nomme l'appel ; une entrée qui n'est pas un objet est écartée.
|
|
34
|
+
*/
|
|
35
|
+
async function ghEmails(accessToken) {
|
|
36
|
+
const url = `${API}/user/emails`;
|
|
37
|
+
const list = await readJsonBounded(await ghGet(url, accessToken), MAX_PROFILE_BYTES, `GitHub API ${url}`);
|
|
38
|
+
if (!Array.isArray(list)) throw new Error(`GitHub API ${url} → corps inattendu (tableau JSON attendu)`);
|
|
39
|
+
return list.filter(isPlainObject);
|
|
29
40
|
}
|
|
30
41
|
/**
|
|
31
42
|
* Codes d'erreur PROPRES à GitHub, rendus dans leur équivalent RFC 6749 §5.2
|
|
@@ -86,7 +97,7 @@ function createGithubProvider(ctx) {
|
|
|
86
97
|
},
|
|
87
98
|
async fetchProfile(tokens) {
|
|
88
99
|
const accessToken = tokens.accessToken();
|
|
89
|
-
const user = await ghGet(`${API}/user`, accessToken);
|
|
100
|
+
const user = await readJsonObjectBounded(await ghGet(`${API}/user`, accessToken), MAX_PROFILE_BYTES, `GitHub API ${API}/user`);
|
|
90
101
|
const id = user.id;
|
|
91
102
|
if (typeof id !== "number" && typeof id !== "string") throw new Error("GitHub /user sans identifiant.");
|
|
92
103
|
const login = typeof user.login === "string" ? user.login : null;
|
|
@@ -94,7 +105,7 @@ function createGithubProvider(ctx) {
|
|
|
94
105
|
let email = typeof user.email === "string" ? user.email : null;
|
|
95
106
|
let emailVerified = email !== null;
|
|
96
107
|
if (email === null) {
|
|
97
|
-
const list = await
|
|
108
|
+
const list = await ghEmails(accessToken);
|
|
98
109
|
const primary = list.find((e) => e.primary === true) ?? list.at(0) ?? null;
|
|
99
110
|
if (primary && typeof primary.email === "string") {
|
|
100
111
|
email = primary.email;
|
|
@@ -112,5 +123,4 @@ function createGithubProvider(ctx) {
|
|
|
112
123
|
}
|
|
113
124
|
};
|
|
114
125
|
}
|
|
115
|
-
//#endregion
|
|
116
126
|
export { createGithubProvider };
|