@oslokommune/auth-bff 2.0.2 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/README.md +63 -3
  2. package/dist/package.json +1 -1
  3. package/dist/src/OpenIdConfigManager.d.ts +1 -1
  4. package/dist/src/OpenIdConfigManager.d.ts.map +1 -1
  5. package/dist/src/OpenIdConfigManager.js +1 -1
  6. package/dist/src/config/config.d.ts +147 -0
  7. package/dist/src/config/config.d.ts.map +1 -0
  8. package/dist/src/config/config.js +54 -0
  9. package/dist/src/config/variable-loaders.d.ts +3 -0
  10. package/dist/src/config/variable-loaders.d.ts.map +1 -0
  11. package/dist/src/config/variable-loaders.js +20 -0
  12. package/dist/src/config.d.ts +17 -0
  13. package/dist/src/config.d.ts.map +1 -1
  14. package/dist/src/config.js +32 -15
  15. package/dist/src/middleware/OidcMiddleware.d.ts +1 -1
  16. package/dist/src/middleware/OidcMiddleware.d.ts.map +1 -1
  17. package/dist/src/middleware/inject-config.d.ts +4 -0
  18. package/dist/src/middleware/inject-config.d.ts.map +1 -0
  19. package/dist/src/middleware/inject-config.js +10 -0
  20. package/dist/src/middleware/proxy-routes.d.ts +1 -1
  21. package/dist/src/middleware/proxy-routes.d.ts.map +1 -1
  22. package/dist/src/middleware/proxy-routes.js +17 -0
  23. package/dist/src/middleware/security-headers.d.ts +1 -1
  24. package/dist/src/middleware/security-headers.d.ts.map +1 -1
  25. package/dist/src/middleware/sessions/sessions.d.ts +1 -1
  26. package/dist/src/middleware/sessions/sessions.d.ts.map +1 -1
  27. package/dist/src/middleware/static-routes.d.ts +1 -1
  28. package/dist/src/middleware/static-routes.d.ts.map +1 -1
  29. package/dist/src/middleware/static-routes.js +3 -7
  30. package/dist/src/server.js +1 -1
  31. package/dist/src/vite-plugin.d.ts.map +1 -1
  32. package/dist/src/vite-plugin.js +3 -1
  33. package/package.json +1 -1
package/README.md CHANGED
@@ -81,7 +81,7 @@ WORKDIR /application
81
81
  EXPOSE 8080
82
82
  COPY --from=build /home/app/dist /application/dist
83
83
  ENV NODE_ENV=production
84
- RUN npm install -g @oslokommune/auth-bff@2.0.2
84
+ RUN npm install -g @oslokommune/auth-bff@2.1.0
85
85
  COPY bff.config.json /application/
86
86
  CMD ["auth-bff"]
87
87
  ```
@@ -140,6 +140,8 @@ Standalone:
140
140
  auth-bff --configFile /path/to/bff.config.json
141
141
  ```
142
142
 
143
+ ### Loading values from environment or AWS Parameter Store
144
+
143
145
  The config file supports two special forms for loading values from other sources. Primarily meant for loading secrets:
144
146
 
145
147
  Environment values:
@@ -162,7 +164,30 @@ This loads from the configured AWS environment. For this to work on your local m
162
164
  variable must be set, and you must be signed in to that profile
163
165
 
164
166
  > [!NOTE]
165
- >️ See [`config.ts`](src/config.ts) for a description of all config parameters
167
+ >️ See [`config.ts`](src/config/config.ts) for a description of all config parameters
168
+
169
+ ### Mixing public and protected routes
170
+
171
+ If the application has some routes that should be reachable without an authenticated session (e.g. a
172
+ public catalog or a download endpoint polled by CI), list them under `publicProxyTargets`:
173
+
174
+ ```json
175
+ {
176
+ "proxyTargets": {
177
+ "/api": "http://localhost:8080/api"
178
+ },
179
+ "publicProxyTargets": {
180
+ "/api/public": "http://localhost:8080/api/public",
181
+ "/export.zip": "http://localhost:8080/export.zip"
182
+ }
183
+ }
184
+ ```
185
+
186
+ Public targets are proxied through anonymously — no session lookup, no Authorization header. The
187
+ session cookie is stripped on the way through.
188
+
189
+ Public targets are registered before protected ones, so overlapping paths resolve to the public
190
+ mapping (e.g. `publicProxyTargets["/api/public"]` wins over `proxyTargets["/api"]`).
166
191
 
167
192
  ## Using with ID-porten (via `okdata`):
168
193
 
@@ -394,4 +419,39 @@ To use a nonce in your app, use `__CSP_NONCE__` in your html. It will be replace
394
419
  <script nonce="__CSP_NONCE__">
395
420
  ...
396
421
  </script>
397
- ```
422
+ ```
423
+
424
+ ## Inject config
425
+
426
+ If your application requires configuration that needs to be determined at runtime, you can use `injectConfig` to inject
427
+ these values into a served html file:
428
+
429
+ ```json
430
+ {
431
+ "injectConfig": {
432
+ "enableSomeFeature": true,
433
+ "publicToken": "{env:MY_PUBLIC_TOKEN}"
434
+ }
435
+ }
436
+ ```
437
+
438
+ Then add something this to your `index.html`:
439
+ ```html
440
+ <script nonce="__CSP_NONCE__">
441
+ const injectedConfig = __INJECTED_CONFIG__
442
+ </script>
443
+ ```
444
+
445
+ When rendered, `__INJECTED_CONFIG__` will be replaced with the values from `injectConfig`:
446
+
447
+ ```html
448
+ <script nonce="abcdef123456">
449
+ const injectedConfig = {
450
+ "enableSomeFeature": true,
451
+ "publicToken": "pub123abc"
452
+ }
453
+ </script>
454
+ ```
455
+
456
+ > [!WARNING]
457
+ > Do not put any secrets or other sensitive values in the injected config, they will be publicly visible.
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oslokommune/auth-bff",
3
- "version": "2.0.2",
3
+ "version": "2.2.0",
4
4
  "repository": "https://github.com/oslokommune/auth-bff.git",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -1,5 +1,5 @@
