Tutorial 1 - Installing & Configuring Keycloak

What are we doing?

We install and configure Keycloak in a scripted manner.

The shown configuration will use the same values as in the Getting Started tutorials from the Keycloak website. Because of that, we can reuse the single page application (SPA) as is from the Keycloak website to test our result.

A big difference to most of the other blogs/tutorials/articles about Keycloak is, that we show all configurations by using the Admin CLI of Keycloak.

We demonstrate the usage of the Admin CLI to customize the master realm and how to create a new realm myrealm with one user and one client.

We also propose a set of shell scripts for a clean and readable configuration procedure. In all upcoming tutorials the same procedure for configuring Keycloak will be applied.

You can select from 3 different runtime environments for your installation:

You only need to execute one of the above variants to complete this scenario and be prepared for the other scenarios.

Why are we doing this?

Doing all configurations in a scripted manner has many advantages:

There are also alternatives for a scripted configuration, but the Admin CLI is available in all runtime environments and can be used as is.

Using the Admin REST API directly is an option, which requires additional custom development.

Keycloak Operator can only be used in a Kubernetes based runtime.

Requirements

For processing the JSON structures resulting from Admin CLI calls easily, we use the jq tool.

Steps

  1. Installing and starting the Keycloak server
  2. Connecting the Admin CLI
  3. Configuring

Step 1: Installing and starting the Keycloak server

Installation by distribution file

Let’s start with the download of the Keycloak distribution.

export KC_VERSION=21.0.2
curl -LO https://github.com/keycloak/keycloak/releases/download/"${KC_VERSION}"/keycloak-"${KC_VERSION}".zip

We continue with unpacking the archive.

unzip keycloak-${KC_VERSION}.zip

We now change to the new directory.

cd keycloak-${KC_VERSION}

This directory contains a Keycloak Quarkus application.

When we start the server for the first time, we have to set the admin user and the admin password:

KEYCLOAK_ADMIN=admin KEYCLOAK_ADMIN_PASSWORD=admin ./bin/kc.sh start-dev

When we start again, it is not necessary to set these variables, again. You can start the server with:

./bin/kc.sh start-dev

start-dev runs the quarkus application in DEV-mode. Do not use this in production.

By default, the Keycloak server is using the following ports. They are only served from the localhost loopback address 127.0.0.1:

One of the last lines from the log output is:

2023-04-11 13:23:29,545 INFO  [io.quarkus] (main) Keycloak 21.0.2 on JVM (powered by Quarkus 2.13.7.Final) started in 4.418s. Listening on: http://0.0.0.0:8080

We can now open the Administration Console from localhost and do the login with the just created admin user.

The distribution also contains the Admin CLI. This is the shell script ./bin/kcadm.sh.

We define the environment variable KCADM for the kcadm.sh script. It must be the absolute path to the kcadm.sh script from the above Keycloak installation.

export KCADM="/path/to/keycloak/bin/kcadm.sh"
export HOST_FOR_KCADM=localhost

Installation by docker image

To install and run Keycloak as a docker container a single command is necessary.

export KC_VERSION=21.0.2
docker run -p 8080:8080 -e KEYCLOAK_ADMIN=admin -e KEYCLOAK_ADMIN_PASSWORD=admin quay.io/keycloak/keycloak:${KC_VERSION} start-dev

In the next steps we are using the Admin CLI script (kcadm.sh). It is also contained in the Keycloak docker image. This means every call of the Admin CLI is executing the script from within the docker image.

docker run --rm --entrypoint /opt/keycloak/bin/kcadm.sh quay.io/keycloak/keycloak:${KC_VERSION}

We define the environment variable KCADM for the above command. Additionally, we mount the $HOME/.keycloak folder from the docker host at /opt/.keycloak.

export KCADM="docker run --rm --entrypoint /opt/keycloak/bin/kcadm.sh -v ${HOME}/.keycloak:/opt/keycloak/.keycloak quay.io/keycloak/keycloak:${KC_VERSION}"

When we executed $KCADM successfully the following output is shown:

Keycloak Admin CLI

Use 'kcadm.sh config credentials' command with username and password to start a session against a specific
server and realm.

For example:

  $ kcadm.sh config credentials --server http://localhost:8080/auth --realm master --user admin
  Enter password:
  Logging into http://localhost:8080/auth as user admin of realm master

Any configured username can be used for login, but to perform admin operations the user
needs proper roles, otherwise operations will fail.

Usage: kcadm.sh COMMAND [ARGUMENTS]

Global options:
  -x            Print full stack trace when exiting with error
  --help        Print help for specific command
  --config      Path to the config file (~/.keycloak/kcadm.config by default)

Commands:
  config        Set up credentials, and other configuration settings using the config file
  create        Create new resource
  get           Get a resource
  update        Update a resource
  delete        Delete a resource
  get-roles     List roles for a user or a group
  add-roles     Add role to a user or a group
  remove-roles  Remove role from a user or a group
  set-password  Re-set password for a user
  help          This help

Use 'kcadm.sh help <command>' for more information about a given command.

When executing this script with a command (like config, create, get etc.) it connects to the Keycloak instance running in another docker container. Depending on the docker environment you are using, the host name of the Keycloak instance must be specified differently. For Docker Desktop environments the host name can be defined as host.docker.internal.

export HOST_FOR_KCADM=host.docker.internal

Installation by docker image within OpenShift

Coming soon!

see also Getting Started with Keycloak on OpenShift

Step 2: Connecting the Admin CLI

