@nodefony/framework 10.0.0-beta.2 → 10.0.0-beta.3
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.152.0 → _@oxc-project_runtime@0.153.0}/helpers/esm/decorate.js +1 -1
- package/dist/_virtual/{_@oxc-project_runtime@0.152.0 → _@oxc-project_runtime@0.153.0}/helpers/esm/decorateMetadata.js +1 -1
- package/dist/index.js +9 -5
- package/dist/nodefony/controller/AdminApiController.js +7 -4
- package/dist/nodefony/controller/ApiKeyController.js +3 -3
- package/dist/nodefony/controller/LoginPageController.js +124 -0
- package/dist/nodefony/controller/OAuth2Controller.js +131 -14
- package/dist/nodefony/controller/SessionAuthController.js +37 -9
- package/dist/nodefony/controller/TokenAuthController.js +11 -4
- package/dist/nodefony/controller/TotpController.js +5 -5
- package/dist/nodefony/controller/WebAuthnController.js +26 -9
- package/dist/nodefony/decorators/routerDecorators.js +114 -13
- package/dist/nodefony/service/AdminBroker.js +2 -2
- package/dist/nodefony/service/IdempotencyStore.js +2 -2
- package/dist/nodefony/service/router.js +51 -22
- package/dist/nodefony/src/Controller.js +49 -4
- package/dist/nodefony/src/FrameworkAdminApi.js +2 -1
- package/dist/nodefony/src/KernelAdminApi.js +76 -10
- package/dist/nodefony/src/Resolver.js +39 -2
- package/dist/nodefony/src/Route.js +52 -5
- package/dist/nodefony/src/SecurityApiController.js +24 -0
- package/dist/nodefony/src/docsReader.js +38 -21
- package/dist/nodefony/src/idempotency.js +13 -20
- package/dist/nodefony/src/loginPage.js +431 -0
- package/dist/nodefony/src/requireAuthentication.js +36 -0
- package/dist/types/index.d.ts +4 -3
- package/dist/types/nodefony/controller/ApiKeyController.d.ts +2 -2
- package/dist/types/nodefony/controller/LoginPageController.d.ts +32 -0
- package/dist/types/nodefony/controller/OAuth2Controller.d.ts +67 -8
- package/dist/types/nodefony/controller/SessionAuthController.d.ts +39 -8
- package/dist/types/nodefony/controller/TokenAuthController.d.ts +6 -4
- package/dist/types/nodefony/controller/TotpController.d.ts +2 -2
- package/dist/types/nodefony/controller/WebAuthnController.d.ts +12 -4
- package/dist/types/nodefony/decorators/routerDecorators.d.ts +88 -3
- package/dist/types/nodefony/interfaces/IRoute.d.ts +1 -2
- package/dist/types/nodefony/service/router.d.ts +13 -0
- package/dist/types/nodefony/src/Controller.d.ts +40 -1
- package/dist/types/nodefony/src/Resolver.d.ts +19 -0
- package/dist/types/nodefony/src/Route.d.ts +18 -1
- package/dist/types/nodefony/src/SecurityApiController.d.ts +17 -0
- package/dist/types/nodefony/src/docsReader.d.ts +10 -6
- package/dist/types/nodefony/src/idempotency.d.ts +12 -10
- package/dist/types/nodefony/src/loginPage.d.ts +144 -0
- package/dist/types/nodefony/src/requireAuthentication.d.ts +29 -0
- package/docs/admin.md +28 -23
- package/docs/controller.md +76 -74
- package/docs/decorateurs.md +69 -67
- package/docs/idempotence.md +30 -28
- package/docs/routing.md +52 -52
- package/docs/templates.md +11 -11
- package/package.json +11 -11
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
//#region \0@oxc-project+runtime@0.
|
|
1
|
+
//#region \0@oxc-project+runtime@0.153.0/helpers/esm/decorate.js
|
|
2
2
|
function __decorate(decorators, target, key, desc) {
|
|
3
3
|
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
4
|
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
//#region \0@oxc-project+runtime@0.
|
|
1
|
+
//#region \0@oxc-project+runtime@0.153.0/helpers/esm/decorateMetadata.js
|
|
2
2
|
function __decorateMetadata(k, v) {
|
|
3
3
|
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
4
4
|
}
|
package/dist/index.js
CHANGED
|
@@ -5,16 +5,17 @@ import { RedisIdempotencyStore } from "./nodefony/src/RedisIdempotencyStore.js";
|
|
|
5
5
|
import { scheduleIdempotencyGc } from "./nodefony/src/idempotencyGc.js";
|
|
6
6
|
import Route from "./nodefony/src/Route.js";
|
|
7
7
|
import Controller from "./nodefony/src/Controller.js";
|
|
8
|
-
import { All, Anonymous, Body, BypassFirewall, Cookie, Csp, CsrfExempt, CsrfProtect, CurrentUser, Delete, Domain, Get, Head, Header, Headers, HttpCode, Idempotent, IsGranted, Options, Param, Patch, Post, Put, Query, Redirect, Req, RequireScope, Res, Scope, Session, UploadedFile, UploadedFiles, UseSession, controller, controllers, route, routeExpectsBodyStream } from "./nodefony/decorators/routerDecorators.js";
|
|
8
|
+
import { All, Anonymous, Body, BypassFirewall, Cookie, Csp, CsrfExempt, CsrfProtect, CurrentUser, Delete, Domain, Get, Head, Header, Headers, HttpCode, Idempotent, IsGranted, Options, Param, Patch, Post, Put, Query, Redirect, Req, RequireAuthentication, RequireScope, Res, Scope, Session, UploadedFile, UploadedFiles, UseSession, buildContextParamArgs, controller, controllers, getParamArgsMeta, route, routeExpectsBodyStream } from "./nodefony/decorators/routerDecorators.js";
|
|
9
9
|
import Resolver from "./nodefony/src/Resolver.js";
|
|
10
|
-
import __decorateMetadata from "./_virtual/_@oxc-project_runtime@0.
|
|
11
|
-
import __decorate from "./_virtual/_@oxc-project_runtime@0.
|
|
10
|
+
import __decorateMetadata from "./_virtual/_@oxc-project_runtime@0.153.0/helpers/esm/decorateMetadata.js";
|
|
11
|
+
import __decorate from "./_virtual/_@oxc-project_runtime@0.153.0/helpers/esm/decorate.js";
|
|
12
12
|
import router_default from "./nodefony/service/router.js";
|
|
13
13
|
import ResourceController from "./nodefony/src/ResourceController.js";
|
|
14
14
|
import AdminApiController from "./nodefony/controller/AdminApiController.js";
|
|
15
15
|
import AdminBroker_default from "./nodefony/service/AdminBroker.js";
|
|
16
16
|
import IdempotencyStore_default from "./nodefony/service/IdempotencyStore.js";
|
|
17
17
|
import SessionAuthController, { mountSessionAuthRoutes } from "./nodefony/controller/SessionAuthController.js";
|
|
18
|
+
import LoginPageController, { mountLoginPage } from "./nodefony/controller/LoginPageController.js";
|
|
18
19
|
import TokenAuthController, { mountTokenAuthRoutes } from "./nodefony/controller/TokenAuthController.js";
|
|
19
20
|
import IssuerMetadataController, { mountIssuerMetadataRoutes } from "./nodefony/controller/IssuerMetadataController.js";
|
|
20
21
|
import ProtectedResourceMetadataController, { collectProtectedResources, mountProtectedResourceRoutes, protectedResourceRoutePaths } from "./nodefony/controller/ProtectedResourceMetadataController.js";
|
|
@@ -176,7 +177,10 @@ let Framework = class Framework extends Module {
|
|
|
176
177
|
}
|
|
177
178
|
broker.mountAll();
|
|
178
179
|
}
|
|
179
|
-
if (this.kernel?.container?.get("authFlow"))
|
|
180
|
+
if (this.kernel?.container?.get("authFlow")) {
|
|
181
|
+
mountSessionAuthRoutes(this);
|
|
182
|
+
mountLoginPage(this);
|
|
183
|
+
}
|
|
180
184
|
if (this.kernel?.container?.get("tokenService")) {
|
|
181
185
|
mountTokenAuthRoutes(this);
|
|
182
186
|
const issuer = this.kernel.container.get("tokenService")?.publishedIssuer();
|
|
@@ -209,4 +213,4 @@ const graphql = {
|
|
|
209
213
|
};
|
|
210
214
|
var framework_default = Framework;
|
|
211
215
|
//#endregion
|
|
212
|
-
export { AdminApiController, AdminBroker_default as AdminBroker, All, Anonymous, ApiKeyController, BenchController, Body, BypassFirewall, Controller, Cookie, Csp, CsrfExempt, CsrfProtect, CurrentUser, Delete, Domain, Eta, Get, Head, Header, Headers, HttpCode, Idempotent, IsGranted, IssuerMetadataController, IdempotencyStore_default as MemoryIdempotencyStore, OAuth2Controller, Options, Param, Patch, Post, ProtectedResourceMetadataController, Put, Query, Redirect, Req, RequireScope, Res, Resolver, ResourceController, Route, router_default as Router, Scope, Session, SessionAuthController, TokenAuthController, TotpController, UploadedFile, UploadedFiles, UseSession, WebAuthnController, buildPlaygroundSnapshot, collectProtectedResources, controller, controllers, createFrameworkAdminApi, createKernelAdminApi, createSyslogAdminApi, framework_default as default, defineFrameworkConfig, frameworkConfigJsonSchema, frameworkConfigSchema, getIdempotencyStoreFactory, graphql, listIdempotencyStores, mountApiKeyRoutes, mountBenchRoutes, mountIssuerMetadataRoutes, mountOAuth2Routes, mountProtectedResourceRoutes, mountSessionAuthRoutes, mountTokenAuthRoutes, mountTotpRoutes, mountWebAuthnRoutes, protectedResourceRoutePaths, registerIdempotencyStore, route, routeExpectsBodyStream };
|
|
216
|
+
export { AdminApiController, AdminBroker_default as AdminBroker, All, Anonymous, ApiKeyController, BenchController, Body, BypassFirewall, Controller, Cookie, Csp, CsrfExempt, CsrfProtect, CurrentUser, Delete, Domain, Eta, Get, Head, Header, Headers, HttpCode, Idempotent, IsGranted, IssuerMetadataController, LoginPageController, IdempotencyStore_default as MemoryIdempotencyStore, OAuth2Controller, Options, Param, Patch, Post, ProtectedResourceMetadataController, Put, Query, Redirect, Req, RequireAuthentication, RequireScope, Res, Resolver, ResourceController, Route, router_default as Router, Scope, Session, SessionAuthController, TokenAuthController, TotpController, UploadedFile, UploadedFiles, UseSession, WebAuthnController, buildContextParamArgs, buildPlaygroundSnapshot, collectProtectedResources, controller, controllers, createFrameworkAdminApi, createKernelAdminApi, createSyslogAdminApi, framework_default as default, defineFrameworkConfig, frameworkConfigJsonSchema, frameworkConfigSchema, getIdempotencyStoreFactory, getParamArgsMeta, graphql, listIdempotencyStores, mountApiKeyRoutes, mountBenchRoutes, mountIssuerMetadataRoutes, mountLoginPage, mountOAuth2Routes, mountProtectedResourceRoutes, mountSessionAuthRoutes, mountTokenAuthRoutes, mountTotpRoutes, mountWebAuthnRoutes, protectedResourceRoutePaths, registerIdempotencyStore, route, routeExpectsBodyStream };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import Controller from "../src/Controller.js";
|
|
2
2
|
import { computeFingerprint, evaluateIdempotency, resolveIdempotencyKey, resolveIdentity } from "../src/idempotency.js";
|
|
3
|
-
import { RequestContext, RpcError, executeAdminEndpoint } from "nodefony";
|
|
3
|
+
import { JsonRpcServerError, RequestContext, RpcError, executeAdminEndpoint } from "nodefony";
|
|
4
4
|
//#region nodefony/controller/AdminApiController.ts
|
|
5
5
|
/**
|
|
6
6
|
* Controller pont unique du data plane admin (Studio).
|
|
@@ -46,7 +46,7 @@ var AdminApiController = class AdminApiController extends Controller {
|
|
|
46
46
|
if (this.context?.type.startsWith("websocket")) {
|
|
47
47
|
if (status >= 400) {
|
|
48
48
|
const message = body?.error ?? "admin error";
|
|
49
|
-
throw new RpcError(message,
|
|
49
|
+
throw new RpcError(message, JsonRpcServerError.DEFAULT, {
|
|
50
50
|
status,
|
|
51
51
|
body
|
|
52
52
|
});
|
|
@@ -84,7 +84,10 @@ var AdminApiController = class AdminApiController extends Controller {
|
|
|
84
84
|
request: this.buildRequest(args),
|
|
85
85
|
requiredRole: adminRoute.role,
|
|
86
86
|
gate: (request) => this.idempotencyGate(adminRoute, request),
|
|
87
|
-
onServerError: (error) => this.log(error, "ERROR")
|
|
87
|
+
onServerError: (error) => this.log(error, "ERROR"),
|
|
88
|
+
assertCredentialTransport: () => {
|
|
89
|
+
this.get("credentialTransport")?.assert(this.context ?? {}, `admin ${name}`);
|
|
90
|
+
}
|
|
88
91
|
});
|
|
89
92
|
}
|
|
90
93
|
/**
|
|
@@ -104,7 +107,7 @@ var AdminApiController = class AdminApiController extends Controller {
|
|
|
104
107
|
const store = this.get("idempotencyStore");
|
|
105
108
|
const verdict = await evaluateIdempotency({
|
|
106
109
|
store,
|
|
107
|
-
identity: resolveIdentity(
|
|
110
|
+
identity: resolveIdentity(),
|
|
108
111
|
clientKey: request.idempotencyKey,
|
|
109
112
|
fingerprint: computeFingerprint([
|
|
110
113
|
adminRoute.name,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import Controller from "../src/Controller.js";
|
|
2
1
|
import router_default from "../service/router.js";
|
|
2
|
+
import SecurityApiController from "../src/SecurityApiController.js";
|
|
3
3
|
import { collectDeclaredApiScopes } from "../src/scopeCatalog.js";
|
|
4
4
|
//#region nodefony/controller/ApiKeyController.ts
|
|
5
5
|
let mounted = false;
|
|
@@ -24,7 +24,7 @@ let mounted = false;
|
|
|
24
24
|
* Erreurs mappées par DUCK-TYPING sur `code` (400/409/503) — framework ne peut pas
|
|
25
25
|
* importer les classes d'erreur de security.
|
|
26
26
|
*/
|
|
27
|
-
var ApiKeyController = class extends
|
|
27
|
+
var ApiKeyController = class extends SecurityApiController {
|
|
28
28
|
constructor(context) {
|
|
29
29
|
super("ApiKeyController", context);
|
|
30
30
|
}
|
|
@@ -89,7 +89,7 @@ var ApiKeyController = class extends Controller {
|
|
|
89
89
|
const flow = this.#flow();
|
|
90
90
|
if (!flow) return null;
|
|
91
91
|
const me = await flow.me(this.context);
|
|
92
|
-
return me && typeof me.
|
|
92
|
+
return me && typeof me.identifier === "string" ? me.identifier : null;
|
|
93
93
|
}
|
|
94
94
|
#renderApiKeyError(e) {
|
|
95
95
|
const code = e.code;
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { isDevelopment } from "../src/singletonGuard.js";
|
|
2
|
+
import Controller from "../src/Controller.js";
|
|
3
|
+
import { Csp } from "../decorators/routerDecorators.js";
|
|
4
|
+
import __decorateMetadata from "../../_virtual/_@oxc-project_runtime@0.153.0/helpers/esm/decorateMetadata.js";
|
|
5
|
+
import __decorate from "../../_virtual/_@oxc-project_runtime@0.153.0/helpers/esm/decorate.js";
|
|
6
|
+
import router_default from "../service/router.js";
|
|
7
|
+
import { LOGIN_PAGE_ASSET_FILES, LOGIN_PAGE_TEMPLATE, buildLoginPageView, keepSuccess } from "../src/loginPage.js";
|
|
8
|
+
import path from "node:path";
|
|
9
|
+
import { LOGIN_PAGE_ASSETS_BASE, LOGIN_PAGE_SKINS, loginPageSkinFile } from "nodefony";
|
|
10
|
+
import { createHash } from "node:crypto";
|
|
11
|
+
import { readFile } from "node:fs/promises";
|
|
12
|
+
import { fileURLToPath } from "node:url";
|
|
13
|
+
import { Eta } from "eta";
|
|
14
|
+
//#region nodefony/controller/LoginPageController.ts
|
|
15
|
+
const NO_STORE = { "cache-control": "no-store" };
|
|
16
|
+
let mounted = false;
|
|
17
|
+
let engine = null;
|
|
18
|
+
let defaultTemplate = null;
|
|
19
|
+
let appTemplate = null;
|
|
20
|
+
/** Dossier des fichiers de la page (`dist/login/` du paquet `nodefony`). */
|
|
21
|
+
function assetsDir() {
|
|
22
|
+
return path.dirname(fileURLToPath(import.meta.resolve("nodefony/login.js")));
|
|
23
|
+
}
|
|
24
|
+
function eta() {
|
|
25
|
+
return engine ??= new Eta({
|
|
26
|
+
autoEscape: true,
|
|
27
|
+
useWith: false
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
function templateFor(page) {
|
|
31
|
+
if (page.template === null) return Promise.resolve(defaultTemplate ??= eta().compile(LOGIN_PAGE_TEMPLATE));
|
|
32
|
+
if (appTemplate?.file !== page.template) {
|
|
33
|
+
const file = page.template;
|
|
34
|
+
appTemplate = {
|
|
35
|
+
file,
|
|
36
|
+
template: keepSuccess(() => readFile(file, "utf8").then((source) => eta().compile(source)))
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
return appTemplate.template();
|
|
40
|
+
}
|
|
41
|
+
const version = keepSuccess(async () => {
|
|
42
|
+
const dir = assetsDir();
|
|
43
|
+
const hash = createHash("sha256");
|
|
44
|
+
const skins = LOGIN_PAGE_SKINS.map(loginPageSkinFile).filter((file) => file !== null);
|
|
45
|
+
for (const file of [
|
|
46
|
+
LOGIN_PAGE_ASSET_FILES.script,
|
|
47
|
+
LOGIN_PAGE_ASSET_FILES.style,
|
|
48
|
+
...skins
|
|
49
|
+
]) hash.update(await readFile(path.join(dir, file)));
|
|
50
|
+
return hash.digest("hex").slice(0, 12);
|
|
51
|
+
});
|
|
52
|
+
/**
|
|
53
|
+
* Page de connexion par défaut (ADR-0015) : gabarit F03 rendu côté serveur,
|
|
54
|
+
* feuille `nodefony/login.css`, script `nodefony/login.js`. Aucun front, aucun
|
|
55
|
+
* build côté application.
|
|
56
|
+
*
|
|
57
|
+
* Joignable sans session ; jamais mise en cache (`no-store`) ni affichable
|
|
58
|
+
* dans un cadre (`frame-ancestors 'none'`).
|
|
59
|
+
*/
|
|
60
|
+
var LoginPageController = class extends Controller {
|
|
61
|
+
constructor(context) {
|
|
62
|
+
super("LoginPageController", context);
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Rend la page de connexion.
|
|
66
|
+
*
|
|
67
|
+
* @returns la page HTML, ou 404 quand security ne la décrit plus
|
|
68
|
+
*/
|
|
69
|
+
async page() {
|
|
70
|
+
const page = this.get("authFlow")?.describeLoginPage?.() ?? null;
|
|
71
|
+
if (page === null) return this.renderJson({ error: "Not Found" }, 404, NO_STORE);
|
|
72
|
+
const query = this.queryGet;
|
|
73
|
+
const view = buildLoginPageView(page, {
|
|
74
|
+
from: query?.from,
|
|
75
|
+
reason: query?.reason,
|
|
76
|
+
theme: query?.theme,
|
|
77
|
+
skin: query?.skin,
|
|
78
|
+
layout: query?.layout,
|
|
79
|
+
preview: isDevelopment(this.kernel?.environment),
|
|
80
|
+
nonce: this.context?.cspNonce ?? "",
|
|
81
|
+
projectName: this.kernel?.projectName ?? "Nodefony",
|
|
82
|
+
assetsVersion: await version()
|
|
83
|
+
});
|
|
84
|
+
const template = await templateFor(page);
|
|
85
|
+
return this.render(template.call(eta(), view), "utf8", 200, {
|
|
86
|
+
...NO_STORE,
|
|
87
|
+
"content-type": "text/html; charset=utf-8"
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
__decorate([
|
|
92
|
+
Csp({ "frame-ancestors": ["'none'"] }),
|
|
93
|
+
__decorateMetadata("design:type", Function),
|
|
94
|
+
__decorateMetadata("design:paramtypes", []),
|
|
95
|
+
__decorateMetadata("design:returntype", Promise)
|
|
96
|
+
], LoginPageController.prototype, "page", null);
|
|
97
|
+
/**
|
|
98
|
+
* Monte la page de connexion par défaut quand security la décrit : la route
|
|
99
|
+
* sur `loginPage.path`, et ses fichiers sous `LOGIN_PAGE_ASSETS_BASE`.
|
|
100
|
+
*
|
|
101
|
+
* La page est le chemin vers la session : elle échappe au pare-feu, comme les
|
|
102
|
+
* routes de connexion — sinon une application qui protège `^/` exigerait
|
|
103
|
+
* d'être connecté pour se connecter.
|
|
104
|
+
*
|
|
105
|
+
* @param frameworkModule - module framework, qui porte les contrôleurs
|
|
106
|
+
*/
|
|
107
|
+
function mountLoginPage(frameworkModule) {
|
|
108
|
+
if (mounted) return;
|
|
109
|
+
const page = frameworkModule.get("authFlow")?.describeLoginPage?.() ?? null;
|
|
110
|
+
if (page === null) return;
|
|
111
|
+
if (!page.password && !page.passkey && page.providers.length === 0) frameworkModule.log(`${page.path} : aucun moyen de connexion — loginPage.password à false, passkeys coupées, aucun fournisseur opérationnel`, "WARNING");
|
|
112
|
+
router_default.createRoute("security.login.page", {
|
|
113
|
+
path: page.path,
|
|
114
|
+
constructor: LoginPageController,
|
|
115
|
+
classMethod: "page",
|
|
116
|
+
requirements: { methods: ["GET", "HEAD"] },
|
|
117
|
+
bypassFirewall: true
|
|
118
|
+
});
|
|
119
|
+
frameworkModule.get("server-static")?.addMount(LOGIN_PAGE_ASSETS_BASE, assetsDir());
|
|
120
|
+
if (!Object.prototype.hasOwnProperty.call(LoginPageController.prototype, "module")) router_default.setController(LoginPageController, frameworkModule);
|
|
121
|
+
mounted = true;
|
|
122
|
+
}
|
|
123
|
+
//#endregion
|
|
124
|
+
export { LoginPageController as default, mountLoginPage };
|
|
@@ -1,20 +1,80 @@
|
|
|
1
|
-
import Controller from "../src/Controller.js";
|
|
2
1
|
import router_default from "../service/router.js";
|
|
2
|
+
import SecurityApiController from "../src/SecurityApiController.js";
|
|
3
|
+
import { OAUTH2_API_BASE, OAUTH2_FAILED_REASON, safeRedirectPath } from "nodefony";
|
|
3
4
|
//#region nodefony/controller/OAuth2Controller.ts
|
|
4
5
|
const STATE_KEY = "oauth2:state";
|
|
5
6
|
const VERIFIER_KEY = "oauth2:verifier";
|
|
6
7
|
const PROVIDER_KEY = "oauth2:provider";
|
|
8
|
+
const FROM_KEY = "oauth2:from";
|
|
9
|
+
const BACK_KEY = "oauth2:back";
|
|
7
10
|
let mounted = false;
|
|
8
11
|
/**
|
|
12
|
+
* Codes d'erreur qu'un serveur d'autorisation peut renvoyer au callback, et
|
|
13
|
+
* SEULS codes que l'écran d'arrivée reçoit : OAuth 2.0 (RFC 6749 §4.1.2.1) et
|
|
14
|
+
* OpenID Connect Core (§3.1.2.6). Une liste fermée — jamais la chaîne reçue
|
|
15
|
+
* telle quelle : le paramètre `error` vient de l'URL, donc de n'importe qui.
|
|
16
|
+
*/
|
|
17
|
+
const AUTHORIZATION_ERRORS = /* @__PURE__ */ new Set([
|
|
18
|
+
"invalid_request",
|
|
19
|
+
"unauthorized_client",
|
|
20
|
+
"access_denied",
|
|
21
|
+
"unsupported_response_type",
|
|
22
|
+
"invalid_scope",
|
|
23
|
+
"server_error",
|
|
24
|
+
"temporarily_unavailable",
|
|
25
|
+
"interaction_required",
|
|
26
|
+
"login_required",
|
|
27
|
+
"account_selection_required",
|
|
28
|
+
"consent_required"
|
|
29
|
+
]);
|
|
30
|
+
/**
|
|
31
|
+
* Thème que la page appelante a rendu (`?theme=`), à transmettre à l'écran du
|
|
32
|
+
* fournisseur. Liste FERMÉE : la chaîne vient de l'URL, toute autre valeur est
|
|
33
|
+
* ignorée sans un mot — rien d'arbitraire n'atteint la requête d'autorisation.
|
|
34
|
+
*
|
|
35
|
+
* @param value - paramètre `theme` de la requête, ou `null`
|
|
36
|
+
* @returns l'option à passer à `createAuthorization`, vide hors liste
|
|
37
|
+
*/
|
|
38
|
+
function providerThemeOption(value) {
|
|
39
|
+
return value === "light" || value === "dark" ? { theme: value } : {};
|
|
40
|
+
}
|
|
41
|
+
const PROBE_ORIGIN = "http://redirect.invalid";
|
|
42
|
+
/**
|
|
43
|
+
* Compose l'adresse de l'écran d'échec : la raison de l'échec, et la page où
|
|
44
|
+
* aller une fois connecté, pour que l'écran la garde à la tentative suivante.
|
|
45
|
+
*
|
|
46
|
+
* `reason` vaut le code rendu par le fournisseur s'il appartient à la liste
|
|
47
|
+
* normalisée (`access_denied` = l'utilisateur a choisi de revenir), sinon
|
|
48
|
+
* {@link OAUTH2_FAILED_REASON} — jamais la chaîne reçue, qui vient de l'URL.
|
|
49
|
+
* Une `reason` ou un `from` déjà présents sont REMPLACÉS (l'écran qui a lancé
|
|
50
|
+
* le flux peut porter ceux d'une tentative précédente) ; le reste de la
|
|
51
|
+
* requête et le fragment sont conservés, et une adresse relative reste relative.
|
|
52
|
+
*
|
|
53
|
+
* @param failure - l'écran d'échec : `back` revalidé, ou `failureRedirect`
|
|
54
|
+
* @param error - le paramètre `error` reçu au callback, ou `null`
|
|
55
|
+
* @param from - la page où aller une fois connecté (revalidée), ou `null`
|
|
56
|
+
* @returns l'adresse de redirection
|
|
57
|
+
*/
|
|
58
|
+
function oauth2FailureTarget(failure, error, from) {
|
|
59
|
+
const reason = error !== null && AUTHORIZATION_ERRORS.has(error) ? error : OAUTH2_FAILED_REASON;
|
|
60
|
+
const absolute = /^[a-z][a-z0-9+.-]*:/i.test(failure);
|
|
61
|
+
const url = new URL(failure, PROBE_ORIGIN);
|
|
62
|
+
url.searchParams.set("reason", reason);
|
|
63
|
+
if (from === null) url.searchParams.delete("from");
|
|
64
|
+
else url.searchParams.set("from", from);
|
|
65
|
+
return absolute ? url.href : `${url.pathname}${url.search}${url.hash}`;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
9
68
|
* Endpoints HTTP du **social login OAuth 2.0** (P6 J9) — adaptateurs MINCES
|
|
10
69
|
* au-dessus du service `oauth2` (`@nodefony/security`) :
|
|
11
70
|
*
|
|
12
|
-
* - `GET /nodefony/security/api/oauth2/{provider}/authorize
|
|
13
|
-
* pose `state`+`code_verifier` en session (anonyme),
|
|
14
|
-
* fournisseur.
|
|
71
|
+
* - `GET /nodefony/security/api/oauth2/{provider}/authorize?from=&back=` —
|
|
72
|
+
* démarre le flux : pose `state`+`code_verifier` en session (anonyme), avec
|
|
73
|
+
* les retours revalidés en chemins locaux, redirige (302) vers le fournisseur.
|
|
15
74
|
* - `GET /nodefony/security/api/oauth2/{provider}/callback` — valide le `state`
|
|
16
75
|
* (anti-CSRF), échange le `code`, provisionne le Shadow User et OUVRE la
|
|
17
|
-
* session BFF (302 vers `successRedirect`).
|
|
76
|
+
* session BFF (302 vers `from`, sinon `successRedirect`). Un échec mène à
|
|
77
|
+
* `back`, sinon à `failureRedirect`, avec `reason` et `from`.
|
|
18
78
|
*
|
|
19
79
|
* Montés UNIQUEMENT si le service `oauth2` existe (social login activé) — 404
|
|
20
80
|
* sinon, zéro surface.
|
|
@@ -25,7 +85,7 @@ let mounted = false;
|
|
|
25
85
|
* La session anonyme ne porte que `state`/`verifier` ; `establishSessionFor`
|
|
26
86
|
* régénère l'ID (anti-fixation) à la promotion.
|
|
27
87
|
*/
|
|
28
|
-
var OAuth2Controller = class extends
|
|
88
|
+
var OAuth2Controller = class extends SecurityApiController {
|
|
29
89
|
constructor(context) {
|
|
30
90
|
super("OAuth2Controller", context);
|
|
31
91
|
}
|
|
@@ -55,7 +115,7 @@ var OAuth2Controller = class extends Controller {
|
|
|
55
115
|
if (!svc.listProviders().includes(provider)) return this.renderJson({ error: "Unknown provider" }, 404);
|
|
56
116
|
let auth;
|
|
57
117
|
try {
|
|
58
|
-
auth = await svc.createAuthorization(provider);
|
|
118
|
+
auth = await svc.createAuthorization(provider, providerThemeOption(this.#queryString("theme")));
|
|
59
119
|
} catch (error) {
|
|
60
120
|
this.log(`oauth2 authorize "${provider}" : ${error.message}`, "ERROR");
|
|
61
121
|
return this.renderJson({ error: "OAuth provider unavailable" }, 503);
|
|
@@ -65,6 +125,8 @@ var OAuth2Controller = class extends Controller {
|
|
|
65
125
|
session.set(STATE_KEY, auth.state);
|
|
66
126
|
session.set(VERIFIER_KEY, auth.codeVerifier);
|
|
67
127
|
session.set(PROVIDER_KEY, provider);
|
|
128
|
+
session.set(FROM_KEY, this.#localPath("from"));
|
|
129
|
+
session.set(BACK_KEY, this.#localPath("back"));
|
|
68
130
|
await session.save();
|
|
69
131
|
this.redirect(auth.url, 302);
|
|
70
132
|
}
|
|
@@ -78,28 +140,73 @@ var OAuth2Controller = class extends Controller {
|
|
|
78
140
|
const expectedState = session?.get(STATE_KEY);
|
|
79
141
|
const storedVerifier = session?.get(VERIFIER_KEY);
|
|
80
142
|
const expectedProvider = session?.get(PROVIDER_KEY);
|
|
143
|
+
const from = safeRedirectPath(session?.get(FROM_KEY), "") || null;
|
|
144
|
+
const back = safeRedirectPath(session?.get(BACK_KEY), "") || null;
|
|
81
145
|
session?.set(STATE_KEY, null);
|
|
82
146
|
session?.set(VERIFIER_KEY, null);
|
|
83
147
|
session?.set(PROVIDER_KEY, null);
|
|
148
|
+
session?.set(FROM_KEY, null);
|
|
149
|
+
session?.set(BACK_KEY, null);
|
|
84
150
|
await session?.save();
|
|
151
|
+
const failedAt = back ?? failure;
|
|
85
152
|
const code = this.#queryString("code");
|
|
86
153
|
const returnedState = this.#queryString("state");
|
|
87
154
|
const returnedIss = this.#queryString("iss");
|
|
88
|
-
|
|
89
|
-
|
|
155
|
+
const stateIsValid = returnedState !== null && typeof expectedState === "string" && returnedState === expectedState && expectedProvider === provider;
|
|
156
|
+
if (code === null || !stateIsValid) {
|
|
157
|
+
const returnedError = code === null && stateIsValid ? this.#queryString("error") : null;
|
|
158
|
+
if (returnedError !== null) {
|
|
159
|
+
const logged = AUTHORIZATION_ERRORS.has(returnedError) ? returnedError : "code non normalisé";
|
|
160
|
+
this.log(`oauth2 callback "${provider}" : le fournisseur a répondu « ${logged} »`, "INFO");
|
|
161
|
+
}
|
|
162
|
+
this.redirect(oauth2FailureTarget(failedAt, returnedError, from), 302);
|
|
90
163
|
return;
|
|
91
164
|
}
|
|
92
165
|
try {
|
|
93
|
-
const { identifier } = await svc.exchangeAndProvision(provider, code, typeof storedVerifier === "string" ? storedVerifier : null, returnedIss);
|
|
166
|
+
const { identifier, logoutHint } = await svc.exchangeAndProvision(provider, code, typeof storedVerifier === "string" ? storedVerifier : null, returnedIss);
|
|
94
167
|
await flow.establishSessionFor(this.context, identifier, "oauth");
|
|
95
|
-
this.
|
|
168
|
+
const opened = this.context.session;
|
|
169
|
+
if (opened && logoutHint !== void 0 && svc.rememberLogout) svc.rememberLogout(opened, logoutHint);
|
|
170
|
+
this.redirect(from ?? success, 302);
|
|
96
171
|
return;
|
|
97
172
|
} catch (error) {
|
|
98
173
|
this.log(`oauth2 callback "${provider}" : ${error.message.slice(0, 200)}`, "WARNING");
|
|
99
|
-
this.redirect(
|
|
174
|
+
this.redirect(oauth2FailureTarget(failedAt, null, from), 302);
|
|
100
175
|
return;
|
|
101
176
|
}
|
|
102
177
|
}
|
|
178
|
+
/**
|
|
179
|
+
* Canal arrière : le fournisseur demande de fermer les sessions qu'il
|
|
180
|
+
* désigne (OpenID Connect Back-Channel Logout 1.0 §2.5-2.8).
|
|
181
|
+
*
|
|
182
|
+
* Appelé serveur à serveur — ni cookie, ni session, ni jeton CSRF : la
|
|
183
|
+
* SIGNATURE du `logout_token` est la seule authentification. Réponses de la
|
|
184
|
+
* norme (§2.8) : 200 si la déconnexion a eu lieu (ou n'avait plus lieu
|
|
185
|
+
* d'être), 400 sinon — jeton refusé (`invalid_request`) comme déconnexion
|
|
186
|
+
* impossible (la cause part au journal, jamais au fournisseur). Toujours
|
|
187
|
+
* `Cache-Control: no-store`.
|
|
188
|
+
*/
|
|
189
|
+
async backchannelLogout(provider) {
|
|
190
|
+
const noStore = { "cache-control": "no-store" };
|
|
191
|
+
const svc = this.#service();
|
|
192
|
+
if (!svc || !svc.listProviders().includes(provider)) return this.renderJson({ error: "Unknown provider" }, 404, noStore);
|
|
193
|
+
if (!svc.backchannelLogout) return this.renderJson({ error: "Not implemented" }, 501, noStore);
|
|
194
|
+
const token = this.queryPost?.logout_token;
|
|
195
|
+
if (typeof token !== "string" || token.length === 0) return this.renderJson({ error: "invalid_request" }, 400, noStore);
|
|
196
|
+
let outcome;
|
|
197
|
+
try {
|
|
198
|
+
({outcome} = await svc.backchannelLogout(provider, token));
|
|
199
|
+
} catch (error) {
|
|
200
|
+
this.log(`oauth2 back-channel logout "${provider}" : ${error.message.slice(0, 200)}`, "ERROR");
|
|
201
|
+
return this.renderJson({ error: "invalid_request" }, 400, noStore);
|
|
202
|
+
}
|
|
203
|
+
if (outcome === "unsupported") return this.renderJson({ error: "Not implemented" }, 501, noStore);
|
|
204
|
+
if (outcome === "refused") {
|
|
205
|
+
this.log(`oauth2 back-channel logout "${provider}" : jeton de déconnexion refusé`, "WARNING");
|
|
206
|
+
return this.renderJson({ error: "invalid_request" }, 400, noStore);
|
|
207
|
+
}
|
|
208
|
+
return this.renderResponse("", "utf8", 200, noStore);
|
|
209
|
+
}
|
|
103
210
|
#service() {
|
|
104
211
|
const svc = this.get("oauth2");
|
|
105
212
|
return svc?.isEnabled() ? svc : null;
|
|
@@ -107,6 +214,10 @@ var OAuth2Controller = class extends Controller {
|
|
|
107
214
|
#flow() {
|
|
108
215
|
return this.get("authFlow") ?? null;
|
|
109
216
|
}
|
|
217
|
+
/** Paramètre de query string revalidé en chemin local, ou `null`. */
|
|
218
|
+
#localPath(key) {
|
|
219
|
+
return safeRedirectPath(this.#queryString(key), "") || null;
|
|
220
|
+
}
|
|
110
221
|
/** Lit un paramètre de query string (GET), ou `null`. */
|
|
111
222
|
#queryString(key) {
|
|
112
223
|
const v = this.queryGet?.[key];
|
|
@@ -119,7 +230,7 @@ var OAuth2Controller = class extends Controller {
|
|
|
119
230
|
*/
|
|
120
231
|
function mountOAuth2Routes(frameworkModule) {
|
|
121
232
|
if (mounted) return;
|
|
122
|
-
const base =
|
|
233
|
+
const base = OAUTH2_API_BASE;
|
|
123
234
|
const routes = [
|
|
124
235
|
[
|
|
125
236
|
"security.oauth2.providers",
|
|
@@ -138,6 +249,12 @@ function mountOAuth2Routes(frameworkModule) {
|
|
|
138
249
|
`${base}/{provider}/callback`,
|
|
139
250
|
"GET",
|
|
140
251
|
"callback"
|
|
252
|
+
],
|
|
253
|
+
[
|
|
254
|
+
"security.oauth2.backchannelLogout",
|
|
255
|
+
`${base}/{provider}/backchannel-logout`,
|
|
256
|
+
"POST",
|
|
257
|
+
"backchannelLogout"
|
|
141
258
|
]
|
|
142
259
|
];
|
|
143
260
|
for (const [name, path, method, classMethod] of routes) router_default.createRoute(name, {
|
|
@@ -151,4 +268,4 @@ function mountOAuth2Routes(frameworkModule) {
|
|
|
151
268
|
mounted = true;
|
|
152
269
|
}
|
|
153
270
|
//#endregion
|
|
154
|
-
export { OAuth2Controller as default, mountOAuth2Routes };
|
|
271
|
+
export { OAuth2Controller as default, mountOAuth2Routes, oauth2FailureTarget, providerThemeOption };
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import Controller from "../src/Controller.js";
|
|
2
1
|
import router_default from "../service/router.js";
|
|
2
|
+
import SecurityApiController from "../src/SecurityApiController.js";
|
|
3
|
+
import { AUTH_API_BASE } from "nodefony";
|
|
3
4
|
//#region nodefony/controller/SessionAuthController.ts
|
|
4
5
|
let mounted = false;
|
|
5
6
|
/**
|
|
@@ -8,7 +9,8 @@ let mounted = false;
|
|
|
8
9
|
*
|
|
9
10
|
* - `POST /nodefony/security/api/auth/login` — body JSON `{username, password}`
|
|
10
11
|
* - `POST /nodefony/security/api/auth/logout` — idempotent
|
|
11
|
-
* - `GET /nodefony/security/api/auth/me` — identité de la session courante
|
|
12
|
+
* - `GET /nodefony/security/api/auth/me` — identité de la session courante :
|
|
13
|
+
* TOUJOURS `200`, `{ user }` ou `{ user: null }` sans session
|
|
12
14
|
*
|
|
13
15
|
* Montés par le module framework UNIQUEMENT si le service `authFlow` existe
|
|
14
16
|
* (module security chargé) — sans lui, les routes n'existent pas (404, zéro
|
|
@@ -19,14 +21,20 @@ let mounted = false;
|
|
|
19
21
|
* `Retry-After` (RFC 6585) posé par le throttler NIST ; tout le reste remonte
|
|
20
22
|
* au pipeline d'erreurs standard (500, détail loggé jamais fuité).
|
|
21
23
|
*
|
|
22
|
-
*
|
|
24
|
+
* `me` est une SONDE d'état, pas une ressource protégée : « pas de session »
|
|
25
|
+
* est un état normal du navigateur, pas une faute — d'où `200` + `{ user:
|
|
26
|
+
* null }`, comme les bibliothèques qui livrent aussi le client de connexion
|
|
27
|
+
* (Auth.js, better-auth). Le `401` y écrivait une erreur réseau dans la console
|
|
28
|
+
* de chaque visiteur anonyme, sans rien protéger : la réponse ne révèle rien.
|
|
29
|
+
*
|
|
30
|
+
* @remarks Dérogation RFC 9110 §15.5.2 DÉLIBÉRÉE : les 401 de `login` ne
|
|
23
31
|
* portent PAS de `WWW-Authenticate` — aucun scheme HTTP ne décrit un cookie de
|
|
24
32
|
* session, et un challenge `Basic` mensonger déclencherait le popup natif du
|
|
25
33
|
* navigateur (l'anti-pattern que le BFF élimine). Les zones du firewall, elles,
|
|
26
34
|
* restent strictement conformes (challenge du premier authenticator qui en
|
|
27
35
|
* déclare un).
|
|
28
36
|
*/
|
|
29
|
-
var SessionAuthController = class extends
|
|
37
|
+
var SessionAuthController = class extends SecurityApiController {
|
|
30
38
|
constructor(context) {
|
|
31
39
|
super("SessionAuthController", context);
|
|
32
40
|
}
|
|
@@ -62,19 +70,35 @@ var SessionAuthController = class extends Controller {
|
|
|
62
70
|
return this.#renderAuthError(e);
|
|
63
71
|
}
|
|
64
72
|
}
|
|
65
|
-
/**
|
|
73
|
+
/**
|
|
74
|
+
* Détruit la session (storage + cookie). Toujours 200 (idempotent). Quand la
|
|
75
|
+
* session venait d'un fournisseur qui publie un point de déconnexion, la
|
|
76
|
+
* réponse porte `logoutUrl` : le client y envoie le navigateur pour fermer
|
|
77
|
+
* aussi la session du fournisseur.
|
|
78
|
+
*/
|
|
66
79
|
async logout() {
|
|
67
80
|
const flow = this.#flow();
|
|
68
81
|
if (!flow) return this.renderJson({ error: "Authentication unavailable" }, 503);
|
|
69
|
-
|
|
82
|
+
const context = this.context;
|
|
83
|
+
if (flow.logoutWithProvider) {
|
|
84
|
+
const { logoutUrl } = await flow.logoutWithProvider(context);
|
|
85
|
+
return this.renderJson(logoutUrl ? {
|
|
86
|
+
ok: true,
|
|
87
|
+
logoutUrl
|
|
88
|
+
} : { ok: true });
|
|
89
|
+
}
|
|
90
|
+
await flow.logout(context);
|
|
70
91
|
return this.renderJson({ ok: true });
|
|
71
92
|
}
|
|
72
|
-
/**
|
|
93
|
+
/**
|
|
94
|
+
* Identité de la session courante, re-résolue (rôles frais). Toujours `200` :
|
|
95
|
+
* `{ user: null }` quand il n'y a pas de session (ou qu'elle ne mène plus à
|
|
96
|
+
* un compte actif) — l'absence de session est un état, pas une erreur.
|
|
97
|
+
*/
|
|
73
98
|
async me() {
|
|
74
99
|
const flow = this.#flow();
|
|
75
100
|
if (!flow) return this.renderJson({ error: "Authentication unavailable" }, 503);
|
|
76
101
|
const user = await flow.me(this.context);
|
|
77
|
-
if (user === null) return this.renderJson({ error: "Unauthorized" }, 401);
|
|
78
102
|
return this.renderJson({ user });
|
|
79
103
|
}
|
|
80
104
|
#flow() {
|
|
@@ -87,6 +111,10 @@ var SessionAuthController = class extends Controller {
|
|
|
87
111
|
return this.renderJson({ error: "Too many attempts" }, 429, { "retry-after": String(typeof retry === "number" ? retry : 1) });
|
|
88
112
|
}
|
|
89
113
|
if (code === 401) return this.renderJson({ error: "Invalid credentials" }, 401);
|
|
114
|
+
if (code === 403) {
|
|
115
|
+
const message = e instanceof Error ? e.message : "Forbidden";
|
|
116
|
+
return this.renderJson({ error: message }, 403);
|
|
117
|
+
}
|
|
90
118
|
throw e;
|
|
91
119
|
}
|
|
92
120
|
};
|
|
@@ -100,7 +128,7 @@ var SessionAuthController = class extends Controller {
|
|
|
100
128
|
*/
|
|
101
129
|
function mountSessionAuthRoutes(frameworkModule) {
|
|
102
130
|
if (mounted) return;
|
|
103
|
-
const base =
|
|
131
|
+
const base = AUTH_API_BASE;
|
|
104
132
|
const routes = [
|
|
105
133
|
[
|
|
106
134
|
"security.auth.login",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import Controller from "../src/Controller.js";
|
|
2
1
|
import router_default from "../service/router.js";
|
|
2
|
+
import SecurityApiController from "../src/SecurityApiController.js";
|
|
3
3
|
//#region nodefony/controller/TokenAuthController.ts
|
|
4
4
|
let mounted = false;
|
|
5
5
|
/** Normalise le paramètre `scope` (string OAuth « a b c » ou tableau) → string[]. */
|
|
@@ -24,11 +24,11 @@ function parseScope(scope) {
|
|
|
24
24
|
* framework ne peut pas importer les classes d'erreur de security ; un 429
|
|
25
25
|
* reporte le `Retry-After` (RFC 6585) du throttler NIST.
|
|
26
26
|
*
|
|
27
|
-
* @remarks Dérogation RFC
|
|
27
|
+
* @remarks Dérogation RFC 9110 §15.5.2 (comme `SessionAuthController`) : ces 401 ne
|
|
28
28
|
* portent pas de `WWW-Authenticate` — l'endpoint d'émission n'est pas une
|
|
29
29
|
* ressource protégée par Bearer, il DÉLIVRE le Bearer.
|
|
30
30
|
*/
|
|
31
|
-
var TokenAuthController = class extends
|
|
31
|
+
var TokenAuthController = class extends SecurityApiController {
|
|
32
32
|
constructor(context) {
|
|
33
33
|
super("TokenAuthController", context);
|
|
34
34
|
}
|
|
@@ -38,7 +38,7 @@ var TokenAuthController = class extends Controller {
|
|
|
38
38
|
if (!svc || !svc.isEnabled()) return this.renderJson({ error: "Token issuance unavailable" }, 503);
|
|
39
39
|
const body = this.queryPost ?? {};
|
|
40
40
|
try {
|
|
41
|
-
const tokens = await svc.issueForCredentials(body.username, body.password, parseScope(body.scope), body.resource);
|
|
41
|
+
const tokens = await svc.issueForCredentials(body.username, body.password, parseScope(body.scope), body.resource, this.context);
|
|
42
42
|
return await this.renderJson(tokens);
|
|
43
43
|
} catch (e) {
|
|
44
44
|
return this.#renderAuthError(e);
|
|
@@ -73,6 +73,13 @@ var TokenAuthController = class extends Controller {
|
|
|
73
73
|
return this.renderJson({ error: "Too many attempts" }, 429, { "retry-after": String(typeof retry === "number" ? retry : 1) });
|
|
74
74
|
}
|
|
75
75
|
if (code === 401) return this.renderJson({ error: "invalid_grant" }, 401);
|
|
76
|
+
if (code === 403) {
|
|
77
|
+
const description = e instanceof Error ? e.message : "Forbidden";
|
|
78
|
+
return this.renderJson({
|
|
79
|
+
error: "invalid_request",
|
|
80
|
+
error_description: description
|
|
81
|
+
}, 403);
|
|
82
|
+
}
|
|
76
83
|
throw e;
|
|
77
84
|
}
|
|
78
85
|
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import Controller from "../src/Controller.js";
|
|
2
1
|
import router_default from "../service/router.js";
|
|
2
|
+
import SecurityApiController from "../src/SecurityApiController.js";
|
|
3
3
|
//#region nodefony/controller/TotpController.ts
|
|
4
4
|
let mounted = false;
|
|
5
5
|
/**
|
|
@@ -20,7 +20,7 @@ let mounted = false;
|
|
|
20
20
|
* `totp` existe (security chargé) — sans security, 404 et zéro surface ; 2FA
|
|
21
21
|
* désactivé, les routes répondent 503.
|
|
22
22
|
*/
|
|
23
|
-
var TotpController = class extends
|
|
23
|
+
var TotpController = class extends SecurityApiController {
|
|
24
24
|
constructor(context) {
|
|
25
25
|
super("TotpController", context);
|
|
26
26
|
}
|
|
@@ -43,7 +43,7 @@ var TotpController = class extends Controller {
|
|
|
43
43
|
if (typeof code !== "string" || code.length === 0) return this.renderJson({ error: "Invalid code" }, 400);
|
|
44
44
|
let activation;
|
|
45
45
|
try {
|
|
46
|
-
activation = await svc.confirmEnrollment(subject, code);
|
|
46
|
+
activation = await svc.confirmEnrollment(subject, code, this.context);
|
|
47
47
|
} catch {
|
|
48
48
|
return this.renderJson({ error: "Invalid or expired code" }, 400);
|
|
49
49
|
}
|
|
@@ -55,7 +55,7 @@ var TotpController = class extends Controller {
|
|
|
55
55
|
if (!svc) return this.renderJson({ error: "2FA unavailable" }, 503);
|
|
56
56
|
const subject = await this.#currentSubject();
|
|
57
57
|
if (subject === null) return this.renderJson({ error: "Unauthorized" }, 401);
|
|
58
|
-
await svc.disable(subject);
|
|
58
|
+
await svc.disable(subject, this.context);
|
|
59
59
|
return this.renderJson({ ok: true });
|
|
60
60
|
}
|
|
61
61
|
/** État 2FA du porteur courant (alimente la console « ma sécurité »). */
|
|
@@ -78,7 +78,7 @@ var TotpController = class extends Controller {
|
|
|
78
78
|
const flow = this.#flow();
|
|
79
79
|
if (!flow) return null;
|
|
80
80
|
const me = await flow.me(this.context);
|
|
81
|
-
return me && typeof me.
|
|
81
|
+
return me && typeof me.identifier === "string" ? me.identifier : null;
|
|
82
82
|
}
|
|
83
83
|
};
|
|
84
84
|
/**
|