Keycloak integration
Stellio can be used with the Keycloak IAM solution.
This page presents a basic solution to show how to setup the integration between Stellio and Keycloak. It is only meant for development purposes.
In the remainder of this page, it is considered that Stellio is running using the docker-compose configuration provided on GitHub. Before starting this tutorial, ensure that the Stellio's Kafka broker is advertising its PLAINTEXT_HOST
listener on an IP address that will be resolvable from the Keycloak container (running in its own docker network and not the same Docker network than Stellio), e.g.:
environment:
KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://stellio-kafka:9092,PLAINTEXT_HOST://{kafka_ip}:29092
# Other environment variables
Where kafka_ip
is the IP address where the Stellio's Kafka instance is reachable outside the Docker network (e.g., your laptop or VM IP)
Keycloak will be launched using the docker-compose configuration provided later in this document, in development mode.
Connect Keycloak with Stellio
Configuration of Keycloak is not described here, as it is well documented on the Keycloak site.
In order to connect Stellio with Keycloak, the following steps have to be followed:
- Configure and run the Keycloak Docker image provided by EGM
- Create a realm in Keycloak
- Create the builtin roles known and used by Stellio
- Configure Keycloak to propagate user, group and client events to Stellio
- Activate and configure authentication in Stellio
Use the Keycloak Docker image by EGM
The provided Docker image extends the official Keycloak Docker image to bundle it with three SPIs:
- An event listener that propagates provisioning events to the Kafka message broker used by Stellio
- An account notifier that sends an email to the realm admins whenever a new account is created
- A metrics listener that exposes an endpoint that can be consumed by Prometheus
To start with, you can use this sample Docker compose file (do not forget to create a .env
file with the environment variables used in the docker compose file):
version: '3.5'
services:
keycloak:
container_name: keycloak
image: easyglobalmarket/keycloak:23.0.3
restart: always
environment:
- KEYCLOAK_ADMIN=${KEYCLOAK_ADMIN}
- KEYCLOAK_ADMIN_PASSWORD=${KEYCLOAK_ADMIN_PASSWORD}
- KC_DB=postgres
- KC_DB_URL_HOST=postgres
- KC_DB_URL_DATABASE=${KEYCLOAK_DB_DATABASE}
- KC_DB_USERNAME=${KEYCLOAK_DB_USERNAME}
- KC_DB_PASSWORD=${KEYCLOAK_DB_PASSWORD}
- KC_LOG_LEVEL=${LOG_LEVEL}
- KC_HOSTNAME=${KEYCLOAK_HOSTNAME}
- KC_PROXY=none
# https://www.keycloak.org/server/configuration-provider#_configuration_option_format
- KC_SPI_EVENTS_LISTENER_STELLIO_EVENT_LISTENER_KAFKA_BOOTSTRAP_SERVERS={realm_name}/{kafka_ip}:29092
- KC_SPI_EVENTS_LISTENER_STELLIO_EVENT_LISTENER_KAFKA_KEY_SERIALIZER_CLASS=org.apache.kafka.common.serialization.StringSerializer
- KC_SPI_EVENTS_LISTENER_STELLIO_EVENT_LISTENER_KAFKA_VALUE_SERIALIZER_CLASS=org.apache.kafka.common.serialization.StringSerializer
- KC_SPI_EVENTS_LISTENER_STELLIO_EVENT_LISTENER_KAFKA_ACKS=all
- KC_SPI_EVENTS_LISTENER_STELLIO_EVENT_LISTENER_KAFKA_DELIVERY_TIMEOUT_MS=3000
- KC_SPI_EVENTS_LISTENER_STELLIO_EVENT_LISTENER_KAFKA_REQUEST_TIMEOUT_MS=2000
- KC_SPI_EVENTS_LISTENER_STELLIO_EVENT_LISTENER_KAFKA_LINGER_MS=1
- KC_SPI_EVENTS_LISTENER_STELLIO_EVENT_LISTENER_KAFKA_BATCH_SIZE=16384
- KC_SPI_EVENTS_LISTENER_STELLIO_EVENT_LISTENER_KAFKA_MEMORY_BUFFER=33554432
- KC_SPI_EVENTS_LISTENER_STELLIO_EVENT_LISTENER_TENANTS={realm_name}/{tenant_name}
ports:
# Using a different port than the ones used by Stellio to avoid conflicts when deployed on the same host
- 9080:8080
depends_on:
- postgres
command: "start-dev"
postgres:
container_name: postgres
image: postgres:14-alpine
restart: always
volumes:
- postgres_data:/var/lib/postgresql/data
environment:
POSTGRES_DB: ${KEYCLOAK_DB_DATABASE}
POSTGRES_USER: ${KEYCLOAK_DB_USERNAME}
POSTGRES_PASSWORD: ${KEYCLOAK_DB_PASSWORD}
volumes:
postgres_data:
driver: local
where:
realm_name
is the name of the realm to be created in the next sectiontenant_name
is the name of the tenant in Stellio that the realm will be binded to (see the multitenancy page for more details on this), set it tourn:ngsi-ld:tenant:default
to use the default tenant.kafka_ip
is the IP address where the Stellio's Kafka instance is reachable (e.g., your laptop or VM IP)
The .env
file contains the following environment variables:
KEYCLOAK_ADMIN=keycloak
KEYCLOAK_ADMIN_PASSWORD=keycloakAdminPassword
KEYCLOAK_DB_DATABASE=keycloak
KEYCLOAK_DB_USERNAME=keycloak
KEYCLOAK_DB_PASSWORD=keycloakDbPassword
KEYCLOAK_HOSTNAME={keycloak_ip}
LOG_LEVEL=INFO
Where keycloak_ip
is the IP address where Keycloak is reachable outside the Docker network (e.g., your laptop or VM IP)
Please note that for a production deployment, it is recommended to setup a certificate based authentication between Keycloak and Kafka and of course to only allow https connexions to Keycloak.
Start Keycloak
docker compose up -d
Create a realm
- Go to http://{keycloak_ip}:9080
- Authenticate to the administration console using the keycloak admin credentials defined in the docker compose configuration
- Follow instructions on how to create a new realm in Keycloak
Create the builtin roles
Stellio natively interprets two specific Realm roles that must first be created in Keycloak (see procedure to create a realm role):
stellio-creator
: it gives an user the right to create entities in the context brokerstellio-admin
: it gives an user the administrator rights in the context broker
Configure Keycloak event listener
Go to the Realm settings > Events section and, in the Event listeners field, add stellioEventListener
.
Configure authentication in Stellio
Finally, on Stellio side, activate authentication and configure the Keycloak URLs in search and subscription services in Docker compose config.
In the .env
file, update the following environment variables:
STELLIO_AUTHENTICATION_ENABLED=true
APPLICATION_TENANTS_0_ISSUER=http://{keycloak_ip}:9080/realms/{realm_name}
Then restart Stellio:
docker compose up -d
Validate the configuration
To validate the configuration is fully working, you can create a client in Keycloak and then create an entity in Stellio using this client:
- Create a client in Keycloak
- Activate client authentication
- Select the "Service accounts roles" authentication flow
- Do not set any redirect URIs
- In the "Service account roles" tab, assign the
stellio-creator
realm role - In the "Credentials" tab, copy the client secret (you will need it to get an access token below)
Then in a terminal:
- Get an access token for this client
export ACCESS_TOKEN=$(http --form POST http://{keycloak_ip}:9080/realms/{realm_name}/protocol/openid-connect/token client_id={client_id} client_secret={client_secret} grant_type=client_credentials | jq -r .access_token)
- Create a sample entity
echo -n '{ "id": "urn:ngsi-ld:Entity:01", "type": "Entity" }' | http POST http://localhost:8080/ngsi-ld/v1/entities Authorization:"Bearer $ACCESS_TOKEN"
If everything is correctly configured, you should get a 201 Created
response.
Events raised by Keycloak
While creating and configuring users, groups and clients in Keycloak, the following events are raised by Keycloak and sent to the Kafka message broker operated by Stellio:
- Create an user
{
"operationType":"ENTITY_CREATE",
"tenantUri": "urn:ngsi-ld:tenant:stellio",
"entityId":"urn:ngsi-ld:User:aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
"entityTypes":["User"],
"operationPayload":"{\"id\":\"urn:ngsi-ld:User:aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee\",\"type\":\"User\",\"username\":{\"type\":\"Property\",\"value\":\"user@mail.com\"},\"roles\":{\"type\":\"Property\",\"value\":\"stellio-creator\"}}",
"contexts":["https://easy-global-market.github.io/ngsild-api-data-models/authorization/jsonld-contexts/authorization.jsonld","https://uri.etsi.org/ngsi-ld/v1/ngsi-ld-core-context-v1.7.jsonld"]
}
- Create a group
{
"operationType":"ENTITY_CREATE",
"tenantUri": "urn:ngsi-ld:tenant:stellio",
"entityId":"urn:ngsi-ld:Group:zzzzzzzz-yyyy-xxxx-wwww-vvvvvvvvvvvv",
"entityTypes":["Group"],
"operationPayload":"{\"id\":\"urn:ngsi-ld:Group:zzzzzzzz-yyyy-xxxx-wwww-vvvvvvvvvvvv\",\"type\":\"Group\",\"name\":{\"type\":\"Property\",\"value\":\"Group name\"}}",
"contexts":["https://easy-global-market.github.io/ngsild-api-data-models/authorization/jsonld-contexts/authorization.jsonld","https://uri.etsi.org/ngsi-ld/v1/ngsi-ld-core-context-v1.7.jsonld"]
}
- Create a client
{
"operationType":"ENTITY_CREATE",
"tenantUri": "urn:ngsi-ld:tenant:stellio",
"entityId":"urn:ngsi-ld:Client:ffffffff-gggg-hhhh-iiii-jjjjjjjjjjjj",
"entityTypes":["Client"],
"operationPayload":"{\"id\":\"urn:ngsi-ld:Client:ffffffff-gggg-hhhh-iiii-jjjjjjjjjjjj\",\"type\":\"Client\",\"clientId\":{\"type\":\"Property\",\"value\":\"client-id\"}}",
"contexts":["https://easy-global-market.github.io/ngsild-api-data-models/authorization/jsonld-contexts/authorization.jsonld","https://uri.etsi.org/ngsi-ld/v1/ngsi-ld-core-context-v1.7.jsonld"]
}
- Set the service account id of a client
It is the identifier that is transmitted when a client does a direct request on the context broker (i.e., not on behalf of an user). It is automatically transmitted when realm roles are given to a client.
{
"operationType":"ATTRIBUTE_APPEND",
"tenantUri": "urn:ngsi-ld:tenant:stellio",
"entityId":"urn:ngsi-ld:Client:ffffffff-gggg-hhhh-iiii-jjjjjjjjjjjj",
"entityTypes":["Client"],
"attributeName":"serviceAccountId",
"operationPayload":"{\"type\":\"Property\",\"value\":\"urn:ngsi-ld:User:jjjjjjjj-iiii-hhhh-gggg-ffffffffffff\"}",
"updatedEntity":"",
"contexts":["https://easy-global-market.github.io/ngsild-api-data-models/authorization/jsonld-contexts/authorization.jsonld","https://uri.etsi.org/ngsi-ld/v1/ngsi-ld-core-context-v1.7.jsonld"]
}
- Update realm roles of an user / group / client
An array of realm roles is sent, it is empty if the subject has no longer a realm role.
{
"operationType":"ATTRIBUTE_APPEND",
"tenantUri": "urn:ngsi-ld:tenant:stellio",
"entityId":"urn:ngsi-ld:Client:ffffffff-gggg-hhhh-iiii-jjjjjjjjjjjj",
"entityTypes":["Client"],
"attributeName":"roles",
"operationPayload":"{\"type\":\"Property\",\"value\":[\"stellio-creator\"]}",
"updatedEntity":"",
"contexts":["https://easy-global-market.github.io/ngsild-api-data-models/authorization/jsonld-contexts/authorization.jsonld","https://uri.etsi.org/ngsi-ld/v1/ngsi-ld-core-context-v1.7.jsonld"]
}
- Add an user to a group
{
"operationType":"ATTRIBUTE_APPEND",
"tenantUri": "urn:ngsi-ld:tenant:stellio",
"entityId":"urn:ngsi-ld:User:aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
"entityTypes":["User"],
"attributeName":"isMemberOf",
"datasetId":"urn:ngsi-ld:Dataset:isMemberOf:zzzzzzzz-yyyy-xxxx-wwww-vvvvvvvvvvvv",
"operationPayload":"{\"type\":\"Relationship\",\"object\":\"urn:ngsi-ld:Group:zzzzzzzz-yyyy-xxxx-wwww-vvvvvvvvvvvv\",\"datasetId\":\"urn:ngsi-ld:Dataset:isMemberOf:zzzzzzzz-yyyy-xxxx-wwww-vvvvvvvvvvvv\"}",
"updatedEntity":"",
"contexts":["https://easy-global-market.github.io/ngsild-api-data-models/authorization/jsonld-contexts/authorization.jsonld","https://uri.etsi.org/ngsi-ld/v1/ngsi-ld-core-context-v1.7.jsonld"]
}
- Remove an user from a group
{
"operationType":"ATTRIBUTE_DELETE",
"tenantUri": "urn:ngsi-ld:tenant:stellio",
"entityId":"urn:ngsi-ld:User:aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
"entityTypes":["User"],
"attributeName":"isMemberOf",
"datasetId":"urn:ngsi-ld:Dataset:isMemberOf:zzzzzzzz-yyyy-xxxx-wwww-vvvvvvvvvvvv",
"updatedEntity":"",
"contexts":["https://easy-global-market.github.io/ngsild-api-data-models/authorization/jsonld-contexts/authorization.jsonld","https://uri.etsi.org/ngsi-ld/v1/ngsi-ld-core-context-v1.7.jsonld"]
}
- Update the name of a group
{
"operationType":"ATTRIBUTE_REPLACE",
"tenantUri": "urn:ngsi-ld:tenant:stellio",
"entityId":"urn:ngsi-ld:Group:zzzzzzzz-yyyy-xxxx-wwww-vvvvvvvvvvvv",
"entityTypes":["Group"],
"attributeName":"name",
"operationPayload":"{\"type\":\"Property\",\"value\":\"New group name\"}",
"updatedEntity":"",
"contexts":["https://easy-global-market.github.io/ngsild-api-data-models/authorization/jsonld-contexts/authorization.jsonld","https://uri.etsi.org/ngsi-ld/v1/ngsi-ld-core-context-v1.7.jsonld"]
}
- Delete a user / group / client
{
"operationType":"ENTITY_DELETE",
"tenantUri": "urn:ngsi-ld:tenant:stellio",
"entityId":"urn:ngsi-ld:Group:zzzzzzzz-yyyy-xxxx-wwww-vvvvvvvvvvvv",
"entityTypes":["Group"],
"contexts":["https://easy-global-market.github.io/ngsild-api-data-models/authorization/jsonld-contexts/authorization.jsonld","https://uri.etsi.org/ngsi-ld/v1/ngsi-ld-core-context-v1.7.jsonld"]
}