Now we connect the Keycloak Admin CLI to the API and authenticate with the user created previously. We use two environment variables created in Step 1:

Please make sure they are defined. Their definition is dependent on the runtime you have chosen.

We log in to the master realm with the admin user. By using the options config credentials we request and maintain an authenticated session, which is used for all further calls. Be aware the access and refresh tokens for this session will be stored in the file $HOME/.keycloak/kcadm.config.

$KCADM config credentials --server http://$HOST_FOR_KCADM:8080 --user admin --password admin --realm master

To check the successful authentication and an authenticated session, we make a first request to the API.

$KCADM get serverinfo

The Keycloak server responds with a dump of information about its state and functionality. The same information is also available within the Web Admin Console.

Step 3: Configuring

We configure a new realm with a new client and a new user.

Simple Configuration

We do the complete configuration of Keycloak by executing the Admin CLI commands within shell scripts.

For simplicity reasons all CLI commands in this section are in one file myrealm-keycloak-configuration.sh. The next section shows how the scripts can be organized for a better maintenance.

The complete content of the file myrealm-keycloak-configuration.sh (download)
#!/usr/bin/env bash

set -euo pipefail

# authenticate and create session
$KCADM config credentials --server http://$HOST_FOR_KCADM:8080 --user admin --password admin --realm master

# define the name of the realm used as the target for the following commands
REALM_NAME=master

# enable the storage of admin events including their representation
$KCADM update events/config -r ${REALM_NAME} -s adminEventsEnabled=true -s adminEventsDetailsEnabled=true

# enable the storage of login events and define the expiration of a stored login event
$KCADM update events/config -r ${REALM_NAME} -s eventsEnabled=true -s eventsExpiration=259200

# define the login event types to be stored
$KCADM update events/config -r ${REALM_NAME} -s 'enabledEventTypes=["CLIENT_DELETE", "CLIENT_DELETE_ERROR", "CLIENT_INFO", "CLIENT_INFO_ERROR", "CLIENT_INITIATED_ACCOUNT_LINKING", "CLIENT_INITIATED_ACCOUNT_LINKING_ERROR", "CLIENT_LOGIN", "CLIENT_LOGIN_ERROR", "CLIENT_REGISTER", "CLIENT_REGISTER_ERROR", "CLIENT_UPDATE", "CLIENT_UPDATE_ERROR", "CODE_TO_TOKEN", "CODE_TO_TOKEN_ERROR", "CUSTOM_REQUIRED_ACTION", "CUSTOM_REQUIRED_ACTION_ERROR", "EXECUTE_ACTIONS", "EXECUTE_ACTIONS_ERROR", "EXECUTE_ACTION_TOKEN", "EXECUTE_ACTION_TOKEN_ERROR", "FEDERATED_IDENTITY_LINK", "FEDERATED_IDENTITY_LINK_ERROR", "GRANT_CONSENT", "GRANT_CONSENT_ERROR", "IDENTITY_PROVIDER_FIRST_LOGIN", "IDENTITY_PROVIDER_FIRST_LOGIN_ERROR", "IDENTITY_PROVIDER_LINK_ACCOUNT", "IDENTITY_PROVIDER_LINK_ACCOUNT_ERROR", "IDENTITY_PROVIDER_LOGIN", "IDENTITY_PROVIDER_LOGIN_ERROR", "IDENTITY_PROVIDER_POST_LOGIN", "IDENTITY_PROVIDER_POST_LOGIN_ERROR", "IDENTITY_PROVIDER_RESPONSE", "IDENTITY_PROVIDER_RESPONSE_ERROR", "IDENTITY_PROVIDER_RETRIEVE_TOKEN", "IDENTITY_PROVIDER_RETRIEVE_TOKEN_ERROR", "IMPERSONATE", "IMPERSONATE_ERROR", "INTROSPECT_TOKEN", "INTROSPECT_TOKEN_ERROR", "INVALID_SIGNATURE", "INVALID_SIGNATURE_ERROR", "LOGIN", "LOGIN_ERROR", "LOGOUT", "LOGOUT_ERROR", "PERMISSION_TOKEN", "PERMISSION_TOKEN_ERROR", "REFRESH_TOKEN", "REFRESH_TOKEN_ERROR", "REGISTER", "REGISTER_ERROR", "REGISTER_NODE", "REGISTER_NODE_ERROR", "REMOVE_FEDERATED_IDENTITY", "REMOVE_FEDERATED_IDENTITY_ERROR", "REMOVE_TOTP", "REMOVE_TOTP_ERROR", "RESET_PASSWORD", "RESET_PASSWORD_ERROR", "RESTART_AUTHENTICATION", "RESTART_AUTHENTICATION_ERROR", "REVOKE_GRANT", "REVOKE_GRANT_ERROR", "SEND_IDENTITY_PROVIDER_LINK", "SEND_IDENTITY_PROVIDER_LINK_ERROR", "SEND_RESET_PASSWORD", "SEND_RESET_PASSWORD_ERROR", "SEND_VERIFY_EMAIL", "SEND_VERIFY_EMAIL_ERROR", "TOKEN_EXCHANGE", "TOKEN_EXCHANGE_ERROR", "UNREGISTER_NODE", "UNREGISTER_NODE_ERROR", "UPDATE_CONSENT", "UPDATE_CONSENT_ERROR", "UPDATE_EMAIL", "UPDATE_EMAIL_ERROR", "UPDATE_PASSWORD", "UPDATE_PASSWORD_ERROR", "UPDATE_PROFILE", "UPDATE_PROFILE_ERROR", "UPDATE_TOTP", "UPDATE_TOTP_ERROR", "USER_INFO_REQUEST", "USER_INFO_REQUEST_ERROR", "VALIDATE_ACCESS_TOKEN", "VALIDATE_ACCESS_TOKEN_ERROR", "VERIFY_EMAIL", "VERIFY_EMAIL_ERROR"]'