1
1
  import * as client from 'openid-client';
2
- import { BffConfig } from "./config.js";
2
+ import { BffConfig } from "./config/config.js";
3
3
  export declare class OpenIdConfigManager {
4
4
  #private;
5
5
  constructor(config: BffConfig, serverMetadata?: client.ServerMetadata);
@@ -1 +1 @@
1
- {"version":3,"file":"OpenIdConfigManager.d.ts","sourceRoot":"","sources":["../../src/OpenIdConfigManager.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,MAAM,MAAM,eAAe,CAAA;AACvC,OAAO,EAAC,SAAS,EAAkB,MAAM,aAAa,CAAC;AASvD,qBAAa,mBAAmB;;gBAMlB,MAAM,EAAE,SAAS,EAAE,cAAc,CAAC,EAAE,MAAM,CAAC,cAAc;IAK/D,IAAI;IA4BJ,kBAAkB;IAuCxB,IAAI,YAAY,yBAEf;CAEF"}
1
+ {"version":3,"file":"OpenIdConfigManager.d.ts","sourceRoot":"","sources":["../../src/OpenIdConfigManager.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,MAAM,MAAM,eAAe,CAAA;AACvC,OAAO,EAAC,SAAS,EAAC,MAAM,oBAAoB,CAAC;AAU7C,qBAAa,mBAAmB;;gBAMlB,MAAM,EAAE,SAAS,EAAE,cAAc,CAAC,EAAE,MAAM,CAAC,cAAc;IAK/D,IAAI;IA4BJ,kBAAkB;IAuCxB,IAAI,YAAY,yBAEf;CAEF"}
@@ -14,7 +14,7 @@ var _OpenIdConfigManager_instances, _OpenIdConfigManager_bffConfig, _OpenIdConfi
14
14
  import forge from 'node-forge';
15
15
  import * as jose from 'jose';
16
16
  import * as client from 'openid-client';
17
- import { getSsmParameter } from "./config.js";
17
+ import { getSsmParameter } from "./config/variable-loaders.js";
18
18
  export class OpenIdConfigManager {
19
19
  constructor(config, serverMetadata) {
20
20
  _OpenIdConfigManager_instances.add(this);
@@ -0,0 +1,147 @@
1
+ import { HelmetOptions } from "helmet";
2
+ import session from "express-session";
3
+ export type BffConfig = {
4
+ /**
5
+ * The port at which the app will be served. Only used in standalone mode, to change the port used during development,
6
+ * set it in your vite config instead.
7
+ */
8
+ port: number;
9
+ /**
10
+ * The base root path. Change if app is served from a non-root path.
11
+ *
12
+ * Default: `/`
13
+ */
14
+ basePath?: string;
15
+ /**
16
+ * The root path of the static resources to be served
17
+ *
18
+ * Default: `/dist`
19
+ */
20
+ staticRootPath?: string;
21
+ /**
22
+ * The issuer
23
+ *
24
+ * Example: `https://test.idporten.no/`
25
+ */
26
+ issuer: string;
27
+ /**
28
+ * The ID of the client
29
+ */
30
+ clientId: string;
31
+ /**
32
+ * Sets the scope parameter. Values are case-sensitive. Multiple values must be sepratated by space. Default: `openid profile`
33
+ */
34
+ scope: string;
35
+ /**
36
+ * The client secret. Not used if `okDataIdPortenKeyName` is set.
37
+ */
38
+ clientSecret?: string;
39
+ /**
40
+ * The redirect uri configured for the client
41
+ */
42
+ redirectUri: string;
43
+ /**
44
+ * The intended audience for the tokens. Value(s) set here can be used to verify audience on the recipient end.
45
+ * See https://datatracker.ietf.org/doc/html/draft-ietf-oauth-resource-indicators-05.
46
+ *
47
+ * Example: `["https://example.com/api1", "https://example.com/api2"]`
48
+ */
49
+ resources?: Array<string>;
50
+ /**
51
+ * express-session cookie options, note that if this is set, the below cookie*-options will have no effect
52
+ */
53
+ cookie?: session.CookieOptions;
54
+ /**
55
+ * Sets the Path attribute of the cookie. Should most likely be the same as basePath. See https://expressjs.com/en/resources/middleware/session.html
56
+ *
57
+ * Default: `/`
58
+ */
59
+ cookiePath?: string;
60
+ /**
61
+ * Sets the Secure attribute of the cookie. See https://expressjs.com/en/resources/middleware/session.html
62
+ *
63
+ * Default: `true`
64
+ */
65
+ cookieSecure?: boolean;
66
+ /**
67
+ * Sets the SameSite attribute of the cookie. See https://expressjs.com/en/resources/middleware/session.html
68
+ *
69
+ * Default: `"lax"`
70
+ */
71
+ cookieSameSite: boolean | "lax" | "none" | "strict";
72
+ /**
73
+ * The post logout redirect uri configured for the client
74
+ */
75
+ postLogoutRedirectUri: string;
76
+ /**
77
+ * The name of the key that okdata stored in parameter store.
78
+ *
79
+ * Example: `/okdata/maskinporten/11111111-2222-3333-4444-555555555555/key.json`
80
+ */
81
+ okDataIdPortenKeyName?: string;
82
+ /**
83
+ * Secret used to sign sessions. This can be any string, but should have at least 32 bytes of entropy in production.
84
+ */
85
+ sessionSecret: string;
86
+ /**
87
+ * The type of session store used. Only `memory` and `dynamodb` currently supported. `memory` is only for dev use
88
+ */
89
+ sessionStoreType: 'memory' | 'dynamodb';
90
+ /**
91
+ * Options that will be passed to the chosen sessionStore. Options depend on the type of store chosen
92
+ *
93
+ * Example: `{"table": "my-custom-dynamodb-table"}`
94
+ */
95
+ sessionStoreOptions?: object;
96
+ /**
97
+ * Map of paths and remote targets that will be forwarded by the proxy
98
+ *
99
+ * Example: `{'/api': 'http://example.com/api'}`
100
+ */
101
+ proxyTargets: {
102
+ [path: string]: string;
103
+ };
104
+ /**
105
+ * Like `proxyTargets`, but requests pass through anonymously — no session lookup, no Authorization
106
+ * header. Registered before `proxyTargets`, so a public path takes precedence over an overlapping
107
+ * protected one.
108
+ *
109
+ * Example: `{'/api/public': 'http://example.com/api/public'}`
110
+ */
111
+ publicProxyTargets?: {
112
+ [path: string]: string;
113
+ };
114
+ /**
115
+ * List of claims in the id_token that are returned by the /user-endpoint. By default all are returned
116
+ *
117
+ * Example: `["pid"]`
118
+ */
119
+ userClaims?: Array<string>;
120
+ /**
121
+ * Content security policy configuration passed to helmet.
122
+ * See https://github.com/helmetjs/helmet for details. Note that since the config in limited to json,
123
+ * some features are not supported. To set a nonce value, use the special value `"{nonce}"` instead.
124
+ *
125
+ *
126
+ * Example:
127
+ * ```json
128
+ * {
129
+ * "directives": {
130
+ * "default-src": ["'self'", "https://*.oslo.kommune.no", "https://*.oslo.systems"],
131
+ * "script-src": ["'self'", "{nonce}"],
132
+ * ...
133
+ * }
134
+ * }
135
+ * ```
136
+ */
137
+ contentSecurityPolicy?: Exclude<HelmetOptions['contentSecurityPolicy'], Boolean>;
138
+ /**
139
+ * These values will be injected into index.html, replacing any `__INJECTED_CONFIG__` if present
140
+ */
141
+ injectConfig?: string | boolean | number | null | {
142
+ [k: string]: string | boolean | number | null;
143
+ };
144
+ };
145
+ export declare function replaceConfigValues(value: unknown): any;
146
+ export declare function loadConfig(configFile?: string | Array<string>): Promise<BffConfig>;
147
+ //# sourceMappingURL=config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../../src/config/config.ts"],"names":[],"mappings":"AACA,OAAO,EAAC,aAAa,EAAC,MAAM,QAAQ,CAAC;AACrC,OAAO,OAAO,MAAM,iBAAiB,CAAC;AAGtC,MAAM,MAAM,SAAS,GAAG;IACtB;;;OAGG;IACH,IAAI,EAAE,MAAM,CAAA;IACZ;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;;;;OAIG;IACH,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB;;;;OAIG;IACH,MAAM,EAAE,MAAM,CAAA;IACd;;OAEG;IACH,QAAQ,EAAE,MAAM,CAAA;IAChB;;OAEG;IACH,KAAK,EAAE,MAAM,CAAA;IACb;;OAEG;IACH,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB;;OAEG;IACH,WAAW,EAAE,MAAM,CAAA;IACnB;;;;;OAKG;IACH,SAAS,CAAC,EAAE,KAAK,CAAC,MAAM,CAAC,CAAA;IACzB;;OAEG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC,aAAa,CAAA;IAC9B;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB;;;;OAIG;IACH,YAAY,CAAC,EAAE,OAAO,CAAA;IACtB;;;;OAIG;IACH,cAAc,EAAE,OAAO,GAAG,KAAK,GAAG,MAAM,GAAG,QAAQ,CAAA;IACnD;;OAEG;IACH,qBAAqB,EAAE,MAAM,CAAA;IAC7B;;;;OAIG;IACH,qBAAqB,CAAC,EAAE,MAAM,CAAA;IAC9B;;OAEG;IACH,aAAa,EAAE,MAAM,CAAA;IACrB;;OAEG;IACH,gBAAgB,EAAE,QAAQ,GAAG,UAAU,CAAA;IACvC;;;;OAIG;IACH,mBAAmB,CAAC,EAAE,MAAM,CAAA;IAC5B;;;;OAIG;IACH,YAAY,EAAE;QAAE,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAA;KAAE,CAAA;IACxC;;;;;;OAMG;IACH,kBAAkB,CAAC,EAAE;QAAE,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAA;KAAE,CAAA;IAC/C;;;;OAIG;IACH,UAAU,CAAC,EAAE,KAAK,CAAC,MAAM,CAAC,CAAA;IAC1B;;;;;;;;;;;;;;;;OAgBG;IACH,qBAAqB,CAAC,EAAE,OAAO,CAAC,aAAa,CAAC,uBAAuB,CAAC,EAAE,OAAO,CAAC,CAAA;IAChF;;OAEG;IACH,YAAY,CAAC,EAAE,MAAM,GAAG,OAAO,GAAG,MAAM,GAAG,IAAI,GAAG;QAAC,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,MAAM,GAAG,IAAI,CAAA;KAAC,CAAA;CAClG,CAAA;AAaD,wBAAsB,mBAAmB,CAAC,KAAK,EAAE,OAAO,OAuBvD;AAED,wBAAsB,UAAU,CAAC,UAAU,GAAE,MAAM,GAAG,KAAK,CAAC,MAAM,CAAqB,sBActF"}
@@ -0,0 +1,54 @@
1
+ import { findUp } from 'find-up';
2
+ import { getEnv, getSsmParameter } from "./variable-loaders.js";
3
+ const defaultConfig = {
4
+ basePath: "",
5
+ cookiePath: '/',
6
+ cookieSecure: true,
7
+ cookieSameSite: 'lax',
8
+ staticRootPath: './dist',
9
+ scope: 'openid profile'
10
+ };
11
+ let config;
12
+ export async function replaceConfigValues(value) {
13
+ if (typeof value === "string") {
14
+ const [, varType, varName] = value.match(/\{(\w+):(.*)}/) ?? [];
15
+ if (varType === 'env') {
16
+ return getEnv(varName);
17
+ }
18
+ else if (varType === 'ssm') {
19
+ return await getSsmParameter(varName);
20
+ }
21
+ else if (varType) {
22
+ throw Error(`unknown varType: ${varType}`);
23
+ }
24
+ else {
25
+ return value;
26
+ }
27
+ }
28
+ else if (Array.isArray(value)) {
29
+ return Promise.all(value.map(replaceConfigValues));
30
+ }
31
+ else if (typeof value === "object" && value != null) {
32
+ const res = {};
33
+ for (const [key, val] of Object.entries(value)) {
34
+ res[key] = await replaceConfigValues(val);
35
+ }
36
+ return res;
37
+ }
38
+ else {
39
+ return value;
40
+ }
41
+ }
42
+ export async function loadConfig(configFile = 'bff.config.json') {
43
+ if (config)
44
+ return config;
45
+ const userConfigPath = await findUp(configFile);
46
+ if (!userConfigPath) {
47
+ throw Error(`Could not find config file ${configFile}`);
48
+ }
49
+ console.log('Loading config at', userConfigPath);
50
+ const { default: loadedConfig } = await import(userConfigPath, { with: { type: 'json' } });
51
+ const processedConfig = await replaceConfigValues(loadedConfig);
52
+ config = { ...defaultConfig, ...processedConfig };
53
+ return config;
54
+ }
@@ -0,0 +1,3 @@
1
+ export declare function getEnv(env: string, defaultVal?: string, parseFn?: (val: string) => string): string;
2
+ export declare function getSsmParameter(name: string, withDecryption?: boolean): Promise<string>;
3
+ //# sourceMappingURL=variable-loaders.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"variable-loaders.d.ts","sourceRoot":"","sources":["../../../src/config/variable-loaders.ts"],"names":[],"mappings":"AAEA,wBAAgB,MAAM,CAAC,GAAG,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,MAAM,UAQzF;AAID,wBAAsB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,cAAc,GAAE,OAAc,mBAMjF"}
@@ -0,0 +1,20 @@
1
+ import { GetParameterCommand, SSMClient } from "@aws-sdk/client-ssm";
2
+ export function getEnv(env, defaultVal, parseFn) {
3
+ if (process.env[env]) {
4
+ return parseFn ? parseFn(process.env[env]) : process.env[env];
5
+ }
6
+ else if (defaultVal !== undefined) {
7
+ return defaultVal;
8
+ }
9
+ else {
10
+ throw Error(`Missing env var: ${env}`);
11
+ }
12
+ }
13
+ let ssmClient;
14
+ export async function getSsmParameter(name, withDecryption = true) {
15
+ ssmClient ?? (ssmClient = new SSMClient({}));
16
+ return ssmClient.send(new GetParameterCommand({
17
+ Name: name,
18
+ WithDecryption: withDecryption
19
+ })).then(p => p.Parameter.Value);
20
+ }
@@ -101,6 +101,16 @@ export type BffConfig = {
101
101
  proxyTargets: {
102
102
  [path: string]: string;
103
103
  };
104
+ /**
105
+ * Like `proxyTargets`, but requests pass through anonymously — no session lookup, no Authorization
106
+ * header. Registered before `proxyTargets`, so a public path takes precedence over an overlapping
107
+ * protected one.
108
+ *
109
+ * Example: `{'/api/public': 'http://example.com/api/public'}`
110
+ */
111
+ publicProxyTargets?: {
112
+ [path: string]: string;
113
+ };
104
114
  /**
105
115
  * List of claims in the id_token that are returned by the /user-endpoint. By default all are returned
106
116
  *
@@ -125,8 +135,15 @@ export type BffConfig = {
125
135
  * ```
126
136
  */
127
137
  contentSecurityPolicy?: Exclude<HelmetOptions['contentSecurityPolicy'], Boolean>;
138
+ /**
139
+ * These values will be injected into index.html, replacing any `__INJECTED_CONFIG__` if present
140
+ */
141
+ injectConfig?: string | {
142
+ [k: string]: any;
143
+ };
128
144
  };
129
145
  export declare function getEnv(env: string, defaultVal?: string, parseFn?: (val: string) => string): string;
130
146
  export declare function getSsmParameter(name: string, withDecryption?: boolean): Promise<string>;
147
+ export declare function replaceConfigValues(value: unknown): any;
131
148
  export declare function loadConfig(configFile?: string | Array<string>): Promise<BffConfig>;
132
149
  //# sourceMappingURL=config.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/config.ts"],"names":[],"mappings":"AAEA,OAAO,EAAC,aAAa,EAAC,MAAM,QAAQ,CAAC;AACrC,OAAO,OAAO,MAAM,iBAAiB,CAAC;AAEtC,MAAM,MAAM,SAAS,GAAG;IACtB;;;OAGG;IACH,IAAI,EAAE,MAAM,CAAA;IACZ;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;;;;OAIG;IACH,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB;;;;OAIG;IACH,MAAM,EAAE,MAAM,CAAA;IACd;;OAEG;IACH,QAAQ,EAAE,MAAM,CAAA;IAChB;;OAEG;IACH,KAAK,EAAE,MAAM,CAAA;IACb;;OAEG;IACH,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB;;OAEG;IACH,WAAW,EAAE,MAAM,CAAA;IACnB;;;;;OAKG;IACH,SAAS,CAAC,EAAE,KAAK,CAAC,MAAM,CAAC,CAAA;IACzB;;OAEG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC,aAAa,CAAA;IAC9B;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB;;;;OAIG;IACH,YAAY,CAAC,EAAE,OAAO,CAAA;IACtB;;;;OAIG;IACH,cAAc,EAAE,OAAO,GAAG,KAAK,GAAG,MAAM,GAAG,QAAQ,CAAA;IACnD;;OAEG;IACH,qBAAqB,EAAE,MAAM,CAAA;IAC7B;;;;OAIG;IACH,qBAAqB,EAAE,MAAM,CAAA;IAC7B;;OAEG;IACH,aAAa,EAAE,MAAM,CAAA;IACrB;;OAEG;IACH,gBAAgB,EAAE,QAAQ,GAAG,UAAU,CAAA;IACvC;;;;OAIG;IACH,mBAAmB,CAAC,EAAE,MAAM,CAAA;IAC5B;;;;OAIG;IACH,YAAY,EAAE;QAAE,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAA;KAAE,CAAA;IACxC;;;;OAIG;IACH,UAAU,CAAC,EAAE,KAAK,CAAC,MAAM,CAAC,CAAA;IAC1B;;;;;;;;;;;;;;;;OAgBG;IACH,qBAAqB,CAAC,EAAE,OAAO,CAAC,aAAa,CAAC,uBAAuB,CAAC,EAAE,OAAO,CAAC,CAAA;CACjF,CAAA;AAWD,wBAAgB,MAAM,CAAC,GAAG,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,MAAM,UAQzF;AAID,wBAAsB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,cAAc,GAAE,OAAc,mBAMjF;AAID,wBAAsB,UAAU,CAAC,UAAU,GAAE,MAAM,GAAG,KAAK,CAAC,MAAM,CAAqB,sBAyBtF"}
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/config.ts"],"names":[],"mappings":"AAEA,OAAO,EAAC,aAAa,EAAC,MAAM,QAAQ,CAAC;AACrC,OAAO,OAAO,MAAM,iBAAiB,CAAC;AAEtC,MAAM,MAAM,SAAS,GAAG;IACtB;;;OAGG;IACH,IAAI,EAAE,MAAM,CAAA;IACZ;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;;;;OAIG;IACH,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB;;;;OAIG;IACH,MAAM,EAAE,MAAM,CAAA;IACd;;OAEG;IACH,QAAQ,EAAE,MAAM,CAAA;IAChB;;OAEG;IACH,KAAK,EAAE,MAAM,CAAA;IACb;;OAEG;IACH,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB;;OAEG;IACH,WAAW,EAAE,MAAM,CAAA;IACnB;;;;;OAKG;IACH,SAAS,CAAC,EAAE,KAAK,CAAC,MAAM,CAAC,CAAA;IACzB;;OAEG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC,aAAa,CAAA;IAC9B;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB;;;;OAIG;IACH,YAAY,CAAC,EAAE,OAAO,CAAA;IACtB;;;;OAIG;IACH,cAAc,EAAE,OAAO,GAAG,KAAK,GAAG,MAAM,GAAG,QAAQ,CAAA;IACnD;;OAEG;IACH,qBAAqB,EAAE,MAAM,CAAA;IAC7B;;;;OAIG;IACH,qBAAqB,EAAE,MAAM,CAAA;IAC7B;;OAEG;IACH,aAAa,EAAE,MAAM,CAAA;IACrB;;OAEG;IACH,gBAAgB,EAAE,QAAQ,GAAG,UAAU,CAAA;IACvC;;;;OAIG;IACH,mBAAmB,CAAC,EAAE,MAAM,CAAA;IAC5B;;;;OAIG;IACH,YAAY,EAAE;QAAE,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAA;KAAE,CAAA;IACxC;;;;;;OAMG;IACH,kBAAkB,CAAC,EAAE;QAAE,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAA;KAAE,CAAA;IAC/C;;;;OAIG;IACH,UAAU,CAAC,EAAE,KAAK,CAAC,MAAM,CAAC,CAAA;IAC1B;;;;;;;;;;;;;;;;OAgBG;IACH,qBAAqB,CAAC,EAAE,OAAO,CAAC,aAAa,CAAC,uBAAuB,CAAC,EAAE,OAAO,CAAC,CAAA;IAChF;;OAEG;IACH,YAAY,CAAC,EAAE,MAAM,GAAG;QAAC,CAAC,CAAC,EAAE,MAAM,GAAG,GAAG,CAAA;KAAC,CAAA;CAC3C,CAAA;AAWD,wBAAgB,MAAM,CAAC,GAAG,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,MAAM,UAQzF;AAID,wBAAsB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,cAAc,GAAE,OAAc,mBAMjF;AAID,wBAAsB,mBAAmB,CAAC,KAAK,EAAE,OAAO,OAuBvD;AAED,wBAAsB,UAAU,CAAC,UAAU,GAAE,MAAM,GAAG,KAAK,CAAC,MAAM,CAAqB,sBActF"}
@@ -28,6 +28,36 @@ export async function getSsmParameter(name, withDecryption = true) {
28
28
  })).then(p => p.Parameter.Value);
29
29
  }
30
30
  let config;
31
+ export async function replaceConfigValues(value) {
32
+ if (typeof value === "string") {
33
+ const [, varType, varName] = value.match(/\{(\w+):(.*)}/) ?? [];
34
+ if (varType === 'env') {
35
+ return getEnv(varName);
36
+ }
37
+ else if (varType === 'ssm') {
38
+ return await getSsmParameter(varName);
39
+ }
40
+ else if (varType) {
41
+ throw Error(`unknown varType: ${varType}`);
42
+ }
43
+ else {
44
+ return value;
45
+ }
46
+ }
47
+ else if (Array.isArray(value)) {
48
+ return Promise.all(value.map(replaceConfigValues));
49
+ }
50
+ else if (typeof value === "object" && value != null) {
51
+ const res = {};
52
+ for (const [key, val] of Object.entries(value)) {
53
+ res[key] = await replaceConfigValues(val);
54
+ }
55
+ return res;
56
+ }
57
+ else {
58
+ return value;
59
+ }
60
+ }
31
61
  export async function loadConfig(configFile = 'bff.config.json') {
32
62
  if (config)
33
63
  return config;
@@ -37,20 +67,7 @@ export async function loadConfig(configFile = 'bff.config.json') {
37
67
  }
38
68
  console.log('Loading config at', userConfigPath);
39
69
  const { default: loadedConfig } = await import(userConfigPath, { with: { type: 'json' } });
40
- for (const [key, value] of Object.entries(loadedConfig)) {
41
- if (typeof value === "string") {
42
- const [, varType, varName] = value.match(/\{(\w+):(.*)}/) ?? [];
43
- if (varType === 'env') {
44
- loadedConfig[key] = getEnv(varName);
45
- }
46
- else if (varType === 'ssm') {
47
- loadedConfig[key] = await getSsmParameter(varName);
48
- }
49
- else if (varType) {
50
- throw Error(`unknown varType: ${varType}`);
51
- }
52
- }
53
- }
54
- config = { ...defaultConfig, ...loadedConfig };
70
+ const processedConfig = await replaceConfigValues(loadedConfig);
71
+ config = { ...defaultConfig, ...processedConfig };
55
72
  return config;
56
73
  }
@@ -1,5 +1,5 @@
1
1
  import { OpenIdConfigManager } from "../OpenIdConfigManager.js";
2
- import { BffConfig } from "../config.js";
2
+ import { BffConfig } from "../config/config.js";
3
3
  import type { Request, Response, NextFunction } from 'express';
4
4
  export declare class OidcMiddleware {
5
5
  #private;
@@ -1 +1 @@
1
- {"version":3,"file":"OidcMiddleware.d.ts","sourceRoot":"","sources":["../../../src/middleware/OidcMiddleware.ts"],"names":[],"mappings":"AACA,OAAO,EAAC,mBAAmB,EAAC,MAAM,2BAA2B,CAAC;AAE9D,OAAO,EAAC,SAAS,EAAC,MAAM,cAAc,CAAC;AACvC,OAAO,KAAK,EAAC,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAC,MAAM,SAAS,CAAA;AAK5D,qBAAa,cAAc;;IAKzB;;;;OAIG;gBACS,MAAM,EAAE,SAAS,EAAE,aAAa,EAAE,mBAAmB;WASpD,MAAM,CAAC,MAAM,EAAE,SAAS;IAgErC,IAAI,gBAAgB,KACV,KAAK,OAAO,EAAE,KAAK,QAAQ,EAAE,MAAM,YAAY,UAWxD;IAED,IAAI,KAAK,KACO,KAAK,OAAO,EAAE,KAAK,QAAQ,EAAE,MAAM,YAAY,mBA+B9D;IAqBD,IAAI,QAAQ,KACI,KAAK,OAAO,EAAE,KAAK,QAAQ,EAAE,MAAM,YAAY,mBAuC9D;IAED,IAAI,IAAI,KACQ,KAAK,OAAO,EAAE,KAAK,QAAQ,EAAE,MAAM,YAAY,iDAc9D;IAED,IAAI,MAAM,KACA,KAAK,OAAO,EAAE,KAAK,QAAQ,UASpC;IAED,IAAI,kBAAkB,KACN,KAAK,OAAO,EAAE,KAAK,QAAQ,mBAc1C;CACF"}
1
+ {"version":3,"file":"OidcMiddleware.d.ts","sourceRoot":"","sources":["../../../src/middleware/OidcMiddleware.ts"],"names":[],"mappings":"AACA,OAAO,EAAC,mBAAmB,EAAC,MAAM,2BAA2B,CAAC;AAE9D,OAAO,EAAC,SAAS,EAAC,MAAM,qBAAqB,CAAC;AAC9C,OAAO,KAAK,EAAC,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAC,MAAM,SAAS,CAAA;AAK5D,qBAAa,cAAc;;IAKzB;;;;OAIG;gBACS,MAAM,EAAE,SAAS,EAAE,aAAa,EAAE,mBAAmB;WASpD,MAAM,CAAC,MAAM,EAAE,SAAS;IAgErC,IAAI,gBAAgB,KACV,KAAK,OAAO,EAAE,KAAK,QAAQ,EAAE,MAAM,YAAY,UAWxD;IAED,IAAI,KAAK,KACO,KAAK,OAAO,EAAE,KAAK,QAAQ,EAAE,MAAM,YAAY,mBA+B9D;IAqBD,IAAI,QAAQ,KACI,KAAK,OAAO,EAAE,KAAK,QAAQ,EAAE,MAAM,YAAY,mBAuC9D;IAED,IAAI,IAAI,KACQ,KAAK,OAAO,EAAE,KAAK,QAAQ,EAAE,MAAM,YAAY,iDAc9D;IAED,IAAI,MAAM,KACA,KAAK,OAAO,EAAE,KAAK,QAAQ,UASpC;IAED,IAAI,kBAAkB,KACN,KAAK,OAAO,EAAE,KAAK,QAAQ,mBAc1C;CACF"}
@@ -0,0 +1,4 @@
1
+ import { BffConfig } from "../config/config.js";
2
+ import { Request, Response } from "express";
3
+ export declare function injectConfig(config: BffConfig): (req: Request, originalResponse: Response, next: import("express").NextFunction) => void;
4
+ //# sourceMappingURL=inject-config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"inject-config.d.ts","sourceRoot":"","sources":["../../../src/middleware/inject-config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,SAAS,EAAC,MAAM,qBAAqB,CAAC;AAC9C,OAAO,EAAC,OAAO,EAAE,QAAQ,EAAC,MAAM,SAAS,CAAC;AAG1C,wBAAgB,YAAY,CAAC,MAAM,EAAE,SAAS,4FAQ7C"}
@@ -0,0 +1,10 @@
1
+ import { stringReplace } from "string-replace-middleware";
2
+ export function injectConfig(config) {
3
+ const injectConfigString = JSON.stringify(config.injectConfig)?.replace(/[&'<>]/g, '_ILLEGAL_CHAR_');
4
+ return stringReplace({
5
+ '__CSP_NONCE__': (_, res) => res.locals.cspNonce,
6
+ '__INJECTED_CONFIG__': injectConfigString,
7
+ }, {
8
+ contentTypeFilterRegexp: /^text\/html/
9
+ });
10
+ }
@@ -1,4 +1,4 @@
1
- import { BffConfig } from "../config.js";
1
+ import { BffConfig } from "../config/config.js";
2
2
  import { OidcMiddleware } from "./OidcMiddleware.js";
3
3
  export declare function proxyRoutes(config: BffConfig, oidcMiddleware: OidcMiddleware): import("express-serve-static-core").Router;
4
4
  //# sourceMappingURL=proxy-routes.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"proxy-routes.d.ts","sourceRoot":"","sources":["../../../src/middleware/proxy-routes.ts"],"names":[],"mappings":"AAEA,OAAO,EAAC,SAAS,EAAC,MAAM,cAAc,CAAC;AACvC,OAAO,EAAC,cAAc,EAAC,MAAM,qBAAqB,CAAC;AAEnD,wBAAgB,WAAW,CAAC,MAAM,EAAE,SAAS,EAAE,cAAc,EAAE,cAAc,8CA6B5E"}
1
+ {"version":3,"file":"proxy-routes.d.ts","sourceRoot":"","sources":["../../../src/middleware/proxy-routes.ts"],"names":[],"mappings":"AAEA,OAAO,EAAC,SAAS,EAAC,MAAM,qBAAqB,CAAC;AAC9C,OAAO,EAAC,cAAc,EAAC,MAAM,qBAAqB,CAAC;AAEnD,wBAAgB,WAAW,CAAC,MAAM,EAAE,SAAS,EAAE,cAAc,EAAE,cAAc,8CAoD5E"}
@@ -2,6 +2,23 @@ import express from "express";
2
2
  import { createProxyMiddleware } from "http-proxy-middleware";
3
3
  export function proxyRoutes(config, oidcMiddleware) {
4
4
  const router = express.Router();
5
+ // Public targets first so they win over overlapping protected paths.
6
+ for (const [path, target] of Object.entries(config.publicProxyTargets ?? {})) {
7
+ console.log(`Setting up public proxy: ${path} -> ${target}`);
8
+ router.use(path, createProxyMiddleware({
9
+ target: target,
10
+ changeOrigin: true,
11
+ on: {
12
+ proxyReq: (proxyReq) => {
13
+ proxyReq.removeHeader("Cookie");
14
+ },
15
+ proxyRes: (proxyRes, req) => {
16
+ // @ts-ignore //TODO: proxyRes har en mystisk type som mangler req, men den er der
17
+ console.log(`Proxied (public): ${req.method} ${req.originalUrl} -> ${proxyRes.req.protocol}//${proxyRes.req.host}${proxyRes.req.path}, status=${proxyRes.statusCode}`);
18
+ }
19
+ }
20
+ }));
21
+ }
5
22
  for (const [path, target] of Object.entries(config.proxyTargets)) {
6
23
  console.log(`Setting up auth proxy: ${path} -> ${target}`);
7
24
  router.use(path, oidcMiddleware.ensureFreshToken, createProxyMiddleware({
@@ -1,4 +1,4 @@
1
1
  import { Request, Response, NextFunction } from 'express';
2
- import { BffConfig } from "../config.js";
2
+ import { BffConfig } from "../config/config.js";
3
3
  export declare function securityHeaders(config: BffConfig): ((_: Request, res: Response, next: NextFunction) => void)[];
4
4
  //# sourceMappingURL=security-headers.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"security-headers.d.ts","sourceRoot":"","sources":["../../../src/middleware/security-headers.ts"],"names":[],"mappings":"AAEA,OAAO,EAAC,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAC,MAAM,SAAS,CAAA;AACvD,OAAO,EAAC,SAAS,EAAC,MAAM,cAAc,CAAC;AAEvC,wBAAgB,eAAe,CAAC,MAAM,EAAE,SAAS,QAcR,OAAO,OAAO,QAAQ,QAAQ,YAAY,aAkBlF"}
1
+ {"version":3,"file":"security-headers.d.ts","sourceRoot":"","sources":["../../../src/middleware/security-headers.ts"],"names":[],"mappings":"AAEA,OAAO,EAAC,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAC,MAAM,SAAS,CAAA;AACvD,OAAO,EAAC,SAAS,EAAC,MAAM,qBAAqB,CAAC;AAE9C,wBAAgB,eAAe,CAAC,MAAM,EAAE,SAAS,QAcR,OAAO,OAAO,QAAQ,QAAQ,YAAY,aAkBlF"}
@@ -1,3 +1,3 @@
1
- import { BffConfig } from "../../config.js";
1
+ import { BffConfig } from "../../config/config.js";
2
2
  export declare function sessions(config: BffConfig): import("express").RequestHandler<import("express-serve-static-core").ParamsDictionary, any, any, import("qs").ParsedQs, Record<string, any>>[];
3
3
  //# sourceMappingURL=sessions.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"sessions.d.ts","sourceRoot":"","sources":["../../../../src/middleware/sessions/sessions.ts"],"names":[],"mappings":"AAGA,OAAO,EAAC,SAAS,EAAC,MAAM,iBAAiB,CAAC;AAG1C,wBAAgB,QAAQ,CAAC,MAAM,EAAE,SAAS,kJAiCzC"}
1
+ {"version":3,"file":"sessions.d.ts","sourceRoot":"","sources":["../../../../src/middleware/sessions/sessions.ts"],"names":[],"mappings":"AAGA,OAAO,EAAC,SAAS,EAAC,MAAM,wBAAwB,CAAC;AAGjD,wBAAgB,QAAQ,CAAC,MAAM,EAAE,SAAS,kJAiCzC"}
@@ -1,3 +1,3 @@
1
- import { BffConfig } from "../config.js";
1
+ import { BffConfig } from "../config/config.js";
2
2
  export declare function staticRoutes(config: BffConfig): import("express-serve-static-core").Router;
3
3
  //# sourceMappingURL=static-routes.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"static-routes.d.ts","sourceRoot":"","sources":["../../../src/middleware/static-routes.ts"],"names":[],"mappings":"AAGA,OAAO,EAAC,SAAS,EAAC,MAAM,cAAc,CAAC;AAEvC,wBAAgB,YAAY,CAAC,MAAM,EAAE,SAAS,8CAiB7C"}
1
+ {"version":3,"file":"static-routes.d.ts","sourceRoot":"","sources":["../../../src/middleware/static-routes.ts"],"names":[],"mappings":"AAGA,OAAO,EAAC,SAAS,EAAC,MAAM,qBAAqB,CAAC;AAG9C,wBAAgB,YAAY,CAAC,MAAM,EAAE,SAAS,8CAY7C"}
@@ -1,19 +1,15 @@
1
1
  import express from "express";
2
- import { stringReplace } from "string-replace-middleware";
3
2
  import path from "path";
3
+ import { injectConfig } from "./inject-config.js";
4
4
  export function staticRoutes(config) {
5
5
  const router = express.Router();
6
- router.use(stringReplace({
7
- '__CSP_NONCE__': (_, res) => res.locals.cspNonce
8
- }, {
9
- contentTypeFilterRegexp: /^text\/html/
10
- }));
6
+ router.use(injectConfig(config));
11
7
  const staticPath = path.resolve(process.cwd(), config.staticRootPath);
12
8
  console.log(`Serving static content from '${staticPath}'`);
13
9
  router.use(express.static(staticPath, { index: false }));
14
10
  router.get('*', function (_, res) {
15
11
  res.set('Cache-Control', 'no-store');
16
- res.sendFile(path.resolve(staticPath, 'index.html'));
12
+ res.sendFile(path.resolve(staticPath, 'index.html'), { etag: false });
17
13
  });
18
14
  return router;
19
15
  }
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import express from "express";
3
3
  import compression from "compression";
4
- import { loadConfig } from './config.js';
4
+ import { loadConfig } from './config/config.js';
5
5
  import { proxyRoutes } from "./middleware/proxy-routes.js";
6
6
  import { staticRoutes } from "./middleware/static-routes.js";
7
7
  import { securityHeaders } from "./middleware/security-headers.js";
@@ -1 +1 @@
1
- {"version":3,"file":"vite-plugin.d.ts","sourceRoot":"","sources":["../../src/vite-plugin.ts"],"names":[],"mappings":"AAGA,OAAO,EAAgB,MAAM,EAAC,MAAM,MAAM,CAAA;AAsB1C,MAAM,CAAC,OAAO,UAAU,GAAG,CAAC,EAAC,UAAU,EAAC,GAAE;IAAC,UAAU,CAAC,EAAE,MAAM,GAAG,KAAK,CAAC,MAAM,CAAC,CAAA;CAAM,GAAG,MAAM,CAO5F"}
1
+ {"version":3,"file":"vite-plugin.d.ts","sourceRoot":"","sources":["../../src/vite-plugin.ts"],"names":[],"mappings":"AAGA,OAAO,EAAgB,MAAM,EAAC,MAAM,MAAM,CAAA;AAwB1C,MAAM,CAAC,OAAO,UAAU,GAAG,CAAC,EAAC,UAAU,EAAC,GAAE;IAAC,UAAU,CAAC,EAAE,MAAM,GAAG,KAAK,CAAC,MAAM,CAAC,CAAA;CAAM,GAAG,MAAM,CAO5F"}
@@ -1,6 +1,7 @@
1
1
  import express from "express";
2
- import { loadConfig } from "./config.js";
2
+ import { loadConfig } from "./config/config.js";
3
3
  import { OidcMiddleware } from "./middleware/OidcMiddleware.js";
4
+ import { injectConfig } from "./middleware/inject-config.js";
4
5
  function configureServer(configFilePath) {
5
6
  return async ({ middlewares }) => {
6
7
  const { oidcRoutes } = await import("./middleware/oidc-routes.js");
@@ -13,6 +14,7 @@ function configureServer(configFilePath) {
13
14
  app.use(sessions(config));
14
15
  app.use(basePath, oidcRoutes(oidcMiddleware));
15
16
  app.use(basePath, proxyRoutes(config, oidcMiddleware));
17
+ app.use(basePath, injectConfig(config));
16
18
  middlewares.use(app);
17
19
  };
18
20
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oslokommune/auth-bff",
3
- "version": "2.0.2",
3
+ "version": "2.2.0",
4
4
  "repository": "https://github.com/oslokommune/auth-bff.git",
5
5
  "publishConfig": {
6
6
  "access": "public"