@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,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
|
+
}
|