@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.
- package/README.md +3 -0
- package/blz-base/health/index.js +215 -0
- package/blz-base/index.js +1466 -0
- package/blz-cache/LruCache.js +44 -0
- package/blz-cache/index.js +29 -0
- package/blz-config/index.js +434 -0
- package/blz-core/index.js +364 -0
- package/blz-cryptography/index.js +54 -0
- package/blz-datetimes/index.js +356 -0
- package/blz-file/example.dat +2545 -0
- package/blz-file/fileService.js +205 -0
- package/blz-file/index.js +94 -0
- package/blz-file/index.test.js +31 -0
- package/blz-file/lab.js +33 -0
- package/blz-hazelcast/index.js +189 -0
- package/blz-hazelcast/lib/credentials.js +25 -0
- package/blz-hazelcast/lib/credentialsFactory.js +12 -0
- package/blz-hazelcast/lib/hazelcastCache.js +234 -0
- package/blz-iterable/index.js +446 -0
- package/blz-json-schema/index.js +11 -0
- package/blz-jwt/index.js +121 -0
- package/blz-kafka/index.js +522 -0
- package/blz-math/index.js +131 -0
- package/blz-mongodb/index.js +326 -0
- package/blz-rds/__test__/scape.test.js +58 -0
- package/blz-rds/blz-rds-executor.js +578 -0
- package/blz-rds/blz-rds-helper.js +310 -0
- package/blz-rds/commands/core/add.js +13 -0
- package/blz-rds/commands/core/and.js +18 -0
- package/blz-rds/commands/core/asc.js +10 -0
- package/blz-rds/commands/core/avg.js +10 -0
- package/blz-rds/commands/core/column-ref.js +8 -0
- package/blz-rds/commands/core/count-distinct.js +10 -0
- package/blz-rds/commands/core/count.js +10 -0
- package/blz-rds/commands/core/decimal.js +8 -0
- package/blz-rds/commands/core/desc.js +10 -0
- package/blz-rds/commands/core/distinct.js +10 -0
- package/blz-rds/commands/core/divide.js +11 -0
- package/blz-rds/commands/core/embedded-exists.js +17 -0
- package/blz-rds/commands/core/embedded-select.js +17 -0
- package/blz-rds/commands/core/equals.js +9 -0
- package/blz-rds/commands/core/false.js +8 -0
- package/blz-rds/commands/core/greater-or-equal.js +9 -0
- package/blz-rds/commands/core/greater.js +9 -0
- package/blz-rds/commands/core/in.js +9 -0
- package/blz-rds/commands/core/integer.js +8 -0
- package/blz-rds/commands/core/is-not-null.js +11 -0
- package/blz-rds/commands/core/is-null-or-value.js +10 -0
- package/blz-rds/commands/core/is-null.js +11 -0
- package/blz-rds/commands/core/less-or-equal.js +9 -0
- package/blz-rds/commands/core/less-unary.js +12 -0
- package/blz-rds/commands/core/less.js +9 -0
- package/blz-rds/commands/core/like.js +12 -0
- package/blz-rds/commands/core/max.js +10 -0
- package/blz-rds/commands/core/min.js +10 -0
- package/blz-rds/commands/core/multiply.js +13 -0
- package/blz-rds/commands/core/not-equals.js +9 -0
- package/blz-rds/commands/core/not-in.js +9 -0
- package/blz-rds/commands/core/not.js +13 -0
- package/blz-rds/commands/core/null.js +8 -0
- package/blz-rds/commands/core/nvl.js +11 -0
- package/blz-rds/commands/core/or.js +13 -0
- package/blz-rds/commands/core/parameter.js +34 -0
- package/blz-rds/commands/core/remainder.js +16 -0
- package/blz-rds/commands/core/string.js +8 -0
- package/blz-rds/commands/core/subtract.js +13 -0
- package/blz-rds/commands/core/sum.js +10 -0
- package/blz-rds/commands/core/true.js +8 -0
- package/blz-rds/commands/core/tuple.js +13 -0
- package/blz-rds/commands/datetimes/add-days.js +11 -0
- package/blz-rds/commands/datetimes/add-hours.js +11 -0
- package/blz-rds/commands/datetimes/add-milliseconds.js +11 -0
- package/blz-rds/commands/datetimes/add-minutes.js +11 -0
- package/blz-rds/commands/datetimes/add-months.js +11 -0
- package/blz-rds/commands/datetimes/add-seconds.js +11 -0
- package/blz-rds/commands/datetimes/add-years.js +11 -0
- package/blz-rds/commands/datetimes/date-diff.js +11 -0
- package/blz-rds/commands/datetimes/date.js +12 -0
- package/blz-rds/commands/datetimes/datetime-diff.js +11 -0
- package/blz-rds/commands/datetimes/datetime.js +15 -0
- package/blz-rds/commands/datetimes/day.js +10 -0
- package/blz-rds/commands/datetimes/hour.js +10 -0
- package/blz-rds/commands/datetimes/millisecond.js +10 -0
- package/blz-rds/commands/datetimes/minute.js +10 -0
- package/blz-rds/commands/datetimes/month-text.js +10 -0
- package/blz-rds/commands/datetimes/month.js +10 -0
- package/blz-rds/commands/datetimes/now.js +9 -0
- package/blz-rds/commands/datetimes/second.js +10 -0
- package/blz-rds/commands/datetimes/subtract-days.js +11 -0
- package/blz-rds/commands/datetimes/subtract-hours.js +11 -0
- package/blz-rds/commands/datetimes/subtract-milliseconds.js +11 -0
- package/blz-rds/commands/datetimes/subtract-minutes.js +11 -0
- package/blz-rds/commands/datetimes/subtract-seconds.js +11 -0
- package/blz-rds/commands/datetimes/time-diff.js +11 -0
- package/blz-rds/commands/datetimes/time.js +13 -0
- package/blz-rds/commands/datetimes/today.js +9 -0
- package/blz-rds/commands/datetimes/week-day-text.js +10 -0
- package/blz-rds/commands/datetimes/week-day.js +10 -0
- package/blz-rds/commands/datetimes/week.js +10 -0
- package/blz-rds/commands/datetimes/year.js +10 -0
- package/blz-rds/commands/math/abs.js +10 -0
- package/blz-rds/commands/math/acos.js +10 -0
- package/blz-rds/commands/math/asin.js +10 -0
- package/blz-rds/commands/math/atan.js +10 -0
- package/blz-rds/commands/math/atan2.js +11 -0
- package/blz-rds/commands/math/ceil.js +10 -0
- package/blz-rds/commands/math/cos.js +10 -0
- package/blz-rds/commands/math/cosh.js +10 -0
- package/blz-rds/commands/math/exp.js +10 -0
- package/blz-rds/commands/math/floor.js +10 -0
- package/blz-rds/commands/math/log.js +18 -0
- package/blz-rds/commands/math/log10.js +10 -0
- package/blz-rds/commands/math/pow.js +11 -0
- package/blz-rds/commands/math/random.js +9 -0
- package/blz-rds/commands/math/round.js +18 -0
- package/blz-rds/commands/math/sign.js +10 -0
- package/blz-rds/commands/math/sin.js +10 -0
- package/blz-rds/commands/math/sinh.js +10 -0
- package/blz-rds/commands/math/sqrt.js +10 -0
- package/blz-rds/commands/math/tan.js +10 -0
- package/blz-rds/commands/math/tanh.js +10 -0
- package/blz-rds/commands/math/trunc.js +18 -0
- package/blz-rds/commands/strings/concat.js +20 -0
- package/blz-rds/commands/strings/contains.js +12 -0
- package/blz-rds/commands/strings/ends-with.js +12 -0
- package/blz-rds/commands/strings/index-of.js +11 -0
- package/blz-rds/commands/strings/is-null-or-empty.js +11 -0
- package/blz-rds/commands/strings/is-null-or-white-space.js +11 -0
- package/blz-rds/commands/strings/join.js +22 -0
- package/blz-rds/commands/strings/last-index-of.js +11 -0
- package/blz-rds/commands/strings/length.js +10 -0
- package/blz-rds/commands/strings/pad-left.js +20 -0
- package/blz-rds/commands/strings/pad-right.js +20 -0
- package/blz-rds/commands/strings/replace.js +12 -0
- package/blz-rds/commands/strings/starts-with.js +12 -0
- package/blz-rds/commands/strings/substring.js +12 -0
- package/blz-rds/commands/strings/to-lower.js +10 -0
- package/blz-rds/commands/strings/to-upper.js +10 -0
- package/blz-rds/commands/strings/trim-end.js +10 -0
- package/blz-rds/commands/strings/trim-start.js +10 -0
- package/blz-rds/commands/strings/trim.js +10 -0
- package/blz-rds/index.js +744 -0
- package/blz-rds-mysql/base.js +857 -0
- package/blz-rds-mysql/connection-manager.js +129 -0
- package/blz-rds-mysql/execute-bulk-insert.js +35 -0
- package/blz-rds-mysql/execute-bulk-merge.js +45 -0
- package/blz-rds-mysql/execute-non-query.js +34 -0
- package/blz-rds-mysql/execute-query.js +50 -0
- package/blz-rds-mysql/index.js +41 -0
- package/blz-rds-mysql/stored-procedure.js +207 -0
- package/blz-rds-mysql/syntaxis.json +114 -0
- package/blz-rds-mysqlx/base.js +846 -0
- package/blz-rds-mysqlx/connection-manager.js +141 -0
- package/blz-rds-mysqlx/execute-bulk-insert.js +35 -0
- package/blz-rds-mysqlx/execute-bulk-merge.js +45 -0
- package/blz-rds-mysqlx/execute-non-query.js +29 -0
- package/blz-rds-mysqlx/execute-query.js +39 -0
- package/blz-rds-mysqlx/index.js +41 -0
- package/blz-rds-mysqlx/stored-procedure.js +179 -0
- package/blz-rds-mysqlx/syntaxis.json +105 -0
- package/blz-rds-oracle/index.js +540 -0
- package/blz-rds-oracle/syntaxis.json +112 -0
- package/blz-rds-postgres/base.js +861 -0
- package/blz-rds-postgres/connection-manager.js +225 -0
- package/blz-rds-postgres/execute-bulk-insert.js +81 -0
- package/blz-rds-postgres/execute-bulk-merge.js +93 -0
- package/blz-rds-postgres/execute-non-query.js +23 -0
- package/blz-rds-postgres/execute-query.js +37 -0
- package/blz-rds-postgres/index.js +41 -0
- package/blz-rds-postgres/result-set.js +51 -0
- package/blz-rds-postgres/stored-procedure.js +116 -0
- package/blz-rds-postgres/syntaxis.json +114 -0
- package/blz-redis/index.js +217 -0
- package/blz-redis/lib/redisCache.js +265 -0
- package/blz-regex/index.js +25 -0
- package/blz-security/.eslintrc.js +15 -0
- package/blz-security/__test__/AuthorizationKpn.yaml +1043 -0
- package/blz-security/__test__/FinancingSetting.yaml +177 -0
- package/blz-security/__test__/KpnConfigPortal.yaml +330 -0
- package/blz-security/__test__/OrderManagement.yaml +5190 -0
- package/blz-security/__test__/Security.yaml +128 -0
- package/blz-security/__test__/autorization.test.js +105 -0
- package/blz-security/__test__/orderManagement.test.js +26 -0
- package/blz-security/__test__/secureUrl.test.js +79 -0
- package/blz-security/__test__/solveMergeRule.test.js +109 -0
- package/blz-security/__test__/sqlInjectionGuard.test.js +203 -0
- package/blz-security/__test__/xssGuard.test.js +204 -0
- package/blz-security/authorizationService.js +536 -0
- package/blz-security/config/global.js +8 -0
- package/blz-security/config/welcome +8 -0
- package/blz-security/doc/README.md +75 -0
- package/blz-security/filescanner/index.js +46 -0
- package/blz-security/helpers/consts.js +229 -0
- package/blz-security/helpers/utils.js +267 -0
- package/blz-security/implementations/cache.js +90 -0
- package/blz-security/implementations/oidc.js +404 -0
- package/blz-security/implementations/pkceCacheStore.js +23 -0
- package/blz-security/implementations/saml.js +10 -0
- package/blz-security/implementations/uma.js +63 -0
- package/blz-security/implementations/webAuthn.js +9 -0
- package/blz-security/implementations/wstg.js +72 -0
- package/blz-security/index.js +77 -0
- package/blz-security/lab/index.js +27 -0
- package/blz-security/middleware/HapiServerAzureAd.js +641 -0
- package/blz-security/middleware/HapiServerKeycloak.js +840 -0
- package/blz-security/middleware/HapiServerSimToken.js +247 -0
- package/blz-security/middleware/hapi.js +515 -0
- package/blz-security/middleware/hapiServer.js +974 -0
- package/blz-security/navigationMemoryRepository.js +15 -0
- package/blz-security/navigationMongoDbRepository.js +73 -0
- package/blz-security/secureUrlService.js +47 -0
- package/blz-security/securityService.js +409 -0
- package/blz-security/sqlInjectionGuard.js +162 -0
- package/blz-security/templates/forbidden.html +0 -0
- package/blz-security/templates/session-iframe-azure-ad.html +7 -0
- package/blz-security/templates/session-iframe.html +73 -0
- package/blz-security/templates/unauthorized.html +1 -0
- package/blz-security/xssGuard.js +87 -0
- package/blz-strings/index.js +167 -0
- package/blz-uuid/index.js +7 -0
- package/blz-yaml/index.js +19 -0
- package/index.js +84 -0
- package/package.json +97 -0
- 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
|
+
}
|