/keycloak-nodejs-example

A simply step by step Keycloak, MySQL and Node.js integration tutorial. There are some docker examples as well.

Primary LanguageJavaScript

keycloak-nodejs-example

This is a simply Node.js REST application with checking permissions. The code with permissions check: https://github.com/v-ladynev/keycloak-nodejs-example/blob/master/app.js

This applications has REST API to work with customers, campaigns and reports. We will protect all endpoints based on permissions are configured using Keycloak.

URL Method Permission Resource Scope Roles
/customers POST customer-create res:customer scopes:create admin
/customers GET customer-view res:customer scopes:view admin, customer-advertiser, customer-analyst
/campaigns POST campaign-create res:campaign scopes:create admin, customer-advertiser
/campaigns GET campaign-view res:campaign scopes:view admin, customer-advertiser, customer-analyst
/reports POST report-create res:report scopes:create customer-analyst
/reports GET report-view res:report scopes:view admin, customer-advertiser, customer-analyst

The application will use a combination of (resource, scope) to check a permission. We will configure Keycloak to use polices are based on roles. But for the application a combination of (resource, scope) is important only. We can configure Keycloak using something other than roles, without changing the application.

The Most Useful Features

  • Custom login without using Keycloak login page.
  • Stateless Node.js server without using a session. Keycloak token is stored using cookies.
  • A centralized middleware to check permissions. Routes are not described explicity can't be accessed.
  • Configuration without keycloak.json. It can be used to having configuration for multiple envirements. For exampe — DEV, QA.
  • Examples of using Keycloak REST API to create users, roles and custom attributes. It can be used to work with users list from application UI.

Keycloak Configuration

Download Keycloak

Download the last version of Keycloak (this example uses 3.2.1.Final) http://www.keycloak.org/downloads.html

Configure Keycloak to use MySQL

Perform this steps to get MySQL configured for Keycloak: http://www.keycloak.org/docs/latest/server_installation/topics/database/checklist.html

Important: There is an error in the documentation — driver should be in the modules/system/layers/base/com/mysql/driver/main catalog.

The last MySQL driver https://mvnrepository.com/artifact/mysql/mysql-connector-java

module.xml
<module xmlns="urn:jboss:module:1.3" name="com.mysql.driver">
 <resources>
  <resource-root path="mysql-connector-java-6.0.5.jar" />
 </resources>
 <dependencies>
  <module name="javax.api"/>
  <module name="javax.transaction.api"/>
 </dependencies>
</module>
part of standalone.xml

You will need to create a keycloak schema in the MySQL database for this example. Also don't forget to remove existing java:jboss/datasources/KeycloakDS datasource.

<datasources>
...
<datasource jndi-name="java:jboss/datasources/KeycloakDS" pool-name="KeycloakDS" enabled="true" use-java-context="true">
<connection-url>jdbc:mysql://localhost:3306/keycloak</connection-url>
    <driver>mysql</driver>
    <pool>
        <max-pool-size>20</max-pool-size>
    </pool>
    <security>
        <user-name>root</user-name>
        <password>root</password>
    </security>
</datasource>
...
</datasources>

<drivers>
...
<driver name="mysql" module="com.mysql.driver">
    <driver-class>com.mysql.jdbc.Driver</driver-class>
</driver>
...
</drivers>

To fix time zone error during startup, connection-url can be jdbc:mysql://localhost:3306/keycloak?serverTimezone=UTC

Database schema creation takes a long time.

Import Users, Realm, Client and Polices

Realm, Client and Polices configuration can be imported using this file: CAMPAIGN_REALM-realm.json

Users can be imported from this file: CAMPAIGN_REALM-users-0.json

Import via Keycloak UI

You will need to select a file on the Add Realm page to import a realm . http://www.keycloak.org/docs/latest/getting_started/topics/first-realm/realm.html

Users can be imported via Manage -> Import

Import at server boot time

Export and import is triggered at server boot time and its parameters are passed in via Java system properties. http://www.keycloak.org/docs/latest/server_admin/topics/export-import.html

Basic configuration

  1. Run server using standalone.sh (standalone.bat)

  2. You should now have the Keycloak server up and running. To check that it's working open http://localhost:8080. You will need to create a Keycloak admin user. Then click on Admin Console http://www.keycloak.org/docs/latest/server_admin/topics/admin-console.html

