@ti-engine/web-framework 1.13.1 → 1.14.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,24 @@
2
2
 
3
3
  This document will contain the list of changes made to the framework. The format is based on the [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) specification.
4
4
 
5
+ ## Version 1.14.1
6
+
7
+ Security hardening for the web-server CodeQL findings raised after the scanner was modernized in CA-90 (CA-91).
8
+
9
+ * fix(web-framework): rewrite the default unprotected static-asset route matchers from the ambiguous `(?:.+/)*` to segment-anchored `(?:[^/]+/)*` — the previous form backtracked exponentially and is evaluated against the raw request path in `isUnprotectedRoute()` before authentication, making it a pre-authentication denial-of-service vector (CodeQL js/redos); the matched language for realistic asset paths is unchanged, and the matchers are now the `RE_STATIC_UNPROTECTED` / `RE_WELL_KNOWN_UNPROTECTED` module constants
10
+ * fix(web-framework): replace the login-fragment section stripper's `open[\s\S]*?close` global-regex removal with a linear `indexOf`-based `stripMarkerSpans()` helper (also used for the per-provider stripper), eliminating the polynomial-time rescan on hostile input (CodeQL js/polynomial-redos)
11
+ * docs(web-framework): document that Helmet's built-in Content-Security-Policy is intentionally disabled because a per-request, nonce-based CSP is enforced by `cspHeaderHandler()` on the following middleware — added an explanatory comment and an inline CodeQL suppression (the alert is a false positive)
12
+ * build(release): bump package version from `1.14.0` to `1.14.1`
13
+
14
+ ## Version 1.14.0
15
+
16
+ `TI_WEB_*` environment-variable overrides for the web server configuration, enabling 12-factor container deployments without per-environment config files (CA-90).
17
+
18
+ * feat(web-framework): add `applyWebConfigEnvOverrides( config, env = process.env )` (`#web-config-env`) — a pure helper applying `TI_WEB_HOST`, `TI_WEB_PORT`, `TI_WEB_USE_TLS`, `TI_WEB_TLS_CERT_PATH`, `TI_WEB_TLS_KEY_PATH`, and `TI_WEB_COOKIE_SECRET` overrides onto the merged `TiWebServer` configuration, only when each variable is defined (fully backward compatible)
19
+ * fix(web-framework): skip an enabled OpenID Connect provider that has no client ID instead of crashing the instance during discovery — an OAuth-less deployment (e.g. a container started without OAuth credentials) now boots on its remaining methods, with a warning, and reports the dropped provider as unavailable so a sign-in attempt against it fails per-request rather than at startup
20
+ * feat(web-framework): the login page now renders an OpenID provider button only when that provider is an effective enabled auth method — the web server passes the post-drop enabled methods to the app manager, which strips the Google/Azure button (and the whole "or continue with" section when no provider is available) from `frame-login.html` at render time
21
+ * build(release): bump package version from `1.13.2` to `1.14.0`
22
+
5
23
  ## Version 1.13.0
6
24
 
7
25
  A reusable role-based screen gate and a per-screen title override (back the competence screen-access work and the evaluation/scores screen split).
package/README.md CHANGED
@@ -8,6 +8,16 @@ Flexible framework for the creation of microservices with node.js.
8
8
 
9
9
  This is a customizable web framework based on the **ti-engine** framework. Currently under development.
10
10
 
11
+ ## Environment variables
12
+
13
+ The web server configuration (host, port, TLS, cookies, etc.) is normally provided via the service configuration file merged in the `TiWebServer` constructor. The following environment variables can override individual values at runtime — useful for container/12-factor deployments where the same image is configured per environment:
14
+
15
+ * `TI_WEB_HOST` overrides the bind address (e.g. `0.0.0.0` in a container). Defaults to the value in the web server config.
16
+ * `TI_WEB_PORT` overrides the listen port.
17
+ * `TI_WEB_USE_TLS` (`true`/`false`) toggles in-app TLS. Set `false` when a reverse proxy / ingress terminates TLS.
18
+ * `TI_WEB_TLS_CERT_PATH` / `TI_WEB_TLS_KEY_PATH` override the TLS certificate/key paths (only used when TLS is enabled).
19
+ * `TI_WEB_COOKIE_SECRET` sets the session cookie signing secret. Set a stable, private value for durable sessions and multi-replica deployments (otherwise a random per-process value is used).
20
+
11
21
  ## Configure HTTPS for development
