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.
- 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.
You can run already configured Keycloak, using Docker and this instruction:
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. Then click on
Admin Console
https://www.keycloak.org/docs/latest/server_admin/index.html#admin-console
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. 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 has their own "client roles", scoped only to the client https://www.keycloak.org/docs/latest/server_admin/index.html#client-roles -
Create users (don't forget to disable
Temporary
password) https://www.keycloak.org/docs/latest/server_admin/index.html#_create-new-user
- 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 a
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 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:
*
Using Authorization -> Policies
add role based polices
https://www.keycloak.org/docs/latest/authorization_services/index.html#_policy_rbac
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 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 |
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/
-
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
https://stackoverflow.com/questions/42186537/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
Keycloak uses JSON web token (JWT) as a bearer token format. To decode such tokens: https://jwt.io/