# change target realm
REALM_NAME=myrealm

# create a new realm
$KCADM create realms -s realm="${REALM_NAME}" -s enabled=true

# enable the storage of admin events including their representation
$KCADM update events/config -r ${REALM_NAME} -s adminEventsEnabled=true -s adminEventsDetailsEnabled=true

# enable the storage of login events and define the expiration of a stored login event
$KCADM update events/config -r ${REALM_NAME} -s eventsEnabled=true -s eventsExpiration=259200

# define the login event types to be stored
$KCADM update events/config -r ${REALM_NAME} -s 'enabledEventTypes=["CLIENT_DELETE", "CLIENT_DELETE_ERROR", "CLIENT_INFO", "CLIENT_INFO_ERROR", "CLIENT_INITIATED_ACCOUNT_LINKING", "CLIENT_INITIATED_ACCOUNT_LINKING_ERROR", "CLIENT_LOGIN", "CLIENT_LOGIN_ERROR", "CLIENT_REGISTER", "CLIENT_REGISTER_ERROR", "CLIENT_UPDATE", "CLIENT_UPDATE_ERROR", "CODE_TO_TOKEN", "CODE_TO_TOKEN_ERROR", "CUSTOM_REQUIRED_ACTION", "CUSTOM_REQUIRED_ACTION_ERROR", "EXECUTE_ACTIONS", "EXECUTE_ACTIONS_ERROR", "EXECUTE_ACTION_TOKEN", "EXECUTE_ACTION_TOKEN_ERROR", "FEDERATED_IDENTITY_LINK", "FEDERATED_IDENTITY_LINK_ERROR", "GRANT_CONSENT", "GRANT_CONSENT_ERROR", "IDENTITY_PROVIDER_FIRST_LOGIN", "IDENTITY_PROVIDER_FIRST_LOGIN_ERROR", "IDENTITY_PROVIDER_LINK_ACCOUNT", "IDENTITY_PROVIDER_LINK_ACCOUNT_ERROR", "IDENTITY_PROVIDER_LOGIN", "IDENTITY_PROVIDER_LOGIN_ERROR", "IDENTITY_PROVIDER_POST_LOGIN", "IDENTITY_PROVIDER_POST_LOGIN_ERROR", "IDENTITY_PROVIDER_RESPONSE", "IDENTITY_PROVIDER_RESPONSE_ERROR", "IDENTITY_PROVIDER_RETRIEVE_TOKEN", "IDENTITY_PROVIDER_RETRIEVE_TOKEN_ERROR", "IMPERSONATE", "IMPERSONATE_ERROR", "INTROSPECT_TOKEN", "INTROSPECT_TOKEN_ERROR", "INVALID_SIGNATURE", "INVALID_SIGNATURE_ERROR", "LOGIN", "LOGIN_ERROR", "LOGOUT", "LOGOUT_ERROR", "PERMISSION_TOKEN", "PERMISSION_TOKEN_ERROR", "REFRESH_TOKEN", "REFRESH_TOKEN_ERROR", "REGISTER", "REGISTER_ERROR", "REGISTER_NODE", "REGISTER_NODE_ERROR", "REMOVE_FEDERATED_IDENTITY", "REMOVE_FEDERATED_IDENTITY_ERROR", "REMOVE_TOTP", "REMOVE_TOTP_ERROR", "RESET_PASSWORD", "RESET_PASSWORD_ERROR", "RESTART_AUTHENTICATION", "RESTART_AUTHENTICATION_ERROR", "REVOKE_GRANT", "REVOKE_GRANT_ERROR", "SEND_IDENTITY_PROVIDER_LINK", "SEND_IDENTITY_PROVIDER_LINK_ERROR", "SEND_RESET_PASSWORD", "SEND_RESET_PASSWORD_ERROR", "SEND_VERIFY_EMAIL", "SEND_VERIFY_EMAIL_ERROR", "TOKEN_EXCHANGE", "TOKEN_EXCHANGE_ERROR", "UNREGISTER_NODE", "UNREGISTER_NODE_ERROR", "UPDATE_CONSENT", "UPDATE_CONSENT_ERROR", "UPDATE_EMAIL", "UPDATE_EMAIL_ERROR", "UPDATE_PASSWORD", "UPDATE_PASSWORD_ERROR", "UPDATE_PROFILE", "UPDATE_PROFILE_ERROR", "UPDATE_TOTP", "UPDATE_TOTP_ERROR", "USER_INFO_REQUEST", "USER_INFO_REQUEST_ERROR", "VALIDATE_ACCESS_TOKEN", "VALIDATE_ACCESS_TOKEN_ERROR", "VERIFY_EMAIL", "VERIFY_EMAIL_ERROR"]'

# create a new client
$KCADM create clients -r ${REALM_NAME} -s clientId="myclient" -s enabled=true -s name="My Client" -s protocol=openid-connect -s publicClient=true -s standardFlowEnabled=true -s 'redirectUris=["https://www.keycloak.org/app/*"]' -s baseUrl="https://www.keycloak.org/app/" -s 'webOrigins=["*"]'

