-
Notifications
You must be signed in to change notification settings - Fork 420
/
keycloak.js
430 lines (386 loc) · 15.4 KB
/
keycloak.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
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
/*
* Copyright 2016 Red Hat Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
const BearerStore = require('./stores/bearer-store')
const CookieStore = require('./stores/cookie-store')
const SessionStore = require('./stores/session-store')
const Config = require('./middleware/auth-utils/config')
const GrantManager = require('./middleware/auth-utils/grant-manager')
const Setup = require('./middleware/setup')
const Admin = require('./middleware/admin')
const Logout = require('./middleware/logout')
const PostAuth = require('./middleware/post-auth')
const GrantAttacher = require('./middleware/grant-attacher')
const Protect = require('./middleware/protect')
const Enforcer = require('./middleware/enforcer')
const CheckSso = require('./middleware/check-sso')
/**
* Instantiate a Keycloak.
*
* The `config` and `keycloakConfig` hashes are both optional.
*
* The `config` hash, if provided, may include either `store`, pointing
* to the actual session-store used by your application, or `cookies`
* with boolean `true` as the value to support using cookies as your
* authentication store. Bear in mind that cookies session store expects
* a cookie parser to be present, e.g. "cookie-parser" in case of Express.js.
*
* A session-based store is recommended, as it allows more assured control
* from the Keycloak console to explicitly logout some or all sessions.
*
* In all cases, also, authentication through a Bearer authentication
* header is supported for non-interactive APIs.
*
* The `keycloakConfig` object, by default, is populated by the contents of
* a `keycloak.json` file installed alongside your application, copied from
* the Keycloak administration console when you provision your application.
*
* @constructor
*
* @param {Object} config Configuration for the Keycloak connector.
* @param {Object} keycloakConfig Keycloak-specific configuration.
*
* @return {Keycloak} A constructed Keycloak object.
*
*/
function Keycloak (config, keycloakConfig) {
// If keycloakConfig is null, Config() will search for `keycloak.json`.
this.config = new Config(keycloakConfig)
this.grantManager = new GrantManager(this.config)
this.stores = [BearerStore]
if (!config) {
throw new Error('Adapter configuration must be provided.')
}
// Add the custom scope value
this.config.scope = config.scope
if (config && config.store && config.cookies) {
throw new Error('Either `store` or `cookies` may be set, but not both')
}
if (config && config.store) {
this.stores.push(new SessionStore(config.store))
} else if (config && config.cookies) {
this.stores.push(CookieStore)
}
this.config.idpHint = config.idpHint
}
/**
* Obtain an array of middleware for use in your application.
*
* Generally this should be installed at the root of your application,
* as it provides general wiring for Keycloak interaction, without actually
* causing Keycloak to get involved with any particular URL until asked
* by using `protect(...)`.
*
* Example:
*
* var app = express();
* var keycloak = new Keycloak();
* app.use( keycloak.middleware() );
*
* Options:
*
* - `logout` URL for logging a user out. Defaults to `/logout`.
* - `admin` Root URL for Keycloak admin callbacks. Defaults to `/`.
*
* @param {Object} options Optional options for specifying details.
*/
Keycloak.prototype.middleware = function (options) {
if (!options) {
options = { logout: '', admin: '' }
}
options.logout = options.logout || '/logout'
options.admin = options.admin || '/'
const middlewares = []
middlewares.push(Setup)
middlewares.push(PostAuth(this))
middlewares.push(Admin(this, options.admin))
middlewares.push(GrantAttacher(this))
middlewares.push(Logout(this, options.logout))
return middlewares
}
/**
* Apply protection middleware to an application or specific URL.
*
* If no `spec` parameter is provided, the subsequent handlers will
* be invoked if the user is authenticated, regardless of what roles
* he or she may or may not have.
*
* If a user is not currently authenticated, the middleware will cause
* the authentication workflow to begin by redirecting the user to the
* Keycloak installation to login. Upon successful login, the user will
* be redirected back to the originally-requested URL, fully-authenticated.
*
* If a `spec` is provided, the same flow as above will occur to ensure that
* a user it authenticated. Once authenticated, the spec will then be evaluated
* to determine if the user may or may not access the following resource.
*
* The `spec` may be either a `String`, specifying a single required role,
* or a function to make more fine-grained determination about access-control
*
* If the `spec` is a `String`, then the string will be interpreted as a
* role-specification according to the following rules:
*
* - If the string starts with `realm:`, the suffix is treated as the name
* of a realm-level role that is required for the user to have access.
* - If the string contains a colon, the portion before the colon is treated
* as the name of an application within the realm, and the portion after the
* colon is treated as a role within that application. The user then must have
* the named role within the named application to proceed.
* - If the string contains no colon, the entire string is interpreted as
* as the name of a role within the current application (defined through
* the installed `keycloak.json` as provisioned within Keycloak) that the
* user must have in order to proceed.
*
* Example
*
* // Users must have the `special-people` role within this application
* app.get( '/special/:page', keycloak.protect( 'special-people' ), mySpecialHandler );
*
* If the `spec` is a function, it may take up to two parameters in order to
* assist it in making an authorization decision: the access token, and the
* current HTTP request. It should return `true` if access is allowed, otherwise
* `false`.
*
* The `token` object has a method `hasRole(...)` which follows the same rules
* as above for `String`-based specs.
*
* // Ensure that users have either `nicepants` realm-level role, or `mr-fancypants` app-level role.
* function pants(token, request) {
* return token.hasRole( 'realm:nicepants') || token.hasRole( 'mr-fancypants');
* }
*
* app.get( '/fancy/:page', keycloak.protect( pants ), myPantsHandler );
*
* With no spec, simple authentication is all that is required:
*
* app.get( '/complain', keycloak.protect(), complaintHandler );
*
* @param {String} spec The protection spec (optional)
*/
Keycloak.prototype.protect = function (spec) {
return Protect(this, spec)
}
/**
* Enforce access based on the given permissions. This method operates in two modes, depending on the `response_mode`
* defined for this policy enforcer.
*
* If `response_mode` is set to `token`, permissions are obtained using an specific grant type. As a consequence, the
* token with the permissions granted by the server is updated and made available to the application via `request.kauth.grant.access_token`.
* Use this mode when your application is using sessions and you want to cache previous decisions from the server, as well automatically handle
* refresh tokens. This mode is especially useful for applications acting as client and resource server.
*
* If `response_mode` is set to `permissions`, the server only returns the list of granted permissions (no oauth2 response).
* Previous decisions are not cached and the policy enforcer will query the server every time to get a decision.
* This is the default `response_mode`.
*
* You can set `response_mode` as follows:
*
* keycloak.enforcer('item:read', {response_mode: 'token'})
*
* In all cases, if the request is already populated with a valid access token (for instance, bearer tokens sent by clients to the application),
* the policy enforcer will first try to resolve permissions from the current token before querying the server.
*
* By default, the policy enforcer will use the `client_id` defined to the application (for instance, via `keycloak.json`) to
* reference a client in Keycloak that supports Keycloak Authorization Services. In this case, the client can not be public given
* that it is actually a resource server.
*
* If your application is acting as a client and resource server, you can use the following configuration to specify the client
* in Keycloak with the authorization settings:
*
* keycloak.enforcer('item:read', {resource_server_id: 'nodejs-apiserver'})
*
* It is recommended to use separated clients in Keycloak to represent your frontend and backend.
*
* If the application you are protecting is enabled with Keycloak authorization services and you have defined client credentials
* in `keycloak.json`, you can push additional claims to the server and make them available to your policies in order to make decisions.
* For that, you can define a `claims` configuration option which expects a `function` that returns a JSON with the claims you want to push:
*
* app.get('/protected/resource', keycloak.enforcer(['resource:view', 'resource:write'], {
claims: function(request) {
return {
"http.uri": ["/protected/resource"],
"user.agent": // get user agent from request
}
}
}), function (req, res) {
// access granted
});
*
* @param {String[]} expectedPermissions A single string representing a permission or an arrat of strings representing the permissions. For instance, 'item:read' or ['item:read', 'item:write'].
*/
Keycloak.prototype.enforcer = function (permissions, config) {
return new Enforcer(this, config).enforce(permissions)
}
/**
* Apply check SSO middleware to an application or specific URL.
*
* Check SSO will only authenticate the client if the user is already logged-in,
* if the user is not logged-in the browser will be redirected back
* to the originally-requested URL and remain unauthenticated.
*
*/
Keycloak.prototype.checkSso = function () {
return CheckSso(this)
}
/**
* Callback made upon successful authentication of a user.
*
* By default, this a no-op, but may assigned to another
* function for application-specific login which may be useful
* for linking authentication information from Keycloak to
* application-maintained user information.
*
* The `request.kauth.grant` object contains the relevant tokens
* which may be inspected.
*
* For instance, to obtain the unique subject ID:
*
* request.kauth.grant.id_token.sub => bf2056df-3803-4e49-b3ba-ff2b07d86995
*
* @param {Object} request The HTTP request.
*/
Keycloak.prototype.authenticated = function (request) {
// no-op
}
/**
* Callback made upon successful de-authentication of a user.
*
* By default, this is a no-op, but may be used by the application
* in the case it needs to remove information from the user's session
* or otherwise perform additional logic once a user is logged out.
*
* @param {Object} request The HTTP request.
*/
Keycloak.prototype.deauthenticated = function (request) {
// no-op
}
/**
* Replaceable function to handle access-denied responses.
*
* In the event the Keycloak middleware decides a user may
* not access a resource, or has failed to authenticate at all,
* this function will be called.
*
* By default, a simple string of "Access denied" along with
* an HTTP status code for 403 is returned. Chances are an
* application would prefer to render a fancy template.
*/
Keycloak.prototype.accessDenied = function (request, response) {
response.status(403)
response.end('Access denied')
}
/*! ignore */
Keycloak.prototype.getGrant = function (request, response) {
let rawData
for (let i = 0; i < this.stores.length; ++i) {
rawData = this.stores[i].get(request)
if (rawData) {
// store = this.stores[i];
break
}
}
let grantData = rawData
if (typeof (grantData) === 'string') {
grantData = JSON.parse(grantData)
}
if (grantData && !grantData.error) {
const self = this
return this.grantManager.createGrant(JSON.stringify(grantData))
.then(grant => {
self.storeGrant(grant, request, response)
return grant
})
.catch(() => { return Promise.reject(new Error('Could not store grant code error')) })
}
return Promise.reject(new Error('Could not obtain grant code error'))
}
Keycloak.prototype.storeGrant = function (grant, request, response) {
if (this.stores.length < 2 || BearerStore.get(request)) {
// cannot store bearer-only, and should not store if grant is from the
// authorization header
return
}
if (!grant) {
this.accessDenied(request, response)
return
}
this.stores[1].wrap(grant)
grant.store(request, response)
return grant
}
Keycloak.prototype.unstoreGrant = function (sessionId) {
if (this.stores.length < 2) {
// cannot unstore, bearer-only, this is weird
return
}
this.stores[1].clear(sessionId)
}
Keycloak.prototype.getGrantFromCode = function (code, request, response) {
if (this.stores.length < 2) {
// bearer-only, cannot do this;
throw new Error('Cannot exchange code for grant in bearer-only mode')
}
const sessionId = request.session.id
const self = this
return this.grantManager.obtainFromCode(request, code, sessionId)
.then(function (grant) {
self.storeGrant(grant, request, response)
return grant
})
}
Keycloak.prototype.checkPermissions = function (authzRequest, request, callback) {
const self = this
return this.grantManager.checkPermissions(authzRequest, request, callback)
.then(function (grant) {
if (!authzRequest.response_mode) {
self.storeGrant(grant, request)
}
return grant
})
}
Keycloak.prototype.loginUrl = function (uuid, redirectUrl) {
let url = this.config.realmUrl +
'/protocol/openid-connect/auth' +
'?client_id=' + encodeURIComponent(this.config.clientId) +
'&state=' + encodeURIComponent(uuid) +
'&redirect_uri=' + encodeURIComponent(redirectUrl) +
'&scope=' + encodeURIComponent(this.config.scope ? 'openid ' + this.config.scope : 'openid') +
'&response_type=code'
if (this.config && this.config.idpHint) {
url += '&kc_idp_hint=' + encodeURIComponent(this.config.idpHint)
}
return url
}
Keycloak.prototype.logoutUrl = function (redirectUrl, idTokenHint) {
const url = new URL(this.config.realmUrl + '/protocol/openid-connect/logout')
if (redirectUrl && idTokenHint) {
url.searchParams.set('id_token_hint', idTokenHint)
url.searchParams.set('post_logout_redirect_uri', redirectUrl)
}
return url.toString()
}
Keycloak.prototype.accountUrl = function () {
return this.config.realmUrl + '/account'
}
Keycloak.prototype.getAccount = function (token) {
return this.grantManager.getAccount(token)
}
Keycloak.prototype.redirectToLogin = function (request) {
return !this.config.bearerOnly
}
Keycloak.prototype.getConfig = function () {
return this.config
}
module.exports = Keycloak