When you define your initial admin account, you are creating an account in the master realm. Your initial login to the admin console will also be through the master realm. http://www.keycloak.org/docs/latest/server_admin/topics/realms/master.html

  1. Create a CAMPAIGN_REALM realm http://www.keycloak.org/docs/latest/server_admin/topics/realms/create.html

  2. Create realm roles: admin, customer-advertiser, customer-analyst http://www.keycloak.org/docs/latest/server_admin/topics/roles/realm-roles.html

    Noitice: Each client can has their own "client roles", scoped only to the client http://www.keycloak.org/docs/latest/server_admin/topics/roles/client-roles.html

  3. Create users (don't forget to disable Temporary password) http://www.keycloak.org/docs/latest/server_admin/topics/users/create-user.html

  • login: admin_user, password: admin_user
  • login: advertiser_user, password: advertiser_user
  • login: analyst_user, password: analyst_user
  1. Add roles to users:
  1. Create a CAMPAIGN_CLIENT http://www.keycloak.org/docs/latest/server_admin/topics/clients/client-oidc.html
  • Client ID: CAMPAIGN_CLIENT
  • Client Protocol: openid-connect
  • Access Type: Confidential
  • Standard Flow Enabled: ON
  • Implicit Flow Enabled: OFF
  • Direct Access Grants Enabled: ON Important: it should be ON for the custom login (to provide login/password via an application login page)
  • Service Accounts Enabled: ON
  • Authorization Enabled: ON Important: to add polices
  • Valid Redirect URIs: http://localhost:3000/*. Keycloak will use this value to check redirect URL at least for logout. It can be just a wildcard *.
  • Web Origins: *

Configure permissions

Add polices

Using Authorization -> Policies add role based polices http://www.keycloak.org/docs/latest/authorization_services/topics/policy/role-policy.html

Policy Role
Admin admin
Advertiser customer-advertiser
Analyst customer-analyst
Admin or Advertiser or Analyst Aggregated Policy*

Aggregated Policy* This policy consist of an aggregation of other polices http://www.keycloak.org/docs/latest/authorization_services/topics/policy/aggregated-policy.html

  • Polycy name: Admin or Advertiser or Analyst
  • Apply Policy: Admin, Advertiser, Analyst
  • Decision Strategy: Affirmative

Add scopes

Using Authorization -> Authorization Scopes add scopes

  • scopes:create
  • scopes:view

Add resources

Using Authorization -> Resources add resourcess. Scopes should be entered in the Scopes field for every resource.

Resource Name Scopes
res:campaign scopes:create, scopes:view
res:customer scopes:create, scopes:view
res:report scopes:create, scopes:view

Add scope-based permissions

Using Authorization -> Permissions add scope-based permissions http://www.keycloak.org/docs/latest/authorization_services/topics/permission/create-scope.html

Set decision strategy for every permission

  • Decision Strategy: Affirmative
Permission Resource Scope Polices
customer-create res:customer scopes:create Admin
customer-view res:customer scopes:view Admin or Advertiser or Analyst
campaign-create res:campaign scopes:create Admin, Advertiser
campaign-view res:campaign scopes:view Admin or Advertiser or Analyst
report-create res:report scopes:create Analyst
report-view res:report scopes:view Admin or Advertiser or Analyst
  1. Download keycloak.json using CAMPAIGN_CLIENT -> Installation : http://www.keycloak.org/docs/latest/securing_apps/topics/oidc/nodejs-adapter.html

Download and run application

  1. Clone this project https://github.com/v-ladynev/keycloak-nodejs-example.git

  2. Replace keycloak.json in the root of this project with downloaded keycloak.json.

  3. Run npm install in the project directory to install Node.js libraries

  4. npm start to run node.js application

  5. Login to the application using this URL http://localhost:3000/

Add custom attribute

  1. Add a user attribute customerId to the advanced_user
    http://www.keycloak.org/docs/latest/server_admin/topics/users/attributes.html

  2. Create a mapper and add customerId to ID token
    http://stackoverflow.com/a/32890003/3405171

  3. customerId value will be in the decoded ID token

Keycloak docker image

Using official jboss/keycloak-mysql with MySQL on localhost

You shold have MySQL runing on localhost with KEYCLOAK_DEV database, and login=root password=root

sudo docker run --name keycloak_dev \
--network="host" \
-e MYSQL_PORT_3306_TCP_ADDR=localhost -e MYSQL_PORT_3306_TCP_PORT=3306 \
-e MYSQL_DATABASE=KEYCLOAK_DEV -e MYSQL_USERNAME=root -e MYSQL_PASSWORD=root \ 
-e KEYCLOAK_USER=admin -e KEYCLOAK_PASSWORD=admin \
jboss/keycloak-mysql 

This creates a Keycloak admin user with password admin. Keycloak will run on localhost:8080. You will need to add users, roles and permissions manually.

Using ladynev/keycloak-mysql-realm-users with MySQL on localhost

sudo docker run --name keycloak_dev \
--network="host" \
-e MYSQL_PORT_3306_TCP_ADDR=localhost -e MYSQL_PORT_3306_TCP_PORT=3306 \
-e MYSQL_DATABASE=KEYCLOAK_DEV -e MYSQL_USERNAME=root -e MYSQL_PASSWORD=root \
-e KEYCLOAK_USER=admin -e KEYCLOAK_PASSWORD=admin \
ladynev/keycloak-mysql-realm-users

This creates a Keycloak admin user with password admin. Keycloak will run on localhost:8080. It will already have predefined users, roles and permissions from this example, because of ladynev/keycloak-mysql-realm-users image imports this data from json files during start up.

Using ladynev/keycloak-mysql-realm-users with MySQL docker image

  1. First start a MySQL instance using the MySQL docker image:

    sudo docker run --name mysql \
    -e MYSQL_DATABASE=KEYCLOAK_DEV -e MYSQL_USER=keycloak -e MYSQL_PASSWORD=keycloak \
    -e MYSQL_ROOT_PASSWORD=root_password \
    -d mysql
  2. Start a Keycloak instance and connect to the MySQL instance:

    sudo docker run --name keycloak_dev \
    --link mysql:mysql \
    -p 8080:8080 \
    -e MYSQL_DATABASE=KEYCLOAK_DEV -e MYSQL_USERNAME=keycloak -e MYSQL_PASSWORD=keycloak \
    -e KEYCLOAK_USER=admin -e KEYCLOAK_PASSWORD=admin \
    ladynev/keycloak-mysql-realm-users

This creates a Keycloak admin user with password admin and imports users, roles, permissions.

  1. Get IP address of ladynev/keycloak-mysql-realm-users container

    sudo docker network inspect bridge
  2. Keycloak will run on ip_address:8080. For example: http://172.17.0.3:8080 (for Windows it looks like http://192.168.99.100:8080)

  3. To run keycloak-nodejs-example, it is need to fix keycloak.json with server IP-address. Other option is generatekeycloak.json with Keycloak UI CAMPAIGN_CLIENT -> Installation.

Build docker image from the root of the project

sudo docker build -t keycloak-mysql-realm-users ./docker/import_realm_users

After that new image can be tagged

docker tag keycloak-mysql-realm-users ladynev/keycloak-mysql-realm-users

and pushed to the docker

docker push ladynev/keycloak-mysql-realm-users

Examples of using Admin REST API and Custom Login

Example of custom login

Keycloak, by default, uses an own page to login a user. There is an example, how to use an application login page. Direct Access Grants should be enabled in that case (https://github.com/v-ladynev/keycloak-nodejs-example#basic-configuration) The file app.js

 app.get('/customLoginEnter', function (req, res) {
     let rptToken = null
     keycloak.grantManager.obtainDirectly(req.query.login, req.query.password).then(grant => {
         keycloak.storeGrant(grant, req, res);
         renderIndex(req, res, rptToken);
     }, error => {
         renderIndex(req, res, rptToken, "Error: " + error);
     });
 });

What happens with custom login

To perform custom login we need to obtain tokens from Keycloak. We can do this by HTTP request:

curl -X POST \
  http://localhost:8080/auth/realms/CAMPAIGN_REALM/protocol/openid-connect/token \
  -H 'authorization: Basic Q0FNUEFJR05fQ0xJRU5UOjZkOTc5YmU1LWNiODEtNGQ1Yy05ZmM3LTQ1ZDFiMGM3YTc1ZQ==' \
  -H 'content-type: application/x-www-form-urlencoded' \
  -d 'client_id=CAMPAIGN_CLIENT&username=admin_user&password=admin_user&grant_type=password'

authorization: Basic Q0FNUEFJR05fQ0xJRU5UOjZkOTc5YmU1LWNiODEtNGQ1Yy05ZmM3LTQ1ZDFiMGM3YTc1ZQ== is computed as

'Basic ' + btoa(clientId + ':' + secret);

where (they can be obtained from keycloak.json)

client_id = CAMPAIGN_CLIENT
secret = 6d979be5-cb81-4d5c-9fc7-45d1b0c7a75e

This is just an example, the secret can be different.

We will have, as a result, a response with access_token, refresh_token and id_token (The response has 2447 bytes length)

{
    "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJfT3B2Wm5lSkR3T0NqczZSZmFObjdIc0lKZmRhMWxfU0ZkYUo2SU1hV0k0In0.eyJqdGkiOiI0ODM0OWQ5NS03NjNkLTQ5NTQtODNmMy01NGYzOTY0Y2I4NTQiLCJleHAiOjE1MDk0NzYyODAsIm5iZiI6MCwiaWF0IjoxNTA5NDc1OTgwLCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjgwODAvYXV0aC9yZWFsbXMvQ0FNUEFJR05fUkVBTE0iLCJhdWQiOiJDQU1QQUlHTl9DTElFTlQiLCJzdWIiOiI1ZGMzMDBjOS04NmM4LTQ5OTUtYjJiOS0zNjhmOTA0OWJhM2YiLCJ0eXAiOiJCZWFyZXIiLCJhenAiOiJDQU1QQUlHTl9DTElFTlQiLCJhdXRoX3RpbWUiOjAsInNlc3Npb25fc3RhdGUiOiI3OGRhOWJhMi00YmRmLTRlNTYtODE4NC00N2QxYjgxNGEwZGEiLCJhY3IiOiIxIiwiYWxsb3dlZC1vcmlnaW5zIjpbIioiXSwicmVhbG1fYWNjZXNzIjp7InJvbGVzIjpbImFkbWluIiwidW1hX2F1dGhvcml6YXRpb24iXX0sInJlc291cmNlX2FjY2VzcyI6eyJhY2NvdW50Ijp7InJvbGVzIjpbIm1hbmFnZS1hY2NvdW50IiwibWFuYWdlLWFjY291bnQtbGlua3MiLCJ2aWV3LXByb2ZpbGUiXX19LCJwcmVmZXJyZWRfdXNlcm5hbWUiOiJhZG1pbl91c2VyIn0.Qa2PXHhRs_JpMPHYYwKVcpb3kfHN8l6QUGCyWkIRhl6eoI6IlWu3FG11NOtuDhKn5DvKHdnpft9nK7W5b87WSHa5lXawm6Dcp4RLfD5WvK7W7yFceFGhvC8vuM8xXOhvWDbhnX1eP_Tanrpqs19nWbTjLQ2E8iFqzxnJ1PQNNDFL2BXQ3Y58jt0uwaebJnjIhU0Mpb0plTPaRbnMBNfsjfCurXXWN6MM0rVFAHEDDrrW0M3kKeVyDuq9PYvcDvedlETOlCx3Ss9DXtZY2u__qGfABk3aNbCuUtkn9xy-HYJLBUTZIpPW0ImBKM4-tM4tEzQLvb9b6P4iWYFsaQR08w",
    "expires_in": 300,
    "refresh_expires_in": 1800,
    "refresh_token": "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJfT3B2Wm5lSkR3T0NqczZSZmFObjdIc0lKZmRhMWxfU0ZkYUo2SU1hV0k0In0.eyJqdGkiOiJjMzdhNWFiYi1kZDNlLTQxMGMtOGQxMy1mMWU5NTU0ZjhmNzMiLCJleHAiOjE1MDk0Nzc3ODAsIm5iZiI6MCwiaWF0IjoxNTA5NDc1OTgwLCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjgwODAvYXV0aC9yZWFsbXMvQ0FNUEFJR05fUkVBTE0iLCJhdWQiOiJDQU1QQUlHTl9DTElFTlQiLCJzdWIiOiI1ZGMzMDBjOS04NmM4LTQ5OTUtYjJiOS0zNjhmOTA0OWJhM2YiLCJ0eXAiOiJSZWZyZXNoIiwiYXpwIjoiQ0FNUEFJR05fQ0xJRU5UIiwiYXV0aF90aW1lIjowLCJzZXNzaW9uX3N0YXRlIjoiNzhkYTliYTItNGJkZi00ZTU2LTgxODQtNDdkMWI4MTRhMGRhIiwicmVhbG1fYWNjZXNzIjp7InJvbGVzIjpbImFkbWluIiwidW1hX2F1dGhvcml6YXRpb24iXX0sInJlc291cmNlX2FjY2VzcyI6eyJhY2NvdW50Ijp7InJvbGVzIjpbIm1hbmFnZS1hY2NvdW50IiwibWFuYWdlLWFjY291bnQtbGlua3MiLCJ2aWV3LXByb2ZpbGUiXX19fQ.E46pp4oqM9o9Xa0d44YYzZ7fI61kB1KCDYksoXnUIw0Qbv67VoEWcloMKC2Lr6pmPeu6ptjkK6QJKjmoaeiFNcGHE7SoU5RTq0cyKjTFqg4GkTZuK-y0tk2ek-Beq64Zu69HzTfWGT0zSIDfd2l7EiEN8ptSCS-Tugsgmk1Snvrb2nC_1-U87qUFBR_qVryhwRk8Ie_AAwTVRWk5jATu5PPsLsCXqfM5_VVu-lc_qbOJaPeg1Ag2WXhE4lf_3BzVeRlgsxDr2EuzZG56O4Y6QeyV2J-XsZF2C7n3CcNPVXD42-MGB7Jhn5l2onl074JsJqhE6bzKB063jSf_wzyB4Q",
    "token_type": "bearer",
    "not-before-policy": 0,
    "session_state": "78da9ba2-4bdf-4e56-8184-47d1b814a0da"
}

if we decode access_token (using https://jwt.io/), we will have (there are roles in the token)

{
  "jti": "48349d95-763d-4954-83f3-54f3964cb854",
  "exp": 1509476280,
  "nbf": 0,
  "iat": 1509475980,
  "iss": "http://localhost:8080/auth/realms/CAMPAIGN_REALM",
  "aud": "CAMPAIGN_CLIENT",
  "sub": "5dc300c9-86c8-4995-b2b9-368f9049ba3f",
  "typ": "Bearer",
  "azp": "CAMPAIGN_CLIENT",
  "auth_time": 0,
  "session_state": "78da9ba2-4bdf-4e56-8184-47d1b814a0da",
  "acr": "1",
  "allowed-origins": [
    "*"
  ],
  "realm_access": {
    "roles": [
      "admin",
      "uma_authorization"
    ]
  },
  "resource_access": {
    "account": {
      "roles": [
        "manage-account",
        "manage-account-links",
        "view-profile"
      ]
    }
  },
  "preferred_username": "admin_user"
}

Examples of Admin REST API

The file adminClient.js

  • Realms list
  • Users list for CAMPAIGN_REALM
  • Create user test_user (password: test_user)
  • Get user test_user
  • Delete user test_user
  • Update user test_user
  • Set test_user customerId=123
  • Remove test_user customerId
  • Create Role TEST_ROLE
  • Add TEST_ROLE to test_user
  • Remove TEST_ROLE from test_user

Update custom attribute using REST API

Update the user
http://www.keycloak.org/docs-api/2.5/rest-api/index.html#_update_the_user

Using UserRepresentation, attributes field
http://www.keycloak.org/docs-api/2.5/rest-api/index.html#_userrepresentation

Check permissions using REST API

Requesting Entitlements

https://stackoverflow.com/questions/42186537/resources-scopes-permissions-and-policies-in-keycloak

Secure URL

https://stackoverflow.com/questions/12276046/nodejs-express-how-to-secure-a-url

Links

Keycloak Admin REST API
Change Keycloak login page, get security tokens using REST
Obtain access token for user

Keycloak uses JSON web token (JWT) as a barier token format. To decode such tokens: https://jwt.io/