# create a new user
$KCADM create users -r $REALM_NAME -s username="myuser" -s enabled=true -s firstName="My" -s lastName="User" -s email="[email protected]"

# set the password for the new user
$KCADM set-password -r $REALM_NAME --username "myuser" --new-password "myuser"

Subsequently, we explain every CLI call of the above script.

We log in to the master realm with the admin user and request an authenticated session, which is used for all further calls.

$KCADM config credentials --server http://localhost:8080/auth --user admin --password admin --realm master

The admin user has access to all realms. Therefore, we need to specify to which realm each command should be applied to. This can be done with the -r option followed by the name of the realm. We define a variable for the realm name used with the -r option in all upcoming calls.

REALM_NAME=master

Keycloak knows a rich set of events. They are divided into admin and login events. Because Keycloak doesn’t store any of these events by default, they cannot be viewed in the Admin Console. We first enable the storage of admin events with their representation details.

$KCADM update events/config -r ${REALM_NAME} -s adminEventsEnabled=true -s adminEventsDetailsEnabled=true

We also enable the storage of login events and set the expiration period for a stored event to 3 days (in seconds 259200).

$KCADM update events/config -r ${REALM_NAME} -s eventsEnabled=true -s eventsExpiration=259200

We define the list of login event types, which are stored. The easiest way to get the complete list of login event types is from the serverinfo.

$KCADM update events/config -r ${REALM_NAME} -s 'enabledEventTypes=["CLIENT_DELETE", "CLIENT_DELETE_ERROR", "CLIENT_INFO", "CLIENT_INFO_ERROR", "CLIENT_INITIATED_ACCOUNT_LINKING", "CLIENT_INITIATED_ACCOUNT_LINKING_ERROR", "CLIENT_LOGIN", "CLIENT_LOGIN_ERROR", "CLIENT_REGISTER", "CLIENT_REGISTER_ERROR", "CLIENT_UPDATE", "CLIENT_UPDATE_ERROR", "CODE_TO_TOKEN", "CODE_TO_TOKEN_ERROR", "CUSTOM_REQUIRED_ACTION", "CUSTOM_REQUIRED_ACTION_ERROR", "EXECUTE_ACTIONS", "EXECUTE_ACTIONS_ERROR", "EXECUTE_ACTION_TOKEN", "EXECUTE_ACTION_TOKEN_ERROR", "FEDERATED_IDENTITY_LINK", "FEDERATED_IDENTITY_LINK_ERROR", "GRANT_CONSENT", "GRANT_CONSENT_ERROR", "IDENTITY_PROVIDER_FIRST_LOGIN", "IDENTITY_PROVIDER_FIRST_LOGIN_ERROR", "IDENTITY_PROVIDER_LINK_ACCOUNT", "IDENTITY_PROVIDER_LINK_ACCOUNT_ERROR", "IDENTITY_PROVIDER_LOGIN", "IDENTITY_PROVIDER_LOGIN_ERROR", "IDENTITY_PROVIDER_POST_LOGIN", "IDENTITY_PROVIDER_POST_LOGIN_ERROR", "IDENTITY_PROVIDER_RESPONSE", "IDENTITY_PROVIDER_RESPONSE_ERROR", "IDENTITY_PROVIDER_RETRIEVE_TOKEN", "IDENTITY_PROVIDER_RETRIEVE_TOKEN_ERROR", "IMPERSONATE", "IMPERSONATE_ERROR", "INTROSPECT_TOKEN", "INTROSPECT_TOKEN_ERROR", "INVALID_SIGNATURE", "INVALID_SIGNATURE_ERROR", "LOGIN", "LOGIN_ERROR", "LOGOUT", "LOGOUT_ERROR", "PERMISSION_TOKEN", "PERMISSION_TOKEN_ERROR", "REFRESH_TOKEN", "REFRESH_TOKEN_ERROR", "REGISTER", "REGISTER_ERROR", "REGISTER_NODE", "REGISTER_NODE_ERROR", "REMOVE_FEDERATED_IDENTITY", "REMOVE_FEDERATED_IDENTITY_ERROR", "REMOVE_TOTP", "REMOVE_TOTP_ERROR", "RESET_PASSWORD", "RESET_PASSWORD_ERROR", "RESTART_AUTHENTICATION", "RESTART_AUTHENTICATION_ERROR", "REVOKE_GRANT", "REVOKE_GRANT_ERROR", "SEND_IDENTITY_PROVIDER_LINK", "SEND_IDENTITY_PROVIDER_LINK_ERROR", "SEND_RESET_PASSWORD", "SEND_RESET_PASSWORD_ERROR", "SEND_VERIFY_EMAIL", "SEND_VERIFY_EMAIL_ERROR", "TOKEN_EXCHANGE", "TOKEN_EXCHANGE_ERROR", "UNREGISTER_NODE", "UNREGISTER_NODE_ERROR", "UPDATE_CONSENT", "UPDATE_CONSENT_ERROR", "UPDATE_EMAIL", "UPDATE_EMAIL_ERROR", "UPDATE_PASSWORD", "UPDATE_PASSWORD_ERROR", "UPDATE_PROFILE", "UPDATE_PROFILE_ERROR", "UPDATE_TOTP", "UPDATE_TOTP_ERROR", "USER_INFO_REQUEST", "USER_INFO_REQUEST_ERROR", "VALIDATE_ACCESS_TOKEN", "VALIDATE_ACCESS_TOKEN_ERROR", "VERIFY_EMAIL", "VERIFY_EMAIL_ERROR"]'

