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.
- 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 explicitly can't be accessed.
- Configuration without
keycloak.json. It can be used to having configuration for multiple environments. For example — DEV, QA. - Examples of using Keycloak REST API to create users, roles and custom attributes. It can be accessed from the application UI to work with users list.
- 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_REALMhas to be selected. Go to theClientsmenu.
-
Press on the
Installationtab. -
Choose
Format Option: Keycloak OIDC JSONand clickDownloadto downloadkeycloak.json
-
Replace
keycloak-nodejs-example\keycloak.jsonin the root of the project with the downloadedkeycloak.json. -
Run
npm installin the project directory to install Node.js libraries -
Run
npm startto run node.js application -
Login to the application using this URL http://localhost:3000/
with any of these credentials:
- login: admin_user, password: admin_user
- login: advertiser_user, password: advertiser_user
- login: analyst_user, password: analyst_user
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 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>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 Consolehttp://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_REALMrealm https://www.keycloak.org/docs/latest/server_admin/index.html#_create-realm -
Create realm roles:
admin,customer-advertiser,customer-analysthttps://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
* Click `Add User` button, specify user's login and click `Save` button * After that, to specify a user's password go to the Credentials tab (don't forget to disable `Temporary` password)
Add these users:
- login:
admin_user, password:admin_user - login:
advertiser_user, password:advertiser_user - login:
analyst_user, password:analyst_user
- Add roles to users:
admin_user—adminadvertiser_user—customer-advertiseranalyst_user—customer-analyst
https://www.keycloak.org/docs/latest/server_admin/index.html#user-role-mappings
- Create an OIDC client
CAMPAIGN_CLIENThttps://www.keycloak.org/docs/latest/server_admin/index.html#oidc-clients
- Client ID:
CAMPAIGN_CLIENT - Client Protocol:
openid-connect - Access Type:
Confidential - Standard Flow Enabled:
ON - Implicit Flow Enabled:
OFF - Direct Access Grants Enabled:
ONImportant: it should beONfor the custom login (to provide login/password via this example application login page) - Service Accounts Enabled:
ON - Authorization Enabled:
ONImportant: 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:
*
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
- Polycy name:
Admin or Advertiser or Analyst - Apply Policy:
Admin,Advertiser,Analyst - Decision Strategy:
Affirmative
Using Authorization -> Authorization Scopes add scopes
- scopes:create
- scopes:view
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
- 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 |
- Download
keycloak.jsonusingCAMPAIGN_CLIENT -> Installation: https://www.keycloak.org/docs/latest/securing_apps/index.html#_nodejs_adapter
-
Clone this project https://github.com/v-ladynev/keycloak-nodejs-example.git
-
Replace
keycloak.jsonin the root of this project with downloadedkeycloak.json. -
Run
npm installin the project directory to install Node.js libraries -
npm startto run node.js application -
Login to the application using this URL http://localhost:3000/
and any of these credentials: * login: admin_user, password: admin_user * login: advertiser_user, password: advertiser_user * login: analyst_user, password: analyst_user
-
Add a user attribute
customerIdto theadvanced_user
https://www.keycloak.org/docs/latest/server_admin/index.html#user-attributes -
Create a mapper and add
customerIdtoID token
http://stackoverflow.com/a/32890003/3405171 -
customerIdvalue will be in the decodedID 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-usersThis 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-userscontainersudo 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 fixkeycloak.jsonwith server IP-address. Other option is generatekeycloak.jsonwith Keycloak UICAMPAIGN_CLIENT -> Installation.
sudo docker build -t keycloak-mysql-realm-users ./docker/import_realm_usersAfter that new image can be tagged
docker tag keycloak-mysql-realm-users ladynev/keycloak-mysql-realm-usersand pushed to the docker
docker push ladynev/keycloak-mysql-realm-usersKeycloak, 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
- 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_usercustomerId=123 - Remove
test_usercustomerId - Create Role
TEST_ROLE - Add
TEST_ROLEtotest_user - Remove
TEST_ROLEfromtest_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/
