hapi-auth-keycloak
v5.1.2
Published
JSON Web Token based Authentication powered by Keycloak
Downloads
1,534
Maintainers
Readme
hapi-auth-keycloak
JSON Web Token based Authentication powered by Keycloak
Introduction
hapi-auth-keycloak is a plugin for hapi.js which enables to protect your endpoints in a smart but professional manner using Keycloak as authentication service. It is inspired by the related express.js middleware. The plugin validates the passed Bearer
token offline with a provided public key or online with help of the Keycloak server. Optionally, the successfully validated tokens and the related user data get cached using 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-integrated authentication/authorization feature. Besides the authentication strategy it is possible to validate tokens by yourself, e.g. to authenticate incoming websocket or queue messages, and to register/use multiple strategies via jscheffner/hapi-auth-any.
The modules standard
and ava
are used to grant a high quality implementation.
Compatibility
| Major Release | hapi.js version | node version |
| ------------- | ------------------------------------------------- | ------------ |
| v5
| >=18.4 @hapi/hapi
| >=12
|
| v4.1
| >=18.3.1 @hapi/hapi
| >=8
|
| v4
| >=18 hapi
| >=8
|
| v3
| >=17 hapi
| >=8
|
| v2
| >=12 hapi
| >=6
|
Installation
For installation use npm:
$ npm install --save hapi-auth-keycloak
or clone the repository:
$ git clone https://github.com/felixheck/hapi-auth-keycloak
Usage
Import
First you have to import the module:
const authKeycloak = require("hapi-auth-keycloak");
Create hapi server
Afterwards create your hapi server if not already done:
const hapi = require("@hapi/hapi");
const server = hapi.server({ port: 8888 });
Registration
Finally register the plugin, set the correct options and the authentication strategy:
await server.register({ plugin: authKeycloak });
server.auth.strategy("keycloak-jwt", "keycloak-jwt", {
realmUrl: "https://localhost:8080/auth/realms/testme",
clientId: "foobar",
minTimeBetweenJwksRequests: 15,
cache: true,
userInfo: ["name", "email"],
});
Route Configuration & Scope
Define your routes and add keycloak-jwt
when necessary. It is possible to define the necessary scope like documented by the express.js middleware:
- To secure an endpoint with a resource's role , use the role name (e.g.
editor
). - To secure an endpoint with another resource's role, prefix the role name (e.g.
other-resource:creator
) - To secure an endpoint with a realm role, prefix the role name with
realm:
(e.g.realm:admin
). - To secure an endpoint with fine-grained scope definitions, prefix the Keycloak scopes with
scope:
(e.g.scope:foo.READ
). - To secure an endpoint with a OAuth2 client scope, prefix the client scope with
clientscope:
(e.g.clientscope:profile
).
server.route([
{
method: "GET",
path: "/",
config: {
description: "protected endpoint",
auth: {
strategies: ["keycloak-jwt"],
access: {
scope: [
"realm:admin",
"editor",
"other-resource:creator",
"scope:foo.READ",
"clientscope:profile",
],
},
},
handler() {
return "hello world";
},
},
},
]);
API
Plugin Options
apiKey {Object}
— The options object enabling an api key service as middleware Optional. Default:undefined
.url {string}
— The absolute url to be requested. It's possible to use apupa
template with placeholders calledrealm
andclientId
getting rendered based on the passed plugin-related options. Example:http://barfoo.com/foo/{clientId}
Required.in {string}
— Whether the api key is placed in the headers or query. Allowed values:headers
&query
Optional. Default:headers
.name {string}
— The name of the related headers field or query key. Optional. Default:authorization
.prefix {string}
— An optional prefix of the related api key value. Mind a trailing space if necessary. Optional. Default:Api-Key
.tokenPath {string}
— The path to the access token in the response its body as dot notation. Optional. Default:access_token
.request {Object}
– The detailed request options forgot
. Optional. Default:{}
Plugin + Strategy Options
These options can be set in both ways: strategy-specific and as default for all strategies via plugin-specific options. When defining just one strategy, it's totally fine to set the whole option object as plugin options. Keep in mind that name
needs to be unique for each strategy, default is default
.
By default, the Keycloak server has built-in two ways to authenticate 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 ofsecret
/publicKey
/entitlement
and the terminology for further information.| Strategies | Online* | Live** | Scopes | Truthy Option | Note | | :--------- | :------: | :------: | :-----------: | :------------ | :----------- | | (1) + (2) | | | |
publicKey
| fast | | (1) + (2) | x | | | | flexible | | (1) | x | x | |secret
| accurate | | (1) + (2) | x | x | x |entitlement
| fine-grained |*: Plugin interacts with the Keycloak API > **: Plugin validates token with help of the Keycloak API
Please mind that the accurate strategy is 4-5x faster than the fine-grained one. > Hint: If you define neither
secret
norpublic
norentitlement
, the plugin retrieves the public key itself from{realmUrl}/protocol/openid-connect/certs
.
name {string}
– The unique name of the strategy Required. ExampleBizApps
realmUrl {string}
– The absolute uri of the Keycloak realm. Required. Example:https://localhost:8080/auth/realms/testme
clientId {string}
– The identifier of the Keycloak client/application. Required. Example:foobar
secret {string}
– The related secret of the Keycloak client/application. Defining this option enables the traditional method described in the OAuth2 specification and performs an introspect request. Optional. Example:1234-bar-4321-foo
publicKey {string|Buffer|Object}
– The realm its public key related to the private key used to sign the token. Defining this option enables the offline and non-live validation. The public key has to be in PEM ({string|Buffer}
) or JWK ({Object}
) format. Algorithm has to beRSA-SHA256
compatible. Optional.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 thatfalse
is an invalid value. Optional. Default:undefined
.minTimeBetweenJwksRequests {number}
– The minimum time between JWKS requests in seconds. This is relevant for the online/non-live strategy retrieving JWKS from the Keycloak server. The value have to be a positive integer. Optional. Default:0
.userInfo {Array.<?string>}
— List of properties which should be included in therequest.auth.credentials
object besidesscope
andsub
. Optional. Default:[]
.cache {Object|boolean}
— The configuration of the hapi.js cache powered by catbox. If the propertyexp
('expires at') is undefined, the plugin uses 60 seconds as default TTL. Otherwise the cache entry expires as soon as the token itself expires. Please mind that an enabled cache leads to disabled live validation after the related token is cached once. Iffalse
the cache is disabled. Usetrue
or an empty object ({}
) to use the built-in default cache. Otherwise just drop in your own cache configuration. Optional. Default:false
.
await server.kjwt.validate(field {string}, name {string})
field {string}
— TheBearer
field, including the scheme (bearer
) itself. Example:bearer 12345.abcde.67890
. Required.name {string}
— Thename
strategy option, to select the strategy to be used. Example:BizApps
. Required.
If an error occurs, it gets thrown — so take care and implement a kind of catching.
If the token is invalid, the result
is false
. Otherwise it is an object containing all relevant credentials.
Example
routes.js
async function register(server, options) {
server.route([
{
method: "GET",
path: "/",
config: {
auth: {
strategies: ["keycloak-jwt"],
access: {
scope: [
"realm:admin",
"editor",
"other-resource:creator",
"scope:foo.READ",
],
},
},
handler(req, reply) {
reply(req.auth.credentials);
},
},
},
]);
}
module.exports = {
register,
name: "example-routes",
version: "0.0.1",
};
index.js
const hapi = require("@hapi/hapi");
const authKeycloak = require("hapi-auth-keycloak");
const routes = require("./routes");
const server = hapi.server({ port: 3000 });
const pluginOptions = {
apiKey: {
url: "http://barfoo.com/foo/foobar",
},
};
const strategyOptions = {
realmUrl: "https://localhost:8080/auth/realms/testme",
clientId: "foobar",
minTimeBetweenJwksRequests: 15,
cache: true,
userInfo: ["name", "email"],
};
process.on("SIGINT", async () => {
try {
await server.stop();
} catch (err) {
process.exit(err ? 1 : 0);
}
});
(async () => {
try {
await server.register({
plugin: authKeycloak,
options: pluginOptions,
});
server.auth.strategy("keycloak-jwt", "keycloak-jwt", strategyOptions);
await server.register({ plugin: routes });
await server.start();
console.log("Server started successfully");
} catch (err) {
console.error(err);
}
})();
Migration Guides
v4.2
to v4.3
Features
- It's now possible to register multiple strategies with the same scheme
keycloak-jwt
Changes
name
is a new unique strategy-related option.apiKey.url
its placeholders are replaced with plugin-related options, not the unique strategy ones.- The option setup changed. All plugin-related options are used as defaults for strategy-related options.
- Even though every strategy-related option can also set via the plugin options,
apiKey
can only be set once in the plugin options.
In case of multiple registered strategies for this scheme:
- Use at least a different
name
option in each strategy. server.kjwt.validate
requiresname
as second argument
Attention
- Needs a custom scheme when multiple strategies are used on single routes. Check jscheffner/hapi-auth-any.
Developing and Testing
First you have to install all dependencies:
$ npm install
To execute all unit tests once, use:
$ npm test
or to run tests based on file watcher, use:
$ npm start
To get information about the test coverage, use:
$ npm run coverage
Contribution
Fork this repository and push in your ideas.
Do not forget to add corresponding tests to keep up 100% test coverage. For further information read the contributing guideline.