-
Notifications
You must be signed in to change notification settings - Fork 8
/
utils.js
193 lines (182 loc) · 5.78 KB
/
utils.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
const boom = require('@hapi/boom')
const joi = require('@hapi/joi')
const jwkToPem = require('jwk-to-pem')
/**
* @type Object
* @private
*
* The plugin options scheme
*/
const pluginScheme = joi.object({
apiKey: joi.object({
in: joi.string().valid('headers', 'query').default('headers')
.description('Whether the api key is placed in the headers or query')
.example('query'),
name: joi.string().min(1).default('authorization')
.description('The name of the related headers field or query key')
.example('x-api-key'),
prefix: joi.string().min(1).default('Api-Key ')
.description('An optional prefix of the related api key value')
.example('Apikey '),
url: joi.string().min(1).required()
.description('The absolute url to be requested')
.example('https://foobar.com/api'),
request: joi.object().default({})
.description('The detailed request options for `got`')
.example({ retries: 2 }),
tokenPath: joi.string().min(1).default('access_token')
.description('The path to the access token in the response its body as dot notation')
.example('foo.bar')
}).unknown(false)
.description('The configuration of an optional api key strategy interaction with another service')
})
.unknown(true)
.required()
const strategyScheme = joi.object({
name: joi.string().min(1).default('default')
.description('Descriptive unique name of the strategy')
.example('BizApps'),
realmUrl: joi.string().uri().required()
.description('The absolute uri of the Keycloak realm')
.example('https://localhost:8080/auth/realms/testme'),
clientId: joi.string().min(1).required()
.description('The identifier of the Keycloak client/application')
.example('foobar'),
secret: joi.string().min(1)
.description('The related secret of the Keycloak client/application')
.example('1234-bar-4321-foo'),
publicKey: joi.alternatives().try(
joi.string().regex(/^-----BEGIN RSA PUBLIC KEY-----[\s\S]*-----END RSA PUBLIC KEY-----\s?$/im, 'PEM'),
joi.object().instance(Buffer),
joi.object({
kty: joi.string().required()
}).unknown(true)
).description('The realm its public key related to the private key used to sign the token'),
entitlement: joi.boolean().invalid(false)
.description('The token should be validated with the entitlement API')
.example('true'),
minTimeBetweenJwksRequests: joi.number().integer().positive().allow(0).default(0)
.description('The minimum time between JWKS requests in seconds')
.example(15),
cache: joi.alternatives().try(joi.object({
segment: joi.string().default('keycloakJwt')
}), joi.boolean()).default(false)
.description('The configuration of the hapi.js cache powered by catbox')
.example('true'),
userInfo: joi.array().items(joi.string().min(1))
.description('List of properties which should be included in the `request.auth.credentials` object')
.example([['name', 'email']])
})
.without('entitlement', ['secret', 'publicKey'])
.without('secret', ['entitlement', 'publicKey'])
.without('publicKey', ['entitlement', 'secret'])
.unknown(false)
.required()
/**
* @function
* @private
*
* Check whether the passed in value is a JSON Web Key.
*
* @param {*} key The value to be tested
* @returns {boolean} Whether the value is a JWK
*/
function isJwk (key) {
return !!(key && key.kty)
}
/**
* @function
* @public
*
* Validate the plugin related options.
*
* @param {Object} opts The plugin related options
* @returns {Object} The validated options
*
* @throws {Error} If options are invalid
*/
function verifyPluginOptions (opts) {
return joi.attempt(opts, pluginScheme)
}
/**
* @function
* @public
*
* Validate the strategy related options.
* If `publicKey` is JWK transform to PEM.
*
* @param {Object} opts The plugin related options
* @returns {Object} The validated options
*
* @throws {TypeError} If JWK is malformed or invalid
* @throws {Error} If JWK has an unsupported key type
* @throws {Error} If options are invalid
*/
function verifyStrategyOptions (opts) {
if (isJwk(opts.publicKey)) {
opts.publicKey = jwkToPem(opts.publicKey)
}
return joi.attempt(opts, strategyScheme)
}
/**
* @function
* @public
*
* Get `Boom.unauthorized` error with bound scheme and
* further attributes If error is available, use its
* message. Otherwise the provided message.
*
* @param {Error|null|undefined} err The error object
* @param {string} message The error message
* @param {string} reason The reason for the thrown error
* @param {string} name The strategy name
* @param {string} [scheme = 'Bearer'] The related scheme
* @returns {Boom.unauthorized} The created `Boom` error
*/
function raiseUnauthorized (error, reason, name, scheme = 'Bearer') {
return boom.unauthorized(
error !== errorMessages.missing ? error : null,
scheme,
{
strategy: name ? `keycloak-jwt (${name})` : 'keycloak-jwt',
...(error === errorMessages.missing ? { error } : {}),
...(reason && error !== reason ? { reason } : {})
}
)
}
/**
* @type Object
* @public
*
* Used pre-defined error messages
*/
const errorMessages = {
invalid: 'Invalid credentials',
missing: 'Missing authorization header',
missingName: 'Missing or non-existent strategy name',
rpt: 'Retrieving the RPT failed',
apiKey: 'Retrieving the token with the api key failed'
}
/**
* @function
* @public
*
* Fake `Hapi` reply toolkit to provide an `authenticated` method.
*
* @param {Object|Function} h The original toolkit/mock
* @returns {Object|Function} The decorated toolkit/mock
*/
function fakeToolkit (h) {
if (!h.authenticated && typeof h === 'function') {
h.authenticated = h
}
return h
}
module.exports = {
isJwk,
raiseUnauthorized,
errorMessages,
fakeToolkit,
verifyPluginOptions,
verifyStrategyOptions
}