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
-
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
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
-
Create a
CAMPAIGN_REALM
realm http://www.keycloak.org/docs/latest/server_admin/topics/realms/create.html -
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 -
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
- Add roles to users:
admin_user
—admin
advertiser_user
—customer-advertiser
analyst_user
—customer-analyst
http://www.keycloak.org/docs/latest/server_admin/topics/roles/user-role-mappings.html
- 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 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:
*
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 |
- Download
keycloak.json
usingCAMPAIGN_CLIENT -> Installation
: http://www.keycloak.org/docs/latest/securing_apps/topics/oidc/nodejs-adapter.html
Download and run application
-
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 custom attribute
-
Add a user attribute
customerId
to theadvanced_user
http://www.keycloak.org/docs/latest/server_admin/topics/users/attributes.html -
Create a mapper and add
customerId
toID token
http://stackoverflow.com/a/32890003/3405171 -
customerId
value will be in the decodedID 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
-
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
.
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
totest_user
- Remove
TEST_ROLE
fromtest_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
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/