12
22
 
13
23
  Use the `mkcert` tool to create a certificate for development.
@@ -1,37 +1,37 @@
1
- <div x-id="['component-flyout']"
2
- x-data="tiComponentSidebarFlyout('{ti-config-key}')"
3
- class="ti-sidebar-flyout-container">
4
- <!--
5
- Trigger slot. The placeholder consumer can replace this with any custom button — typically a user avatar
6
- or app menu trigger. The trigger MUST set x-ref="flyoutButton" and call toggle() to integrate with the
7
- encapsulated flyout state. If the consumer omits inner content, this default icon button is used.
8
- -->
9
- <ti-slot>
10
- <button x-on:click.stop="toggle()"
11
- x-on:ti-close-all-flyout="close()"
12
- x-bind:aria-expanded="isOpen"
13
- x-bind:aria-controls="$id('component-flyout')"
14
- x-bind:aria-label="menuTitle"
15
- x-ref="flyoutButton"
16
- type="button" class="ti-sidebar-button">
17
- <span class="ti-icon" x-bind:class="icon" aria-hidden="true"></span>
18
- </button>
19
- </ti-slot>
20
- <div x-on:click.outside="close()"
21
- x-bind:id="$id('component-flyout')"
22
- x-show="isOpen"
23
- x-ref="flyoutPanel"
24
- role="menu" class="ti-sidebar-flyout">
25
- <template x-for="(buttonConfig, index) in buttonConfigs">
26
- <button x-on:click="close()"
27
- x-bind:hx-get="(!buttonConfig.action.method || buttonConfig.action.method === 'get') ? buttonConfig.action.href : null"
28
- x-bind:hx-post="(buttonConfig.action.method && buttonConfig.action.method === 'post') ? buttonConfig.action.href : null"
29
- x-bind:hx-target="buttonConfig.action.target"
30
- x-bind:hx-swap="buttonConfig.action.swap"
31
- hx-push-url="true"
32
- type="button" class="ti-sidebar-flyout-item">
33
- <span class="ti-icon" x-bind:class="buttonConfig.icon" aria-hidden="true"></span><span x-text="buttonConfig.title"></span>
34
- </button>
35
- </template>
36
- </div>
1
+ <div x-id="['component-flyout']"
2
+ x-data="tiComponentSidebarFlyout('{ti-config-key}')"
3
+ class="ti-sidebar-flyout-container">
4
+ <!--
5
+ Trigger slot. The placeholder consumer can replace this with any custom button — typically a user avatar
6
+ or app menu trigger. The trigger MUST set x-ref="flyoutButton" and call toggle() to integrate with the
7
+ encapsulated flyout state. If the consumer omits inner content, this default icon button is used.
8
+ -->
9
+ <ti-slot>
10
+ <button x-on:click.stop="toggle()"
11
+ x-on:ti-close-all-flyout="close()"
12
+ x-bind:aria-expanded="isOpen"
13
+ x-bind:aria-controls="$id('component-flyout')"
14
+ x-bind:aria-label="menuTitle"
15
+ x-ref="flyoutButton"
16
+ type="button" class="ti-sidebar-button">
17
+ <span class="ti-icon" x-bind:class="icon" aria-hidden="true"></span>
18
+ </button>
19
+ </ti-slot>
20
+ <div x-on:click.outside="close()"
21
+ x-bind:id="$id('component-flyout')"
22
+ x-show="isOpen"
23
+ x-ref="flyoutPanel"
24
+ role="menu" class="ti-sidebar-flyout">
25
+ <template x-for="(buttonConfig, index) in buttonConfigs">
26
+ <button x-on:click="close()"
27
+ x-bind:hx-get="(!buttonConfig.action.method || buttonConfig.action.method === 'get') ? buttonConfig.action.href : null"
28
+ x-bind:hx-post="(buttonConfig.action.method && buttonConfig.action.method === 'post') ? buttonConfig.action.href : null"
29
+ x-bind:hx-target="buttonConfig.action.target"
30
+ x-bind:hx-swap="buttonConfig.action.swap"
31
+ hx-push-url="true"
32
+ type="button" class="ti-sidebar-flyout-item">
33
+ <span class="ti-icon" x-bind:class="buttonConfig.icon" aria-hidden="true"></span><span x-text="buttonConfig.title"></span>
34
+ </button>
35
+ </template>
36
+ </div>
37
37
  </div>
