Dieses Tutorial bietet eine Grundlage für die Verwendung von Keycloak mit einem Passkey. In diesem fall wird ein YubiKey verwendet.

1. Alternativen

  • OTP

    • Email

    • SMS

  • Two-factor authentication

  • …​

2. Kann verwendet werden bei…​

3. Keycloak Konfiguration

fido2 building blocks
Figure 1. Communication

Es wird erwartet das jq auf dem System installiert ist

Um den Lernenden diesen Schritt zu erleichtern wurde die Keycloak-Instanz mit der Keycloak Admin CLI konfiguriert.

kc-init
#!/bin/sh

KC_VERSION=26.1.4
KC_NAME=keycloak-"${KC_VERSION}"

if [ ! -d "${KC_NAME}" ]; then
    echo "================================"
    echo "  DOWNLOADING KEYCLOAK-${KC_VERSION} "
    echo "================================"

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

    unzip "${KC_NAME}".zip
    rm "${KC_NAME}".zip

    echo "==========================================="
    echo "  FINISHED KEYCLOAK-${KC_VERSION} DOWNLOAD "
    echo "==========================================="
else
    echo "================================"
    echo "  FOUND KEYCLOAK-${KC_VERSION} "
    echo "================================"
fi

cd "${KC_NAME}"

./bin/kc.sh build --features="web-authn,account-api,account,passkeys"

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

KC_VERSION=26.1.4
KC_NAME=keycloak-"${KC_VERSION}"
HOST_FOR_KCADM=localhost
KCADM=./bin/kcadm.sh

REALM_NAME=water
CLIENT_ID=drop
USER_NAME=user1

# the new realm is also set as enabled
createRealm() {
    # arguments
    REALM_NAME=$1
    #
    EXISTING_REALM=$($KCADM get realms/$REALM_NAME)
    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)
}

# create a top level flow for the given alias if it doesn't exist yet and return the object id
createTopLevelFlow() {
    # arguments
    REALM_NAME=$1
    ALIAS=$2
    #
    FLOW_ID=$(getTopLevelFlow "$REALM_NAME" "$ALIAS")
    if [ "$FLOW_ID" == "" ]; then
        $KCADM create authentication/flows -r "$REALM_NAME" -s alias="$ALIAS" -s providerId=basic-flow -s topLevel=true -s builtIn=false
    fi
    echo $(getTopLevelFlow "$REALM_NAME" "$ALIAS")
}

deleteTopLevelFlow() {
    # arguments
    REALM_NAME=$1
    ALIAS=$2
    #
    FLOW_ID=$(getTopLevelFlow "$REALM_NAME" "$ALIAS")
    if [ "$FLOW_ID" != "" ]; then
        $KCADM delete authentication/flows/"$FLOW_ID" -r "$REALM_NAME"
    fi
    echo $(getTopLevelFlow "$REALM_NAME" "$ALIAS")
}

getTopLevelFlow() {
    # arguments
    REALM_NAME=$1
    ALIAS=$2
    #
    ID=$($KCADM get authentication/flows -r "$REALM_NAME" --fields id,alias| jq '.[] | select(.alias==("'$ALIAS'")) | .id')
    echo $(sed -e 's/"//g' <<< $ID)
}

# create a new execution for a given providerId (the providerId is defined by AuthenticatorFactory)
createExecution() {
    # arguments
    REALM_NAME=$1
    FLOW=$2
    PROVIDER=$3
    REQUIREMENT=$4
    #
    EXECUTION_ID=$($KCADM create authentication/flows/"$FLOW"/executions/execution -i -b '{"provider" : "'"$PROVIDER"'"}' -r "$REALM_NAME")
    $KCADM update authentication/flows/"$FLOW"/executions -b '{"id":"'"$EXECUTION_ID"'","requirement":"'"$REQUIREMENT"'"}' -r "$REALM_NAME"
}

# create a new subflow
createSubflow() {
    # arguments
    REALM_NAME=$1
    TOPLEVEL=$2
    PARENT=$3
    ALIAS="$4"
    REQUIREMENT=$5
    #
    FLOW_ID=$($KCADM create authentication/flows/"$PARENT"/executions/flow -i -r "$REALM_NAME" -b '{"alias" : "'"$ALIAS"'" , "type" : "basic-flow"}')
    EXECUTION_ID=$(getFlowExecution "$REALM_NAME" "$TOPLEVEL" "$FLOW_ID")
    $KCADM update authentication/flows/"$TOPLEVEL"/executions -r "$REALM_NAME" -b '{"id":"'"$EXECUTION_ID"'","requirement":"'"$REQUIREMENT"'"}'
    echo "Created new subflow with id '$FLOW_ID', alias '"$ALIAS"'"
}

getFlowExecution() {
    # arguments
    REALM_NAME=$1
    TOPLEVEL=$2
    FLOW_ID=$3
    #
    ID=$($KCADM get authentication/flows/"$TOPLEVEL"/executions -r "$REALM_NAME" --fields id,flowId,alias | jq '.[] | select(.flowId==("'"$FLOW_ID"'")) | .id')
    echo $(sed -e 's/"//g' <<< $ID)
}

