Keycloak:Tutorial EN

De Site à José Mans


Tutorial
Keycloak


version 1.1

Created on July 22, 2025
Published online September 21, 2025.
Updates on October 19, 2025.
Language English - French

Author

José Mans

Purpose

Reason

Standardize authentication to the company's web services including MediaWiki and Nextcloud while continuing to use the LDAP directory (AD).

This document describes using a single authentication interface to allow access to the company's services. This doc focuses only on Nextcloud and the Wiki (MediaWiki).

Single sign-on to access all services.

Approach

The idea here is to run your Keycloak service inside a container managed by Docker and have it start when you run compose up. That is why preparation is emphasized over installation in this document.

Estimated time for a complete installation and configuration: 1 hour.

Prerequisites

  • Database: MySQL or MariaDB
  • Docker CE
  • Apache2
  • SSL certificate

Optional

  • OpenLDAP or Active Directory

Advice

Download "Dockerfile" and "docker-compose.yml" before continuing and change parameters/variables as they are mentioned.

The files are available under the names 'Dockerfile.txt' and 'docker-compose.7z' — obviously you should rename and uncompress them to Dockerfile and docker-compose.yml 😉

Other tutorial

Official tutorial: [Getting started with Docker]

Preparation

External database

MariaDB / MySQL

Since Keycloak 20 the utf8mb4 collation is used despite the documentation warning: https://www.keycloak.org/server/db (2025 09 05).

You therefore need to create a database using this format to have long indexes.

Simply replace utf8mb4_unicode_ci with utf8mb4_bin if you want better performance at the expense of accent-insensitive comparisons such as "Èric = eric" which will not match.
CREATE DATABASE keycloak CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'kuser'@'%' IDENTIFIED BY 'UnMotDePassTrèsFort';
GRANT ALL PRIVILEGES ON keycloak.* TO 'kuser'@'%';
FLUSH PRIVILEGES;
Using an external SQL database is mandatory for production use; otherwise all configuration is lost because the default database is ephemeral (H2) and due to the nature of Docker.

KC_DB syntax

Once the database and user are created the docker-compose.yml needs to include these settings through the KC_DB variables as described below:

Example

IP 172.17.0.1 is the default configured by Docker; to be sure run:
ip a show dev docker0

or

docker network inspect $(docker network ls | grep -P "^.*bridge.*bridge.local" | sed -E 's/^([a-zA-Z0-9]+)\s+(bridge)./\1/')

For information, database-related variables are documented at:

https://www.keycloak.org/server/db


Update

It is highly recommended to perform a mysqldump BEFORE any Keycloak update, as there is a risk of data loss when a Liquibase update is performed.

Otherwise, it would be necessary to customize the SQL queries using <sql> or <modifySql> to preserve these attributes... This is not covered in this tutorial.

This is why developers recommend PostgreSQL as the database to avoid this message during the first startup:


Message: [liquibase.changelog.DatabaseChangeLog] (main) Due to mariadb SQL limitations, modifyDataType will lose primary key/autoincrement/not null/comment settings explicitly redefined in the change. Use <sql> or <modifySql> to re-specify all configuration if this is the case


Firewall

Before allowing database connections from the Docker container, ensure Keycloak can reach the external MySQL/MariaDB server.

Therefore check the firewall to allow connections from the docker0 bridge and the MariaDB server.

On my host the firewall does not accept foreign interfaces by default. I therefore added a few lines:

DOCKER=docker0
CHAIN=dock-me

Iptables

iptables -N ${CHAIN}
iptables -A INPUT -i $DOCKER -j ${CHAIN}
iptables -A ${CHAIN} -p TCP --dport 3306 -j ACCEPT
iptables -A ${CHAIN} -j RETURN

nft

nft add chain inet filter ${CHAIN} {}\;
nft add inet filter input iifname "${DOCKER}" counter jump ${CHAIN}
nft add rule inet filter ${CHAIN} tcp dport 3306 counter accept
nft add rule inet filter ${CHAIN} counter return

SSL Certificates

File access rights for Keycloak and use of a Let's Encrypt certificate renewed every 90 days.

The Keycloak image is intentionally minimal for security reasons. It is not appropriate to add tools inside the image to perform certificate renewals.

Therefore the mount method is chosen, because it requires less manipulation within the container. The host performs certificate renewal with certbot.

Mount-based access method

This method is adopted because the server can reload new certificates without service interruption — at worst a simple docker restart.

Re-assigning SSL certificate permissions

By default the container runs with UID=1000 and GID=0. In other words it has read/write/execute rights for any files and directories belonging to the container's root group and — when volumes are mounted — on the host as well.

Without going into details it seems preferable to assign a different GID to the container so it can access the SSL certificates.

# sur le hôte du docker, où sont enregistrés les certificats.
addgroup ssl-access
getent group ssl-access
# affiche le gid '''5020'''
chown root:'''5020''' /etc/letsencrypt/{archive,live} /etc/letsencrypt/archive/domain.tld/priv*.pem
chmod 650 /etc/letsencrypt/archive/domain.tld/priv*.pem /etc/letsencrypt/{archive,live}
## Ceci est ajouté dans la crontab qui gère le renouvellement des certificats :
## vi /usr/local/sbin/crontab_re_hash.sh afin de remodifier les droits à chaque changement par certbot...
The same GID was chosen in docker-compose.yml.

pkcs12 method

Just for completeness: Keycloak recently supports PKCS#12 certificate format.

This method is not recommended, but if you have no other option, here is how to convert certificates:

openssl pkcs12 -export -in /etc/letsencrypt/live/domain.tld/fullchain.pem -inkey /etc/letsencrypt/live/domain.tld/privkey.pem -out server.keystore -name server -passout pass:password

Apache proxy

To avoid having multiple SSL certificates with different domains, a reverse proxy solution was chosen.

