@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,974 @@
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
+ var jwkToPem = require('jwk-to-pem');
30
+ // Uses Issue to cache manage and logout (generators/customs not sure why yet)
31
+ const { Issuer, generators, custom } = require('openid-client') // OpenID Certified Relying Party.
32
+ const { METADATA } = require('../helpers/consts')
33
+ // Azure AD rotates keys, so we jwk used to routinly fetch them
34
+ const jwksClient = require('jwks-rsa') // Retrieve RSA public keys from a JWKS.
35
+ // MS authenticator library
36
+ const { ConfidentialClientApplication } = require('@azure/msal-node');
37
+
38
+ let contextConfig = {}
39
+ let securityService = null
40
+
41
+ class HapiServer {
42
+ constructor (openIdConnect, cookiesName, cache) {
43
+ this.openIdConnect = openIdConnect
44
+ this.COOKIE_NAMES = cookiesName
45
+ this.activateTraceApiMethod = false
46
+ this.queryStringLimit = null;
47
+ this.securityLoginTokenExpToleranceSeconds = 3600 * 5; // Default 5 hours
48
+ this.authServerConfig = null;
49
+ this.authServerFullLoginUrl = null;
50
+ // This cache stores locally the jwt token set for refresh and logout.
51
+ this.cache = cache;
52
+ // To terminate sessions
53
+ this.clientOidc = null;
54
+ // This client keeps a refresh of the rotating keys
55
+ this.clientJwk = null;
56
+ this.publicKeyFetch = null;
57
+ }
58
+
59
+ async connect (_securityService, hapiServer, config) {
60
+ contextConfig = config
61
+ this.authServerConfig = contextConfig;
62
+ securityService = _securityService
63
+ const { authServer, accessTokenSimulation, activateTraceApiMethod } = config
64
+ if (activateTraceApiMethod) {
65
+ this.activateTraceApiMethod = activateTraceApiMethod
66
+ }
67
+ let oidcConfiguration = {}
68
+ const stateOption = {
69
+ clearInvalid: true,
70
+ encoding: 'base64',
71
+ isSecure: true,
72
+ isHttpOnly: true,
73
+ isSameSite: 'Lax',
74
+ path: '/',
75
+ strictHeader: true
76
+ }
77
+ if (accessTokenSimulation && !authServer) {
78
+ hapiServer.config = config
79
+ hapiServer.state(this.COOKIE_NAMES.ACCESS_TOKEN, stateOption)
80
+ this.authServerSimulation(context)
81
+ } else {
82
+ try {
83
+ if (authServer.sessionCookiesDomain) {
84
+ stateOption.domain = authServer.sessionCookiesDomain
85
+ }
86
+ const isHttpOnlyForSessionState = authServer.isHttpOnlyForSessionState !== undefined ? authServer.isHttpOnlyForSessionState : false
87
+ // hapiServer.state(this.COOKIE_NAMES.SID, stateOption)
88
+ // stateOption.encoding = 'none'
89
+ // stateOption.strictHeader = false
90
+ // stateOption.isHttpOnly = isHttpOnlyForSessionState
91
+ hapiServer.state(this.COOKIE_NAMES.SESSION_STATE, stateOption)
92
+ oidcConfiguration = await this.configuration(authServer)
93
+ if (oidcConfiguration.clientOidc) {
94
+ this.clientOidc = oidcConfiguration.clientOidc;
95
+ }
96
+ if (!authServer.scope || !authServer.scope.split(' ').some((reg) => reg === 'openid')) {
97
+ authServer.scope = `openid ${authServer.scope || ''}`
98
+ }
99
+ if (authServer.tokenEndpoint && !authServer.tokenEndpoint.match(/https.*/)) {
100
+ hapiServer.states.cookies[this.COOKIE_NAMES.SID].isSecure = false
101
+ hapiServer.states.cookies[this.COOKIE_NAMES.SESSION_STATE].isSecure = false
102
+ }
103
+ trace('INFO', 'The following configuration was initialized')
104
+ const securityConfiguration = Object.fromEntries(Object.entries(authServer).filter((entry) => !['clientSecret', 'PrivateKey', 'PublicKey'].includes(entry[0])))
105
+ trace('INFO', oidcConfiguration.tokenEndpoint ? oidcConfiguration : securityConfiguration)
106
+ } catch (err) {
107
+ trace('ERROR', `Exception ${err.message}`)
108
+ trace('ERROR', err.stack)
109
+ }
110
+
111
+ this.prepareMemoryValues();
112
+ // Add Plugins
113
+ this.configurePlugins(hapiServer);
114
+ // onPreAuth: Here we check if the jwtToken is stored in the yar, refresh, and recompose the authorization header before hapi jwt module auth.
115
+ // Http protocol does not redirect all headers on a 3XX code.
116
+ hapiServer.ext('onPreAuth', async (request, h) => {
117
+ // Retrieve the token from the yar storage, second parameter absent so that the token is not lost on read
118
+ let tokenInfo = request.yar.get('jwtToken');
119
+ if(tokenInfo) {
120
+ // check if token is about to be expired, if expired, update
121
+ let aboutToExpire = await me.tokenAboutToExpire(tokenInfo.token, 10);
122
+ if (aboutToExpire) {
123
+ if (me.authServerConfig.authServer.msalClient) {
124
+ // Get session name from yar storage: should be in request.yar.get('jwtToken')
125
+
126
+ // To refresh the tokens, Azure uses a silent re authentication
127
+ const silentRereshTokenResponse = await me.authServerConfig.authServer.msalClient.acquireTokenSilent({
128
+ account: tokenInfo.account, // Use stored account details
129
+ scopes: ["User.Read"], // Adjust scopes as needed
130
+ });
131
+ //let refreshedTokens = await this.silentAuthenticationAzure({redirectUri: me.getBaseUrl(request), idToken: tokenInfo.token})
132
+ // Check that all the needed data comes in the silent Authentication, if not send to relog
133
+ if (silentRereshTokenResponse && silentRereshTokenResponse.idToken) {
134
+ // Update the session with the new access token
135
+ const session = request.yar.get('session');
136
+ request.yar.set('session', {
137
+ ...session,
138
+ token: silentRereshTokenResponse.accessToken
139
+ });
140
+ const obtainedTokens = {};
141
+ obtainedTokens.tokenType = 'Bearer';
142
+ obtainedTokens.token = silentRereshTokenResponse.idToken;
143
+ obtainedTokens.tokenSubType = 'id_token';
144
+ obtainedTokens.account = silentRereshTokenResponse.account;
145
+ request.yar.set('jwtToken', obtainedTokens );
146
+ // let refreshedTokenInfo = { tokenType: 'Bearer', token: refreshedTokens.id_token, tokenSubType: 'id_token'};
147
+ // request.yar.set('jwtToken', refreshedTokenInfo);
148
+ // tokenInfo = refreshedTokenInfo;
149
+ await request.yar.commit(h);
150
+ } else {
151
+ // no valid set of tokens has returned, clear the yar storage and continue
152
+ request.yar.get('jwtToken', true);
153
+ await request.yar.commit(h);
154
+ delete request.headers.authorization; // Remove the authorization header
155
+ return h.continue;
156
+ }
157
+ } else {
158
+ // If Provider is Keycloak, execute this block
159
+ // If refresh token is expired as well, then the user MUST re-login
160
+ let isRefreshTokenExpired = await this.isRefreshTokenExpired(tokenInfo.refreshToken);
161
+ let refreshTokenPresent = 'refreshToken' in tokenInfo;
162
+ if (isRefreshTokenExpired && refreshTokenPresent) {
163
+ // clear token from cookies and exit
164
+ request.yar.get('jwtToken', true);
165
+ delete request.headers.authorization; // Remove the authorization header
166
+ await request.yar.commit(h);
167
+ return h.continue;
168
+ } else {
169
+ // If refresh token is present and not expired, attempt refresh
170
+ let refreshedTokens = await this.refreshToken(tokenInfo.refreshToken);
171
+ // Check that this method returned a valid set of tokens
172
+ if (refreshedTokens && refreshedTokens.token_type &&
173
+ refreshedTokens.id_token && refreshedTokens.session_state &&
174
+ refreshedTokens.access_token && refreshedTokens.refresh_token ) {
175
+ let refreshedTokenInfo = { tokenType: 'Bearer', token: refreshedTokens.id_token, tokenSubType: 'id_token', refreshToken: refreshedTokens.refresh_token };
176
+ request.yar.set('jwtToken', refreshedTokenInfo);
177
+ await request.yar.commit(h);
178
+ tokenInfo = refreshedTokenInfo;
179
+ } else {
180
+ // Refresh token failed, clear and continue
181
+ request.yar.get('jwtToken', true);
182
+ delete request.headers.authorization;
183
+ await request.yar.commit(h);
184
+ return h.continue;
185
+ }
186
+ }
187
+ }
188
+ }
189
+ switch(tokenInfo.tokenType) {
190
+ case 'Bearer':
191
+ case 'bearer': {
192
+ request.headers.authorization = `Bearer ${tokenInfo.token}`;
193
+ break;
194
+ }
195
+ default:
196
+ break;
197
+ }
198
+ }
199
+ return h.continue;
200
+ });
201
+ hapiServer.ext('onPreResponse', async (request, h) => {
202
+ const response = request.response;
203
+
204
+ let authError = request.yar.get('authError', true);
205
+ // By this point, token refresh was already attempted in onPreAuth event, so it redirects to login on unauthorized
206
+ if (response.isBoom && response.output.statusCode === 401 && !request.path.startsWith('/auth/callback') && !authError) {
207
+ if (this.authServerConfig.authServer.provider=== 'ad-azure') {
208
+ return h.redirect('/login').takeover();
209
+ const authUrl = await this.authServerConfig.authServer.msalClient.getAuthCodeUrl({
210
+ redirectUri: me.getBaseUrl(request) + 'auth/callback',
211
+ scopes: ['user.read'],
212
+ });
213
+
214
+ return h.redirect(authUrl);
215
+ }
216
+ // Create the url query string parameters. with a random code verifier, store in yar and get the codeChallenge
217
+ const codeVerifier = crypto.randomBytes(32).toString('base64url');
218
+ request.yar.set('code_verifier', codeVerifier); // For PKCE auth flow
219
+ request.yar.set('originalUrlPathName', me.getFullUrl(request)); // For redirect after login
220
+ await request.yar.commit(h);
221
+
222
+ const codeChallenge = crypto.createHash('sha256').update(codeVerifier).digest('base64url');
223
+ const responseType = 'code'; // Authorization code grant
224
+ const redirectUri = me.getBaseUrl(request) + 'auth/callback';
225
+ const codeChallengeMethod = 'S256'; // PKCE method
226
+ const scope = (authServer.scope) ? authServer.scope.replace(/\s+/g, '%20') : 'openid';
227
+
228
+ const authLoginUrlWithParams = new URL(authServer.authorizationEndpoint);
229
+ authLoginUrlWithParams.searchParams.set('client_id', me.authServerConfig.authServer.clientId);
230
+ authLoginUrlWithParams.searchParams.set('response_type', responseType);
231
+ authLoginUrlWithParams.searchParams.set('redirect_uri', redirectUri);
232
+ authLoginUrlWithParams.searchParams.set('scope', scope);
233
+ authLoginUrlWithParams.searchParams.set('code_challenge', codeChallenge);
234
+ authLoginUrlWithParams.searchParams.set('code_challenge_method', codeChallengeMethod);
235
+
236
+ // Redirect to Keycloak
237
+ return h.redirect(authLoginUrlWithParams.toString()).takeover();
238
+ }
239
+ return h.continue;
240
+ });
241
+ // /login so that i can redirect my user to a login.
242
+ hapiServer.route({
243
+ method: 'GET',
244
+ path: '/login',
245
+ options: {
246
+ auth: false, // Disable authentication for this route
247
+ },
248
+ handler: async (request, h) => {
249
+ const authUrl = await me.authServerConfig.authServer.msalClient.getAuthCodeUrl({
250
+ redirectUri: me.getBaseUrl(request) + 'auth/callback',
251
+ scopes: ['user.read'],
252
+ });
253
+
254
+ return h.redirect(authUrl);
255
+ }
256
+ });
257
+ // /auth/callback
258
+ // Resolves the jwt token on a callback after the login (keycloak/azure)
259
+ hapiServer.route({
260
+ method: 'GET',
261
+ path: '/auth/callback',
262
+ options: {
263
+ auth: false, // Disable authentication for this route
264
+ },
265
+ handler: async (request, h) => {
266
+ const authCode = request.query.code;
267
+ if (!authCode) {
268
+ return h.response('Authorization code missing').code(400);
269
+ }
270
+ try {
271
+ let obtainedTokens = {};
272
+ // If we have azure-AD use that lifecycle
273
+ if (me.authServerConfig.authServer.msalClient) {
274
+ if (!authCode) {
275
+ return h.response('Missing authorization code').code(400);
276
+ }
277
+
278
+ try {
279
+ const tokenResponse = await me.authServerConfig.authServer.msalClient.acquireTokenByCode({
280
+ code: authCode,
281
+ redirectUri: me.getBaseUrl(request) + 'auth/callback',
282
+ scopes: ['user.read'],
283
+ });
284
+
285
+ request.yar.set('session', { token: tokenResponse.accessToken, user: tokenResponse.account });
286
+ obtainedTokens.tokenType = 'Bearer';
287
+ obtainedTokens.token = tokenResponse.idToken;
288
+ obtainedTokens.tokenSubType = 'id_token';
289
+ obtainedTokens.account = tokenResponse.account;
290
+ //return h.redirect('/');
291
+ } catch (error) {
292
+ console.error('Auth error:', error);
293
+ return h.response('Authentication failed').code(500);
294
+ }
295
+ } else {
296
+ // This code-block is for keycloak or other oauth0 for now
297
+ // Grab the code verifier
298
+ let codeVerifier = request.yar.get('code_verifier', true);
299
+ tokenResponse = await axios.post(
300
+ me.authServerConfig.authServer.tokenEndpoint,
301
+ new URLSearchParams({
302
+ grant_type: 'authorization_code',
303
+ client_id: me.authServerConfig.authServer.clientId,
304
+ client_secret: me.authServerConfig.authServer.clientSecret, // If required
305
+ code: authCode,
306
+ redirect_uri: me.getRedirectUri(request),
307
+ code_verifier: codeVerifier
308
+ }).toString(),
309
+ {
310
+ headers: {
311
+ 'Content-Type': 'application/x-www-form-urlencoded',
312
+ },
313
+ }
314
+ );
315
+ if (!tokenResponse.statusText ==='OK') {
316
+ throw new Error('Failed to exchange code for tokens');
317
+ }
318
+ obtainedTokens.tokenType = 'Bearer';
319
+ obtainedTokens.token = tokenResponse.data.id_token;
320
+ obtainedTokens.tokenSubType = 'id_token';
321
+ obtainedTokens.refreshToken = tokenResponse.data.refresh_token;
322
+ }
323
+ let originalUrlPathName = request.yar.get('originalUrlPathName') ?? '/'
324
+ // Set session state
325
+ const sessionState = request.query.session_state;
326
+ h.state(this.COOKIE_NAMES.SESSION_STATE, sessionState);
327
+
328
+ // Store the JWT token in the `Authorization` header or a cookie
329
+ switch (obtainedTokens.tokenType){
330
+ case 'Bearer':
331
+ case 'bearer': {
332
+ request.yar.set('jwtToken', obtainedTokens);
333
+ await request.yar.commit(h);
334
+ return h.redirect(originalUrlPathName).takeover();
335
+ }
336
+ default: {
337
+ break;
338
+
339
+ }
340
+ }
341
+ return h.continue; // Continue in case no token_type -> no auth header configured
342
+ } catch (error) {
343
+ request.yar.set('authError', true);
344
+ await request.yar.commit(h);
345
+ console.error('Failed to exchange code for token:', error.response?.data || error.message);
346
+ return h.response('Failed to authenticate').code(500).takeover();
347
+ }
348
+ },
349
+ });
350
+ const me = this
351
+ // /get-authorization
352
+ hapiServer.route({
353
+ method: 'GET',
354
+ path: '/get-authorization',
355
+ handler: async (request, h) => {
356
+ try {
357
+ const { session_state: ckSessionState } = request.state
358
+ if (!ckSessionState) {
359
+
360
+ throw new Exception("Hapi get-authorization: Session cookie doesn't exist.", 'CookiesError', 404)
361
+ }
362
+ const tokenSet = await me.openIdConnect.tokenSet()
363
+ const tokens = await tokenSet.tokens(ckSessionState)
364
+ const uma = await Uma.permission()
365
+ const token = await uma.ticket({ tokenUrl: authServer.tokenEndpoint || authServer.tokenUrl, token: tokens.access_token, audience: authServer.clientId })
366
+ const sourceData = Jsonwebtoken.decode(token.access_token)
367
+ return h.response(JSON.stringify(sourceData.authorization)).takeover()
368
+ } catch (err) {
369
+ return errorResponse(h, err, 401)
370
+ }
371
+ }
372
+ })
373
+ // /get-security-rules
374
+ hapiServer.route({
375
+ method: 'GET',
376
+ path: '/get-security-rules',
377
+ handler: async (request, h) => {
378
+ try {
379
+ const securityRules = await securityService.getFrontendSecurityRules(request)
380
+ return h.response(JSON.stringify(securityRules)).takeover()
381
+ } catch (err) {
382
+ return errorResponse(h, err, 401)
383
+ }
384
+ }
385
+ })
386
+ // /get-permissions
387
+ hapiServer.route({
388
+ method: 'GET',
389
+ path: '/get-permissions',
390
+ handler: async (request, h) => {
391
+ try {
392
+ const permissions = await securityService.getPermissions()
393
+ return h.response(JSON.stringify(permissions)).takeover()
394
+ } catch (err) {
395
+ return errorResponse(h, err, 401)
396
+ }
397
+ }
398
+ })
399
+ // /get-user-info
400
+ hapiServer.route({
401
+ method: 'GET',
402
+ path: '/get-user-info',
403
+ handler: async (request, h) => {
404
+ try {
405
+ const userInfo = await securityService.getUserInfo(request)
406
+ return h
407
+ .response(JSON.stringify(userInfo))
408
+ .takeover()
409
+ } catch (err) {
410
+ return errorResponse(h, err, 500)
411
+ }
412
+ }
413
+ })
414
+ // /logout
415
+ hapiServer.route({
416
+ path: '/logout',
417
+ method: 'GET',
418
+ handler: async (request, h) => {
419
+ try {
420
+ const ckSessionState = request.state[this.COOKIE_NAMES.SESSION_STATE]
421
+ request.yar.clear('jwtToken');
422
+ await request.yar.commit(h);
423
+ let endSessionUrl = await me.endSessionUrl(me.getRedirectUri(request), me.clientOidc);
424
+ return h
425
+ .response()
426
+ .unstate(this.COOKIE_NAMES.SID)
427
+ .unstate(this.COOKIE_NAMES.SESSION_STATE)
428
+ .unstate(this.COOKIE_NAMES.AUTH_TOKEN)
429
+ .redirect(endSessionUrl)
430
+ .takeover()
431
+ } catch (err) {
432
+ return errorResponse(h, err, 500)
433
+ }
434
+ }
435
+ })
436
+ // /invalid-session
437
+ hapiServer.route({
438
+ path: '/invalid-session',
439
+ method: 'GET',
440
+ handler: async (request, h) => {
441
+ try {
442
+ const endSessionUrl = await me.openIdConnect.endSessionUrl({
443
+ redirectUri: this.getRedirectUri(request),
444
+ sessionState: request.state[this.COOKIE_NAMES.SESSION_STATE]
445
+ })
446
+ return h
447
+ .response()
448
+ .unstate(this.COOKIE_NAMES.SID)
449
+ .unstate(this.COOKIE_NAMES.SESSION_STATE)
450
+ .redirect(endSessionUrl)
451
+ .takeover()
452
+ } catch (err) {
453
+ return errorResponse(h, err, 500)
454
+ }
455
+ }
456
+ })
457
+ // /check-session-iframe.html
458
+ hapiServer.route({
459
+ path: '/check-session-iframe.html',
460
+ method: 'GET',
461
+ handler: async (_request, h) => {
462
+ try {
463
+ let content = '<html/>'
464
+ if (authServer && authServer.checkSessionIframe) {
465
+ const { checkSessionIframe: sessionIframeUrl, clientId, sessionCookiesPrefix } = authServer
466
+ if (sessionIframeUrl && sessionIframeUrl.includes('https://')) {
467
+ trace('INFO', `Session management url: ${sessionIframeUrl}`)
468
+ content = getTemplate('session-iframe', {
469
+ sessionIframeUrl,
470
+ clientId,
471
+ sessionCookiesPrefix: sessionCookiesPrefix || ''
472
+ })
473
+ } else {
474
+ 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].')
475
+ }
476
+ }
477
+ return h
478
+ .response(content)
479
+ .header('Content-Type', 'text/html')
480
+ } catch (err) {
481
+ return errorResponse(h, err, 500)
482
+ }
483
+ }
484
+ });
485
+ }
486
+ }
487
+
488
+ authServerSimulation (context) {
489
+ if (!context.config || !context.config.accessTokenSimulation) {
490
+ throw new Exception('Error parsing metadata for simulation', 'ConfigurationError', 404)
491
+ }
492
+ let { algorithm, payload, secret } = context.config.accessTokenSimulation
493
+ const me = this
494
+ context.ext('onPreAuth', async function (request, h) {
495
+ if (request.state && request.state[me.COOKIE_NAMES.ACCESS_TOKEN]) {
496
+ return h.continue
497
+ } else {
498
+ switch (algorithm) {
499
+ case 'HMAC-SHA384': {
500
+ algorithm = 'HS384'
501
+ break
502
+ }
503
+ case 'HMAC-SHA512': {
504
+ algorithm = 'HS512'
505
+ break
506
+ }
507
+ default: {
508
+ algorithm = 'HS256'
509
+ }
510
+ }
511
+ const jwt = me.openIdConnect.jwt().sign({ payload, secret, algorithm })
512
+ return h
513
+ .response()
514
+ .state(me.COOKIE_NAMES.ACCESS_TOKEN, jwt)
515
+ .redirect(me.getRedirectUri(request))
516
+ .takeover()
517
+ }
518
+ })
519
+ // /get-authorization
520
+ context.route({
521
+ path: '/get-authorization',
522
+ method: 'GET',
523
+ handler: async function (_request, h) {
524
+ return h
525
+ .response('[]')
526
+ .code(200)
527
+ }
528
+ })
529
+ // /get-security-rules
530
+ context.route({
531
+ path: '/get-security-rules',
532
+ method: 'GET',
533
+ handler: async function (_request, h) {
534
+ let securityRules = []
535
+ if (securityService && context.config.accessTokenSimulation.playload) {
536
+ const groups = securityService.getGroups(context.config.accessTokenSimulation.playload)
537
+ securityRules = securityService.getFrontendSecurityRules([groups])
538
+ }
539
+ return h
540
+ .response(JSON.stringify(securityRules))
541
+ .code(200)
542
+ }
543
+ })
544
+ // /get-permissions
545
+ context.route({
546
+ path: '/get-permissions',
547
+ method: 'GET',
548
+ handler: async function (_request, h) {
549
+ const permissions = (securityService) ? securityService.getPermissions() : []
550
+ return h
551
+ .response(JSON.stringify(permissions))
552
+ .code(200)
553
+ }
554
+ })
555
+ // /get-user-info
556
+ context.route({
557
+ path: '/get-user-info',
558
+ method: 'GET',
559
+ handler: async function (_request, h) {
560
+ return h
561
+ .response(JSON.stringify(payload))
562
+ .code(200)
563
+ }
564
+ })
565
+ // /logout
566
+ context.route({
567
+ path: '/logout',
568
+ method: 'GET',
569
+ handler: async function (_request, h) {
570
+ return h
571
+ .response()
572
+ .unstate(this.COOKIE_NAMES.ACCESS_TOKEN)
573
+ .takeover()
574
+ }
575
+ })
576
+ }
577
+
578
+ getRedirectUri (request) {
579
+ return contextConfig.authServer.redirectUri || getFullUrl(request)
580
+ }
581
+ getFullUrl (request) {
582
+ return `${getProtocol(request)}://${getHost(request)}${getPathname(request)}`
583
+ }
584
+ getBaseUrl (request) {
585
+ return `${getProtocol(request)}://${getHost(request)}/`
586
+ }
587
+ async authenticate (h, scope) {
588
+ const { request } = h
589
+ const pkceCode = await this.openIdConnect.pkceCode()
590
+ const requestUrl = getFullUrl(request)
591
+ let oidcMetadata = await this.openIdConnect.oidcMetadata()
592
+ if (!oidcMetadata || !oidcMetadata.openid_configuration) {
593
+ oidcMetadata = await this.configuration(contextConfig.authServer)
594
+ }
595
+ if (requestUrl.match(new RegExp(/^(https?:\/{2}.*):?(\d*)/.source + getHost(request) + /\/?$/.source))) {
596
+ const authorizationUrl = await this.openIdConnect.authorizationUrl({ scope, redirectUri: this.getRedirectUri(request), pkceCode })
597
+ trace('INFO', `Authenticate redirecting to ${authorizationUrl}`)
598
+ return h
599
+ .response()
600
+ .state(this.COOKIE_NAMES.SID, pkceCode)
601
+ .redirect(authorizationUrl)
602
+ .takeover()
603
+ } else if (getPathname(request) === '/logout') {
604
+ return h.continue
605
+ } else {
606
+ const tokenSet = await this.openIdConnect.tokenSet()
607
+ const { state } = request
608
+ if (tokenSet && state && state[this.COOKIE_NAMES.SESSION_STATE]) {
609
+ const tokens = await tokenSet.tokens(state[this.COOKIE_NAMES.SESSION_STATE])
610
+ if (!tokens || tokens.refresh_expires_in <= getTokenTolerance(0)) {
611
+ throw new Exception('Error when getting token', 'ExpirationError', 403)
612
+ }
613
+ return h.continue
614
+ } else {
615
+ return h
616
+ .response()
617
+ .code(401)
618
+ .takeover()
619
+ }
620
+ }
621
+ }
622
+
623
+ async configurePlugins (server) {
624
+ // Add Yar to save info in the cookies across session calls
625
+ const hapiYarPassword = process.env.blz_hapiYarPassword || 'your-super-secure-yar-atleast-32-bytes-password';
626
+ await server.register({
627
+ plugin: hapiYar,
628
+ options: {
629
+ cookieOptions: {
630
+ password: hapiYarPassword,
631
+ isSecure: true, // Use true in production
632
+ isHttpOnly: true,
633
+ isSameSite: 'Lax', // 'Strict', 'Lax', or 'None'
634
+ clearInvalid: true,
635
+ ignoreErrors: true
636
+ },
637
+ storeBlank: false, // Prevent saving blank sessions
638
+ maxCookieSize: 0 // Use server-side storage for larger payloads
639
+ }
640
+ });
641
+ // Register @hapi/jwt plugin
642
+ await server.register(hapiJwt);
643
+
644
+ // Check for static certificate or rotating.
645
+ let keysFetch = true;
646
+ if (true) {
647
+ // Azure rotating certificates, prepare for the hapi jwt module
648
+ this.startupJwksClient();
649
+ // set up the function in this.publickKeyFetch
650
+ this.startupPublickKeyFetch();
651
+ keysFetch = this.publicKeyFetch;
652
+ } else {
653
+ // Esto es para un certificado estatico. Keycloak lo permite
654
+ const response = await axios.get(this.authServerConfig.authServer.jwksUri);
655
+ const jwks = response.data; // JWKS data is directly accessible from response.data
656
+ const kidValue = this.authServerConfig.authServer.oAuthKid; // Kid from keycloak/azure, in realm settings
657
+ const key = jwks.keys.find(k => k.kid === kidValue);
658
+ if (!key) throw new Error(`Key with ID ${kid} not found`);
659
+ const pemPublicKey = jwkToPem(key);
660
+ this.authServerConfig.authServer.PublicKey = pemPublicKey;
661
+ keysFetch = {
662
+ key: pemPublicKey,
663
+ algorithms: ['RS256'],
664
+ kid: kidValue
665
+ };
666
+ }
667
+ if (this.authServerConfig.authServer.provider=== 'ad-azure') {
668
+ const tenant_id = this.authServerConfig.authServer.issuer.match(/login\.microsoftonline\.com\/([^/]+)/)?.[1]
669
+ this.authServerConfig.authServer.msalConfig = {
670
+ auth: {
671
+ clientId: this.authServerConfig.authServer.clientId,
672
+ authority: `https://login.microsoftonline.com/${tenant_id}`,
673
+ clientSecret: this.authServerConfig.authServer.clientSecret,
674
+ },
675
+ };
676
+ const msalClient = new ConfidentialClientApplication(this.authServerConfig.authServer.msalConfig);
677
+ this.authServerConfig.authServer.msalClient = msalClient;
678
+ }
679
+
680
+
681
+ // Define the auth strategy
682
+ server.auth.strategy('jwtAuth', 'jwt', {
683
+ keys: keysFetch,
684
+ verify: {
685
+ aud: this.authServerConfig.authServer.clientId,
686
+ iss: this.authServerConfig.authServer.issuer,
687
+ exp: true, // validate expiration
688
+ sub: false
689
+ },
690
+ validate: false
691
+ // validate: async (artifacts, request, h) => {
692
+ // // Validate the token payload (you can perform additional checks here if needed)
693
+ // const { exp } = artifacts.decoded.payload;
694
+
695
+ // if (Date.now() >= exp * 1000) {
696
+ // throw h.unauthorized('Token expired', { redirectToLogin: true });
697
+ // }
698
+
699
+ // return { isValid: true, credentials: artifacts.decoded.payload };
700
+ // }
701
+ });
702
+
703
+ // Register the @hapi/cookie plugin
704
+ await server.register(hapiCookie);
705
+
706
+ // Define the cookie-based auth strategy
707
+ server.auth.strategy('cookieAuth', 'cookie', {
708
+ cookie: {
709
+ name: 'sid', // Primary session cookie
710
+ password: 'supersecretpasswordmustbeatleast32characterslong', // Encryption key
711
+ isSecure: true, // Should be true in production
712
+ isHttpOnly: true, // Prevents client-side JavaScript access
713
+ isSameSite: 'Lax', // Protects against CSRF
714
+ },
715
+ keepAlive: true, // automatically sets the session cookie after validation to extend the current session for a new ttl duration. Defaults to false.
716
+ redirectTo: false, //function(request) {}, // Redirect if authentication fails
717
+ });
718
+ // Set default auth strategy to try both JWT and cookies
719
+ server.auth.default({
720
+ strategies: ['jwtAuth', 'cookieAuth'], // Try JWT first, then Cookie
721
+ });
722
+ }
723
+
724
+ async configuration (authServer) {
725
+ if (!authServer) {
726
+ throw new Exception('Error when getting configuration attributes ')
727
+ }
728
+ const { clientId, clientSecret } = authServer
729
+ await this.openIdConnect.client({ clientId, clientSecret })
730
+ if (authServer.openIdConfigurationEndpoint) {
731
+ return await this.openIdConnect.configuration(authServer.openIdConfigurationEndpoint)
732
+ } else {
733
+ // If configuration uri does not exist but the auth server form has been filled in.
734
+ return await this.openIdConnect.configuration({
735
+ issuer: authServer.issuer,
736
+ authorization_endpoint: authServer.authorizationEndpoint,
737
+ token_endpoint: authServer.tokenEndpoint,
738
+ userinfo_endpoint: authServer.userinfoEndpoint,
739
+ end_session_endpoint: authServer.endSessionEndpoint,
740
+ jwks_uri: authServer.jwksUri
741
+ })
742
+ }
743
+ }
744
+ async prepareMemoryValues(){
745
+ //this.authServerFullLoginUrl = ;
746
+ }
747
+ async endSessionUrl (redirectUri, clientOidc) {
748
+ redirectUri = redirectUri.replace(/logout|invalid-session/gmi, '')
749
+ // Log off specific session.
750
+ if (!clientOidc) {
751
+ throw new Error('Unable to get configuration from identity provider', 'ConfigurationError', 404);
752
+ }
753
+ return clientOidc.endSessionUrl({ post_logout_redirect_uri: redirectUri })
754
+ }
755
+ oidcMetadataKey() {
756
+ return this.authServerConfig.authServer.sessionCookiesDomain || 'oidcMetadata'
757
+ }
758
+ async configuration (context) {
759
+ let metadata = await this.cache.get(this.oidcMetadataKey())
760
+ if (typeof context === 'string' && !context.match(/(https?:\/\/.*):?(\d*)\/?(.*)/gi)) {
761
+ throw new Exception('Wrong OpenId Provider configuration URI entered', 'AttributeError', 403)
762
+ }
763
+ if (!metadata || !metadata.issuer) {
764
+ if (context.issuer) {
765
+ metadata = { ...(metadata || {}), ...context }
766
+ } else {
767
+ metadata = metadata || {}
768
+ metadata.openid_configuration = context
769
+ metadata = { ...metadata, ...(await Issuer.discover(context.issuer)) } // Discover an issuer configuration, must be an url
770
+ }
771
+ await this.cache.set(this.oidcMetadataKey(), metadata, 864e5) // 1 day of cache
772
+ }
773
+ return new Iss(metadata)
774
+ }
775
+ async refreshToken (refreshToken) {
776
+ // Make a POST request to Keycloak to refresh the token
777
+ const response = await axios.post(this.authServerConfig.authServer.tokenEndpoint,
778
+ new URLSearchParams({
779
+ grant_type: 'refresh_token',
780
+ client_id: this.authServerConfig.authServer.clientId,
781
+ client_secret: this.authServerConfig.authServer.clientSecret,
782
+ refresh_token: refreshToken,
783
+ }).toString(),
784
+ {
785
+ headers: {
786
+ 'Content-Type': 'application/x-www-form-urlencoded',
787
+ },
788
+ }
789
+ );
790
+
791
+
792
+ if (!(response.status === 200)) {
793
+ const errorResponse = await response.json();
794
+ console.error('Error refreshing token:', errorResponse);
795
+ return errorResponse;
796
+ }
797
+ // Refresh token response may change from time to time, here are two possible responses
798
+ try {
799
+ return await response.json(); // all tokens refershed
800
+
801
+ } catch (error) {
802
+
803
+ }
804
+ try {
805
+ return response.data;
806
+ }
807
+ catch {
808
+
809
+ }
810
+
811
+ }
812
+ async decodeJwtToken(token) {
813
+ const decodedToken = hapiJwt.token.decode(token);
814
+ return decodedToken;
815
+ }
816
+ async tokenAboutToExpire(token, minutesBeforeExpiration = 0) {
817
+ const decodedToken = hapiJwt.token.decode(token);
818
+ const expirationTime = decodedToken.decoded.payload.exp * 1000; // Convert to milliseconds
819
+ const currentTime = Date.now();
820
+ const expirationThreshold = minutesBeforeExpiration * 60 * 1000; // Convert minutes to milliseconds
821
+
822
+ // Check if the token is expired or about to expire within the specified minutes
823
+ const isAboutToExpire = expirationTime - currentTime <= expirationThreshold;
824
+ return isAboutToExpire;
825
+ }
826
+ async isRefreshTokenExpired (refreshToken) {
827
+ try {
828
+ // Decode the token without verifying its signature.
829
+ const decodedRefreshToken = hapiJwt.token.decode(refreshToken);
830
+ // Get the current timestamp (in seconds).
831
+ const currentTimestamp = Math.floor(Date.now() / 1000);
832
+
833
+ if (decodedRefreshToken && decodedRefreshToken.decoded && decodedRefreshToken.decoded.payload && decodedRefreshToken.decoded.payload.exp) {
834
+ return (decodedRefreshToken.decoded.payload.exp < currentTimestamp)
835
+ } else
836
+ return true;
837
+ } catch (error) {
838
+ // if there is an error treat as if expired, so a re-login is prompted
839
+ console.error('Failed to decode the token: Invalid Refresh token format', error);
840
+ return true;
841
+ }
842
+ }
843
+
844
+ async silentAuthenticationAzure ({redirectUri, idToken}) {
845
+ const authUrl = this.authServerConfig.authServer.authorizationEndpoint;
846
+ const decodedToken = await this.decodeJwtToken(idToken);
847
+
848
+ try {
849
+ const response = await axios.get(authUrl, {
850
+ params: {
851
+ client_id: this.authServerConfig.authServer.clientId,
852
+ response_type: "id_token",
853
+ redirect_uri: redirectUri,
854
+ scope: this.authServerConfig.authServer.scope ?? 'openid',
855
+ prompt: "none",
856
+ response_mode: "fragment",
857
+ nonce: "random_nonce", // Should be a securely generated nonce
858
+ login_hint: decodedToken.decoded.payload.preferred_username
859
+ },
860
+ maxRedirects: 0, // Prevent following redirects automatically
861
+ validateStatus: (status) => status === 302 // Expecting a redirect response
862
+ });
863
+
864
+ // Extract the token from the redirect location
865
+ const location = response.headers.location;
866
+ if (!location) throw new Error("No redirect location found");
867
+
868
+ const params = new URLSearchParams(location.split("#")[1]);
869
+ if (params.has("id_token")) {
870
+ return { idToken: params.get("id_token") };
871
+ } else {
872
+ throw new Error("No ID token returned");
873
+ }
874
+ } catch (error) {
875
+ console.error("Silent authentication failed:", error.response?.data || error.message);
876
+ return null; // Handle failure gracefully
877
+ }
878
+ }
879
+
880
+ async startupJwksClient () {
881
+ // Azure rotating certificates, prepare for the hapi jwt module
882
+ this.clientJwk = jwksClient({
883
+ jwksUri: this.authServerConfig.authServer.jwksUri,
884
+ cache: true, // Cache signing keys to avoid frequent network calls
885
+ rateLimit: true, // Rate limit the number of requests to the JWKS URI
886
+ jwksRequestsPerMinute: 10, // Limit to 10 requests per minute
887
+ });
888
+ }
889
+ async startupPublickKeyFetch () {
890
+ // Function to get the signing key
891
+ const getKey = async (kid) => {
892
+ return new Promise((resolve, reject) => {
893
+ this.clientJwk.getSigningKey(kid, (err, key) => {
894
+ if (err) {
895
+ return reject(err);
896
+ }
897
+ const signingKey = key.getPublicKey(); // Public key for signature verification
898
+ resolve(signingKey);
899
+ });
900
+ });
901
+ };
902
+ this.publicKeyFetch = async (artifacts) => {
903
+ const kid = artifacts.decoded.header.kid; // Extract 'kid' from JWT header
904
+ return getKey(kid); // Fetch the corresponding public key
905
+ }
906
+ }
907
+ }
908
+
909
+ class Iss {
910
+ /**
911
+ * @constructor
912
+ * @param {Object} metadata
913
+ */
914
+ constructor (metadata) {
915
+ if (!metadata.id_token_signing_alg_values_supported) {
916
+ metadata.id_token_signing_alg_values_supported = ['RS256']
917
+ }
918
+ if (!metadata.response_types_supported) {
919
+ metadata.response_types_supported = ['code', 'none', 'id_token', 'token', 'id_token token', 'code id_token', 'code token', 'code id_token token']
920
+ }
921
+ if (!metadata.subject_types_supported) {
922
+ metadata.subject_types_supported = ['public']
923
+ }
924
+ const claimsRequired = METADATA.filter(({ type }) => type === 'REQUIRED');
925
+ const missingClaims = [];
926
+
927
+ for (const claim of claimsRequired) {
928
+ const normalizedToCamelClaimName = claim.name.toLowerCase().replace(/_([a-z])/g, (_, letter) => letter.toUpperCase());
929
+ const attributeCamelCase = metadata[normalizedToCamelClaimName]; // Directly access metadata
930
+ const attributeSnakeCase = metadata[claim.name]; // Directly access metadata
931
+ if (!attributeSnakeCase && !attributeCamelCase) {
932
+ missingClaims.push(claim);
933
+ }
934
+ }
935
+
936
+ if (missingClaims.length > 0) {
937
+ console.error(JSON.stringify(missingClaims));
938
+ throw new Error(JSON.stringify(missingClaims));
939
+ }
940
+
941
+ // Issuer needs the metadata in snake_case
942
+ const issuer = metadata.Client ? metadata : new Issuer(this.#camelToSnakeCase(metadata))
943
+ // Client instance for the authorization server of that issuer.
944
+ const clientPayload = {
945
+ client_id: metadata.clientId,
946
+ response_type: 'code'
947
+ }
948
+ if (metadata.clientSecret) {
949
+ clientPayload.client_secret = metadata.clientSecret
950
+ }
951
+ this.clientOidc = new issuer.Client(clientPayload);
952
+ }
953
+ #camelToSnakeCase (obj) {
954
+ const toSnakeCase = str => str.replace(/[A-Z]/g, letter => `_${letter.toLowerCase()}`);
955
+
956
+ if (typeof obj !== 'object' || obj === null) return obj;
957
+
958
+ if (Array.isArray(obj)) {
959
+ return obj.map(item => this.#camelToSnakeCase(item));
960
+ }
961
+
962
+ return Object.entries(obj).reduce((acc, [key, value]) => {
963
+ const newKey = toSnakeCase(key);
964
+ acc[newKey] = typeof value === 'object' && value !== null
965
+ ? this.#camelToSnakeCase(value)
966
+ : value;
967
+ return acc;
968
+ }, {});
969
+ }
970
+ }
971
+
972
+ module.exports = {
973
+ HapiServer
974
+ }