registerRequiredAction() {
    #arguments
    REALM_NAME="$1"
    PROVIDER_ID="$2"
    NAME="$3"

    $KCADM delete authentication/required-actions/"$PROVIDER_ID" -r "$REALM_NAME"
    $KCADM create authentication/register-required-action -r "$REALM_NAME" -s providerId="$PROVIDER_ID" -s name="$NAME"
}

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

cd "${KC_NAME}"

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

createRealm $REALM_NAME
# enable user registration
$KCADM update realms/$REALM_NAME -s registrationAllowed=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"]'

# clients
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=["*"]'

#############
# webauthn authentication flow with nested subflows
#

# set browser flow back to default so we can delete our flow
$KCADM update realms/$REALM_NAME -s browserFlow=browser

# flows: new flow
TOP_LEVEL_FLOW_NAME=Browser-Webauthn
deleteTopLevelFlow $REALM_NAME $TOP_LEVEL_FLOW_NAME
createTopLevelFlow $REALM_NAME $TOP_LEVEL_FLOW_NAME

# Cookie
createExecution $REALM_NAME $TOP_LEVEL_FLOW_NAME auth-cookie ALTERNATIVE

# create subflow for all user interactions
FORMS_SUBFLOW_NAME=Forms
createSubflow $REALM_NAME $TOP_LEVEL_FLOW_NAME $TOP_LEVEL_FLOW_NAME $FORMS_SUBFLOW_NAME ALTERNATIVE
# Username
createExecution $REALM_NAME $FORMS_SUBFLOW_NAME auth-username-form REQUIRED
# create subflow
PWD_OR_2FA_SUBFLOW_NAME="Passwordless_Or_Two-factors"
createSubflow $REALM_NAME "$TOP_LEVEL_FLOW_NAME" $FORMS_SUBFLOW_NAME "$PWD_OR_2FA_SUBFLOW_NAME" REQUIRED
# Passwordless
createExecution "$REALM_NAME" "$PWD_OR_2FA_SUBFLOW_NAME" webauthn-authenticator-passwordless ALTERNATIVE
# create subflow
PWD_AND_2FA_SUBFLOW_NAME="Password_And_Second-factor"
createSubflow "$REALM_NAME" "$TOP_LEVEL_FLOW_NAME" "$PWD_OR_2FA_SUBFLOW_NAME" "$PWD_AND_2FA_SUBFLOW_NAME" ALTERNATIVE
# Password
createExecution "$REALM_NAME" "$PWD_AND_2FA_SUBFLOW_NAME" auth-password-form REQUIRED
# create subflow 2FA
SECOND_FACTOR_SUBFLOW_NAME="Second-factor"
createSubflow "$REALM_NAME" "$TOP_LEVEL_FLOW_NAME" "$PWD_AND_2FA_SUBFLOW_NAME" "$SECOND_FACTOR_SUBFLOW_NAME" CONDITIONAL
#
createExecution "$REALM_NAME" "$SECOND_FACTOR_SUBFLOW_NAME" conditional-user-configured REQUIRED
# SecurityKey 2FA
createExecution "$REALM_NAME" "$SECOND_FACTOR_SUBFLOW_NAME" webauthn-authenticator ALTERNATIVE
# OTP 2FA
createExecution "$REALM_NAME" "$SECOND_FACTOR_SUBFLOW_NAME" auth-otp-form ALTERNATIVE

# bindings: set the new flow for the browser flow
$KCADM update realms/$REALM_NAME -s browserFlow=Browser-Webauthn

# required actions
registerRequiredAction $REALM_NAME "webauthn-register" "Webauthn Register"
registerRequiredAction $REALM_NAME "webauthn-register-passwordless" "Webauthn Register Passwordless"

# every new user gets this required action attached by default
$KCADM update authentication/required-actions/webauthn-register-passwordless -r $REALM_NAME -s defaultAction=true

# policy
$KCADM update realms/$REALM_NAME -s webAuthnPolicyPasswordlessUserVerificationRequirement=required

#############
# users

# user with username/password, a security key must be configured upon first login because of the webauthn-register-passwordless required-action
USER_ID=$(createUser $REALM_NAME $USER_NAME)
$KCADM set-password -r $REALM_NAME --username $USER_NAME --new-password $USER_NAME
$KCADM update users/$USER_ID -r $REALM_NAME -s firstName="My" -s lastName="User" -s email="my.user@email.com"

Die Variablen werden gesetzt damit die Konfiguration durch die Änderung dieser leicht angepasst werden kann.

Variablen
KC_VERSION=26.1.4
KC_NAME=keycloak-"${KC_VERSION}"
HOST_FOR_KCADM=localhost
KCADM=./bin/kcadm.sh

REALM_NAME=water
CLIENT_ID=drop
USER_NAME=user1

