UNPKG

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