Because Keycloak suggests the master realm should only be used for administration purposes of other realms, we create a new realm with the name myrealm.

REALM_NAME=myrealm
$KCADM create realms -s realm="${REALM_NAME}" -s enabled=true

We configure the same event storage for our new realm as the master realm.

$KCADM update events/config -r ${REALM_NAME} -s adminEventsEnabled=true -s adminEventsDetailsEnabled=true
$KCADM update events/config -r ${REALM_NAME} -s eventsEnabled=true -s eventsExpiration=259200
$KCADM update events/config -r ${REALM_NAME} -s 'enabledEventTypes=["CLIENT_DELETE", "CLIENT_DELETE_ERROR", "CLIENT_INFO", "CLIENT_INFO_ERROR", "CLIENT_INITIATED_ACCOUNT_LINKING", "CLIENT_INITIATED_ACCOUNT_LINKING_ERROR", "CLIENT_LOGIN", "CLIENT_LOGIN_ERROR", "CLIENT_REGISTER", "CLIENT_REGISTER_ERROR", "CLIENT_UPDATE", "CLIENT_UPDATE_ERROR", "CODE_TO_TOKEN", "CODE_TO_TOKEN_ERROR", "CUSTOM_REQUIRED_ACTION", "CUSTOM_REQUIRED_ACTION_ERROR", "EXECUTE_ACTIONS", "EXECUTE_ACTIONS_ERROR", "EXECUTE_ACTION_TOKEN", "EXECUTE_ACTION_TOKEN_ERROR", "FEDERATED_IDENTITY_LINK", "FEDERATED_IDENTITY_LINK_ERROR", "GRANT_CONSENT", "GRANT_CONSENT_ERROR", "IDENTITY_PROVIDER_FIRST_LOGIN", "IDENTITY_PROVIDER_FIRST_LOGIN_ERROR", "IDENTITY_PROVIDER_LINK_ACCOUNT", "IDENTITY_PROVIDER_LINK_ACCOUNT_ERROR", "IDENTITY_PROVIDER_LOGIN", "IDENTITY_PROVIDER_LOGIN_ERROR", "IDENTITY_PROVIDER_POST_LOGIN", "IDENTITY_PROVIDER_POST_LOGIN_ERROR", "IDENTITY_PROVIDER_RESPONSE", "IDENTITY_PROVIDER_RESPONSE_ERROR", "IDENTITY_PROVIDER_RETRIEVE_TOKEN", "IDENTITY_PROVIDER_RETRIEVE_TOKEN_ERROR", "IMPERSONATE", "IMPERSONATE_ERROR", "INTROSPECT_TOKEN", "INTROSPECT_TOKEN_ERROR", "INVALID_SIGNATURE", "INVALID_SIGNATURE_ERROR", "LOGIN", "LOGIN_ERROR", "LOGOUT", "LOGOUT_ERROR", "PERMISSION_TOKEN", "PERMISSION_TOKEN_ERROR", "REFRESH_TOKEN", "REFRESH_TOKEN_ERROR", "REGISTER", "REGISTER_ERROR", "REGISTER_NODE", "REGISTER_NODE_ERROR", "REMOVE_FEDERATED_IDENTITY", "REMOVE_FEDERATED_IDENTITY_ERROR", "REMOVE_TOTP", "REMOVE_TOTP_ERROR", "RESET_PASSWORD", "RESET_PASSWORD_ERROR", "RESTART_AUTHENTICATION", "RESTART_AUTHENTICATION_ERROR", "REVOKE_GRANT", "REVOKE_GRANT_ERROR", "SEND_IDENTITY_PROVIDER_LINK", "SEND_IDENTITY_PROVIDER_LINK_ERROR", "SEND_RESET_PASSWORD", "SEND_RESET_PASSWORD_ERROR", "SEND_VERIFY_EMAIL", "SEND_VERIFY_EMAIL_ERROR", "TOKEN_EXCHANGE", "TOKEN_EXCHANGE_ERROR", "UNREGISTER_NODE", "UNREGISTER_NODE_ERROR", "UPDATE_CONSENT", "UPDATE_CONSENT_ERROR", "UPDATE_EMAIL", "UPDATE_EMAIL_ERROR", "UPDATE_PASSWORD", "UPDATE_PASSWORD_ERROR", "UPDATE_PROFILE", "UPDATE_PROFILE_ERROR", "UPDATE_TOTP", "UPDATE_TOTP_ERROR", "USER_INFO_REQUEST", "USER_INFO_REQUEST_ERROR", "VALIDATE_ACCESS_TOKEN", "VALIDATE_ACCESS_TOKEN_ERROR", "VERIFY_EMAIL", "VERIFY_EMAIL_ERROR"]'

We create a new client. Please note that a client has two very similar attributes within Keycloak: the id and the clientId. The clientId must be given to the CLI call, and the id is generated by Keycloak and written to the output upon client creation. For other CLI calls either the id or the clientId must be used.

$KCADM create clients -r ${REALM_NAME} -s clientId="myclient" -s enabled=true -s name="My Client" -s protocol=openid-connect -s publicClient=true -s standardFlowEnabled=true -s 'redirectUris=["https://www.keycloak.org/app/*"]' -s baseUrl="https://www.keycloak.org/app/" -s 'webOrigins=["*"]'

We create a new user with the username myuser.

$KCADM create users -r $REALM_NAME -s username="myuser" -s enabled=true -s firstName="My" -s lastName="User" -s email="[email protected]"

We set the password myuser for the new user.