The domain "domain.tld" is used with a dedicated path; Keycloak will be reachable from the public-facing server at:

https://domain.tld/auth/

cookie_not_found

A recurring error between Keycloak and an HTTP server such as Nginx or Apache.

Using a proxy often triggers this error: error="cookie_not_found"

Using tools like "curl" or "http toolkit" helps to understand why it happens. In my case the problem was a cookie rewrite from a previous rule where "Path: /TrucMuche/" had been added to the Set-Cookie: header.

To avoid this, locate any ProxyPassReverseCookiePath rule and adapt it inside the <Location "/auth/">... block and you're done :)

Here are the Apache2 directives to adapt and place in your site configuration so redirects work properly:

# --- Keycloak START ---

ProxyRequests off
ProxyPreserveHost On

ProxyPass /auth/ https://domain.tld:8086/
ProxyPassReverse https://domain.tld:8086/(.*)$ https://domain.tld/auth/$1

# En-têtes X-Forwarded essentiels
Header always set X-Forwarded-Proto "https"
Header always set X-Forwarded-Port "443"
RequestHeader set X-Forwarded-Proto "https"
RequestHeader set X-Forwarded-Port "443"
RequestHeader set X-Forwarded-For %{REMOTE_ADDR}s
<Location "/auth/">
# Gestion des cookies sécurisés
ProxyPassReverseCookieDomain domain.tld domain.tld
ProxyPassReverseCookiePath / /auth/
</Location>
#CAS DECOLE: Header edit Set-Cookie "^(.*)$" "$1; Secure; SameSite=None"
# --- Keycloak END ---

Installation

The Keycloak version used is the Docker-optimized image. Therefore Docker must be installed before proceeding (installation tutorial)

Remember to download "Dockerfile" and "docker-compose.yml" so you can edit them while reading the following chapters.

Building the image

The provided Dockerfile contains the essential directives to build the image required to run the final container.

For example:

FROM quay.io/keycloak/keycloak:26.3.4 AS builder

# Enable health and metrics support
# NOTE: not supported inside this build: hence the --.* options in RUN are commented out

#ENV KC_HEALTH_ENABLED=true
#ENV KC_METRICS_ENABLED=true

WORKDIR /opt/keycloak

# for demonstration purposes only, please make sure to use proper certificates in production instead

#RUN keytool -genkeypair -storepass password -storetype PKCS12 -keyalg RSA -keysize 2048 -dname "CN=domain.tld" -alias server -ext "SAN:c=DNS:localhost,IP:127.0.0.1" -keystore conf/server.keystore
#COPY /opt/keycloak/ /opt/keycloak/

FROM quay.io/keycloak/keycloak:26.3.4
COPY --from=builder /opt/keycloak/ /opt/keycloak/

RUN /opt/keycloak/bin/kc.sh build --db=mariadb --health-enabled=true --metrics-enabled=true --features="user-event-metrics,persistent-user-sessions"

ENTRYPOINT ["/opt/keycloak/bin/kc.sh"]
If another database is used, replace `--db=mariadb` with the appropriate driver.

e.g., for MySQL use --db=mysql

See supported drivers: https://www.keycloak.org/server/db

Once the file is ready, build the image with:

 docker build -t keycloak . # Dockerfile must be in the current directory...

Pre-image options

Options usable only during the image build. They determine the Keycloak server behavior.

RUN /opt/keycloak/bin/kc.sh build ... --features="Option1,Option2"

<!> no spaces allowed between features — use a comma "," only

Creating the final container

The docker-compose.yml contains the essential settings for the Keycloak server.

Of course adapt it to your company requirements. Here is its content:

services:
  keycloak:
    container_name: keycloak2
    user: "keycloak:5020" # UID and GID from /etc/passwd container / image
    image: keycloak:26.3.4
    #image: quay.io/keycloak/keycloak:26.3.2
    ports:
    #- ":8084:8080"
    #- ":8087:9000"
    - ":8086:8443"
    volumes:
    - /etc/letsencrypt/:/etc/letsencrypt/:ro

    environment:
    # BASE DBASE
    #KC_DB: mariadb # Already declared in the image!
    KC_DB_URL: jdbc:mariadb://172.17.0.1/keycloak?characterEncoding=UTF-8&useSSL=false
    KC_DB_URL_PORT: 3306
    KC_DB_USERNAME: kuser
    KC_DB_PASSWORD: "UnMotDePassTrèsFort" # The database password

    # Paramètres d'administration de Keycloak
    #Déclassé: KEYCLOAK_ADMIN: admin
    #Déclassé: KEYCLOAK_ADMIN_PASSWORD: admin
    KC_BOOTSTRAP_ADMIN_USERNAME: admin
    KC_BOOTSTRAP_ADMIN_PASSWORD: *********

    # Hostname and Proxy Configuration
    # Base URL where Keycloak can be accessed from a local network or the internet
    KC_HOSTNAME: https://domain.tld/auth/ # ou l'option: command: ... --hostname=https://domain.tld/auth/

    # Health Settings and Metrics
    #KC_HEALTH_ENABLED: "true"
    #KC_METRICS_ENABLED: "true"

    # LOGs
    KC_LOG: console
    KC_LOG_LEVEL: info
    KC_LOG_COLOR: true
    # Too verbose :
    #KC_LOG_CONSOLE_LEVEL: all
   

    #Valid SSL certificates and hourly reloading
    KC_HTTPS_CERTIFICATE_FILE: /etc/letsencrypt/live/domain.tld/fullchain.pem
    KC_HTTPS_CERTIFICATE_KEY_FILE: /etc/letsencrypt/live/domain.tld/privkey.pem
    KC_HTTPS_CERTIFICATES_RELOAD_PERIOD: 1h


    # Version for tests
    #command: start-dev --hostname-strict=false --proxy-headers forwarded --verbose
    command: start --verbose --http-enabled=true --proxy-trusted-addresses='''IP_SERVEUR'''/32,127.0.0.0/8 --proxy-headers=xforwarded --optimized

