UNPKG

5.5 kBJavaScriptView Raw
1const got = require('got')
2const GrantManager = require('keycloak-connect/middleware/auth-utils/grant-manager')
3const KeycloakToken = require('keycloak-connect/middleware/auth-utils/token')
4const apiKey = require('./apiKey')
5const cache = require('./cache')
6const token = require('./token')
7const { raiseUnauthorized, errorMessages, fakeToolkit, verify } = require('./utils')
8const pkg = require('../package.json')
9
10/**
11 * @type {Object}
12 * @private
13 *
14 * The plugin related options and instances.
15 */
16let options
17let manager
18let store
19
20/**
21 * @function
22 * @private
23 *
24 * Verify the signed token offline with help of the related
25 * public key or online with the Keycloak server and JWKS.
26 * Both are non-live. Resolve if the verification succeeded.
27 *
28 * @param {string} tkn The token to be validated
29 * @returns {Promise} The error-handled promise
30 */
31async function verifySignedJwt (tkn) {
32 const kcTkn = new KeycloakToken(tkn, options.clientId)
33 await manager.validateToken(kcTkn, 'Bearer')
34
35 return tkn
36}
37
38/**
39 * @function
40 * @private
41 *
42 * Validate the token live with help of the related
43 * Keycloak server, the client identifier and its secret.
44 * Resolve if the request succeeded and token is valid.
45 *
46 * @param {string} tkn The token to be validated
47 * @returns {Promise} The error-handled promise
48 *
49 * @throws {Error} If token is invalid or request failed
50 */
51async function introspect (tkn) {
52 try {
53 const isValid = await manager.validateAccessToken(tkn)
54 if (isValid === false) throw Error(errorMessages.invalid)
55 } catch (err) {
56 throw Error(errorMessages.invalid)
57 }
58
59 return tkn
60}
61
62/**
63 * @function
64 * @private
65 *
66 * Retrieve the Requesting Party Token from the Keycloak Server.
67 *
68 * @param {string} tkn The token to be used for authentication
69 * @returns {Promise} The modified, non-error-handling promise
70 *
71 * @throws {Error} If token is invalid or request failed
72 */
73async function getRpt (tkn) {
74 let body = {}
75
76 try {
77 ({ body } = await got.get(`${options.realmUrl}/authz/entitlement/${options.clientId}`, {
78 headers: { authorization: `bearer ${tkn}` }
79 }))
80 } catch (err) {
81 throw Error(errorMessages.rpt)
82 }
83
84 return body.rpt
85}
86
87/**
88 * @function
89 * @private
90 *
91 * Get validation strategy based on the options.
92 * If `secret` is set the token gets introspected.
93 * If `entitlement` is truthy it retrieves the RPT.
94 * Else perform a non-live validation with public keys.
95 *
96 * @returns {Function} The related validation strategy
97 */
98function getValidateFn () {
99 return options.secret ? introspect : options.entitlement ? getRpt : verifySignedJwt
100}
101
102/**
103 * @function
104 * @public
105 *
106 * Validate a token either with the help of Keycloak
107 * or a related public key. Store the user data in
108 * cache if enabled.
109 *
110 * @param {string} tkn The token to be validated
111 * @param {Function} h The toolkit
112 *
113 * @throws {Boom.unauthorized} If previous validation fails
114 */
115async function handleKeycloakValidation (tkn, h) {
116 try {
117 const info = await getValidateFn()(tkn)
118 const { expiresIn, credentials } = token.getData(info || tkn, options)
119 const userData = { credentials }
120
121 await cache.set(store, tkn, userData, expiresIn)
122 return h.authenticated(userData)
123 } catch (err) {
124 throw raiseUnauthorized(errorMessages.invalid, err.message)
125 }
126}
127
128/**
129 * @function
130 * @public
131 *
132 * Check if token is already cached in memory.
133 * If yes, return cached user data. Otherwise
134 * handle validation with help of Keycloak.
135 *
136 * @param {string} field The authorization field, e.g. the value of `Authorization`
137 * @param {Object} h The reply toolkit
138 *
139 * @throws {Boom.unauthorized} If header is missing or has an invalid format
140 */
141async function validate (field, h = (data) => data) {
142 if (!field) {
143 throw raiseUnauthorized(errorMessages.missing)
144 }
145
146 const tkn = token.create(field)
147 const reply = fakeToolkit(h)
148
149 if (!tkn) {
150 throw raiseUnauthorized(errorMessages.invalid)
151 }
152
153 const cached = await cache.get(store, tkn)
154 return cached ? reply.authenticated(cached) : handleKeycloakValidation(tkn, reply)
155}
156
157/**
158 * @function
159 * @private
160 *
161 * The authentication strategy based on keycloak.
162 * Expect `Authorization: bearer x.y.z` as header.
163 * If the token was sent before and is still cached,
164 * return the cached user data as credentials.
165 *
166 * @param {Hapi.Server} server The created server instance
167 * @returns {Object} The authentication scheme
168 */
169function strategy (server) {
170 return {
171 authenticate (request, h) {
172 return validate(request.raw.req.headers.authorization, h)
173 }
174 }
175}
176
177/**
178 * @function
179 * @public
180 *
181 * The authentication plugin handler.
182 * Initialize memory cache, grant manager for
183 * Keycloak and register Basic Auth.
184 *
185 * @param {Hapi.Server} server The created server instance
186 * @param {Object} opts The plugin related options
187 */
188function register (server, opts) {
189 options = verify(opts)
190 manager = new GrantManager(options)
191 store = cache.create(server, options.cache)
192
193 apiKey.init(server, options)
194
195 if (options.schemeName in server.auth._schemes) {
196 throw Error(`Authentication strategy named ${options.schemeName} already exists.`)
197 }
198
199 if (options.decoratorName in server._core._decorations['server']) {
200 throw Error(`Server decorator named ${options.decoratorName} already exists.`)
201 }
202
203 server.auth.scheme(options.schemeName, strategy)
204 server.decorate('server', options.decoratorName, { validate })
205}
206
207module.exports = { register, pkg, multiple: true }