@@ -46,6 +46,7 @@
46
46
  </button>
47
47
  </form>
48
48
 
49
+ <!--ti-auth-oauth-section-->
49
50
  <!-- Divider -->
50
51
  <div class="ti-login-divider">
51
52
  <div class="ti-login-divider-line"></div>
@@ -55,6 +56,7 @@
55
56
 
56
57
  <!-- Social auth -->
57
58
  <div class="ti-login-social">
59
+ <!--ti-auth-method:openid-google-->
58
60
  <a href="/login/openid-google" class="ti-btn full">
59
61
  <svg width="16" height="16" viewBox="0 0 24 24" fill="none" aria-hidden="true">
60
62
  <path d="M21.6 12.23c0-.68-.06-1.33-.17-1.96H12v3.71h5.39a4.6 4.6 0 0 1-2 3.02v2.51h3.23c1.9-1.74 2.98-4.31 2.98-7.28Z" fill="#4285F4"/>
@@ -67,13 +69,17 @@
67
69
  </svg>
68
70
  <span x-text-label="interface.default.login.sign-in-google">Sign in with Google</span>
69
71
  </a>
72
+ <!--/ti-auth-method-->
73
+ <!--ti-auth-method:openid-azure-->
70
74
  <a href="/login/openid-azure" class="ti-btn full">
71
75
  <svg width="16" height="16" viewBox="0 0 24 24" fill="none" aria-hidden="true">
72
76
  <path d="M11.5 2L4 8.5l4 1-4 6.5h8l-2-4 4-1-2.5 8H20L14 2h-2.5Z" fill="#0078D4"/>
73
77
  </svg>
74
78
  <span x-text-label="interface.default.login.sign-in-azure">Sign in with Azure</span>
75
79
  </a>
80
+ <!--/ti-auth-method-->
76
81
  </div>
82
+ <!--/ti-auth-oauth-section-->
77
83
  </div>
78
84
 
79
85
  <!-- TEMPORARY: Test user selector for augmentSession() experimentation. Remove once AD-driven identity is wired up. -->
@@ -20,6 +20,71 @@ const RE_CSRF_ATTR = /\{ti-csrf-placeholder}/g;
20
20
  const RE_HTMX_CONFIG = /\{ti-htmx-config-placeholder}/g;
21
21
  const RE_CSP_NONCE = /^[A-Za-z0-9+/=_-]{16,}$/;
22
22
  const TI_NESTED_FRAME_PLACEHOLDER = "ti-nested-frame-placeholder";
