1 | # hapi-auth-keycloak
|
2 | #### JSON Web Token based Authentication powered by Keycloak
|
3 |
|
4 | [![Travis](https://img.shields.io/travis/felixheck/wurst.svg)](https://travis-ci.org/felixheck/hapi-auth-keycloak/builds/) ![node](https://img.shields.io/node/v/hapi-auth-keycloak.svg) ![npm](https://img.shields.io/npm/dt/hapi-auth-keycloak.svg) [![standard](https://img.shields.io/badge/code_style-standard-brightgreen.svg)](http://standardjs.com/) ![npm](https://img.shields.io/npm/l/hapi-auth-keycloak.svg) [![Coverage Status](https://coveralls.io/repos/github/felixheck/hapi-auth-keycloak/badge.svg?branch=master)](https://coveralls.io/github/felixheck/hapi-auth-keycloak?branch=master)
|
5 | ---
|
6 |
|
7 | 1. [Introduction](#introduction)
|
8 | 2. [Installation](#installation)
|
9 | 3. [Usage](#usage)
|
10 | 4. [API](#api)
|
11 | 5. [Example](#example)
|
12 | 6. [Developing and Testing](#developing-and-testing)
|
13 | 7. [Contribution](#contribution)
|
14 |
|
15 | ---
|
16 |
|
17 | ## Introduction
|
18 | **hapi-auth-keycloak** is a plugin for [hapi.js][hapijs] which enables to protect your endpoints in a smart but professional manner using [Keycloak][keycloak] as authentication service. It is inspired by the related [express.js middleware][keycloak-node]. The plugin validates the passed [`Bearer` token][bearer] offline with a provided public key or online with help of the [Keycloak][keycloak] server. Optionally, the successfully validated tokens and the related user data get cached using [`catbox`][catbox]. The caching enables a fast processing even though the user data don't get changed until the token expires. Furthermore it is possible to enable an api key interceptor proxying the request to an api key service which returns the temporary bearer token. It plays well with the [hapi.js][hapijs]-integrated [authentication/authorization feature][hapi-route-options]. Besides the authentication strategy it is possible to validate tokens by yourself, e.g. to authenticate incoming websocket or queue messages.
|
19 |
|
20 | The modules [`standard`][standardjs] and [`ava`][avajs] are used to grant a high quality implementation.
|
21 |
|
22 | #### Compatibility
|
23 | | Major Release | [hapi.js](https://github.com/hapijs/hapi) version | node version |
|
24 | | --- | --- | --- |
|
25 | | `v4.1` | `>=18.3.1 @hapi/hapi` | `>=8` |
|
26 | | `v4` | `>=18 hapi` | `>=8` |
|
27 | | `v3` | `>=17 hapi` | `>=8` |
|
28 | | `v2` | `>=12 hapi` | `>=6` |
|
29 |
|
30 | ## Installation
|
31 | For installation use [npm][npm]:
|
32 | ```
|
33 | $ npm install --save hapi-auth-keycloak
|
34 | ```
|
35 |
|
36 | or clone the repository:
|
37 | ```
|
38 | $ git clone https://github.com/felixheck/hapi-auth-keycloak
|
39 | ```
|
40 |
|
41 | ## Usage
|
42 | #### Import
|
43 | First you have to import the module:
|
44 | ``` js
|
45 | const authKeycloak = require('hapi-auth-keycloak');
|
46 | ```
|
47 |
|
48 | #### Create hapi server
|
49 | Afterwards create your hapi server if not already done:
|
50 | ``` js
|
51 | const hapi = require('@hapi/hapi');
|
52 |
|
53 | const server = hapi.server({ port: 8888 });
|
54 | ```
|
55 |
|
56 | #### Registration
|
57 | Finally register the plugin, set the correct options and the authentication strategy:
|
58 | ``` js
|
59 | await server.register({
|
60 | plugin: authKeycloak,
|
61 | options: {
|
62 | realmUrl: 'https://localhost:8080/auth/realms/testme',
|
63 | clientId: 'foobar',
|
64 | minTimeBetweenJwksRequests: 15,
|
65 | cache: true,
|
66 | userInfo: ['name', 'email']
|
67 | }
|
68 | });
|
69 |
|
70 | server.auth.strategy('keycloak-jwt', 'keycloak-jwt');
|
71 | ```
|
72 |
|
73 | #### Route Configuration & Scope
|
74 | Define your routes and add `keycloak-jwt` when necessary. It is possible to define the necessary scope like documented by the [express.js middleware][keycloak-node]:
|
75 |
|
76 | - To secure an endpoint with a resource's role , use the role name (e.g. `editor`).
|
77 | - To secure an endpoint with another resource's role, prefix the role name (e.g. `other-resource:creator`)
|
78 | - To secure an endpoint with a realm role, prefix the role name with `realm:` (e.g. `realm:admin`).
|
79 | - To secure an endpoint with [fine-grained scope definitions][rpt], prefix the Keycloak scopes with `scope:` (e.g. `scope:foo.READ`).
|
80 |
|
81 | ``` js
|
82 | server.route([
|
83 | {
|
84 | method: 'GET',
|
85 | path: '/',
|
86 | config: {
|
87 | description: 'protected endpoint',
|
88 | auth: {
|
89 | strategies: ['keycloak-jwt'],
|
90 | access: {
|
91 | scope: ['realm:admin', 'editor', 'other-resource:creator', 'scope:foo.READ']
|
92 | }
|
93 | },
|
94 | handler () {
|
95 | return 'hello world';
|
96 | }
|
97 | }
|
98 | },
|
99 | ]);
|
100 | ```
|
101 |
|
102 | ## API
|
103 | #### Plugin Options
|
104 |
|
105 | > By default, the Keycloak server has built-in [two ways to authenticate][client-auth] the client: client ID and client secret **(1)**, or with a signed JWT **(2)**. This plugin supports both. If a non-live strategy is used, ensure that the identifier of the related realm key is included in their header as `kid`. Check the description of `secret`/`publicKey`/`entitlement` and the [terminology][rpt-terms] for further information.
|
106 | >
|
107 | > | Strategies | Online* | Live** |[Scopes][rpt] | Truthy Option | Note |
|
108 | > |:-----------|:------:|:----:|:-------------:|:---------------|:-------------|
|
109 | > | (1) + (2) | | | | `publicKey` | fast |
|
110 | > | (1) + (2) | x | | | | flexible |
|
111 | > | (1) | x | x | | `secret` | accurate |
|
112 | > | (1) + (2) | x | x | x | `entitlement` | fine-grained |
|
113 | >
|
114 | > **\***: Plugin interacts with the Keycloak API<br/>
|
115 | > **\*\***: Plugin validates token with help of the Keycloak API<br/>
|
116 | >
|
117 | > Please mind that the accurate strategy is 4-5x faster than the fine-grained one.<br/>
|
118 | > **Hint:** If you define neither `secret` nor `public` nor `entitlement`, the plugin retrieves the public key itself from `{realmUrl}/protocol/openid-connect/certs`.
|
119 |
|
120 | - `schemeName {string}` — The name used for the authentication scheme of the hapi server. Optional. Default: `keycloak-jwt`.
|
121 |
|
122 | - `decoratorName {string}` — The name used for the server decorator to validate the token, [see below](#await-serverdecoratorname--kjwtvalidatefield-string). Optional. Default: `kjwt`.
|
123 |
|
124 | - `realmUrl {string}` – The absolute uri of the Keycloak realm.<br/>
|
125 | Required. Example: `https://localhost:8080/auth/realms/testme`<br/>
|
126 |
|
127 | - `clientId {string}` – The identifier of the Keycloak client/application.<br/>
|
128 | Required. Example: `foobar`<br/>
|
129 |
|
130 | - `secret {string}` – The related secret of the Keycloak client/application.<br/>
|
131 | Defining this option enables the traditional method described in the OAuth2 specification and performs an [introspect][introspect] request.<br/>
|
132 | Optional. Example: `1234-bar-4321-foo`<br/>
|
133 |
|
134 | - `publicKey {string|Buffer|Object}` – The realm its public key related to the private key used to sign the token.<br/>
|
135 | Defining this option enables the offline and non-live validation. The public key has to be in [PEM][pem] (`{string|Buffer}`) or [JWK][jwk] (`{Object}`) format. Algorithm has to be `RSA-SHA256` compatible.<br/>
|
136 | Optional.
|
137 |
|
138 | - `entitlement {boolean=true}` – The token should be validated with the entitlement API to enable fine-grained authorization. Enabling this option decelerates the process marginally. Mind that `false` is an invalid value.<br/>
|
139 | Optional. Default: `undefined`.
|
140 |
|
141 | - `minTimeBetweenJwksRequests {number}` – The minimum time between JWKS requests in seconds.<br/>
|
142 | This is relevant for the online/non-live strategy retrieving JWKS from the Keycloak server.<br/>
|
143 | The value have to be a positive integer.<br/>
|
144 | Optional. Default: `0`.
|
145 |
|
146 | - `userInfo {Array.<?string>}` — List of properties which should be included in the `request.auth.credentials` object besides `scope` and `sub`.<br/>
|
147 | Optional. Default: `[]`.
|
148 |
|
149 | - `cache {Object|boolean}` — The configuration of the [hapi.js cache][hapi-server-cache] powered by [catbox][catbox]. If the property `exp` ('expires at') is undefined, the plugin uses 60 seconds as default TTL. Otherwise the cache entry expires as soon as the token itself expires.<br/>
|
150 | Please mind that an enabled cache leads to disabled live validation after the related token is cached once.<br/>
|
151 | If `false` the cache is disabled. Use `true` or an empty object (`{}`) to use the built-in default cache. Otherwise just drop in your own cache configuration.<br/>
|
152 | Optional. Default: `false`.
|
153 |
|
154 | - `apiKey {Object}` — The options object enabling an api key service as middleware<br/>
|
155 | Optional. Default: `undefined`.
|
156 |
|
157 | - `url {string}` — The absolute url to be requested. It's possible to use a [`pupa` template][pupa] with placeholders called `realm` and `clientId` getting rendered based on the passed options.<br/>
|
158 | Example: `http://barfoo.com/foo/{clientId}`<br/>
|
159 | Required.
|
160 |
|
161 | - `in {string}` — Whether the api key is placed in the headers or query.<br/>
|
162 | Allowed values: `headers` & `query`<br/>
|
163 | Optional. Default: `headers`.
|
164 |
|
165 | - `name {string}` — The name of the related headers field or query key.<br/>
|
166 | Optional. Default: `authorization`.
|
167 |
|
168 | - `prefix {string}` — An optional prefix of the related api key value. Mind a trailing space if necessary.<br/>
|
169 | Optional. Default: `Api-Key `.
|
170 |
|
171 | - `tokenPath {string}` — The path to the access token in the response its body as dot notation.<br/>
|
172 | Optional. Default: `access_token`.
|
173 |
|
174 | - `request {Object}` – The detailed request options for [`got`][got].<br/>
|
175 | Optional. Default: `{}`
|
176 |
|
177 | #### `await server[decoratorName = 'kjwt'].validate(field {string})`
|
178 | - `field {string}` — The `Bearer` field, including the scheme (`bearer`) itself.<br/>
|
179 | Example: `bearer 12345.abcde.67890`.<br/>
|
180 | Required.
|
181 |
|
182 | If an error occurs, it gets thrown — so take care and implement a kind of catching.<br/>
|
183 | If the token is invalid, the `result` is `false`. Otherwise it is an object containing all relevant credentials.
|
184 |
|
185 | ## Example
|
186 | #### `routes.js`
|
187 |
|
188 | ``` js
|
189 | async function register (server, options) {
|
190 | server.route([
|
191 | {
|
192 | method: 'GET',
|
193 | path: '/',
|
194 | config: {
|
195 | auth: {
|
196 | strategies: ['keycloak-jwt'],
|
197 | access: {
|
198 | scope: ['realm:admin', 'editor', 'other-resource:creator', 'scope:foo.READ']
|
199 | }
|
200 | },
|
201 | handler (req, reply) {
|
202 | reply(req.auth.credentials);
|
203 | }
|
204 | }
|
205 | }
|
206 | ]);
|
207 | }
|
208 |
|
209 | module.exports = {
|
210 | register,
|
211 | name: 'example-routes',
|
212 | version: '0.0.1'
|
213 | };
|
214 | ```
|
215 |
|
216 | #### `index.js`
|
217 | ``` js
|
218 | const hapi = require('@hapi/hapi');
|
219 | const authKeycloak = require('hapi-auth-keycloak');
|
220 | const routes = require('./routes');
|
221 |
|
222 | const server = hapi.server({ port: 3000 });
|
223 |
|
224 | const options = {
|
225 | realmUrl: 'https://localhost:8080/auth/realms/testme',
|
226 | clientId: 'foobar',
|
227 | minTimeBetweenJwksRequests: 15,
|
228 | cache: true,
|
229 | userInfo: ['name', 'email']
|
230 | };
|
231 |
|
232 | process.on('SIGINT', async () => {
|
233 | try {
|
234 | await server.stop();
|
235 | } catch (err) {
|
236 | process.exit(err ? 1 : 0);
|
237 | }
|
238 | });
|
239 |
|
240 | (async () => {
|
241 | try {
|
242 | await server.register({ plugin: authKeycloak, options });
|
243 | server.auth.strategy('keycloak-jwt', 'keycloak-jwt');
|
244 | await server.register({ plugin: routes });
|
245 | await server.start();
|
246 | console.log('Server started successfully');
|
247 | } catch (err) {
|
248 | console.error(err);
|
249 | }
|
250 | })();
|
251 | ```
|
252 |
|
253 | ## Developing and Testing
|
254 | First you have to install all dependencies:
|
255 | ```
|
256 | $ npm install
|
257 | ```
|
258 |
|
259 | To execute all unit tests once, use:
|
260 | ```
|
261 | $ npm test
|
262 | ```
|
263 |
|
264 | or to run tests based on file watcher, use:
|
265 | ```
|
266 | $ npm start
|
267 | ```
|
268 |
|
269 | To get information about the test coverage, use:
|
270 | ```
|
271 | $ npm run coverage
|
272 | ```
|
273 |
|
274 | ## Contribution
|
275 | Fork this repository and push in your ideas.
|
276 |
|
277 | Do not forget to add corresponding tests to keep up 100% test coverage.<br/>
|
278 | For further information read the [contributing guideline](CONTRIBUTING.md).
|
279 |
|
280 | [keycloak]: http://www.keycloak.org/
|
281 | [keycloak-node]: https://keycloak.gitbooks.io/documentation/content/securing_apps/topics/oidc/nodejs-adapter.html
|
282 | [hapijs]: https://hapijs.com/
|
283 | [avajs]: https://github.com/avajs/ava
|
284 | [standardjs]: https://standardjs.com/
|
285 | [babel]: https://babeljs.io/
|
286 | [npm]: https://github.com/npm/npm
|
287 | [jwt]: https://jwt.io/
|
288 | [catbox]: https://github.com/hapijs/catbox
|
289 | [bearer]: https://tools.ietf.org/html/rfc6750
|
290 | [hapi-server-cache]: https://hapijs.com/api#-servercacheoptions
|
291 | [hapi-route-options]: https://hapijs.com/api#route-options
|
292 | [jwk]: https://tools.ietf.org/html/rfc7517
|
293 | [pem]: https://tools.ietf.org/html/rfc1421
|
294 | [client-auth]: https://www.keycloak.org/docs/3.1/securing_apps/topics/oidc/java/client-authentication.html
|
295 | [introspect]: https://www.keycloak.org/docs/3.2/authorization_services/topics/service/protection/token-introspection.html
|
296 | [rpt]: https://www.keycloak.org/docs/3.2/authorization_services/topics/service/entitlement/entitlement-api-aapi.html
|
297 | [rpt-terms]: https://www.keycloak.org/docs/3.2/authorization_services/topics/overview/terminology.html
|
298 | [got]: https://github.com/sindresorhus/got
|
299 | [pupa]: https://github.com/sindresorhus/pupa
|