This is a simply Node.js REST application with checking permissions. The code with permissions check: keycloak-nodejs-example/app.js
Just go to the Quick Start Section, if you don't want to read.
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. For the application a combination of (resource, scope) is important only. We can configure Keycloak using something other than roles, without changing the application.
keycloak.json
. It can be used to having configuration for multiple environments. For example — DEV, QA.Docker has to be installed in the system
Type in the console in a root of the project directory to run already configured Keycloak (with users, roles and scopes). Keycloak will need time to initialize a database schema and start (about 1 minute).
docker-compose up
Go to the Keycloak administration console http://localhost:8080/auth/admin/
Enter credentials (it was specified in the docker-compose.yml
)
Username or email: admin
Password: admin
After Sign in
, CAMPAIGN_REALM
has to be selected. Go to the Clients
menu.
Choose CAMPAIGN_CLIENT
in the Clients
list.
Press on the Installation
tab.
Choose Format Option: Keycloak OIDC JSON
and click Download
to download keycloak.json
Replace keycloak-nodejs-example\keycloak.json
in the root of the project with the downloaded keycloak.json
.
Run npm install
in the project directory to install Node.js libraries
Run npm start
to run node.js application
Login to the application using this URL http://localhost:3000/
with any of these credentials:
Download the last version of Keycloak (this example uses 3.2.1.Final) http://www.keycloak.org/downloads.html
Perform this steps to get MySQL configured for Keycloak: https://www.keycloak.org/docs/latest/server_installation/index.html#_rdbms-setup-checklist
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.
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
You will need to select a file on the Add Realm
page to import a realm .
https://www.keycloak.org/docs/latest/server_admin/index.html#_create-realm
Users can be imported via Manage -> Import
Export and import is triggered at server boot time and its parameters are passed in via Java system properties. https://www.keycloak.org/docs/latest/server_admin/index.html#_export_import
Run server using standalone.sh (standalone.bat)
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:
click on Administration Console
http://localhost:8080/auth/admin/
// TODO When you boot Keycloak for the first time Keycloak creates a pre-defined realm for you. This initial realm is the master realm. It is the highest level in the hierarchy of realms. Admin accounts in this realm have permissions to view and manage any other realm created on the server instance. When you define your initial admin account, you create an account in the master realm. Your initial login to the admin console will also be via the master realm. https://www.keycloak.org/docs/latest/server_admin/index.html#the-master-realm
Create a CAMPAIGN_REALM
realm https://www.keycloak.org/docs/latest/server_admin/index.html#_create-realm
Create realm roles: admin
, customer-advertiser
, customer-analyst
https://www.keycloak.org/docs/latest/server_admin/index.html#realm-roles
Noitice: Each client can have their own "client roles", scoped only to the client
https://www.keycloak.org/docs/latest/server_admin/index.html#client-roles
Create users
https://www.keycloak.org/docs/latest/server_admin/index.html#_create-new-user
Add User
button, specify user's login and click Save
button Temporary
password)Add these users:
admin_user
, password: admin_user
advertiser_user
, password: advertiser_user
analyst_user
, password: analyst_user
Add roles to users:
admin_user
— admin
advertiser_user
— customer-advertiser
analyst_user
— customer-analyst
Create an OIDC client CAMPAIGN_CLIENT
https://www.keycloak.org/docs/latest/server_admin/index.html#oidc-clients
CAMPAIGN_CLIENT
openid-connect
Confidential
ON
OFF
ON
Important: it should be ON
for the custom login (to provide login/password via this example application login page) ON
ON
Important: to add policeshttp://localhost:3000/*
. Keycloak will use this value to check redirect URL at least for logout.
It can be just a wildcard *
.*
Using Authorization -> Policies
add role based polices to the CAMPAIGN_CLIENT
https://www.keycloak.org/docs/latest/authorization_services/index.html#_policy_rbac
Policy Name | 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 https://www.keycloak.org/docs/latest/authorization_services/index.html#_policy_aggregated
Admin or Advertiser or Analyst
Admin
, Advertiser
, Analyst
Decision Strategy: Affirmative
Using Authorization -> Authorization Scopes
add scopes
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 |
Enter Rsource Name
column value to the Name
and Display Name
fields
Using Authorization -> Permissions
add scope-based permissions
https://www.keycloak.org/docs/latest/authorization_services/index.html#_permission_create_scope
Set decision strategy for every permission
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 |
keycloak.json
using CAMPAIGN_CLIENT -> Installation
:
https://www.keycloak.org/docs/latest/securing_apps/index.html#_nodejs_adapterClone this project https://github.com/v-ladynev/keycloak-nodejs-example.git
Replace keycloak.json
in the root of this project
with downloaded keycloak.json
.
Run npm install
in the project directory to install Node.js libraries
npm start
to run node.js application
Login to the application using this URL http://localhost:3000/
and any of these credentials:
Add a user attribute customerId
to the advanced_user
https://www.keycloak.org/docs/latest/server_admin/index.html#user-attributes
Create a mapper and add customerId
to ID token
http://stackoverflow.com/a/32890003/3405171
customerId
value will be in the decoded ID token
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.
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.
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
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.
Get IP address of ladynev/keycloak-mysql-realm-users
container
sudo docker network inspect bridge
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)
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
.
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
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);
});
});
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"
}
The file adminClient.js
CAMPAIGN_REALM
test_user
(password: test_user
)test_user
test_user
test_user
test_user
customerId=123
test_user
customerId
TEST_ROLE
TEST_ROLE
to test_user
TEST_ROLE
from test_user
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
Obtaining Permissions
Resources, scopes, permissions and policies in keycloak
https://stackoverflow.com/questions/12276046/nodejs-express-how-to-secure-a-url
Keycloak Admin REST API
Change Keycloak login page, get security tokens using REST
Obtain access token for user
Stop using JWT for sessions
Integrating Keycloak 4 with Spring Boot 2 Microservices
Video Keycloak intro part 2 - Resources, Permissions, Scope and Policies
Keycloak uses JSON web token (JWT) as a bearer token format. To decode such tokens: https://jwt.io/