Um die Keycloak Admin CLI nun zu verwendung muss man sich zuerst beim Keycloak anmelden.

Einloggen
USER=admin
PASSWORD=admin

$KCADM config credentials --server http://$HOST_FOR_KCADM:8080 --user ${USER} --password ${PASSWORD} --realm master

Erstellung eines Clients für die Verwendung.

Client Konfigurieren
# clients
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=["*"]'

Für dieses Projekt erstellen wir einen neuen realm mit einer Funktion die von Keycloak angeboten wird.

Realm Erstellung
createRealm $REALM_NAME

Das Herzstück dieses Tutorials liegt in der neuen Definierung des Browser-Flows, um Passkeys zur Anmeldung verwenden zu können.

Flow Erstellung
$KCADM update realms/$REALM_NAME -s browserFlow=browser

TOP_LEVEL_FLOW_NAME=Browser-Webauthn
deleteTopLevelFlow $REALM_NAME $TOP_LEVEL_FLOW_NAME
createTopLevelFlow $REALM_NAME $TOP_LEVEL_FLOW_NAME

Die einzelnen Schritte müssen angegeben werden, wobei auch wieder Funktion von Keycloak verwendet werden.

Flow Konfigurieren
# Cookie
createExecution $REALM_NAME $TOP_LEVEL_FLOW_NAME auth-cookie ALTERNATIVE

# create subflow for all user interactions
FORMS_SUBFLOW_NAME=Forms
createSubflow $REALM_NAME $TOP_LEVEL_FLOW_NAME $TOP_LEVEL_FLOW_NAME $FORMS_SUBFLOW_NAME ALTERNATIVE
# Username
createExecution $REALM_NAME $FORMS_SUBFLOW_NAME auth-username-form REQUIRED
# create subflow
PWD_OR_2FA_SUBFLOW_NAME="Passwordless_Or_Two-factors"
createSubflow $REALM_NAME "$TOP_LEVEL_FLOW_NAME" $FORMS_SUBFLOW_NAME "$PWD_OR_2FA_SUBFLOW_NAME" REQUIRED
# Passwordless
createExecution "$REALM_NAME" "$PWD_OR_2FA_SUBFLOW_NAME" webauthn-authenticator-passwordless ALTERNATIVE
# create subflow
PWD_AND_2FA_SUBFLOW_NAME="Password_And_Second-factor"
createSubflow "$REALM_NAME" "$TOP_LEVEL_FLOW_NAME" "$PWD_OR_2FA_SUBFLOW_NAME" "$PWD_AND_2FA_SUBFLOW_NAME" ALTERNATIVE
# Password
createExecution "$REALM_NAME" "$PWD_AND_2FA_SUBFLOW_NAME" auth-password-form REQUIRED
# create subflow 2FA
SECOND_FACTOR_SUBFLOW_NAME="Second-factor"
createSubflow "$REALM_NAME" "$TOP_LEVEL_FLOW_NAME" "$PWD_AND_2FA_SUBFLOW_NAME" "$SECOND_FACTOR_SUBFLOW_NAME" CONDITIONAL
#
createExecution "$REALM_NAME" "$SECOND_FACTOR_SUBFLOW_NAME" conditional-user-configured REQUIRED
# SecurityKey 2FA
createExecution "$REALM_NAME" "$SECOND_FACTOR_SUBFLOW_NAME" webauthn-authenticator ALTERNATIVE
# OTP 2FA
createExecution "$REALM_NAME" "$SECOND_FACTOR_SUBFLOW_NAME" auth-otp-form ALTERNATIVE

Nun kann der erstellte Flow verwendet werden. Required Actions

Default Browser-Flow setzen
$KCADM update realms/$REALM_NAME -s browserFlow=Browser-Webauthn

registerRequiredAction $REALM_NAME "webauthn-register" "Webauthn Register"
registerRequiredAction $REALM_NAME "webauthn-register-passwordless" "Webauthn Register Passwordless"
required
Figure 2. Required Actions von Keycloak

Es kann auch ein User mit der Keycloak Admin CLI erstellt werden.

User Erstellen
USER_ID=$(createUser $REALM_NAME $USER_NAME)
$KCADM set-password -r $REALM_NAME --username $USER_NAME --new-password $USER_NAME
$KCADM update users/$USER_ID -r $REALM_NAME -s firstName="My" -s lastName="User" -s email="my.user@email.com"

Um diesen Tutorial zu folgen können nun die beiden Shell-Scripts ausgeführt werden.

Init um eine Keycloak-Instanz zu installieren
./kc-init.sh
Setup um die Keycloak-Instanz aufzusetzen
./kc-setup.sh
flows overview
Figure 3. Browser-Flow ersetzt
flow graph
Figure 4. Browser-Webauthn-Flow Schritte

4. Verwendung

Nach dem Ausführen der Shell-Scripts kann die folgende Website verwendet werden, um die Installation zu überprüfen.