23
+ const OAUTH_METHODS = [ "openid-google", "openid-azure" ];
24
+ const RE_AUTH_MARKERS = /<!--\/?ti-auth-(?:oauth-section|method(?::[a-z-]+)?)-->/g;
25
+
26
+ /**
27
+ * Removes every `<openMarker>…<closeMarker>` span (inclusive) from `html` in a single linear pass. The markers are
28
+ * matched as fixed strings via `indexOf`, so — unlike an `openMarker[\s\S]*?closeMarker` regular expression under a
29
+ * global replace — this cannot exhibit super-linear backtracking on hostile input containing many opening markers
30
+ * (CodeQL js/polynomial-redos). Matches the lazy-regex semantics: each opening marker pairs with the *next* closing
31
+ * marker after it. An opening marker with no matching closing marker is left untouched (the caller strips any stray
32
+ * markers afterwards with {@link RE_AUTH_MARKERS}).
33
+ *
34
+ * @param {string} html
35
+ * @param {string} openMarker
36
+ * @param {string} closeMarker
37
+ * @returns {string}
38
+ */
39
+ function stripMarkerSpans( html, openMarker, closeMarker ) {
40
+ let result = "";
41
+ let cursor = 0;
42
+ for ( ; ; ) {
43
+ const open = html.indexOf( openMarker, cursor );
44
+ if ( open === -1 ) {
45
+ result += html.slice( cursor );
46
+ break;
47
+ }
48
+ const close = html.indexOf( closeMarker, open + openMarker.length );
49
+ if ( close === -1 ) {
50
+ result += html.slice( cursor );
51
+ break;
52
+ }
53
+ result += html.slice( cursor, open );
54
+ cursor = close + closeMarker.length;
55
+ }
56
+ return result;
57
+ }
58
+
59
+ /**
60
+ * Removes login-page OpenID provider markup for any OAuth method that is not currently enabled, and removes the
61
+ * entire "or continue with" section when no OAuth method is enabled at all. The login fragment delimits the
62
+ * relevant blocks with HTML-comment markers: `<!--ti-auth-method:METHOD-->…<!--/ti-auth-method-->` around each
63
+ * provider button, and `<!--ti-auth-oauth-section-->…<!--/ti-auth-oauth-section-->` around the divider + social
64
+ * block. Any remaining markers are stripped so clean HTML ships. Fragments without these markers (every non-login
65
+ * fragment) are returned unchanged.
66
+ *
67
+ * @param {string} html
68
+ * @param {string[]} [enabledMethods] The effective enabled authentication methods.
69
+ * @returns {string}
70
+ */
71
+ function applyAuthMethodVisibility( html, enabledMethods ) {
72
+ let result = String( html );
73
+ const enabled = Array.isArray( enabledMethods ) ? enabledMethods : [];
74
+
75
+ if ( !OAUTH_METHODS.some( ( method ) => enabled.includes( method ) ) ) {
76
+ // No OAuth providers available — drop the whole "or continue with" section, then any stray markers.
77
+ return stripMarkerSpans( result, "<!--ti-auth-oauth-section-->", "<!--/ti-auth-oauth-section-->" ).replace( RE_AUTH_MARKERS, "" );
78
+ }
79
+
80
+ // Drop the button block for each OAuth method that is not enabled, then strip the remaining markers.
81
+ OAUTH_METHODS.forEach( ( method ) => {
82
+ if ( !enabled.includes( method ) ) {
83
+ result = stripMarkerSpans( result, "<!--ti-auth-method:" + method + "-->", "<!--/ti-auth-method-->" );
84
+ }
85
+ } );
86
+ return result.replace( RE_AUTH_MARKERS, "" );
87
+ }
23
88
 
