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_REALM
has to be selected. Go to theClients
menu. -
Press on the
Installation
tab. -
Choose
Format Option: Keycloak OIDC JSON
and clickDownload
to downloadkeycloak.json
-
Replace
keycloak-nodejs-example\keycloak.json
in the root of the project with the downloadedkeycloak.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:
- 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 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
* 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
—admin
advertiser_user
—customer-advertiser
analyst_user
—customer-analyst
https://www.keycloak.org/docs/latest/server_admin/index.html#user-role-mappings
- Create an OIDC client
CAMPAIGN_CLIENT
https://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:
ON
Important: it should beON
for the custom login (to provide login/password via this example 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:
*
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.json
usingCAMPAIGN_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.json
in the root of this project with downloadedkeycloak.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: * login: admin_user, password: admin_user * login: advertiser_user, password: advertiser_user * login: analyst_user, password: analyst_user
-
Add a user attribute
customerId
to theadvanced_user
https://www.keycloak.org/docs/latest/server_admin/index.html#user-attributes -
Create a mapper and add
customerId
toID token
http://stackoverflow.com/a/32890003/3405171 -
customerId
value 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-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
containersudo 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.json
with server IP-address. Other option is generatekeycloak.json
with Keycloak UICAMPAIGN_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
- 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
totest_user
- Remove
TEST_ROLE
fromtest_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/