Adjust the variables:

  • KC_HOSTNAME (v2)
    • Contains the hostname Keycloak should use.
    • It can also contain the external access URL.
      • In my case I want Keycloak accessible from the internet using the main domain's valid SSL certificate. That implies using a proxy so external requests to https://domain.tld/auth transparently reach Keycloak and, MOST IMPORTANT, responses are adapted to the expected paths in Keycloak's HTML output...
  • KC_BOOTSTRAP_ADMIN_PASSWORD
    • Password for the admin console
  • KC_HTTPS_CERTIFICATE_FILE
    • Public certificate
  • KC_HTTPS_CERTIFICATE_KEY_FILE
    • Private key
  • --proxy-trusted-addresses
    • Replace with the IPs allowed to access Keycloak directly, including IP_SERVEUR.

First launch

Once docker-compose.yml is modified, create and start it with:

docker compose up

The container should then be created and start without issue.

During the process, there will be warnings regarding MariaDB and what it doesn't support, followed by a long wait (2 minutes...).

Here is an excerpt of a message confirming that the server started correctly:

keycloak2  | 2025-09-24 13:22:15,825 INFO  [io.quarkus] (main) Profile prod activated.
keycloak2  | 2025-09-24 13:22:15,825 INFO  [io.quarkus] (main) Installed features: [agroal, cdi, hibernate-orm, jdbc-mariadb, keycloak, micrometer, narayana-jta, opentelemetry, reactive-routes, rest, rest-jackson, smallrye-context-propagation, smallrye-health, vertx]
[+] Screenshoot


Admin console access

After running docker compose up, the admin console is accessible at: Admin console -> https://domain.tld/auth/].

The bootstrap super-admin credentials are specified in the docker-compose.yml via variables:

KC_BOOTSTRAP_ADMIN_USERNAME: admin
KC_BOOTSTRAP_ADMIN_PASSWORD: ********* # Replace the stars with what you set ;)


Before anything

The first login to Keycloak places the admin user in the "master" realm.

It is recommended to create a new administrator account and disable the default 'admin' account.

Security

Create a new admin account for the 'master' realm and for any new realms…

  • Manage realms
    • 'master' (Normally already selected)
    • Users
    • Add user and fill fields
      • Email verified : On
      • Username : kadmin
      • Email : ValidEmail!@domain.tld
      • Create
        • Credentials
          • Set password and enter the password
          • Temporary : Off
          • Save
          • Save password
        • Role mapping
        • Assign role
        • Realm roles
          • admin
          • create-realm
          • Assign
Before disabling the default 'admin' account, verify the new account's access from another browser or a private window at: https://domain.tld/auth/admin/. If you see the same admin console as with the 'admin' account then you can disable (or delete) the default account.

Disable default 'admin' account

First, log out the default 'admin' account in your browser and log in with the new 'kadmin' account
  • Manage realms
    • 'master'
    • Users
      • 'admin'
      • click 'Enable' so the button becomes 'Disable'
      • disable

Creation

From this chapter onward, create the realm dedicated to the organization, its groups, users and clients (services). Do not forget to use the LDAP directory to synchronize existing groups and users to avoid re-creating employee accounts...

Realm

Called a "realm" and represents a grouping of services.

Think of a realm as an organization that manages authentication for its services (clients) and its users.

Each realm is unique and isolated. One organization manages its own rules without interfering with another realm.

Create Gyptis as the organization's realm, and modify some settings:

  • Manage realms
    • Create realm
      • Realm name : Gyptis
      • Enable : On
      • Create

After creation, the "Manage" and "Configure" sections will be dedicated to the newly created realm.

Important note: unless stated otherwise, all manipulations in this document must be performed in the realm you just created.

The badge (top-left) indicates the selected realm.

Contrast with which shows the server's 'master' realm selected.

Groups

Group management is similar to any directory (AD, LDAP, ...). Creating a group is straightforward.

Create an administrator group for Gyptis:

  • Manage realms
  • 'master'
  • Groups
    • Create group
      • Name: Gadmins
      • Description : Group for Gyptis administrators
      • Save

Assign admin rights to Gadmins

  • Groups
    • Gadmins
    • Role mapping
    • Assign role
    • Client roles
      • Search : Type Gyptis-realm and confirm
        • to show only roles related to the realm
      • Select all roles named Gyptis-*
      • Assign
No need to create more groups for now — they will be imported from the company's LDAP via the User Federation menu below.

User account

An administrator account and a test user should be created for the realm Gyptis.

  • The admin account will serve the organization to manage the realm.
    • ATTENTION: this admin must be created from the 'master' realm
  • The test user is for doing the first checks within Gyptis and should be created in its own realm.
    • it can be disabled afterwards...

Admin access for Gyptis

  • Manage realms
  • 'master'
  • Users
    • Add user
      • Email verified : On
      • Username : admin-gyptis
      • Email : a_real_email@domain.tld
      • First Name :
      • Last name :
      • Join Groups
        • Select Gadmins
        • Join *** Create
    • Credentials (new tab)
    • Set password
      • Enter the password twice...
      • Temporary : Off
      • Save
      • Save password
    ----
      • Assign role
      • Client roles
      • Search : Gyptis
        • Tick: all roles named Gyptis-* in the ClientID column
        • Assign

Once finished, switch to the realm (realm) Gyptis

Strongly recommended: From this point you may log out of the "kadmin" account and continue using admin-gyptis to configure the Gyptis realm at: https://domain.tld/auth/realms/Gyptis/account
Assuming you follow this advice I will leave it to you to explore what changed...

Test user