24
89
  /**
25
90
  * A generic web application manager that handles the rendering and behavior of web application views. It is designed to be extended by specific web application
@@ -39,6 +104,7 @@ class TiWebAppManager {
39
104
  #fragments = {};
40
105
  #staticFileCache = {};
41
106
  #staticFileCacheEnabled;
107
+ #enabledAuthMethods = [];
42
108
 
43
109
  /**
44
110
  * @constructor
@@ -173,6 +239,18 @@ class TiWebAppManager {
173
239
  this.#staticFileCache = {};
174
240
  }
175
241
 
242
+ /**
243
+ * Sets the effective enabled authentication methods used to gate login-page provider buttons. The web server
244
+ * calls this at startup, after the auth manager has dropped any enabled-but-unconfigured OpenID providers.
245
+ *
246
+ * @method
247
+ * @param {string[]} methods
248
+ * @public
249
+ */
250
+ setEnabledAuthMethods( methods ) {
251
+ this.#enabledAuthMethods = Array.isArray( methods ) ? [ ...methods ] : [];
252
+ }
253
+
176
254
  /**
177
255
  * Optional HTML transformation hook.
178
256
  * <br/>
@@ -212,6 +290,9 @@ class TiWebAppManager {
212
290
 
213
291
  transformedHtml = transformedHtml.replace( "{ti-title-placeholder}", options.title || "" );
214
292
 
293
+ // Gate login-page OpenID provider buttons to the effective enabled auth methods (no-op on other fragments).
294
+ transformedHtml = applyAuthMethodVisibility( transformedHtml, this.#enabledAuthMethods );
295
+
215
296
  resolve( transformedHtml );
216
297
  } );
217
298
  }
@@ -562,3 +643,4 @@ class TiWebAppManager {
562
643
  }
563
644
 
564
645
  module.exports = TiWebAppManager;
646
+ module.exports.applyAuthMethodVisibility = applyAuthMethodVisibility;
package/bin/web-server.js CHANGED
@@ -24,6 +24,7 @@ const authMethod = require( "#auth-manager" ).authMethod;
24
24
  const authorization = require( "#authorization" );
25
25
  const adminConfigHandlers = require( "#admin-config-handlers" );
26
26
  const configService = require( "#config-service" );
27
+ const applyWebConfigEnvOverrides = require( "#web-config-env" );
27
28
 
28
29
  /** @typedef {import("node:http").Server} NodeServer */
29
30
 
@@ -89,6 +90,25 @@ const configService = require( "#config-service" );
89
90
 
90
91
  const webServerConfig = require( "#web-server-config" );
91
92
 
93
+ /**
94
+ * Default unprotected static-asset route matchers. The path segments are matched with `(?:[^/]+\/)*` rather than
95
+ * `(?:.+\/)*`: the inner `[^/]+` cannot also consume the "/" delimiter, so the pattern is unambiguous and matches
96
+ * in linear time. The previous `.+` form was ambiguous and backtracked exponentially on hostile request paths such
97
+ * as `/static/a/a/…/a/x` (no trailing extension) — and these matchers run against the raw request path in
98
+ * {@link TiWebServer#isUnprotectedRoute} BEFORE authentication, so that was a pre-auth denial-of-service vector
99
+ * (CodeQL js/redos). The matched language for realistic asset paths is unchanged.
100
+ *
101
+ * @type {RegExp}
102
+ */
103
+ const RE_STATIC_UNPROTECTED = /^\/static\/(?:[^/]+\/)*[^/]+\.[^/]+$/i;
104
+
105
+ /**
106
+ * Default unprotected `/.well-known/` route matcher. See {@link RE_STATIC_UNPROTECTED} for the ReDoS rationale.
107
+ *
108
+ * @type {RegExp}
109
+ */
110
+ const RE_WELL_KNOWN_UNPROTECTED = /^\/\.well-known\/(?:[^/]+\/)*[^/]+\.[^/]+$/i;
111
+
92
112
  /**
93
113
  * A web server microservice based on the ti-engine.
94
114
  * <br/>
@@ -121,7 +141,7 @@ class TiWebServer extends ServiceConsumer {
121
141
  * @throws {TiException.E_GEN_JS_INTERNAL_ERROR} If the web application manager cannot be loaded.
122
142
  */
