Authentication integration

Choose an OIDC provider

Stellio can be configured to use any OIDC provider and works with the NGSI-LD and permissions endpoints.

The EGM Keycloak image gives access to the subject endpoints and allows the use of the on-owner-delete-cascade-entities property.

Common configuration

Configure the search-service

The following environment variables are needed to configure authentication in the search service:

  • APPLICATION_AUTHENTICATION_ENABLED: true (a boolean)
  • APPLICATION_AUTHENTICATION_ALLOW_PUBLIC_PERMISSION: a boolean
    • allow the creation of permission giving read access to non-connected users.
  • APPLICATION_TENANTS_0_ISSUER: your-issuer-url/uri
    • The URL of the OIDC issuer.
    • For example, a Keycloak issuer will be https://{keycloak_url}/realms/{realm_name}
  • APPLICATION_AUTHENTICATION_CLAIMS_PATHS: my.claim.path,my.other.claim.path
    • A comma-separated list of paths, each path should correspond to a claim in the JWT generated by your OIDC provider.
    • When evaluating permissions only the specified claims will be used to see if the user gets access to the resource.
    • The configured claims are retrieved from the user token (as well as the user sub) and used
    • default value is : realm_access.roles,groups_uuids

Configure the subscription-service

The subscription service uses the same base variables as the search service.

Additionally, the subscription service uses client-credentials to authenticate on the search service. You should create a service account with the "stellio-admin" claim in your OIDC provider.

With this service account, the following additional variables must be defined:

  • APPLICATION_TENANTS_0_CLIENTID: {my-client_id}
  • APPLICATION_TENANTS_0_CLIENTSECRET: {my-client-secret}
  • APPLICATION_TENANTS_0_ACCESSTOKENURL: {my-access-token-url}
    • The access token URL of the OIDC provider.
    • By default, it uses the Keycloak token URL {issuer}/protocol/openid-connect/token

Validate the configuration

To validate the configuration is fully working, the client credentials can be used to create an entity in Stellio:

  • Get an access token for this client
export ACCESS_TOKEN=$(http --form POST {my-access-token-url} 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, Stellio returns a 201 Created response.

Integrate authentication with EGM Keycloak image

Stellio can be used with the Keycloak IAM solution.

This presents a basic solution to show how to setup the integration between Stellio and Keycloak. It is only meant for development purposes, please refer to Keycloak documentation to set up a production deployment.

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:

  • Choose the correct Keycloak version
  • 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

Choose the correct version of Keycloak

As there are sometimes modifications in the Kafka plugin that transmits user-related events to Stellio (mainly related to modifications of the Stellio internal event model), some versions of Stellio require a minimal version of Keycloak for the communication to happen correctly.

The following table summarizes these compatibility requirements:

Stellio version Keycloak version
2.25.0 26.3.4
2.22.0 26.2.0
2.19.0 26.0.7

Use the Keycloak Docker image by EGM

The provided Docker image extends the official Keycloak Docker image to bundle it with four 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 (https://github.com/aerogear/keycloak-metrics-spi)
  • A token mapper that maps the groups uuids of the subject into the JWT.

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):

Note: By default, Keycloak uses a local dev-file database. Even when running with the start-dev command, it is highly recommended to set KC_DB=postgres as shown below to ensure your data is properly persisted to the external PostgreSQL database.

services:
  keycloak:
    container_name: keycloak
    image: easyglobalmarket/keycloak:26.3.5
    restart: always
    environment:
      - KC_BOOTSTRAP_ADMIN_USERNAME=${KEYCLOAK_ADMIN}
      - KC_BOOTSTRAP_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}
      # 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
      # Health checks
      - 9000:9000
    depends_on:
      - postgres
    command: "start-dev"
  postgres:
    container_name: postgres
    image: postgres:16-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 section
  • tenant_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 to urn: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 a user the right to create entities in the context broker
  • stellio-admin: it gives a 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, NotifyAccountEventLister (to be notified of new accounts, the notification is sent to realm admins), and metrics-listener (if integrated with Prometheus)

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
    • Enable 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 a user
{
  "operationType":"ENTITY_CREATE",
  "tenantName": "urn:ngsi-ld:tenant:default",
  "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",
  "tenantName": "urn:ngsi-ld:tenant:default",
  "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",
  "tenantName":"urn:ngsi-ld:tenant:default",
  "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\":\"test\"},\"internalClientId\":{\"type\":\"Property\",\"value\":\"gggggggg-hhhh-iiii-kkkkkkkkkk\"}}",
  "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 a user / group / client

An array of realm roles is sent, it is empty if the subject has no longer a realm role.

{
  "operationType":"ATTRIBUTE_CREATE",
  "tenantName": "urn:ngsi-ld:tenant:default",
  "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 a user to a group
{
  "operationType":"ATTRIBUTE_CREATE",
  "tenantName": "urn:ngsi-ld:tenant:default",
  "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 a user from a group
{
  "operationType":"ATTRIBUTE_DELETE",
  "tenantName": "urn:ngsi-ld:tenant:default",
  "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",
  "previousPayload": "",
  "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_UPDATE",
  "tenantName": "urn:ngsi-ld:tenant:default",
  "entityId":"urn:ngsi-ld:Group:zzzzzzzz-yyyy-xxxx-wwww-vvvvvvvvvvvv",
  "entityTypes":["Group"],
  "attributeName":"name",
  "operationPayload":"{\"type\":\"Property\",\"value\":\"New group name\"}",
  "previousPayload": "",
  "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",
  "tenantName": "urn:ngsi-ld:tenant:default",
  "entityId":"urn:ngsi-ld:Group:zzzzzzzz-yyyy-xxxx-wwww-vvvvvvvvvvvv",
  "entityTypes":["Group"],
  "previousEntity": "",
  "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"]
}