Only for initial testing...

  • Manage realms
  • Gyptis
    • Users (for realm Gyptis!)
      • Create new user (ou Add user)
        • Email verified : On
        • Username : Guser
        • Email : another_real_email@domain.tld
        • First Name :
        • Last name :
        • Create
    • Credentials (nouvel onglet)
    • Set password
      • Saisir le mot de passe 2 fois...
      • Temporay : Off
      • Save
      • Save password


Direct access

Once the realm is created and configured users can authenticate via the Keycloak UI at: https://domain.tld/auth/realms/Gyptis/account.

This is the case for Guser but not for admin-Gyptis

Client account

An application client represents the rules and information of a service in the realm.

To allow Cloud and the Wiki to use Keycloak, create a 'client' for each service in Manage->Clients: nextcloud and mediawiki.

Creating a client

Create both clients with minimal information; important details will be configured later.

Manage -> Clients -> Create client

  • Client ID: nextcloud
  • Name : (optional)
  • Next
  • Next (arrived at Login settings)
  • Save

Do the same for mediawiki.

Client settings

Nextcloud

Cloud is managed by Nextcloud (also valid for ownCloud) using the 'OpenID Connect user backend' app

  • Clients -> nextcloud
    • General settings
      • Client ID : nextcloud
      • Name : Nextcloud Client (optional)
    • Access settings
      • Root URL : leave blank
      • Home URL : https://domain.tld/NextCloud/index.php
      • Valid redirection URIs : https://domain.tld/NextCloud/*
      • Valid post logout redirects URIs: https://domain.tld/NextCloud/*
      • Web Origin : +
      • Admin URL : https://domain.tld/Nextcloud/index.php/settings/admin
    • Capability config
      • Client authentication : On
      • Authorization : On
        • Standard flow
        • Direct access grants
        • Service account roles
        • Standard Token Exchange
        • OAuth2.0 Device Authorization grants
    • Login settings
      • Login theme : optional
      • Consent required : Off
      • Display client on screen : Off
    • Logout settings
      • Front channel logout : Off
        • Backchannel logout URL : https://domain.tld/Nextcloud/index.php/apps/user_oidc/backchannel-logout/Keycloak
          • 'Keycloak' represents the provider name in the Nextcloud/user_oidc configuration...
        • Backchannel logout session required : On
        • Backchannel logout revoke offline sessions : On
    • Save
    • Keys (tab)
      • Use JWKS URL configs: On
      • JWKS URL : https://domain.tld/NextCloud/index.php/apps/oidc/jwks
      • Save
    • Client scopes
      • address
      • firstName
      • organization
      • phone
      • profile
      • roles
      • service-account
      • web-origins : Default

MediaWiki

The wiki is managed by MediaWiki with the OpenIDConnect extension.

  • Clients -> mediawiki
    • General settings
      • Client ID : mediawiki
      • Name : MediaWiki client (optional)
      • Root URL : leave blank
      • Valid redirection URIs : https://domain.tld/MediaWiki/*
      • Valid post logout redirects URIs: https://domain.tld/MediaWiki/index.php/Special:UserLogout
      • Web Origin : https://domain.tld/MediaWiki/
      • Admin URL : https://domain.tld/MediaWiki/Special:UserLogin
    • Login settings
    • Capability config
      • Client authentication : On
      • Authorization : On
        • Standard flow
    • Logout settings
      • Front channel logout : Off
      • Backchannel logout URL : https://domain.tld/MediaWiki/rest.php/pluggableauth/v1/logout
      • Backchannel logout session required : On
      • Backchannel logout revoke offline sessions : Off
    • Save
    • Client scopes
      • same as for client 'nextcloud'

Once both clients are created, MediaWiki and Nextcloud can use Keycloak to authenticate company users.

However, as stated in the Purpose we want users to authenticate once and gain access to both services, and logging out from one service should log them out of the others...

The next section explains how to achieve that.

Application authorization

This is where Keycloak is configured so that Nextcloud, MediaWiki and Keycloak itself can be used with a single authentication.

Logging into one of these services will grant access to the others without repeating the login process.

Clients side

Create a client-level role for each client.

For MediaWiki and Nextcloud the roles will be named: access-mediawiki and access-nextcloud, i.e. logically: access-[mediawiki|nextcloud]. Assign the role accordingly.

Summary of configuration for each client:
  • Client->[nextcloud | mediawiki]
    • Roles->Create Role : access-[nextcloud | mediawiki]
    • then Client Scopes->[nextcloud | mediawiki]-dedicated
      • Add Mapper, From predefined mapper
      • choose "client roles"

Precisely:

Nextcloud Role MediaWiki Role
  • Client
    • nextcloud
    • Roles
      • Create Role
      • Role name: access-nextcloud
      • Save & Cancel
    • Client Scopes
    • nextcloud-dedicated
      • Add Mapper, Add predefined mapper
        • or according to: From predefined mapper
      • and choose "client roles"
      • Add
  • Client
    • mediawiki
    • Roles
      • Create Role
      • Role name: access-mediawiki
      • Save & Cancel
    • Client Scopes
    • mediawiki-dedicated
      • Add Mapper, From predefined mapper
        • or according to: From predefined mapper
      • and choose "client roles"
      • Add

Groups part

Authorize a group to use single sign-on.

Group-based method — works for groups defined in Keycloak or imported from LDAP/Kerberos...
  • Groups
    • Create group
      • Name : Gyptis_Group
      • Create

Application attribution for the group

  • Groups
  • Gyptis_Group (or any other)
    • Role mapping
      • Assign role
      • client roles and select:
        • access-nextcloud
        • access-mediawiki
      • Assign

For an existing user

  • Users
  • Gyptis_user
    • Role mapping
      • Assign role
      • client role
        • access-nextcloud
        • access-mediawiki
Important note: When the LDAP directory is connected its groups will be imported and visible under the Groups menu. You must then perform the same assignments as above to authorize LDAP group members for access-mediawiki and access-nextcloud.