$KCADM set-password -r $REALM_NAME --username "myuser" --new-password "myuser"

When we execute the [myrealm-keycloak-configuration.sh] script to start a fresh Keycloak instance, it runs through and does the setup in a fully automated way.

You should see an output similar to this:

Logging into http://localhost:8080/auth as user admin of realm master
Created new realm with id 'myrealm'
Created new client with id 'a5e95d8c-2dc0-4e42-a48e-f7873944b3eb'
Created new user with id '559905a6-77bc-49ae-acdb-2788cd4282c4'

After the script has been executed successfully, the configuration can be tested. The easiest way is by using the test application from the Keycloak website. It is a single page application (SPA) with configurable parameters for the Keycloak server to be used. The predefined parameters for Keycloak URL, Realm and Client are well suited for our configuration.

The screen flow can be seen by the following screenshots: SPA ConfigSPA Sign inKeycloak LoginSPA Hello.

In another tutorial we will explain the details steps happening behind the scenes when using this example application.

However, this script is far from being ready for production use, not only because it fails if there is already a realm called myrealm. If you want to execute this script again, you need to delete the myrealm realm. This can be done in the Admin Console or also by a CLI call.

$KCADM delete realms/myrealm

Please note that deleting a realm will also delete the users from that realm. That is the reason why this action is not a good idea for production environments.

Production Configuration

For a better maintenance we propose a refactoring of the above script.

We split the script from the previous section into 5 different scripts

The keycloak-configuration.sh script is the entry point into the Keycloak configuration. Only this script must be started. It handles the authentication using the environment variables KEYCLOAK_USER and KEYCLOAK_PASSWORD and defines the variable KCADM for the kcadm.sh shell script by using the environment variable KEYCLOAK_HOME. The KCADM variable can be used in the other scripts.

The keycloak-configuration-helpers.sh script contains all reusable bash functions for the Admin CLI. It is sourced by the keycloak-configuration.sh script.
Mainly create CLI calls should be defined as a function, which check for existence before doing the call. This way we can assure the idempotence of the whole configuration. As a general pattern we try to keep the create CLI call as minimal as possible and do all other configurations using update CLI calls. For easier handling the create CLI calls should always output the id of the created or already existing resource for further use.

The realms.sh script sources the corresponding shell script, e.g. realm_master.sh and realm_myrealm.sh for each realm to be configured. It is in turn sourced by the keycloak-configuration.sh script.

The realm_master.sh script contains all configurations for the master realm.

The realm_myrealm.sh script contains all configurations for the myrealm realm.

Let’s look at each of these files. The resulting configuration is the same as in the previous section.

keycloak-configuration.sh

This script is the entry point into the Keycloak configuration.

keycloak-configuration.sh (download)
#!/usr/bin/env bash

set -euo pipefail

echo ""
echo "========================================================"
echo "== STARTING KEYCLOAK CONFIGURATION =="
echo "========================================================"

BASEDIR=$(dirname "$0")
source $BASEDIR/keycloak-configuration-helpers.sh

if [ "$KCADM" == "" ]; then
KCADM=$KEYCLOAK_HOME/bin/kcadm.sh
echo "Using $KCADM as the admin CLI."
fi

$KCADM config credentials --server http://$HOST_FOR_KCADM:8080 --user $KEYCLOAK_USER --password $KEYCLOAK_PASSWORD --realm master

source $BASEDIR/realms.sh

echo "========================================================"
echo "== KEYCLOAK CONFIGURATION DONE =="
echo "========================================================"
echo ""

This file rarely changes.

keycloak-configuration-helpers.sh

This script contains all reusable bash functions for the Admin CLI. It is sourced by the keycloak-configuration.sh script.

keycloak-configuration-helpers.sh (download)
#!/usr/bin/env bash

set -euo pipefail

# the new realm is also set as enabled
createRealm() {
# arguments
REALM_NAME=$1
#
EXISTING_REALM=$($KCADM get realms | jq '.[] | select(.realm==("'$REALM_NAME'")) | .id')
if [ "$EXISTING_REALM" == "" ]; then
$KCADM create realms -s realm="${REALM_NAME}" -s enabled=true
fi
}

# the new client is also set as enabled
createClient() {
# arguments
REALM_NAME=$1
CLIENT_ID=$2
#
ID=$(getClient $REALM_NAME $CLIENT_ID)
if [[ "$ID" == "" ]]; then
$KCADM create clients -r $REALM_NAME -s clientId=$CLIENT_ID -s enabled=true
fi
echo $(getClient $REALM_NAME $CLIENT_ID)
}

