@blazedpath/commons 0.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (224) hide show
  1. package/README.md +3 -0
  2. package/blz-base/health/index.js +215 -0
  3. package/blz-base/index.js +1466 -0
  4. package/blz-cache/LruCache.js +44 -0
  5. package/blz-cache/index.js +29 -0
  6. package/blz-config/index.js +434 -0
  7. package/blz-core/index.js +364 -0
  8. package/blz-cryptography/index.js +54 -0
  9. package/blz-datetimes/index.js +356 -0
  10. package/blz-file/example.dat +2545 -0
  11. package/blz-file/fileService.js +205 -0
  12. package/blz-file/index.js +94 -0
  13. package/blz-file/index.test.js +31 -0
  14. package/blz-file/lab.js +33 -0
  15. package/blz-hazelcast/index.js +189 -0
  16. package/blz-hazelcast/lib/credentials.js +25 -0
  17. package/blz-hazelcast/lib/credentialsFactory.js +12 -0
  18. package/blz-hazelcast/lib/hazelcastCache.js +234 -0
  19. package/blz-iterable/index.js +446 -0
  20. package/blz-json-schema/index.js +11 -0
  21. package/blz-jwt/index.js +121 -0
  22. package/blz-kafka/index.js +522 -0
  23. package/blz-math/index.js +131 -0
  24. package/blz-mongodb/index.js +326 -0
  25. package/blz-rds/__test__/scape.test.js +58 -0
  26. package/blz-rds/blz-rds-executor.js +578 -0
  27. package/blz-rds/blz-rds-helper.js +310 -0
  28. package/blz-rds/commands/core/add.js +13 -0
  29. package/blz-rds/commands/core/and.js +18 -0
  30. package/blz-rds/commands/core/asc.js +10 -0
  31. package/blz-rds/commands/core/avg.js +10 -0
  32. package/blz-rds/commands/core/column-ref.js +8 -0
  33. package/blz-rds/commands/core/count-distinct.js +10 -0
  34. package/blz-rds/commands/core/count.js +10 -0
  35. package/blz-rds/commands/core/decimal.js +8 -0
  36. package/blz-rds/commands/core/desc.js +10 -0
  37. package/blz-rds/commands/core/distinct.js +10 -0
  38. package/blz-rds/commands/core/divide.js +11 -0
  39. package/blz-rds/commands/core/embedded-exists.js +17 -0
  40. package/blz-rds/commands/core/embedded-select.js +17 -0
  41. package/blz-rds/commands/core/equals.js +9 -0
  42. package/blz-rds/commands/core/false.js +8 -0
  43. package/blz-rds/commands/core/greater-or-equal.js +9 -0
  44. package/blz-rds/commands/core/greater.js +9 -0
  45. package/blz-rds/commands/core/in.js +9 -0
  46. package/blz-rds/commands/core/integer.js +8 -0
  47. package/blz-rds/commands/core/is-not-null.js +11 -0
  48. package/blz-rds/commands/core/is-null-or-value.js +10 -0
  49. package/blz-rds/commands/core/is-null.js +11 -0
  50. package/blz-rds/commands/core/less-or-equal.js +9 -0
  51. package/blz-rds/commands/core/less-unary.js +12 -0
  52. package/blz-rds/commands/core/less.js +9 -0
  53. package/blz-rds/commands/core/like.js +12 -0
  54. package/blz-rds/commands/core/max.js +10 -0
  55. package/blz-rds/commands/core/min.js +10 -0
  56. package/blz-rds/commands/core/multiply.js +13 -0
  57. package/blz-rds/commands/core/not-equals.js +9 -0
  58. package/blz-rds/commands/core/not-in.js +9 -0
  59. package/blz-rds/commands/core/not.js +13 -0
  60. package/blz-rds/commands/core/null.js +8 -0
  61. package/blz-rds/commands/core/nvl.js +11 -0
  62. package/blz-rds/commands/core/or.js +13 -0
  63. package/blz-rds/commands/core/parameter.js +34 -0
  64. package/blz-rds/commands/core/remainder.js +16 -0
  65. package/blz-rds/commands/core/string.js +8 -0
  66. package/blz-rds/commands/core/subtract.js +13 -0
  67. package/blz-rds/commands/core/sum.js +10 -0
  68. package/blz-rds/commands/core/true.js +8 -0
  69. package/blz-rds/commands/core/tuple.js +13 -0
  70. package/blz-rds/commands/datetimes/add-days.js +11 -0
  71. package/blz-rds/commands/datetimes/add-hours.js +11 -0
  72. package/blz-rds/commands/datetimes/add-milliseconds.js +11 -0
  73. package/blz-rds/commands/datetimes/add-minutes.js +11 -0
  74. package/blz-rds/commands/datetimes/add-months.js +11 -0
  75. package/blz-rds/commands/datetimes/add-seconds.js +11 -0
  76. package/blz-rds/commands/datetimes/add-years.js +11 -0
  77. package/blz-rds/commands/datetimes/date-diff.js +11 -0
  78. package/blz-rds/commands/datetimes/date.js +12 -0
  79. package/blz-rds/commands/datetimes/datetime-diff.js +11 -0
  80. package/blz-rds/commands/datetimes/datetime.js +15 -0
  81. package/blz-rds/commands/datetimes/day.js +10 -0
  82. package/blz-rds/commands/datetimes/hour.js +10 -0
  83. package/blz-rds/commands/datetimes/millisecond.js +10 -0
  84. package/blz-rds/commands/datetimes/minute.js +10 -0
  85. package/blz-rds/commands/datetimes/month-text.js +10 -0
  86. package/blz-rds/commands/datetimes/month.js +10 -0
  87. package/blz-rds/commands/datetimes/now.js +9 -0
  88. package/blz-rds/commands/datetimes/second.js +10 -0
  89. package/blz-rds/commands/datetimes/subtract-days.js +11 -0
  90. package/blz-rds/commands/datetimes/subtract-hours.js +11 -0
  91. package/blz-rds/commands/datetimes/subtract-milliseconds.js +11 -0
  92. package/blz-rds/commands/datetimes/subtract-minutes.js +11 -0
  93. package/blz-rds/commands/datetimes/subtract-seconds.js +11 -0
  94. package/blz-rds/commands/datetimes/time-diff.js +11 -0
  95. package/blz-rds/commands/datetimes/time.js +13 -0
  96. package/blz-rds/commands/datetimes/today.js +9 -0
  97. package/blz-rds/commands/datetimes/week-day-text.js +10 -0
  98. package/blz-rds/commands/datetimes/week-day.js +10 -0
  99. package/blz-rds/commands/datetimes/week.js +10 -0
  100. package/blz-rds/commands/datetimes/year.js +10 -0
  101. package/blz-rds/commands/math/abs.js +10 -0
  102. package/blz-rds/commands/math/acos.js +10 -0
  103. package/blz-rds/commands/math/asin.js +10 -0
  104. package/blz-rds/commands/math/atan.js +10 -0
  105. package/blz-rds/commands/math/atan2.js +11 -0
  106. package/blz-rds/commands/math/ceil.js +10 -0
  107. package/blz-rds/commands/math/cos.js +10 -0
  108. package/blz-rds/commands/math/cosh.js +10 -0
  109. package/blz-rds/commands/math/exp.js +10 -0
  110. package/blz-rds/commands/math/floor.js +10 -0
  111. package/blz-rds/commands/math/log.js +18 -0
  112. package/blz-rds/commands/math/log10.js +10 -0
  113. package/blz-rds/commands/math/pow.js +11 -0
  114. package/blz-rds/commands/math/random.js +9 -0
  115. package/blz-rds/commands/math/round.js +18 -0
  116. package/blz-rds/commands/math/sign.js +10 -0
  117. package/blz-rds/commands/math/sin.js +10 -0
  118. package/blz-rds/commands/math/sinh.js +10 -0
  119. package/blz-rds/commands/math/sqrt.js +10 -0
  120. package/blz-rds/commands/math/tan.js +10 -0
  121. package/blz-rds/commands/math/tanh.js +10 -0
  122. package/blz-rds/commands/math/trunc.js +18 -0
  123. package/blz-rds/commands/strings/concat.js +20 -0
  124. package/blz-rds/commands/strings/contains.js +12 -0
  125. package/blz-rds/commands/strings/ends-with.js +12 -0
  126. package/blz-rds/commands/strings/index-of.js +11 -0
  127. package/blz-rds/commands/strings/is-null-or-empty.js +11 -0
  128. package/blz-rds/commands/strings/is-null-or-white-space.js +11 -0
  129. package/blz-rds/commands/strings/join.js +22 -0
  130. package/blz-rds/commands/strings/last-index-of.js +11 -0
  131. package/blz-rds/commands/strings/length.js +10 -0
  132. package/blz-rds/commands/strings/pad-left.js +20 -0
  133. package/blz-rds/commands/strings/pad-right.js +20 -0
  134. package/blz-rds/commands/strings/replace.js +12 -0
  135. package/blz-rds/commands/strings/starts-with.js +12 -0
  136. package/blz-rds/commands/strings/substring.js +12 -0
  137. package/blz-rds/commands/strings/to-lower.js +10 -0
  138. package/blz-rds/commands/strings/to-upper.js +10 -0
  139. package/blz-rds/commands/strings/trim-end.js +10 -0
  140. package/blz-rds/commands/strings/trim-start.js +10 -0
  141. package/blz-rds/commands/strings/trim.js +10 -0
  142. package/blz-rds/index.js +744 -0
  143. package/blz-rds-mysql/base.js +857 -0
  144. package/blz-rds-mysql/connection-manager.js +129 -0
  145. package/blz-rds-mysql/execute-bulk-insert.js +35 -0
  146. package/blz-rds-mysql/execute-bulk-merge.js +45 -0
  147. package/blz-rds-mysql/execute-non-query.js +34 -0
  148. package/blz-rds-mysql/execute-query.js +50 -0
  149. package/blz-rds-mysql/index.js +41 -0
  150. package/blz-rds-mysql/stored-procedure.js +207 -0
  151. package/blz-rds-mysql/syntaxis.json +114 -0
  152. package/blz-rds-mysqlx/base.js +846 -0
  153. package/blz-rds-mysqlx/connection-manager.js +141 -0
  154. package/blz-rds-mysqlx/execute-bulk-insert.js +35 -0
  155. package/blz-rds-mysqlx/execute-bulk-merge.js +45 -0
  156. package/blz-rds-mysqlx/execute-non-query.js +29 -0
  157. package/blz-rds-mysqlx/execute-query.js +39 -0
  158. package/blz-rds-mysqlx/index.js +41 -0
  159. package/blz-rds-mysqlx/stored-procedure.js +179 -0
  160. package/blz-rds-mysqlx/syntaxis.json +105 -0
  161. package/blz-rds-oracle/index.js +540 -0
  162. package/blz-rds-oracle/syntaxis.json +112 -0
  163. package/blz-rds-postgres/base.js +861 -0
  164. package/blz-rds-postgres/connection-manager.js +225 -0
  165. package/blz-rds-postgres/execute-bulk-insert.js +81 -0
  166. package/blz-rds-postgres/execute-bulk-merge.js +93 -0
  167. package/blz-rds-postgres/execute-non-query.js +23 -0
  168. package/blz-rds-postgres/execute-query.js +37 -0
  169. package/blz-rds-postgres/index.js +41 -0
  170. package/blz-rds-postgres/result-set.js +51 -0
  171. package/blz-rds-postgres/stored-procedure.js +116 -0
  172. package/blz-rds-postgres/syntaxis.json +114 -0
  173. package/blz-redis/index.js +217 -0
  174. package/blz-redis/lib/redisCache.js +265 -0
  175. package/blz-regex/index.js +25 -0
  176. package/blz-security/.eslintrc.js +15 -0
  177. package/blz-security/__test__/AuthorizationKpn.yaml +1043 -0
  178. package/blz-security/__test__/FinancingSetting.yaml +177 -0
  179. package/blz-security/__test__/KpnConfigPortal.yaml +330 -0
  180. package/blz-security/__test__/OrderManagement.yaml +5190 -0
  181. package/blz-security/__test__/Security.yaml +128 -0
  182. package/blz-security/__test__/autorization.test.js +105 -0
  183. package/blz-security/__test__/orderManagement.test.js +26 -0
  184. package/blz-security/__test__/secureUrl.test.js +79 -0
  185. package/blz-security/__test__/solveMergeRule.test.js +109 -0
  186. package/blz-security/__test__/sqlInjectionGuard.test.js +203 -0
  187. package/blz-security/__test__/xssGuard.test.js +204 -0
  188. package/blz-security/authorizationService.js +536 -0
  189. package/blz-security/config/global.js +8 -0
  190. package/blz-security/config/welcome +8 -0
  191. package/blz-security/doc/README.md +75 -0
  192. package/blz-security/filescanner/index.js +46 -0
  193. package/blz-security/helpers/consts.js +229 -0
  194. package/blz-security/helpers/utils.js +267 -0
  195. package/blz-security/implementations/cache.js +90 -0
  196. package/blz-security/implementations/oidc.js +404 -0
  197. package/blz-security/implementations/pkceCacheStore.js +23 -0
  198. package/blz-security/implementations/saml.js +10 -0
  199. package/blz-security/implementations/uma.js +63 -0
  200. package/blz-security/implementations/webAuthn.js +9 -0
  201. package/blz-security/implementations/wstg.js +72 -0
  202. package/blz-security/index.js +77 -0
  203. package/blz-security/lab/index.js +27 -0
  204. package/blz-security/middleware/HapiServerAzureAd.js +641 -0
  205. package/blz-security/middleware/HapiServerKeycloak.js +840 -0
  206. package/blz-security/middleware/HapiServerSimToken.js +247 -0
  207. package/blz-security/middleware/hapi.js +515 -0
  208. package/blz-security/middleware/hapiServer.js +974 -0
  209. package/blz-security/navigationMemoryRepository.js +15 -0
  210. package/blz-security/navigationMongoDbRepository.js +73 -0
  211. package/blz-security/secureUrlService.js +47 -0
  212. package/blz-security/securityService.js +409 -0
  213. package/blz-security/sqlInjectionGuard.js +162 -0
  214. package/blz-security/templates/forbidden.html +0 -0
  215. package/blz-security/templates/session-iframe-azure-ad.html +7 -0
  216. package/blz-security/templates/session-iframe.html +73 -0
  217. package/blz-security/templates/unauthorized.html +1 -0
  218. package/blz-security/xssGuard.js +87 -0
  219. package/blz-strings/index.js +167 -0
  220. package/blz-uuid/index.js +7 -0
  221. package/blz-yaml/index.js +19 -0
  222. package/index.js +84 -0
  223. package/package.json +97 -0
  224. package/process-managers/index.js +422 -0