All necessary steps have been created. The services can use single sign-on with the user 'guser', but first they need to be configured to connect to the Keycloak service.

Connect services to Keycloak

With clients created, configure MediaWiki and Nextcloud to use Keycloak for authentication.

Installation is not complicated and doesn't need yet another tutorial, so here are links to the official guides for the OpenID Connect modules:

MediaWiki : https://www.mediawiki.org/wiki/Extension:OpenID_Connect/fr

Nextcloud : https://github.com/nextcloud/user_oidc

Retrieve client_secret

When creating each client a unique secret is generated. It is intended for the client of each service.

Client->[Nextcloud & mediawiki ] -> Credentials -> Copy 'Client secret' and place it below.

Client-side configuration

MediaWiki

Shell method

Edit LocalSettings.php

wfLoadExtension( 'OpenIDConnect' );

# uri de vérification, si json apparait OK : https://domain.tld/auth/realms/Gyptis/.well-known/openid-configuration
$wgPluggableAuth_Config["Gyptis"] = [
    'plugin' => 'OpenIDConnect',
    'data'  => [
            'providerURL'           => 'https://domain.tld/auth/realms/Gyptis',
            'clientId'              => 'mediawiki',
            'clientSecret'          => '*********',
            'codeChallengeMethod'   => 'S256',
            'scope' => [ 'openid', 'profile', 'email', 'firstName', 'address', 'organization', 'phone', 'profile', 'roles', 'web-origins' ],
                ],
];

$wgOpenIDConnect_SingleLogout      = true; # déconnecter l'user aussi sur KeyC

# Change la redirection, idéale pour aller au Portail quand déco.
#$wgHooks['UserLogoutComplete'][] = function ( $user, &$inject_html, $old_name ) {
#   // Redirige vers l'URL préférée après déconnexion.
#   header( "Location: https://Portail-Entreprise/" );
#   exit;
#};

# Variables pour forcer l'identification et donc empêcher la lecture des articles par les anonymes.
$wgPluggableAuth_EnableAutoLogin = true; # False permet la navigation, 'true' oblige à s'identifier. Mais ne fonctionne pas sans celle ci-dessous!
$wgGroupPermissions['*']['read'] = true;    # Oblige l'identification pour lire les articles.

If you want Keycloak to become the only authentication method, disable all other auth modules, LDAP, and MediaWiki's local auth...

$wgPluggableAuth_EnableLocalLogin = false;
$wgPluggableAuth_Config = array(); # <!> This implies count( $wgPluggableAuth_Config[] ) contains only one element!
$wgPluggableAuth_Config["..."] put the OpenID config here...
...
cd /var/www/MediaWiki
sudo -u www-data -g www-data php maintenance/run.php --conf /var/www/MediaWiki/LocalSettings.php update --quick --force
If caches exist:
Adapt according to your server configuration!

systemctl restart "php*"
systemctl restart redis-server memcached
redis-cli FLUSHDB
redis-cli -n DB_NUMBER FLUSHDB
redis-cli -n DB_NUMBER FLUSHDB ASYNC
redis-cli FLUSHALL
redis-cli FLUSHALL ASYNC
(echo "flush_all" )

Nextcloud

Add the OpenID Connect extension (user_oidc)

  1. Installing OpenID Connect
    • Log into the Nextcloud admin console
    • Open the Apps menu and go to Security.
    • Search for "OpenID Connect user backend", download it and enable it.
  2. Return to the admin console, then open the new "OpenID Connect" menu
    • Registered Providers

Shell method

Use the occ command:

Minimal:
sudo -u www-data -g www-data php occ user_oidc:provider Keycloak --clientid="nextcloud" \
    --clientsecret="***********" \
	--discoveryuri="https://domain.tld/auth/realms/Gyptis/.well-known/openid-configuration"

Complete:

sudo -u www-data -g www-data php occ user_oidc:provider Keycloak \
    --clientid="nextcloud" \
    --clientsecret="***********" \
	--discoveryuri="https://domain.tld/auth/realms/Gyptis/.well-known/openid-configuration" \
	--scope="openid email address organization phone profile roles web-origins organization groupOfNames-scope" \
	--mapping-display-name="displayName" \
	--mapping-email="email" \
	--mapping-uid="entryUUID | username | email" \
	--mapping-groups="groupOfNames" \
	--mapping-language="preferredLanguage" \
	--mapping-website="labeledURI" \
	--mapping-avatar="jpegPhoto" \
	--mapping-phone="phone" \
	--unique-uid=false \
	--check-bearer=true \
	--send-id-token-hint=true \
	--bearer-provisioning=true \
	--group-provisioning=true \
	--group-whitelist-regex="^(cloud|dedie|famille)\$" \
	--resolve-nested-claims=false \
	--group-restrict-login-to-whitelist=true # false for test...

Web console method

Configuration
  • Client configuration
    • Identifier : Keycloak
    • Client ID: nextcloud
    • Client secret : to be retrieved
    • Discovery endpoint : https://domain.tld/auth/realms/Gyptis/.well-known/openid-configuration
    • Custom end session :
    • Scope : openid email address organization phone profile roles web-origins groupOfNames-scope entryUUID-scope
      • The two bold items are essential if you plan to use LDAP (see below). Otherwise, do not add them.
    • Extra claims :
  • Attribute mapping
    • Enable nested and fallback ... :
    • User ID mapping : entryUUID | sub
      • or the attribute adapted to your config, e.g., uid
        • entryUUID => user from LDAP
        • sub => user from Keycloak
        • Before using Keycloak, if you used Nextcloud/ownCloud + LDAP, it's likely users' folder names are based on the entryUUID of each owner.
        • If you want more human-friendly folder names, replace 'sub' with something else like 'email'. Obviously if a user leaves the company and another user with the same name takes their place, they will inherit the old email, which requires removing the departed employee from Nextcloud...
    • Quota mapping :
    • Groups mapping : groups
  • Extra attributes mapping
    • Email mapping : email
    • Language mapping : preferredLanguage
    • Phone mapping : phone
    • Website mapping : labeledURI
    • Avatar mapping : jpegPhoto
  • Authentication and Access Control Settings
    • Use unique user ID :
    • Use provider identifier as prefix for IDs :
    • Use group provisioning :
    • Restrict login for users that are not in any whitelist group :
      • Allows access only to users in the whitelist groups (Group whitelist)
    • Check Bearer token on API and WebDAV .... :
    • Auto provision user when accessing API and WebDAV :
    • Send ID token hint on logout :
  • Submit or Update provider