# get the object id of the client for a given clientId
getClient () {
# arguments
REALM_NAME=$1
CLIENT_ID=$2
#
ID=$($KCADM get clients -r $REALM_NAME --fields id,clientId | jq '
.[] | select(.clientId==("'$CLIENT_ID'")) | .id')
echo $(sed -e '
s/"//g' <<< $ID)
}

# create a user for the given username if it doesn't exist yet and return the object id
createUser() {
# arguments
REALM_NAME=$1
USER_NAME=$2
#
USER_ID=$(getUser $REALM_NAME $USER_NAME)
if [ "
$USER_ID" == "" ]; then
$KCADM create users -r $REALM_NAME -s username=$USER_NAME -s enabled=true
fi
echo $(getUser $REALM_NAME $USER_NAME)
}

# the object id of the user for a given username
getUser() {
# arguments
REALM_NAME=$1
USERNAME=$2
#
USER=$($KCADM get users -r $REALM_NAME -q username=$USERNAME | jq '.[] | select(.username==("'$USERNAME'")) | .id' )
echo $(sed -e '
s/"//g' <<< $USER)
}

This file changes when new functions must be added.

realms.sh

This script sources the corresponding shell script, e.g. realm_master.sh, realm_myrealm.sh, for each realm to be configured.

realms.sh (download)
#!/usr/bin/env bash

set -euo pipefail

######################
### Setup realms via their script
######################

echo "setting up realms..."

source $BASEDIR/realm_master.sh
source $BASEDIR/realm_myrealm.sh

This file changes if the list of realms must be updated.

realm_master.sh
realm_master.sh (download)
#!/usr/bin/env bash

set -euo pipefail

REALM_NAME='master'

echo ""
echo "================================="
echo "setting up realm $REALM_NAME..."
echo "================================="
echo ""

# enable the storage of admin events including their representation
$KCADM update events/config -r ${REALM_NAME} -s adminEventsEnabled=true -s adminEventsDetailsEnabled=true

# enable the storage of login events and define the expiration of a stored login event
$KCADM update events/config -r ${REALM_NAME} -s eventsEnabled=true -s eventsExpiration=259200

# define the login event types to be stored
$KCADM update events/config -r ${REALM_NAME} -s 'enabledEventTypes=["CLIENT_DELETE", "CLIENT_DELETE_ERROR", "CLIENT_INFO", "CLIENT_INFO_ERROR", "CLIENT_INITIATED_ACCOUNT_LINKING", "CLIENT_INITIATED_ACCOUNT_LINKING_ERROR", "CLIENT_LOGIN", "CLIENT_LOGIN_ERROR", "CLIENT_REGISTER", "CLIENT_REGISTER_ERROR", "CLIENT_UPDATE", "CLIENT_UPDATE_ERROR", "CODE_TO_TOKEN", "CODE_TO_TOKEN_ERROR", "CUSTOM_REQUIRED_ACTION", "CUSTOM_REQUIRED_ACTION_ERROR", "EXECUTE_ACTIONS", "EXECUTE_ACTIONS_ERROR", "EXECUTE_ACTION_TOKEN", "EXECUTE_ACTION_TOKEN_ERROR", "FEDERATED_IDENTITY_LINK", "FEDERATED_IDENTITY_LINK_ERROR", "GRANT_CONSENT", "GRANT_CONSENT_ERROR", "IDENTITY_PROVIDER_FIRST_LOGIN", "IDENTITY_PROVIDER_FIRST_LOGIN_ERROR", "IDENTITY_PROVIDER_LINK_ACCOUNT", "IDENTITY_PROVIDER_LINK_ACCOUNT_ERROR", "IDENTITY_PROVIDER_LOGIN", "IDENTITY_PROVIDER_LOGIN_ERROR", "IDENTITY_PROVIDER_POST_LOGIN", "IDENTITY_PROVIDER_POST_LOGIN_ERROR", "IDENTITY_PROVIDER_RESPONSE", "IDENTITY_PROVIDER_RESPONSE_ERROR", "IDENTITY_PROVIDER_RETRIEVE_TOKEN", "IDENTITY_PROVIDER_RETRIEVE_TOKEN_ERROR", "IMPERSONATE", "IMPERSONATE_ERROR", "INTROSPECT_TOKEN", "INTROSPECT_TOKEN_ERROR", "INVALID_SIGNATURE", "INVALID_SIGNATURE_ERROR", "LOGIN", "LOGIN_ERROR", "LOGOUT", "LOGOUT_ERROR", "PERMISSION_TOKEN", "PERMISSION_TOKEN_ERROR", "REFRESH_TOKEN", "REFRESH_TOKEN_ERROR", "REGISTER", "REGISTER_ERROR", "REGISTER_NODE", "REGISTER_NODE_ERROR", "REMOVE_FEDERATED_IDENTITY", "REMOVE_FEDERATED_IDENTITY_ERROR", "REMOVE_TOTP", "REMOVE_TOTP_ERROR", "RESET_PASSWORD", "RESET_PASSWORD_ERROR", "RESTART_AUTHENTICATION", "RESTART_AUTHENTICATION_ERROR", "REVOKE_GRANT", "REVOKE_GRANT_ERROR", "SEND_IDENTITY_PROVIDER_LINK", "SEND_IDENTITY_PROVIDER_LINK_ERROR", "SEND_RESET_PASSWORD", "SEND_RESET_PASSWORD_ERROR", "SEND_VERIFY_EMAIL", "SEND_VERIFY_EMAIL_ERROR", "TOKEN_EXCHANGE", "TOKEN_EXCHANGE_ERROR", "UNREGISTER_NODE", "UNREGISTER_NODE_ERROR", "UPDATE_CONSENT", "UPDATE_CONSENT_ERROR", "UPDATE_EMAIL", "UPDATE_EMAIL_ERROR", "UPDATE_PASSWORD", "UPDATE_PASSWORD_ERROR", "UPDATE_PROFILE", "UPDATE_PROFILE_ERROR", "UPDATE_TOTP", "UPDATE_TOTP_ERROR", "USER_INFO_REQUEST", "USER_INFO_REQUEST_ERROR", "VALIDATE_ACCESS_TOKEN", "VALIDATE_ACCESS_TOKEN_ERROR", "VERIFY_EMAIL", "VERIFY_EMAIL_ERROR"]'
realm_myrealm.sh
realm_myrealm.sh (download)
#!/usr/bin/env bash

set -euo pipefail

REALM_NAME='myrealm'

echo ""
echo "================================="
echo "setting up realm $REALM_NAME..."
echo "================================="
echo ""

createRealm $REALM_NAME

# enable the storage of admin events including their representation
$KCADM update events/config -r ${REALM_NAME} -s adminEventsEnabled=true -s adminEventsDetailsEnabled=true

# enable the storage of login events and define the expiration of a stored login event
$KCADM update events/config -r ${REALM_NAME} -s eventsEnabled=true -s eventsExpiration=259200

# define the login event types to be stored
$KCADM update events/config -r ${REALM_NAME} -s 'enabledEventTypes=["CLIENT_DELETE", "CLIENT_DELETE_ERROR", "CLIENT_INFO", "CLIENT_INFO_ERROR", "CLIENT_INITIATED_ACCOUNT_LINKING", "CLIENT_INITIATED_ACCOUNT_LINKING_ERROR", "CLIENT_LOGIN", "CLIENT_LOGIN_ERROR", "CLIENT_REGISTER", "CLIENT_REGISTER_ERROR", "CLIENT_UPDATE", "CLIENT_UPDATE_ERROR", "CODE_TO_TOKEN", "CODE_TO_TOKEN_ERROR", "CUSTOM_REQUIRED_ACTION", "CUSTOM_REQUIRED_ACTION_ERROR", "EXECUTE_ACTIONS", "EXECUTE_ACTIONS_ERROR", "EXECUTE_ACTION_TOKEN", "EXECUTE_ACTION_TOKEN_ERROR", "FEDERATED_IDENTITY_LINK", "FEDERATED_IDENTITY_LINK_ERROR", "GRANT_CONSENT", "GRANT_CONSENT_ERROR", "IDENTITY_PROVIDER_FIRST_LOGIN", "IDENTITY_PROVIDER_FIRST_LOGIN_ERROR", "IDENTITY_PROVIDER_LINK_ACCOUNT", "IDENTITY_PROVIDER_LINK_ACCOUNT_ERROR", "IDENTITY_PROVIDER_LOGIN", "IDENTITY_PROVIDER_LOGIN_ERROR", "IDENTITY_PROVIDER_POST_LOGIN", "IDENTITY_PROVIDER_POST_LOGIN_ERROR", "IDENTITY_PROVIDER_RESPONSE", "IDENTITY_PROVIDER_RESPONSE_ERROR", "IDENTITY_PROVIDER_RETRIEVE_TOKEN", "IDENTITY_PROVIDER_RETRIEVE_TOKEN_ERROR", "IMPERSONATE", "IMPERSONATE_ERROR", "INTROSPECT_TOKEN", "INTROSPECT_TOKEN_ERROR", "INVALID_SIGNATURE", "INVALID_SIGNATURE_ERROR", "LOGIN", "LOGIN_ERROR", "LOGOUT", "LOGOUT_ERROR", "PERMISSION_TOKEN", "PERMISSION_TOKEN_ERROR", "REFRESH_TOKEN", "REFRESH_TOKEN_ERROR", "REGISTER", "REGISTER_ERROR", "REGISTER_NODE", "REGISTER_NODE_ERROR", "REMOVE_FEDERATED_IDENTITY", "REMOVE_FEDERATED_IDENTITY_ERROR", "REMOVE_TOTP", "REMOVE_TOTP_ERROR", "RESET_PASSWORD", "RESET_PASSWORD_ERROR", "RESTART_AUTHENTICATION", "RESTART_AUTHENTICATION_ERROR", "REVOKE_GRANT", "REVOKE_GRANT_ERROR", "SEND_IDENTITY_PROVIDER_LINK", "SEND_IDENTITY_PROVIDER_LINK_ERROR", "SEND_RESET_PASSWORD", "SEND_RESET_PASSWORD_ERROR", "SEND_VERIFY_EMAIL", "SEND_VERIFY_EMAIL_ERROR", "TOKEN_EXCHANGE", "TOKEN_EXCHANGE_ERROR", "UNREGISTER_NODE", "UNREGISTER_NODE_ERROR", "UPDATE_CONSENT", "UPDATE_CONSENT_ERROR", "UPDATE_EMAIL", "UPDATE_EMAIL_ERROR", "UPDATE_PASSWORD", "UPDATE_PASSWORD_ERROR", "UPDATE_PROFILE", "UPDATE_PROFILE_ERROR", "UPDATE_TOTP", "UPDATE_TOTP_ERROR", "USER_INFO_REQUEST", "USER_INFO_REQUEST_ERROR", "VALIDATE_ACCESS_TOKEN", "VALIDATE_ACCESS_TOKEN_ERROR", "VERIFY_EMAIL", "VERIFY_EMAIL_ERROR"]'

# clients
CLIENT_ID=myclient
ID=$(createClient $REALM_NAME $CLIENT_ID)
$KCADM update clients/$ID -r $REALM_NAME -s name="My Client" -s protocol=openid-connect -s publicClient=true -s standardFlowEnabled=true -s 'redirectUris=["https://www.keycloak.org/app/*"]' -s baseUrl="https://www.keycloak.org/app/" -s 'webOrigins=["*"]'

#users
USER_NAME=myuser
USER_ID=$(createUser $REALM_NAME $USER_NAME)
$KCADM update users/$USER_ID -r $REALM_NAME -s firstName="My" -s lastName="User" -s email="[email protected]"
$KCADM set-password -r $REALM_NAME --username $USER_NAME --new-password $USER_NAME

References

Screenshots