123
143
  constructor( serviceDomainName, serviceConfig ) {
124
- super( serviceDomainName, _.merge( {}, webServerConfig, ( _.isObjectLike( serviceConfig ) ) ? serviceConfig : {} ) );
144
+ super( serviceDomainName, applyWebConfigEnvOverrides( _.merge( {}, webServerConfig, ( _.isObjectLike( serviceConfig ) ) ? serviceConfig : {} ) ) );
125
145
 
126
146
  // Include the current host in the list of allowed hosts:
127
147
  this.#allowedHosts.push( this.serviceConfig.host );
@@ -255,6 +275,12 @@ class TiWebServer extends ServiceConsumer {
255
275
 
256
276
  // Set up security and session middlewares first:
257
277
  this.#webServer.use( webHandlers.nonceGenerationHandler() );
278
+ // Helmet's built-in Content-Security-Policy is intentionally disabled here because a per-request,
279
+ // nonce-based CSP is enforced on the very next line by webHandlers.cspHeaderHandler() (see
280
+ // components/web-handlers.js) — Helmet's static config cannot express per-response nonces. Every other
281
+ // Helmet header (HSTS, X-Content-Type-Options, X-Frame-Options, …) still applies. This is a deliberate
282
+ // architecture, not missing CSP; do not enable Helmet's static CSP here, as that would drop the nonce.
283
+ // codeql[js/insecure-helmet-configuration]
258
284
  this.#webServer.use( helmet( { contentSecurityPolicy: false } ) );
259
285
  this.#webServer.use( webHandlers.cspHeaderHandler() );
260
286
  this.#webServer.use( express.json( { limit: "1mb" } ) );
@@ -305,6 +331,11 @@ class TiWebServer extends ServiceConsumer {
305
331
 
306
332
  return this.#authManager.initialize();
307
333
  } ).then( () => {
334
+ // Hand the web application manager the effective enabled auth methods (after any unconfigured OpenID
335
+ // providers were dropped) so the login page only renders providers a user can actually complete.
336
+ if ( this.#webAppManager && typeof this.#webAppManager.setEnabledAuthMethods === "function" ) {
337
+ this.#webAppManager.setEnabledAuthMethods( this.#authManager.getEnabledMethods() );
338
+ }
308
339
  return this.#beginListening( this.#netServer, this.serviceConfig.port, this.serviceConfig.host );
309
340
  } ).then( ( server ) => {
310
341
  if ( server.listening === true ) {
@@ -537,8 +568,8 @@ class TiWebServer extends ServiceConsumer {
537
568
  this.#unprotectedRoutes.push( "/app/config" );
538
569
  this.#unprotectedRoutes.push( /^\/login\/[^/]+$/i );
539
570
  this.#unprotectedRoutes.push( "/logout" );
540
- this.#unprotectedRoutes.push( /^\/static\/(?:.+\/)*[^/]+\.[^/]+$/i );
541
- this.#unprotectedRoutes.push( /^\/\.well-known\/(?:.+\/)*[^/]+\.[^/]+$/i );
571
+ this.#unprotectedRoutes.push( RE_STATIC_UNPROTECTED );
572
+ this.#unprotectedRoutes.push( RE_WELL_KNOWN_UNPROTECTED );
542
573
  }
543
574
 
544
575
  /* Private interface */
@@ -601,4 +632,7 @@ class TiWebServer extends ServiceConsumer {
601
632
 
602
633
  }
603
634
 
604
- module.exports = TiWebServer;
635
+ module.exports = TiWebServer;
636
+ // Exported for unit testing of the ReDoS-hardened matchers; not part of the customization surface.
637
+ module.exports.RE_STATIC_UNPROTECTED = RE_STATIC_UNPROTECTED;
638
+ module.exports.RE_WELL_KNOWN_UNPROTECTED = RE_WELL_KNOWN_UNPROTECTED;
@@ -105,6 +105,11 @@ class AuthManager {
105
105
  * @public
106
106
  */
107
107
  initialize() {
108
+ // Drop any OpenID Connect provider that is enabled but not configured (missing a client ID) so the
109
+ // instance boots with the remaining methods instead of crashing during discovery — e.g. a container
110
+ // started without OAuth credentials falls back to whatever else is enabled rather than failing to start.
111
+ this.#dropUnconfiguredOpenIDProviders();
112
+
108
113
  let promises = [];
109
114
  if ( this.isAuthEnabled( authMethodEnum.OPENID_GOOGLE ) ) {
110
115
  promises.push( this.#initializeOpenIDClient( this.#authSettings.oauth2.google ).then( ( configuration ) => {
@@ -136,6 +141,19 @@ class AuthManager {
136
141
  return this.#authSettings.enabledMethods.includes( authMethod );
137
142
  }
138
143
 
144
+ /**
145
+ * Returns the list of currently enabled authentication methods, reflecting any OpenID providers dropped by
146
+ * {@link AuthManager#initialize} for being enabled but unconfigured. Callers (e.g. the login-page renderer)
147
+ * use this to present only the methods a user can actually complete.
148
+ *
149
+ * @method
150
+ * @returns {TiAuthMethod[]}
151
+ * @public
152
+ */
153
+ getEnabledMethods() {
154
+ return [ ...this.#authSettings.enabledMethods ];
155
+ }
156
+
139
157
  /**
140
158
  * Used to authenticate a user via the specified authentication method.
141
159
  *
@@ -210,6 +228,41 @@ class AuthManager {
210
228
 
211
229
  /* Private interface */
212
230
 
231
+ /**
232
+ * Removes any OpenID Connect provider that is enabled but not configured (missing a client ID) from the set
233
+ * of enabled authentication methods, logging a warning for each. This prevents a startup crash during OpenID
234
+ * discovery when an enabled provider has no credentials (e.g. a container started without OAuth env vars): the
235
+ * instance boots on its remaining methods, and `isAuthEnabled` then correctly reports the dropped provider as
236
+ * unavailable so a sign-in attempt against it is rejected per-request instead of taking down startup.
237
+ *
238
+ * @method
239
+ * @private
240
+ */
241
+ #dropUnconfiguredOpenIDProviders() {
242
+ const providers = [
243
+ { method: authMethodEnum.OPENID_GOOGLE, oauth2: this.#authSettings.oauth2.google, label: "Google" },
244
+ { method: authMethodEnum.OPENID_AZURE, oauth2: this.#authSettings.oauth2.azure, label: "Azure" }
245
+ ];
246
+ providers.forEach( ( provider ) => {
247
+ if ( this.isAuthEnabled( provider.method ) && !this.#isOpenIDConfigured( provider.oauth2 ) ) {
248
+ this.#authSettings.enabledMethods = this.#authSettings.enabledMethods.filter( ( method ) => method !== provider.method );
249
+ logger.log( `OpenID Connect (${ provider.label }) is enabled but not configured (missing client ID); skipping this provider.`, logger.logSeverity.WARNING );
250
+ }
251
+ } );
252
+ }
253
+
254
+ /**
255
+ * Checks whether an OpenID Connect provider has the minimum configuration required to initialize (a non-empty client ID).
256
+ *
257
+ * @method
258
+ * @param {SettingsOAuth2Client} [oauth2] The provider's OAuth2 settings.
259
+ * @returns {boolean}
260
+ * @private
261
+ */
262
+ #isOpenIDConfigured( oauth2 ) {
263
+ return !!( oauth2 && typeof oauth2.clientID === "string" && oauth2.clientID.trim() !== "" );
264
+ }
265
+
213
266
  /**
214
267
  * Used to initialize the OpenID Connect client for the specified OAuth2 authentication method.
215
268
  * <br/>
@@ -0,0 +1,54 @@
1
+ /*
2
+ * The ti-engine is an open source, free to use—both for personal and commercial projects—framework for the creation of microservice-based solutions using node.js.
3
+ * Copyright © 2021-2026 Boris Kostadinov <kostadinov.boris@gmail.com>
4
+ * This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
5
+ * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
6
+ * You should have received a copy of the GNU General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
7
+ */
8
+
9
+ "use strict";
10
+
11
+ const tools = require( "@ti-engine/core/tools" );
12
+
13
+ /**
14
+ * Applies TI_WEB_* environment-variable overrides onto an (already-merged) web server configuration object.
15
+ * Each override is applied ONLY when its environment variable is defined, so an absent variable leaves the
16
+ * configured/default value untouched (fully backward compatible). This gives ti-engine web servers 12-factor,
17
+ * container-friendly control over network binding, TLS, and the session cookie secret without editing config files.
18
+ *
19
+ * @method
20
+ * @param {Object} config The web server configuration to augment (mutated in place and returned).
21
+ * @param {Object} [env=process.env] The environment source (injectable for testing).
22
+ * @returns {Object} The same config object, with any present overrides applied.
23
+ * @public
24
+ */
25
+ function applyWebConfigEnvOverrides( config, env = process.env ) {
26
+ if ( !config || typeof config !== "object" ) {
27
+ return config;
28
+ }
29
+ if ( env.TI_WEB_HOST !== undefined ) {
30
+ config.host = env.TI_WEB_HOST;
31
+ }
32
+ if ( env.TI_WEB_PORT !== undefined ) {
33
+ const port = Number( env.TI_WEB_PORT );
34
+ if ( Number.isInteger( port ) ) {
35
+ config.port = port;
36
+ }
37
+ }
38
+ if ( env.TI_WEB_USE_TLS !== undefined ) {
39
+ config.useTLS = tools.toBool( env.TI_WEB_USE_TLS );
40
+ }
41
+ if ( env.TI_WEB_TLS_CERT_PATH !== undefined ) {
42
+ config.tlsCertPath = env.TI_WEB_TLS_CERT_PATH;
43
+ }
44
+ if ( env.TI_WEB_TLS_KEY_PATH !== undefined ) {
45
+ config.tlsKeyPath = env.TI_WEB_TLS_KEY_PATH;
46
+ }
47
+ if ( env.TI_WEB_COOKIE_SECRET !== undefined ) {
48
+ config.cookies = config.cookies || {};
49
+ config.cookies.secret = env.TI_WEB_COOKIE_SECRET;
50
+ }
51
+ return config;
52
+ }
53
+
54
+ module.exports = applyWebConfigEnvOverrides;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ti-engine/web-framework",
3
- "version": "1.13.1",
3
+ "version": "1.14.1",
4
4
  "description": "A web-framework based on the ti-engine. It provides a customizable ready-to-use web-server microservice and a set of tools for creating web applications. NOTICE: This is still a work in progress and the full architecture, design, and functionality are not available!",
5
5
  "author": "Boris Kostadinov <kostadinov.boris@gmail.com>",
6
6
  "license": "GPL-3.0-or-later",
@@ -20,12 +20,15 @@
20
20
  "#session-store": "./components/session-store.js",
21
21
  "#user": "./components/user.js",
22
22
  "#web-app-manager": "./bin/web-app-manager.js",
23
+ "#web-config-env": "./components/web-config-env.js",
23
24
  "#web-handlers": "./components/web-handlers.js",
24
25
  "#web-server": "./bin/web-server.js",
25
26
  "#web-server-config": "./bin/web-server.json"
26
27
  },
27
28
  "dependencies": {
28
29
  "@ti-engine/core": "*",
30
+ "@alpinejs/csp": "^3.15.12",
31
+ "htmx.org": "^2.0.10",
29
32
  "ajv": "^8.20.0",
30
33
  "cookie-parser": "^1.4.7",
31
34
  "express": "^5.2.1",
@@ -35,8 +38,6 @@
35
38
  "openid-client": "^6.8.4"
36
39
  },
37
40
  "devDependencies": {
38
- "@alpinejs/csp": "^3.15.12",
39
- "htmx.org": "^2.0.10"
40
41
  },
41
42
  "files": [
42
43
  "bin/",