LDAP configuration

Keycloak can manage users and groups: add, modify, delete, ...

However, companies often already have an LDAP directory (AD or OpenLDAP) and Keycloak allows using it to avoid conversion or migration...

Below are ObjectClasses and attributes related to an OpenLDAP-managed directory on Linux; the principle is the same for Active Directory...

Schemas used: core, cosine, samba, inetorgperson and nis.

If you use FusionDirectory the ObjectClasses / Attributes will be the same.

Finding the base DN:
base=$(ldapsearch -LLL -Q -Y EXTERNAL -H "ldapi:///" "(&(objectClass=organization)(objectClass=dcObject))" "dn:"| sed -re 's/^dn: *(.*)/\1/')
use with the commands below...

To know objectClasses:

Use to find the user's ObjectClasses:

ldapsearch -LLL -Q -Y EXTERNAL -H "ldapi:///" "uid=$(whoami)" "objectclass"

Use to find a group's ObjectClasses:

ldapsearch -LLL -Q -Y EXTERNAL -H "ldapi:///" -b "$(ldapsearch -LLL -Q -Y EXTERNAL -H "ldapi:///" -b "ou=groups,${base}" "(&(objectClass=posixGroup)(memberUid=$(whoami)))" cn | grep -E "^dn: cn=.groups." | sed -re 's/^dn: (.)$/\1/g' | head -1)" objectClass

or

ldapsearch -LLL -Q -Y EXTERNAL -H "ldapi:///" "cn=$(whoami)" objectClass

if the group "$(whoami)" exists!

If the two ldapsearch commands above fail, use:

ldapsearch -LLL -Q -Y EXTERNAL -H "ldapi:///" | less

And search for a user entry and then a group containing that user.

Otherwise contact our friend AI ;)

User federation

Two choices to add a Kerberos and/or LDAP provider

If a provider is already registered, go directly to Add LDAP provider to add an existing LDAP directory...

Add LDAP providers

  • Add new provider->LDAP (Settings section)
    • UI display name : Ldap
      • Vendor : Other

Connection and authentication settings

    • Connection URL : ldap://ldap.domain.tld/
    • Enable StartTLS : Off
    • Use Truststore SPI : Always
    • Connection pooling : On
    • Connection timeout :
    • Bind type : simple
    • Bind DN : cn=ro,dc=gyptis
    • Bind credentials : password for the binding account

LDAP searching and updating

    • Edit mode : READ ONLY
    • Users DN : ou=People,dc=gyptis
    • Relative user creation DN :
    • Username LDAP attribute : cn
    • RDN LDAP attribute : uid
    • UUID LDAP attribute : entryUUID
    • User object classes : posixAccount
    • User LDAP filter : (|(objectclass=gosaMailAccount)(objectclass=inetOrgPerson)(objectclass=organizationalPerson)(objectclass=posixAccount)(objectclass=sambaSamAccount)(objectclass=top))
    • Search scope : One Level * Read timeout :
    • Pagination : Off
    • Referral :

Synchronizations settings

    • Import users : Off (On in production)
    • Sync Registrations : On
    • Batch size : 124000
    • Periodic full sync : Off
    • Periodic changed users sync : Off
    • Remove invalid users during searches : Off

Kerberos integration

    • Allow Kerberos authentication : Off
    • Use Kerberos for password authentication : Off

Cache settings

    • Cache policy : DEFAULT

Advanced settings

    • Enable the LDAPv3 password modify extended operation : Off
    • Validate password policy : Off
    • Trust Email : On
    • Connection trace : Off
  • Save

Import LDAP / AD groups into Keycloak

Mapper

Mappers->Add mapper

  • User federation
    • enter the configuration created above, by default its name is 'ldap'
  • Mappers
  • Add mapper
  • Name : groupOfNames
  • Mapper type : group-ldap-mapper
  • LDAP Groups DN : ou=groups,dc=gyptis
  • Relative creation DN :
  • Group Name LDAP Attribute : cn
  • Group Object Classes : groupOfNames,posixGroup,gosaGroupOfNames
  • Preserve Group Inheritance : On
  • Ignore Missing Groups : Off
  • Membership LDAP Attribute : member (or memberUid with AD)
  • Membership Attribute Type : DN
  • Membership User LDAP Attribute : uid
  • LDAP Filter : (&(|(objectclass=groupOfNames)(objectclass=posixGroup)(objectclass=gosaGroupOfNames)))
  • Mode : READ_ONLY
  • User Groups Retrieve Strategy : LOAD_GROUPS_BY_MEMBER_ATTRIBUTE
  • Member-Of LDAP Attribute : memberOf
  • Mapped Group Attributes :
  • Drop non-existing groups during sync : Off
  • Groups Path : /
  • Save

Use: Sync LDAP groups to Keycloak, from the Action button in the upper right corner.

  • If group retrieval is performed, confirmation should occur after making the selection.
At this stage you can assign certain LDAP groups the right to use single sign-on by following the Groups section procedure


Fields returned to clients

A client may need specific attributes to make the new Keycloak-based identity consistent with its own databases.

