Skip to content

Commit

Permalink
v3 (#12)
Browse files Browse the repository at this point in the history
* removing babel
* excluding local resources
* updating documentation
* updating examples
* upgrading implementation
  • Loading branch information
jkyberneees authored Dec 28, 2022
1 parent 03bf52a commit 76d4dbd
Show file tree
Hide file tree
Showing 17 changed files with 165 additions and 200 deletions.
12 changes: 0 additions & 12 deletions .babelrc

This file was deleted.

6 changes: 5 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -60,4 +60,8 @@ typings/
# transpiled sources
src/

.DS_Store
.DS_Store

*.pem

local
79 changes: 32 additions & 47 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# keycloak-backend
Keycloak Node.js minimalist connector for backend services integration. It aims to serve as base for high performance authorization middlewares.

> Note: Version 2.x uses `jsonwebtoken 8.x`
> In order to use this module, the used Keycloak client `Direct Access Grants Enabled` setting should be `ON`
## Keycloak Introduction
The awesome open-source Identity and Access Management solution develop by RedHat.
Expand All @@ -22,69 +22,54 @@ Keycloak support those very nice features you are looking for:
More about Keycloak: http://www.keycloak.org/

## Using the keycloak-backend module
### Instantiating
### Configuration
```js
const keycloak = require('keycloak-backend')({
"realm": "your realm name",
"auth-server-url": "http://keycloak.dev:8080",
"client_id": "your client name",
"client_secret": "c88a2c21-9d1a-4f83-a18d-66d75c4d8020", // if required
"username": "your service username",
"password": "your service password"
"realm": "realm-name",
"keycloak_base_url": "https://keycloak.example.org",
"client_id": "super-secure-client",
"username": "user@example.org",
"password": "passw0rd",
"is_legacy_endpoint": false
});
```
> The `is_legacy_endpoint` configuration property should be TRUE for older Keycloak versions (under 18)
### Validating access tokens
#### Online validation:
This method requires online connection to the Keycloak service to validate the access token. It is highly secure since it also check for the possible token invalidation. The disadvantage is that a request to the Keycloak service happens on every validation attempt.
### Generating access tokens
```js
let token = await keycloak.jwt.verify(someAccessToken);
//console.log(token.isExpired());
//console.log(token.hasRealmRole('user'));
//console.log(token.hasApplicationRole('app-client-name', 'some-role'));
const accessToken = await keycloak.accessToken.get()
```

#### Offline validation:
This method perform offline JWT verification against the access token using the Keycloak Realm public key. Performance is higher compared to the online method, the disadvantage is that access token invalidation will not work until the token is expired.
```js
let cert = fs.readFileSync('public_cert.pem');
token = await keycloak.jwt.verifyOffline(someAccessToken, cert);
//console.log(token.isExpired());
//console.log(token.hasRealmRole('user'));
//console.log(token.hasApplicationRole('app-client-name', 'some-role'));
```

### Generating service access token
Efficiently maintaining a valid access token can be hard. Get it easy by using:
Or:
```js
let accessToken = await keycloak.accessToken.get()
```
Then:
```js
request.get('http://serviceb.com/v1/fetch/accounts', {
request.get('http://service.example.org/api/endpoint', {
'auth': {
'bearer': await keycloak.accessToken.get()
}
});
```
> For this feature, the authentication details described in the configuration options are used.
### Retrieve users information by id
Sometimes backend services only have a target user identifier to digg for details, in such cases, you can contact the Keycloak service by:

#### Retrieve user details by id
### Validating access tokens
#### Online validation
This method requires online connection to the Keycloak service to validate the access token. It is highly secure since it also check for possible token invalidation. The disadvantage is that a request to the Keycloak service happens on every validation:
```js
let details = await keycloak.users.details(uid);
const token = await keycloak.jwt.verify(accessToken);
//console.log(token.isExpired());
//console.log(token.hasRealmRole('user'));
//console.log(token.hasApplicationRole('app-client-name', 'some-role'));
```

#### Retrieve user roles by id
#### Offline validation
This method perform offline JWT verification against the access token using the Keycloak Realm public key. Performance is higher compared to the online method, as a disadvantage no access token invalidation on Keycloak server is checked:
```js
let details = await keycloak.users.roles(uid, [
// clients id here for roles retrieval
],
true // include realm roles ?
);
const cert = fs.readFileSync('public_cert.pem');
const token = await keycloak.jwt.verifyOffline(accessToken, cert);
//console.log(token.isExpired());
//console.log(token.hasRealmRole('user'));
//console.log(token.hasApplicationRole('app-client-name', 'some-role'));
```

## Tests
WIP
## Breaking changes
### v3
- The `UserManager` class was dropped
- The `auth-server-url` config property was changed to `keycloak_base_url`
- Most recent Keycloak API is supported by default, old versions are still supported through the `is_legacy_endpoint` config property
25 changes: 10 additions & 15 deletions example/index.js → example/all.js
Original file line number Diff line number Diff line change
@@ -1,39 +1,34 @@
const config = require('./config-example')
const keycloak = require('./../index')(config)
const config = require('../local/config-example')
const keycloak = require('../libs/index')(config)
const fs = require('fs');

(async () => {
try {
const someAccessToken = await keycloak.accessToken.get()
// current version of Keycloak requires the openid scope for accessing user info endpoint
const someAccessToken = await keycloak.accessToken.get('openid')
// how to get openid info from access token...
// info.sub contains the user id
const info = await keycloak.accessToken.info(someAccessToken)
console.log(info)

// verify token online, intended for micro-service authorization
let token = await keycloak.jwt.verify(someAccessToken)
console.log(token.isExpired())
// console.log(token.hasRealmRole('user'))
// console.log(token.hasApplicationRole('nodejs-connect', 'vlm-readonly'))
console.log(token.hasRealmRole('user'))
console.log(token.hasApplicationRole('nodejs-connect', 'vlm-readonly'))

// verify token offline, intended for micro-service authorization
// using this method does not consider token invalidation, avoid long-term tokens here
const cert = fs.readFileSync('public_cert.pem')
const cert = fs.readFileSync('./local/public_cert.pem')
token = await keycloak.jwt.verifyOffline(someAccessToken, cert)
console.log(token.isExpired())
// console.log(token.hasRealmRole('user'))
// console.log(token.hasApplicationRole('nodejs-connect', 'vlm-readonly'))

// how to manually refresh custom access token
// (this operation is performed automatically for the service access token)
token = await keycloak.accessToken.refresh(someAccessToken)
console.log(token.isExpired())

// how to get user details given the user id
const [details, roles] = await Promise.all([
keycloak.users.details(info.sub),
keycloak.users.roles(info.sub)
])
console.log(details, roles)
const response = await keycloak.accessToken.refresh(keycloak.accessToken.data.refresh_token)
console.log(response.data)
} catch (err) {
console.log(err)
}
Expand Down
15 changes: 6 additions & 9 deletions example/config-example.json
Original file line number Diff line number Diff line change
@@ -1,11 +1,8 @@
{
"realm": "myrealm",
"auth-server-url": "http://keycloak.dev:8080",
"client_id": "nodejs-connect",
"client_secret": "c88a2c21-9d1a-4f83-a18d-66d75c4d8020",
"username": "user",
"password": "password",
"clients": [
"0448d03b-cb1a-4295-9c3d-c4099b934062"
]
"realm": "realm-name",
"keycloak_base_url": "https://keycloak.example.org",
"client_id": "super-secure-client",
"username": "user@example.org",
"password": "passw0rd",
"is_legacy_endpoint": false
}
10 changes: 0 additions & 10 deletions example/public_cert.pem

This file was deleted.

8 changes: 8 additions & 0 deletions example/refresh-token.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
const config = require('../local/config-example')
const keycloak = require('../libs/index')(config)

keycloak.accessToken.get().then(async (accessToken) => {
// refresh operation is performed automatically on `keycloak.accessToken.get`
const response = await keycloak.accessToken.refresh(keycloak.accessToken.data.refresh_token)
console.log(response.data)
})
9 changes: 9 additions & 0 deletions example/retrieve-decode-token.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
const config = require('../local/config-example')
const keycloak = require('../libs/index')(config)

keycloak.accessToken.get().then(async (accessToken) => {
const token = keycloak.jwt.decode(accessToken)
console.log({ expired: token.isExpired() })
console.log({ content: token.content })
console.log({ hasRole: token.hasApplicationRole('client-name', 'ROLE_NAME') })
})
7 changes: 7 additions & 0 deletions example/user-info.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
const config = require('../local/config-example')
const keycloak = require('../libs/index')(config)

keycloak.accessToken.get('openid').then(async (accessToken) => {
const info = await keycloak.accessToken.info(accessToken)
console.log(info)
})
10 changes: 10 additions & 0 deletions example/verify-offline.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
const config = require('../local/config-example')
const keycloak = require('../libs/index')(config)
const fs = require('fs')

const cert = fs.readFileSync('./local/public_cert.pem')

keycloak.accessToken.get().then(async (accessToken) => {
const token = await keycloak.jwt.verifyOffline(accessToken, cert)
console.log(token.isExpired())
})
7 changes: 7 additions & 0 deletions example/verify-token.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
const config = require('../local/config-example')
const keycloak = require('../libs/index')(config)

keycloak.accessToken.get('openid').then(async (accessToken) => {
const token = await keycloak.jwt.verify(accessToken)
console.log(token.isExpired())
})
40 changes: 28 additions & 12 deletions libs/AccessToken.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
const qs = require('querystring')

class AccessToken {
constructor (cfg, request) {
constructor (cfg, client) {
this.config = cfg
this.request = request
this.client = client
}

async info (accessToken) {
const response = await this.request.get(`/auth/realms/${this.config.realm}/protocol/openid-connect/userinfo`, {
const cfg = this.config

const endpoint = `${cfg.prefix}/realms/${cfg.realm}/protocol/openid-connect/userinfo`
const response = await this.client.get(endpoint, {
headers: {
Authorization: 'Bearer ' + accessToken
}
Expand All @@ -19,25 +22,38 @@ class AccessToken {
refresh (refreshToken) {
const cfg = this.config

return this.request.post(`/auth/realms/${cfg.realm}/protocol/openid-connect/token`, qs.stringify({
const options = {
grant_type: 'refresh_token',
client_id: cfg.client_id,
client_secret: cfg.client_secret,
refresh_token: refreshToken
}))
}
if (cfg.client_secret) {
options.client_secret = cfg.client_secret
}

const endpoint = `${cfg.prefix}/realms/${cfg.realm}/protocol/openid-connect/token`
return this.client.post(endpoint, qs.stringify(options))
}

async get () {
async get (scope) {
const cfg = this.config

if (!this.data) {
const response = await this.request.post(`/auth/realms/${cfg.realm}/protocol/openid-connect/token`, qs.stringify({
const options = {
grant_type: 'password',
username: cfg.username,
password: cfg.password,
client_id: cfg.client_id,
client_secret: cfg.client_secret
}))
client_id: cfg.client_id
}
if (cfg.client_secret) {
options.client_secret = cfg.client_secret
}
if (scope) {
options.scope = scope
}

const endpoint = `${cfg.prefix}/realms/${cfg.realm}/protocol/openid-connect/token`
const response = await this.client.post(endpoint, qs.stringify(options))
this.data = response.data

return this.data.access_token
Expand All @@ -55,7 +71,7 @@ class AccessToken {
} catch (err) {
delete this.data

return this.get()
return this.get(scope)
}
}
}
Expand Down
8 changes: 4 additions & 4 deletions libs/Jwt.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,13 @@ class Jwt {
}

decode (accessToken) {
return new Promise((resolve, reject) => {
resolve(new Token(accessToken))
})
return new Token(accessToken)
}

async verify (accessToken) {
await this.request.get(`/auth/realms/${this.config.realm}/protocol/openid-connect/userinfo`, {
const cfg = this.config

await this.request.get(`${cfg.prefix}/realms/${this.config.realm}/protocol/openid-connect/userinfo`, {
headers: {
Authorization: 'Bearer ' + accessToken
}
Expand Down
Loading

0 comments on commit 76d4dbd

Please sign in to comment.