@@ -0,0 +1,840 @@
1
+ /**
2
+ * @author Blazedpath Team
3
+ * @implements Protecting all resources through hapi middleware
4
+ * @description Hapi.js (derived from Http-API) is an open-source Node.js
5
+ * framework used to build powerful and scalable web applications.
6
+ * @see https://hapi.dev/api/
7
+ */
8
+ const Uma = require('../implementations/uma')
9
+ const Jsonwebtoken = require('jsonwebtoken') // Implementations of JSON Web Tokens.
10
+ const {
11
+ Exception,
12
+ getFullUrl,
13
+ getHost,
14
+ getProtocol,
15
+ getPathname,
16
+ getTemplate,
17
+ getTokenTolerance,
18
+ trace,
19
+ errorResponse
20
+ } = require('../helpers/utils')
21
+ // HapiServer Modules
22
+ const hapiYar = require('@hapi/yar');
23
+ const hapiJwt = require('@hapi/jwt');
24
+ const hapiCookie = require('@hapi/cookie')
25
+ // Quick Http Fetch using axios
26
+ const axios = require('axios');
27
+ // Crypto for code_verifier in token exchange
28
+ const crypto = require('crypto');
29
+ // Uses Issue to cache manage and logout (generators/customs not sure why yet)
30
+ const {
31
+ Issuer
32
+ } = require('openid-client') // OpenID Certified Relying Party.
33
+ const {
34
+ METADATA
35
+ } = require('../helpers/consts')
36
+ // Rotating key-certs, so we jwk used to routinly fetch them
37
+ const jwksClient = require('jwks-rsa') // Retrieve RSA public keys from a JWKS.
38
+
39
+ let contextConfig = {}
40
+ let securityService = null
41
+
42
+ class HapiServerKeycloak {
43
+ constructor(openIdConnect, cookiesName, cache) {
44
+ this.openIdConnect = openIdConnect
45
+ this.COOKIE_NAMES = cookiesName
46
+ this.activateTraceApiMethod = false
47
+ this.queryStringLimit = null;
48
+ this.securityLoginTokenExpToleranceSeconds = 3600 * 5; // Default 5 hours
49
+ this.authServerConfig = null;
50
+ this.authServerFullLoginUrl = null;
51
+ // This cache stores locally the jwt token set for refresh and logout.
52
+ this.cache = cache;
53
+ // To terminate sessions
54
+ this.clientOidc = null;
55
+ // This client keeps a refresh of the rotating keys
56
+ this.clientJwk = null;
57
+ this.publicKeyFetch = null;
58
+ // URL temporal hash
59
+ this.securityService = null;
60
+ this.securityUrlCookieKey = null;
61
+ }
62
+ async generateGuid() {
63
+ return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
64
+ const r = Math.random() * 16 | 0;
65
+ const v = (c === 'x') ? r : (r & 0x3 | 0x8);
66
+ return v.toString(16);
67
+ });
68
+ }
69
+
70
+ async connect(_securityService, hapiServer, config) {
71
+ contextConfig = config
72
+ this.authServerConfig = contextConfig;
73
+ securityService = _securityService
74
+ const {
75
+ authServer,
76
+ activateTraceApiMethod
77
+ } = config
78
+ if (activateTraceApiMethod) {
79
+ this.activateTraceApiMethod = activateTraceApiMethod
80
+ }
81
+ let oidcConfiguration = {}
82
+ const stateOption = {
83
+ clearInvalid: true,
84
+ encoding: 'base64',
85
+ isSecure: true,
86
+ isHttpOnly: true,
87
+ isSameSite: 'Lax',
88
+ path: '/',
89
+ strictHeader: true
90
+ }
91
+ try {
92
+ if (authServer.sessionCookiesDomain) {
93
+ stateOption.domain = authServer.sessionCookiesDomain
94
+ }
95
+ stateOption.isHttpOnly = authServer.isHttpOnlyForSessionState ?? false;
96
+ hapiServer.state(this.COOKIE_NAMES.SESSION_STATE, stateOption)
97
+ oidcConfiguration = await this.configuration(authServer)
98
+ if (oidcConfiguration.clientOidc) {
99
+ this.clientOidc = oidcConfiguration.clientOidc;
100
+ }
101
+ if (!authServer.scope || !authServer.scope.split(' ').some((reg) => reg === 'openid')) {
102
+ authServer.scope = `openid ${authServer.scope || ''}`
103
+ authServer.scope.trim();
104
+ }
105
+ this.authServerConfig.authServer.scope ? this.authServerConfig.authServer.scope.trim().replace(/\s+/g, '%20') : 'openid';
106
+ if (authServer.tokenEndpoint && !authServer.tokenEndpoint.match(/https.*/)) {
107
+ hapiServer.states.cookies[this.COOKIE_NAMES.SID].isSecure = false
108
+ hapiServer.states.cookies[this.COOKIE_NAMES.SESSION_STATE].isSecure = false
109
+ }
110
+ trace('INFO', 'The following configuration was initialized')
111
+ const securityConfiguration = Object.fromEntries(Object.entries(authServer).filter((entry) => !['clientSecret', 'PrivateKey', 'PublicKey'].includes(entry[0])))
112
+ trace('INFO', oidcConfiguration.tokenEndpoint ? oidcConfiguration : securityConfiguration)
113
+
114
+ // cookie specifically used for path hash
115
+ this.securityUrlCookieKey = securityService.getSecureUrlCookieKey();
116
+ if (this.securityUrlCookieKey) {
117
+ const securityUrlCookieStateOptions = {
118
+ ...stateOption,
119
+ isHttpOnly: false,
120
+ ttl: null,
121
+ };
122
+ hapiServer.state(this.securityUrlCookieKey, securityUrlCookieStateOptions);
123
+ }
124
+ } catch (err) {
125
+ trace('ERROR', `Exception ${err.message}`)
126
+ trace('ERROR', err.stack)
127
+ }
128
+ // set the scope
129
+ const me = this
130
+ // Add Plugins
131
+
132
+ this.configurePlugins(hapiServer);
133
+ // onPreAuth: Here we check if the jwtToken in yar, recompose the authorization header before hapi jwt module auth.
134
+ // Http protocol does not redirect all headers on a 3XX code.
135
+ hapiServer.ext('onPreAuth', async (request, h) => {
136
+ // add cookie para lo de flavio
137
+ if (this.securityUrlCookieKey){
138
+ const clientId = request.state[this.securityUrlCookieKey];
139
+ if (!clientId) {
140
+ const securityUrlCookieKeyValue = await this.generateGuid();
141
+ h.state(this.securityUrlCookieKey, securityUrlCookieKeyValue); // Set cookie
142
+ }
143
+ }
144
+
145
+ // Retrieve token info from yar storage
146
+ let tokenInfo = request.yar.get('jwtToken');
147
+ if (tokenInfo) {
148
+ // check if token is about to be expired or absent, if so, update
149
+ let aboutToExpire = await me.tokenAboutToExpire(tokenInfo.token, 10);
150
+ if (aboutToExpire) {
151
+ // If refresh token is expired as well, then the user MUST re-login
152
+ let isRefreshTokenExpired = await this.isRefreshTokenExpired(tokenInfo.refreshToken);
153
+ let refreshTokenPresent = 'refreshToken' in tokenInfo;
154
+ if (isRefreshTokenExpired && refreshTokenPresent) {
155
+ // clear token from cookies and exit
156
+ request.yar.get('jwtToken', true);
157
+ delete request.headers.authorization; // Remove the authorization header
158
+ await request.yar.commit(h);
159
+ return h.continue;
160
+ } else {
161
+ // If refresh token is present and not expired, attempt refresh
162
+ let refreshedTokens = await this.refreshToken(tokenInfo.refreshToken);
163
+ // Check that this method returned a valid set of tokens
164
+ if (refreshedTokens && refreshedTokens.token_type &&
165
+ (refreshedTokens.id_token || refreshedTokens.access_token) && refreshedTokens.session_state &&
166
+ refreshedTokens.refresh_token) {
167
+ let refreshedTokenInfo = {
168
+ tokenType: 'Bearer',
169
+ token: refreshedTokens.id_token,
170
+ tokenSubType: 'id_token',
171
+ refreshToken: refreshedTokens.refresh_token
172
+ };
173
+ request.yar.set('jwtToken', refreshedTokenInfo);
174
+ await request.yar.commit(h);
175
+ tokenInfo = refreshedTokenInfo;
176
+ } else {
177
+ // Refresh token failed, clear and continue
178
+ request.yar.get('jwtToken', true);
179
+ delete request.headers.authorization;
180
+ await request.yar.commit(h);
181
+ return h.continue;
182
+ }
183
+ }
184
+ }
185
+ switch (tokenInfo.tokenType) {
186
+ case 'Bearer':
187
+ case 'bearer': {
188
+ request.headers.authorization = `Bearer ${tokenInfo.token}`;
189
+ break;
190
+ }
191
+ default:
192
+ break;
193
+ }
194
+ }
195
+ return h.continue;
196
+ });
197
+ hapiServer.ext('onPreResponse', async (request, h) => {
198
+ const response = request.response;
199
+
200
+ let authError = request.yar.get('authError', true);
201
+ await request.yar.commit(h);
202
+ // By this point, token refresh was already attempted in onPreAuth event, so it redirects to login on unauthorized
203
+ if (response.isBoom && response.output.statusCode === 401 && !request.path.startsWith('/auth/callback') && !authError) {
204
+ // Create the url query string parameters. with a random code verifier, store in yar and get the codeChallenge
205
+ const codeVerifier = crypto.randomBytes(32).toString('base64url');
206
+ request.yar.set('code_verifier', codeVerifier); // For PKCE auth flow
207
+ request.yar.set('originalUrlPathName', me.getFullUrl(request)); // For redirect after login
208
+ await request.yar.commit(h);
209
+
210
+ const codeChallenge = crypto.createHash('sha256').update(codeVerifier).digest('base64url');
211
+ const responseType = 'code'; // Authorization code grant
212
+ const redirectUri = me.getRedirectUriPath(request, 'auth/callback');
213
+ const codeChallengeMethod = 'S256'; // PKCE method
214
+ const scope = (authServer.scope) ? authServer.scope.trim().replace(/\s+/g, '%20') : 'openid';
215
+
216
+ const authLoginUrlWithParams = new URL(authServer.authorizationEndpoint);
217
+ authLoginUrlWithParams.searchParams.set('client_id', me.authServerConfig.authServer.clientId);
218
+ authLoginUrlWithParams.searchParams.set('response_type', responseType);
219
+ authLoginUrlWithParams.searchParams.set('redirect_uri', redirectUri);
220
+ authLoginUrlWithParams.searchParams.set('scope', scope);
221
+ authLoginUrlWithParams.searchParams.set('code_challenge', codeChallenge);
222
+ authLoginUrlWithParams.searchParams.set('code_challenge_method', codeChallengeMethod);
223
+
224
+ // Redirect to Keycloak
225
+ return h.redirect(authLoginUrlWithParams.toString()).takeover();
226
+ }
227
+ return h.continue;
228
+ });
229
+ // /auth/callback : Resolves the jwt token on a callback after the login
230
+ hapiServer.route({
231
+ method: 'GET',
232
+ path: '/auth/callback',
233
+ options: {
234
+ auth: false, // Disable authentication for this route
235
+ },
236
+ handler: async (request, h) => {
237
+ const authCode = request.query.code;
238
+ if (!authCode) {
239
+ return h.response('Authorization code missing').code(400);
240
+ }
241
+ try {
242
+ // Grab the code verifier
243
+ let codeVerifier = request.yar.get('code_verifier', true);
244
+ let tokenResponse = await axios.post(
245
+ me.authServerConfig.authServer.tokenEndpoint,
246
+ new URLSearchParams({
247
+ grant_type: 'authorization_code',
248
+ client_id: me.authServerConfig.authServer.clientId,
249
+ client_secret: me.authServerConfig.authServer.clientSecret, // If required
250
+ code: authCode,
251
+ redirect_uri: me.getRedirectUriPath(request, 'auth/callback'),
252
+ code_verifier: codeVerifier
253
+ }).toString(), {
254
+ headers: {
255
+ 'Content-Type': 'application/x-www-form-urlencoded',
256
+ },
257
+ }
258
+ );
259
+ if (!tokenResponse.statusText === 'OK') {
260
+ throw new Error('Failed to exchange code for tokens');
261
+ }
262
+ let obtainedTokens = {};
263
+ obtainedTokens.tokenType = 'Bearer';
264
+ if (tokenResponse.data.id_token) {
265
+ obtainedTokens.token = tokenResponse.data.id_token;
266
+ obtainedTokens.tokenSubType = 'id_token';
267
+ } else {
268
+ obtainedTokens.token = tokenResponse.data.access_token;
269
+ obtainedTokens.tokenSubType = 'access_token';
270
+ }
271
+ obtainedTokens.refreshToken = tokenResponse.data.refresh_token;
272
+
273
+ let originalUrlPathName = request.yar.get('originalUrlPathName') ?? '/'
274
+ // Set session state
275
+ const sessionState = request.query.session_state;
276
+ h.state(this.COOKIE_NAMES.SESSION_STATE, sessionState);
277
+
278
+ // Store the JWT token in the `Authorization` header or a cookie
279
+ switch (obtainedTokens.tokenType) {
280
+ case 'Bearer':
281
+ case 'bearer': {
282
+ request.yar.set('jwtToken', obtainedTokens);
283
+ await request.yar.commit(h);
284
+ return h.redirect(originalUrlPathName).takeover();
285
+ }
286
+ default: {
287
+ break;
288
+ }
289
+ }
290
+ return h.continue; // Continue in case no token_type -> no auth header configured
291
+ } catch (error) {
292
+ request.yar.set('authError', false);
293
+ await request.yar.commit(h);
294
+ console.error('Failed to exchange code for token:', error.response?.data || error.message);
295
+ return h.response('Failed to authenticate').code(500).takeover();
296
+ }
297
+ },
298
+ });
299
+ // /get-authorization
300
+ hapiServer.route({
301
+ method: 'GET',
302
+ path: '/get-authorization',
303
+ handler: async (request, h) => {
304
+ try {
305
+ const {
306
+ session_state: ckSessionState
307
+ } = request.state
308
+ if (!ckSessionState) {
309
+ throw new Exception("Keycloack get-authorization: Session cookie doesn't exist.", 'CookiesError', 404)
310
+ }
311
+ const tokenSet = await me.openIdConnect.tokenSet()
312
+ const tokens = await tokenSet.tokens(ckSessionState)
313
+ const uma = await Uma.permission()
314
+ const token = await uma.ticket({
315
+ tokenUrl: authServer.tokenEndpoint || authServer.tokenUrl,
316
+ token: tokens.access_token,
317
+ audience: authServer.clientId
318
+ })
319
+ const sourceData = Jsonwebtoken.decode(token.access_token)
320
+ return h.response(JSON.stringify(sourceData.authorization)).takeover()
321
+ } catch (err) {
322
+ return errorResponse(h, err, 401)
323
+ }
324
+ }
325
+ })
326
+ // /get-security-rules
327
+ hapiServer.route({
328
+ method: 'GET',
329
+ path: '/get-security-rules',
330
+ handler: async (request, h) => {
331
+ try {
332
+ const securityRules = await securityService.getFrontendSecurityRules(request)
333
+ return h.response(JSON.stringify(securityRules)).takeover()
334
+ } catch (err) {
335
+ return errorResponse(h, err, 401)
336
+ }
337
+ }
338
+ })
339
+ // /get-permissions
340
+ hapiServer.route({
341
+ method: 'GET',
342
+ path: '/get-permissions',
343
+ handler: async (request, h) => {
344
+ try {
345
+ const permissions = await securityService.getPermissions()
346
+ return h.response(JSON.stringify(permissions)).takeover()
347
+ } catch (err) {
348
+ return errorResponse(h, err, 401)
349
+ }
350
+ }
351
+ })
352
+ // /get-user-info
353
+ hapiServer.route({
354
+ method: 'GET',
355
+ path: '/get-user-info',
356
+ handler: async (request, h) => {
357
+ try {
358
+ const userInfo = await securityService.getUserInfo(request)
359
+ return h
360
+ .response(JSON.stringify(userInfo))
361
+ .takeover()
362
+ } catch (err) {
363
+ return errorResponse(h, err, 500)
364
+ }
365
+ }
366
+ })
367
+ // /logout
368
+ hapiServer.route({
369
+ path: '/logout',
370
+ method: 'GET',
371
+ options: {
372
+ auth: false, // Disable authentication for this route TODO:
373
+ },
374
+ handler: async (request, h) => {
375
+ try {
376
+ const ckSessionState = request.state[this.COOKIE_NAMES.SESSION_STATE]
377
+ request.yar.clear('jwtToken');
378
+ await request.yar.commit(h);
379
+ let endSessionUrl = await me.endSessionUrl(me.getRedirectUri(request), me.clientOidc);
380
+ return h
381
+ .response()
382
+ .unstate(this.COOKIE_NAMES.SID)
383
+ .unstate(this.COOKIE_NAMES.SESSION_STATE)
384
+ .unstate(this.COOKIE_NAMES.AUTH_TOKEN)
385
+ .redirect(endSessionUrl)
386
+ .takeover()
387
+ } catch (err) {
388
+ return errorResponse(h, err, 500)
389
+ }
390
+ }
391
+ })
392
+ // /invalid-session
393
+ hapiServer.route({
394
+ path: '/invalid-session',
395
+ method: 'GET',
396
+ handler: async (request, h) => {
397
+ try {
398
+ const endSessionUrl = await me.openIdConnect.endSessionUrl({
399
+ redirectUri: this.getRedirectUri(request),
400
+ sessionState: request.state[this.COOKIE_NAMES.SESSION_STATE]
401
+ })
402
+ return h
403
+ .response()
404
+ .unstate(this.COOKIE_NAMES.SID)
405
+ .unstate(this.COOKIE_NAMES.SESSION_STATE)
406
+ .redirect(endSessionUrl)
407
+ .takeover()
408
+ } catch (err) {
409
+ return errorResponse(h, err, 500)
410
+ }
411
+ }
412
+ })
413
+ // /check-session-iframe.html
414
+ hapiServer.route({
415
+ path: '/check-session-iframe.html',
416
+ method: 'GET',
417
+ handler: async (_request, h) => {
418
+ try {
419
+ let content = '<html/>'
420
+ if (authServer && authServer.checkSessionIframe) {
421
+ const {
422
+ checkSessionIframe: sessionIframeUrl,
423
+ clientId,
424
+ sessionCookiesPrefix
425
+ } = authServer
426
+ if (sessionIframeUrl && sessionIframeUrl.includes('https://')) {
427
+ trace('INFO', `Session management url: ${sessionIframeUrl}`)
428
+ content = getTemplate('session-iframe', {
429
+ sessionIframeUrl,
430
+ clientId,
431
+ sessionCookiesPrefix: sessionCookiesPrefix || ''
432
+ })
433
+ } else {
434
+ trace('WARN', 'For session management, it is necessary to get the value from a cookie called session_state, and as a good practice, it should have reached a secure context [TLS].')
435
+ }
436
+ }
437
+ return h
438
+ .response(content)
439
+ .header('Content-Type', 'text/html')
440
+ } catch (err) {
441
+ return errorResponse(h, err, 500)
442
+ }
443
+ }
444
+ });
445
+ // /check-session
446
+ hapiServer.route({
447
+ path: '/check-session',
448
+ options: {
449
+ auth: false
450
+ },
451
+ method: 'GET',
452
+ handler: async (request, h) => {
453
+ let tokenInfo = request.yar.get('jwtToken');
454
+ let tokenIsExpired = { expired: false }
455
+ if (tokenInfo) {
456
+ // check if refresh token is about to be expired
457
+ tokenIsExpired.expired = await this.tokenAboutToExpire(tokenInfo.refreshToken, 0.5);
458
+ if (tokenIsExpired.expired) {
459
+ tokenIsExpired.redirectUrl = await this.getFullKeycloakLoginUri(request, h)
460
+ request.yar.clear('jwtToken');
461
+ request.yar.clear('userRelog');
462
+ }
463
+ }
464
+ return h.response(tokenIsExpired);
465
+ }
466
+ });
467
+ }
468
+ // this function takes h, because i need to set yar storage, and avoid overprocessing
469
+ async getFullKeycloakLoginUri(request, h) {
470
+ const codeVerifier = crypto.randomBytes(32).toString('base64url');
471
+ request.yar.set('code_verifier', codeVerifier); // For PKCE auth flow
472
+ request.yar.set('originalUrlPathName', this.getBaseUrl(request) ); // For redirect after login
473
+ await request.yar.commit(h);
474
+
475
+ const responseType = 'code'; // Authorization code grant
476
+ const redirectUri = this.getRedirectUriPath(request, 'auth/callback');
477
+ const scope = this.authServerConfig.authServer.scope;
478
+ const codeChallenge = crypto.createHash('sha256').update(codeVerifier).digest('base64url');
479
+ const codeChallengeMethod = 'S256'; // PKCE method
480
+
481
+ const authLoginUrlWithParams = new URL(this.authServerConfig.authServer.authorizationEndpoint);
482
+ authLoginUrlWithParams.searchParams.set('client_id', this.authServerConfig.authServer.clientId);
483
+ authLoginUrlWithParams.searchParams.set('response_type', responseType);
484
+ authLoginUrlWithParams.searchParams.set('redirect_uri', redirectUri);
485
+ authLoginUrlWithParams.searchParams.set('scope', scope);
486
+ authLoginUrlWithParams.searchParams.set('code_challenge', codeChallenge);
487
+ authLoginUrlWithParams.searchParams.set('code_challenge_method', codeChallengeMethod);
488
+ return authLoginUrlWithParams.toString();
489
+ }
490
+ getRedirectUri(request) {
491
+ return contextConfig.authServer.redirectUri || getFullUrl(request)
492
+ }
493
+ getRedirectUriPath(request, redirectPath) {
494
+ const baseUrl = this.getBaseUrl(request);
495
+ const path = (redirectPath) ?? this.getPathname(request);
496
+ let url = new URL(path, baseUrl);
497
+
498
+ // If the hostname is not localhost, force HTTPS
499
+ if (url.hostname !== 'localhost') {
500
+ url.protocol = 'https:';
501
+ }
502
+ return url.toString();
503
+ }
504
+
505
+ getFullUrl(request) {
506
+ return `${getProtocol(request)}://${getHost(request)}${getPathname(request)}`
507
+ }
508
+ getBaseUrl(request) {
509
+ return `${getProtocol(request)}://${getHost(request)}/`
510
+ }
511
+ async authenticate(h, scope) {
512
+ const {
513
+ request
514
+ } = h
515
+ const pkceCode = await this.openIdConnect.pkceCode()
516
+ const requestUrl = getFullUrl(request)
517
+ let oidcMetadata = await this.openIdConnect.oidcMetadata()
518
+ if (!oidcMetadata || !oidcMetadata.openid_configuration) {
519
+ oidcMetadata = await this.configuration(contextConfig.authServer)
520
+ }
521
+ if (requestUrl.match(new RegExp(/^(https?:\/{2}.*):?(\d*)/.source + getHost(request) + /\/?$/.source))) {
522
+ const authorizationUrl = await this.openIdConnect.authorizationUrl({
523
+ scope,
524
+ redirectUri: this.getRedirectUri(request),
525
+ pkceCode
526
+ })
527
+ trace('INFO', `Authenticate redirecting to ${authorizationUrl}`)
528
+ return h
529
+ .response()
530
+ .state(this.COOKIE_NAMES.SID, pkceCode)
531
+ .redirect(authorizationUrl)
532
+ .takeover()
533
+ } else if (getPathname(request) === '/logout') {
534
+ return h.continue
535
+ } else {
536
+ const tokenSet = await this.openIdConnect.tokenSet()
537
+ const {
538
+ state
539
+ } = request
540
+ if (tokenSet && state && state[this.COOKIE_NAMES.SESSION_STATE]) {
541
+ const tokens = await tokenSet.tokens(state[this.COOKIE_NAMES.SESSION_STATE])
542
+ if (!tokens || tokens.refresh_expires_in <= getTokenTolerance(0)) {
543
+ throw new Exception('Error when getting token', 'ExpirationError', 403)
544
+ }
545
+ return h.continue
546
+ } else {
547
+ return h
548
+ .response()
549
+ .code(401)
550
+ .takeover()
551
+ }
552
+ }
553
+ }
554
+ async configurePlugins(server) {
555
+ // Hapi Yar module, saves info in the cookies across session calls
556
+ const hapiYarPassword = process.env.blz_hapiYarPassword || 'your-super-secure-yar-atleast-32-bytes-password';
557
+ await server.register({
558
+ plugin: hapiYar,
559
+ options: {
560
+ cookieOptions: {
561
+ password: hapiYarPassword,
562
+ isSecure: true, // Use true in production
563
+ isHttpOnly: true,
564
+ isSameSite: 'Lax', // 'Strict', 'Lax', or 'None'
565
+ clearInvalid: true,
566
+ ignoreErrors: true
567
+ },
568
+ storeBlank: false, // Prevent saving blank sessions
569
+ maxCookieSize: 0 // Use server-side storage for larger payloads
570
+ }
571
+ });
572
+ // Register @hapi/jwt plugin
573
+ await server.register(hapiJwt);
574
+
575
+ // Use rotating certificates with keysFetch function for jwt module
576
+ this.startupJwksClient();
577
+ // set up the function in this.publickKeyFetch
578
+ this.startupPublickKeyFetch();
579
+
580
+ // Define the auth strategy
581
+ server.auth.strategy('jwtAuth', 'jwt', {
582
+ keys: this.publicKeyFetch,
583
+ verify: {
584
+ aud: this.authServerConfig.authServer.audience ?? false,
585
+ iss: this.authServerConfig.authServer.issuer,
586
+ exp: true,
587
+ sub: false
588
+ },
589
+ validate: false
590
+ });
591
+
592
+ // Register the @hapi/cookie plugin
593
+ await server.register(hapiCookie);
594
+
595
+ // Define the cookie-based auth strategy
596
+ server.auth.strategy('cookieAuth', 'cookie', {
597
+ cookie: {
598
+ name: 'sid', // Primary session cookie
599
+ password: 'supersecretpasswordmustbeatleast32characterslong', // Encryption key
600
+ isSecure: true, // Should be true in production
601
+ isHttpOnly: true, // Prevents client-side JavaScript access
602
+ isSameSite: 'Lax', // Protects against CSRF
603
+ },
604
+ keepAlive: true, // automatically sets the session cookie after validation to extend the current session for a new ttl duration. Defaults to false.
605
+ redirectTo: false, //function(request) {}, // Redirect if authentication fails
606
+ });
607
+ // Set default auth strategy to try both JWT and cookies
608
+ server.auth.default({
609
+ strategies: ['jwtAuth', 'cookieAuth'], // Try JWT first, then Cookie
610
+ });
611
+ }
612
+
613
+ async configuration(authServer) {
614
+ if (!authServer) {
615
+ throw new Exception('Error when getting configuration attributes ')
616
+ }
617
+ const {
618
+ clientId,
619
+ clientSecret
620
+ } = authServer
621
+ await this.openIdConnect.client({
622
+ clientId,
623
+ clientSecret
624
+ })
625
+ if (authServer.openIdConfigurationEndpoint) {
626
+ return await this.openIdConnect.configuration(authServer.openIdConfigurationEndpoint)
627
+ } else {
628
+ // If configuration uri does not exist but the auth server form has been filled in.
629
+ return await this.openIdConnect.configuration({
630
+ issuer: authServer.issuer,
631
+ authorization_endpoint: authServer.authorizationEndpoint,
632
+ token_endpoint: authServer.tokenEndpoint,
633
+ userinfo_endpoint: authServer.userinfoEndpoint,
634
+ end_session_endpoint: authServer.endSessionEndpoint,
635
+ jwks_uri: authServer.jwksUri
636
+ })
637
+ }
638
+ }
639
+
640
+ async endSessionUrl(redirectUri, clientOidc) {
641
+ redirectUri = redirectUri.replace(/logout|invalid-session/gmi, '')
642
+ // Log off specific session.
643
+ if (!clientOidc) {
644
+ throw new Error('Unable to get configuration from identity provider', 'ConfigurationError', 404);
645
+ }
646
+ return clientOidc.endSessionUrl({
647
+ post_logout_redirect_uri: redirectUri
648
+ })
649
+ }
650
+ oidcMetadataKey() {
651
+ return this.authServerConfig.authServer.sessionCookiesDomain || 'oidcMetadata'
652
+ }
653
+ async configuration(context) {
654
+ let metadata = await this.cache.get(this.oidcMetadataKey())
655
+ if (typeof context === 'string' && !context.match(/(https?:\/\/.*):?(\d*)\/?(.*)/gi)) {
656
+ throw new Exception('Wrong OpenId Provider configuration URI entered', 'AttributeError', 403)
657
+ }
658
+ if (!metadata || !metadata.issuer) {
659
+ if (context.issuer) {
660
+ metadata = {
661
+ ...(metadata || {}),
662
+ ...context
663
+ }
664
+ } else {
665
+ metadata = metadata || {}
666
+ metadata.openid_configuration = context
667
+ metadata = {
668
+ ...metadata,
669
+ ...(await Issuer.discover(context.issuer))
670
+ } // Discover an issuer configuration, must be an url
671
+ }
672
+ await this.cache.set(this.oidcMetadataKey(), metadata, 864e5) // 1 day of cache
673
+ }
674
+ return new Iss(metadata)
675
+ }
676
+ async refreshToken(refreshToken) {
677
+ // Make a POST request to Keycloak to refresh the token
678
+ const response = await axios.post(this.authServerConfig.authServer.tokenEndpoint,
679
+ new URLSearchParams({
680
+ grant_type: 'refresh_token',
681
+ client_id: this.authServerConfig.authServer.clientId,
682
+ client_secret: this.authServerConfig.authServer.clientSecret,
683
+ refresh_token: refreshToken,
684
+ }).toString(), {
685
+ headers: {
686
+ 'Content-Type': 'application/x-www-form-urlencoded',
687
+ },
688
+ }
689
+ );
690
+
691
+ if (!(response.status === 200)) {
692
+ const errorResponse = await response.json();
693
+ console.error('Error refreshing token:', errorResponse);
694
+ return errorResponse;
695
+ }
696
+ // Refresh token response may change from time to time, here are two possible responses
697
+ try {
698
+ return await response.json(); // all tokens refershed
699
+
700
+ } catch (error) {
701
+
702
+ }
703
+ try {
704
+ return response.data;
705
+ } catch {
706
+
707
+ }
708
+
709
+ }
710
+ async decodeJwtToken(token) {
711
+ const decodedToken = hapiJwt.token.decode(token);
712
+ return decodedToken;
713
+ }
714
+ async tokenAboutToExpire(token, minutesBeforeExpiration = 0) {
715
+ if (!token)
716
+ return true;
717
+ const decodedToken = hapiJwt.token.decode(token);
718
+ const expirationTime = decodedToken.decoded.payload.exp * 1000; // Convert to milliseconds
719
+ const currentTime = Date.now();
720
+ const expirationThreshold = minutesBeforeExpiration * 60 * 1000; // Convert minutes to milliseconds
721
+
722
+ // Check if the token is expired or about to expire within the specified minutes
723
+ const isAboutToExpire = expirationTime - currentTime <= expirationThreshold;
724
+ return isAboutToExpire;
725
+ }
726
+ async isRefreshTokenExpired(refreshToken) {
727
+ try {
728
+ // Decode the token without verifying its signature.
729
+ const decodedRefreshToken = hapiJwt.token.decode(refreshToken);
730
+ // Get the current timestamp (in seconds).
731
+ const currentTimestamp = Math.floor(Date.now() / 1000);
732
+
733
+ if (decodedRefreshToken && decodedRefreshToken.decoded && decodedRefreshToken.decoded.payload && decodedRefreshToken.decoded.payload.exp) {
734
+ return (decodedRefreshToken.decoded.payload.exp < currentTimestamp)
735
+ } else
736
+ return true;
737
+ } catch (error) {
738
+ // if there is an error treat as if expired, so a re-login is prompted
739
+ console.error('Failed to decode the token: Invalid Refresh token format', error);
740
+ return true;
741
+ }
742
+ }
743
+
744
+ async startupJwksClient() {
745
+ // Rotating certificates, prepare for the hapi jwt module
746
+ this.clientJwk = jwksClient({
747
+ jwksUri: this.authServerConfig.authServer.jwksUri,
748
+ cache: true, // Cache signing keys to avoid frequent network calls
749
+ rateLimit: true, // Rate limit the number of requests to the JWKS URI
750
+ jwksRequestsPerMinute: 10, // Limit to 10 requests per minute
751
+ });
752
+ }
753
+ async startupPublickKeyFetch() {
754
+ // Function to get the signing key
755
+ const getKey = async (kid) => {
756
+ return new Promise((resolve, reject) => {
757
+ this.clientJwk.getSigningKey(kid, (err, key) => {
758
+ if (err) {
759
+ return reject(err);
760
+ }
761
+ const signingKey = key.getPublicKey(); // Public key for signature verification
762
+ resolve(signingKey);
763
+ });
764
+ });
765
+ };
766
+ this.publicKeyFetch = async (artifacts) => {
767
+ const kid = artifacts.decoded.header.kid; // Extract 'kid' from JWT header
768
+ return getKey(kid); // Fetch the corresponding public key
769
+ }
770
+ }
771
+ }
772
+
773
+ class Iss {
774
+ /**
775
+ * @constructor
776
+ * @param {Object} metadata
777
+ */
778
+ constructor(metadata) {
779
+ if (!metadata.id_token_signing_alg_values_supported) {
780
+ metadata.id_token_signing_alg_values_supported = ['RS256']
781
+ }
782
+ if (!metadata.response_types_supported) {
783
+ metadata.response_types_supported = ['code', 'none', 'id_token', 'token', 'id_token token', 'code id_token', 'code token', 'code id_token token']
784
+ }
785
+ if (!metadata.subject_types_supported) {
786
+ metadata.subject_types_supported = ['public']
787
+ }
788
+ const claimsRequired = METADATA.filter(({
789
+ type
790
+ }) => type === 'REQUIRED');
791
+ const missingClaims = [];
792
+
793
+ for (const claim of claimsRequired) {
794
+ const normalizedToCamelClaimName = claim.name.toLowerCase().replace(/_([a-z])/g, (_, letter) => letter.toUpperCase());
795
+ const attributeCamelCase = metadata[normalizedToCamelClaimName]; // Directly access metadata
796
+ const attributeSnakeCase = metadata[claim.name]; // Directly access metadata
797
+ if (!attributeSnakeCase && !attributeCamelCase) {
798
+ missingClaims.push(claim);
799
+ }
800
+ }
801
+
802
+ if (missingClaims.length > 0) {
803
+ console.error(JSON.stringify(missingClaims));
804
+ throw new Error(JSON.stringify(missingClaims));
805
+ }
806
+
807
+ // Issuer needs the metadata in snake_case
808
+ const issuer = metadata.Client ? metadata : new Issuer(this.#camelToSnakeCase(metadata))
809
+ // Client instance for the authorization server of that issuer.
810
+ const clientPayload = {
811
+ client_id: metadata.clientId,
812
+ response_type: 'code'
813
+ }
814
+ if (metadata.clientSecret) {
815
+ clientPayload.client_secret = metadata.clientSecret
816
+ }
817
+ this.clientOidc = new issuer.Client(clientPayload);
818
+ }
819
+ #camelToSnakeCase(obj) {
820
+ const toSnakeCase = str => str.replace(/[A-Z]/g, letter => `_${letter.toLowerCase()}`);
821
+
822
+ if (typeof obj !== 'object' || obj === null) return obj;
823
+
824
+ if (Array.isArray(obj)) {
825
+ return obj.map(item => this.#camelToSnakeCase(item));
826
+ }
827
+
828
+ return Object.entries(obj).reduce((acc, [key, value]) => {
829
+ const newKey = toSnakeCase(key);
830
+ acc[newKey] = typeof value === 'object' && value !== null ?
831
+ this.#camelToSnakeCase(value) :
832
+ value;
833
+ return acc;
834
+ }, {});
835
+ }
836
+ }
837
+
838
+ module.exports = {
839
+ HapiServerKeycloak
840
+ }