You must therefore make certain attributes available to clients by defining client scopes and mappers. These attributes were collected from LDAP during provider configuration and are held in Keycloak's memory.

However, this is useless if we don't make their names and contents available to the clients that need them. Here we decide which client will have access to the content of these values.

groupOfNames and entryUUID

Most services need to know a user's groups and also their unique identifier that even a namesake would not share (often the uid).

The groupOfNames and entryUUID attributes are necessary for Nextcloud and MediaWiki; their roles and mappers should be defined as described below.

Particularity for Nextcloud

This is important for Nextcloud/ownCloud which use openLDAP for user identity. The entryUUID attribute is used to create a user's data directory. However Keycloak does not know this attribute by default and if an existing LDAP user who used the cloud before Keycloak logs in they may not see their data and think it was lost.

To prevent this we must create a scope and mapper so Nextcloud can obtain the user's entryUUID attribute, and also configure its "user_oidc" module to request this attribute.


entryUUID-scope groupOfNames-scope
  • Client scopes
    • Create client scope
      • Name : entryUUID-scope
      • Type: Default
      • Display on consent screen : Off
      • Include in token scope : On
      • Display order : 30
      • Save
    • Mappers
      • Configure a new mapper
        • if others exist then : Add mapper
        • then By configuration
      • User Attribute
        • Name : entryUUID
        • User attribute : entryUUID
          • The attribute that will be sent by Keycloak
            • You could return email, username, ...
        • Token Claim Name : entryUUID
        • Claim JSON Type : String
        • Add to ID token : On
        • Add to access token : On
        • Add to lightweight access token : Off
        • Add to userinfo : On
          • userinfo (userinfo_endpoint) represents the information received by the client, so it is not necessary to explicitly include entryUUID-scope in the scope when this is On...
        • Add to introspection : On
        • The rest Off
      • Save && Cancel
  • Client scopes
    • Create client scope
      • Name : groupOfNames-scope
      • Type: Default
        • Default indicates providing this field without having to specify it in the 'scope' (i.e., in authorization_endpoint)
      • Display on consent screen : Off
      • Include in token scope : On
      • Display order : 30
      • Save
    • Mappers
      • Configure a new mapper
        • if others exist then : Add mapper
        • then By configuration
      • Group Membership
        • Name : groupOfNames
        • Token Claim Name : groupOfNames
        • Claim JSON Type : String
        • Full group path : Off
          • When on, the group name is sent with the path. If none is specified, "/" is added to the name, i.e.: /YourGroup...
        • Add to ID token : On
        • Add to access token : On
        • Add to lightweight access token : Off
        • Add to userinfo : On
        • Add to introspection : On
      • Save && Cancel
This method works for any attribute — just replace "entryUUID" with the attribute name present in your LDAP...


Provider / User federation side

In Nextcloud/ownCloud when a user is imported from LDAP, entryUUID is used.

You must configure the LDAP provider to retrieve this attribute.

  • User federation
  • Ldap, so an existing provider ;)
    • Mappers
    • Add mapper
      • Name : entryUUID
      • Mapper type : user-attribute-ldap-mapper
      • User Model Attribute : entryUUID
      • LDAP attribute : entryUUID
      • Read only : On
      • everything else Off
      • 'Save


On the Groups Side

Remember to assign authorized applications to the LDAP base groups that were imported from User federation.

The procedure has already been covered for assigning the test user (guser) to Group_Gyptis.

Here is the shortcut:

  • Groups->(select LDAP group for cloud member...)->Role mapping->Assign role->client roles->[entryUUID-scope | groupOfName-scope ]->Assign

However, you will need to perform the additions to groups imported from the LDAP base. So something other than 'Group_Gyptis'. In our company, users with full rights to use the Nextcloud application are in the group with the same name, i.e., 'nextcloud'. So just follow the procedure above Application attribution for the group, and configure your organization's groups.

On the Client Scopes Side

  • Clients
  • Nextcloud
  • Client scopes
    • Add client scope
      • the two new 'client scopes' created above should appear in the list
      • Select groupOfName-scope and entryUUID-scope
      • Add
      • Default

Do the same for mediawiki, except for entryUUID which it won't need.


Nextcloud side

Instruct Nextcloud / user_oidc to request the desired attribute:

  • Edit the 'keycloak' provider.
  • Under "User ID mapping"
  • Add "entryUUID" (default is 'sub'!)


This change allows LDAP users to log in and see their cloud contents. However users not in LDAP won't be able to use Nextcloud unless you combine attributes like entryUUID | sub.

Prevent user edits of certain attributes

By default users can edit some profile attributes like email, firstName, lastName.

On a public forum that may be fine, but in a corporate environment it's more sensitive.

To prevent users from changing [ email | firstName | lastName ]:

  • Realm settings
  • User profile
  • Edit attributes one by one: email & firstName & lastName
  • Uncheck "Who can edit?" for the User role


Congrulation !

Once all the steps above have been applied, single sign-on should be operational.

Every login or logout to one of the services will apply to the others.

For any questions, you can contact me via the web form, mentioning the Keycloak subject to avoid being filtered by the anti-spam ;)


Annex

Login events log

After creating the new realm we can adjust logging for authentication activity across that realm.

First, keep all connection events logged for 1 hour:

  • Realm settings
    • Events (tab)
      • Event listeners (sub-tab under 'Events')
        • add email
      • User events settings
        • save event: On
        • 1 hours expiration

this keeps connection events for only 1 hour

      • Admin events settings
        • save event: On
        • 1 hours expiration
      • Save

Save the three modified tabs

Tip: repeat these steps in the 'master' realm as well — by default these logs are disabled there; enabling them helps troubleshoot. In production you may want to adjust or disable them depending on your needs...


List of command options

List of Keycloak configuration options when creating the container

Mappings

Concordance between startup options, keycloak.conf and environment variables.


LDAP Group whitelist regex

Adapting examples from the user_oidc interface (cf: Group whitelist regex) with the "Restrict login" option yielded inconsistent results.


This will create and update the users groups depending on the groups claim in the ID token. The Format of the groups claim value should be [{gid: "1", displayName: "group1"}, …], ["group1", "group2", …] or "group1,group2"

Indeed, a user was allowed to open Nextcloud even though they were not in the "Nextcloud" and "mediawiki" groups but in another group with a slightly different name, here "cloud".

This is explained by the PHP method (getSyncGroupsOfToken) that handles comparison using "preg_match" (PCRE). In the example, the old group "cloud" is found inside the string :

"Group whitelist regex": ["Nextcloud","mediawiki"]

To avoid this problem we use the regex form accepted by PCRE and therefore it was useful to inform the reader.


FAQ

Q
After reinstalling the Keycloak server and attempting to access the console a second time, the browser loops endlessly and the server logs show:
keycloak3  | 2025-09-24 13:37:17,883 WARN  [org.keycloak.events] (executor-thread-13) type="REFRESH_TOKEN_ERROR", realmId="******", realmName="master", clientId="security-admin-console", userId="null", ipAddress="*****", error="invalid_token", reason="Invalid refresh token", grant_type="refresh_token", client_auth_method="client-secret"
A
Stop the browser and delete all cookies related to Keycloak. This type of error message usually occurs when you try to log in to two different sessions (e.g., gyptis-admin and guser). The cookies share the same name, and the browser does not isolate them properly...


Q1
Lost password
Q2
Unable to log into the 'master' realm — the (super-admin) account Kadmin is not accepted.
A
Solution using direct SQL access or kc.sh CLI as described below:

If you have no other administrative access, it is impossible to change the admin password because Keycloak requires a valid authentication for any administrative change, whether through the CLI (kc.sh) or the web console.
You must therefore edit the SQL database directly: stop the server, modify the database, then restart Keycloak.

Solution 1: No admin access Solution 2: With admin access
  • Download the Python script keyc-tool.zip into the same folder as docker-compose.yml, which contains the database credentials.
    • When running it for the first time, it will tell you if you need to install additional modules via pip.
BEFORE ANYTHING ELSE, stop the server before modifying the password using the SQL method.
# 'keyc-tools.py' automatically retrieves SQL credentials from docker-compose.yml:
python3 keyc-tools.py realm=master user=kadmin password

Without docker-compose.yml, use the full syntax:

# <!> If the account is disabled, add the "activate" option

python3 keyc-tools.py dbuser=DBUser dbname=DBName dbpassword=DBPass dbhost=localhost dbport=3306 dbtype=mariadb realm=master user=kadmin password
  • Restart the server using docker compose up or docker start ID...

From inside the Docker container:

docker exec -it ID bash
alias kcadm=/opt/keycloak/bin/kcadm.sh

# Authenticate first
kcadm config credentials --server https://domain.tld/auth/ --realm master --user admin

# Retrieve the user ID
kcadm get users -r master --offset 0 --limit 100 --fields 'id,username'

# Change the password
# <!> Replace ID_RECOVERED with the actual user ID...
kcadm update users/ID_RECOVERED/reset-password -r master -s type=password -s value="New password" -s temporary=true -n
Solution 3: Using hc.sh (.bat)
Personally, I am not a fan of the method described in the official manual, especially with the KC_* variable configuration method. The slightest error can block the container.

That’s why I recommend Solution #1 — it only takes about 2 minutes to reset the password!

If you still insist, refer to the official tutorial.


Q
How do I log in inside the container using the CLI kc.sh after running docker exec -it ID bash?
A
Login via CLI inside the container:
docker exec -it ID bash

/opt/keycloak/bin/kcadm.sh config credentials --server https://domain.tld/auth/ --realm master --user kadmin


Q
error="cookie_not_found"
A


Terminology

    • Always display in UI
      • By default, clients that are allowed to be used are only displayed if they have been used recently. By enabling the option, they are always visible in the user's "Applications" menu.
    • Root Url
      • Base address for the client; if other fields use relative paths they will be completed using this Root URL.
    • Home Url
      • Entry point of the client site — the main page.
    • Valid Redirect URIs
      • A list of URIs that define the allowed scope of URLs; they are compared to the beginning of each redirect URL used during authentication.
        • Example: If you put a single URI in this field like "https://my.keycloak.tld/path/*" and one of your redirects points to "https://other.domain.com/" Keycloak will reject it. This reduces phishing risk.
        • The following error will be shown in that case: Invalid parameter: redirect_uri
    • Valid post logout redirect URIs
      • List of URIs allowed to be used by Keycloak for logout — same logic as "Valid Redirect URIs".
    • Web origins
      • List of domains for CORS and "silent refresh" via JavaScript. Appears when Standard flow is selected.
    • Admin URL
      • URL to send admin notifications, forced logout, token revocation, ... Destination must support the backend endpoints or may be left empty.
    • Backchannel logout URL
      • URL used by Keycloak to terminate a user's session when they log out of another client.
      • Typically looks like: https://domain.tld/auth/realms/Gyptis/protocol/openid-connect/auth/device
      • For MediaWiki: https://domain.tld/rest.php/pluggableauth/v1/logout
      • Backchannel logout session required
        • Forces session information to be included in the back-channel procedure
    • Backchannel logout revoke offline sessions
      • Forces revocation of persistent sessions (e.g., "remember me")
    Attributes in the scope
    • web-origins
      • Indicates the URL to return the user to after logout.

LDAP

    • RDN LDAP
      • Relative Distinguished Name, often uid. A short unique identifier...



Translated from French to English with assistance from ChatGPT (GPT-5 Thinking mini, OpenAI). Author reviewed and adapted the translation.
Translated from French to English by DeepSeek AI (v3, DeepSeek Company).