Tutorial 4 - Configuring a SwissID integration

What are we doing?

We configure a Keycloak instance to act as an identity broker. The identity provider will be the service offered by SwissID. This means that the authentication responsibility of a user is delegated to an external partner. The integration is done via the OpenID Connect protocol.

Our Keycloak instance, acting as an identity broker, trusts the SwissID identity provider.

Why are we doing this?

Identity federation increases the security of the overall connected systems and simplifies the login process improving organisational productivity. Users from a corporate domain can access external services by using their domain identity.

Compared to single sign-on (SSO) identity federation connects two identity management systems and acts in a broader space, even across enterprise boundaries. For users it feels like SSO. Using a social login for multiple services is identity federation in action.

Requirements

Steps

  1. Step 1: Configure identity provider
  2. Step 1: Configure authentication

We will apply the configuration by the shell scripts with the structure introduced in tutorial 1. This means that the complete configuration is again contained within the five scripts keycloak-configuration.sh, keycloak-configuration-helpers.sh, realm.sh, realm_master.sh and realm_tutorial_webauthn.sh.

In the next section will show and explain only the important Admin CLI calls coming mainly from the realm_tutorial_swissid.sh script. The mentioned scripts contain the complete configuration. To execute the configuration, please just run the keycloak-configuration.sh script.

Step 1: Configure identity provider

All shown CLI calls of this step are coming from the file realm_tutorial_swissid.sh (download)
#!/usr/bin/env bash

REALM_NAME='federations'

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"]'

#############
# identity providers
# swissid
IDENTITY_PROVIDER_ALIAS='swissid'
createIdentityProvider $REALM_NAME $IDENTITY_PROVIDER_ALIAS "SwissID" oidc
$KCADM update identity-provider/instances/$IDENTITY_PROVIDER_ALIAS -r $REALM_NAME -s trustEmail=true -s 'config={"clientId": "'$CLIENT_ID_ISSUED_BY_SWISSID'", "clientSecret" : "'$CLIENT_SECRET_ISSUED_BY_SWISSID'", "tokenUrl": "https://login.int.swissid.ch:443/idp/oauth2/access_token", "validateSignature": "true", "useJwksUrl": "true", "jwksUrl": "https://login.int.swissid.ch:443/idp/oauth2/connect/jwk_uri", "authorizationUrl": "https://login.int.swissid.ch:443/idp/oauth2/authorize", "clientAuthMethod": "client_secret_post", "syncMode": "FORCE", "defaultScope": "openid profile email address" }'
# mappers
MAPPER_ID=$(createIdentityProviderMapper $REALM_NAME $IDENTITY_PROVIDER_ALIAS "given_name -> firstName" oidc-user-attribute-idp-mapper)
$KCADM update identity-provider/instances/$IDENTITY_PROVIDER_ALIAS/mappers/$MAPPER_ID -r $REALM_NAME -s 'config={"syncMode": "INHERIT", "claim": "given_name", "user.attribute": "firstName"}'

MAPPER_ID=$(createIdentityProviderMapper $REALM_NAME $IDENTITY_PROVIDER_ALIAS "family_name -> lastName" oidc-user-attribute-idp-mapper)
$KCADM update identity-provider/instances/$IDENTITY_PROVIDER_ALIAS/mappers/$MAPPER_ID -r $REALM_NAME -s 'config={"syncMode": "INHERIT", "claim": "family_name", "user.attribute": "lastName"}'

MAPPER_ID=$(createIdentityProviderMapper $REALM_NAME $IDENTITY_PROVIDER_ALIAS "gender -> gender" oidc-user-attribute-idp-mapper)
$KCADM update identity-provider/instances/$IDENTITY_PROVIDER_ALIAS/mappers/$MAPPER_ID -r $REALM_NAME -s 'config={"syncMode": "INHERIT", "claim": "gender", "user.attribute": "gender"}'

MAPPER_ID=$(createIdentityProviderMapper $REALM_NAME $IDENTITY_PROVIDER_ALIAS "language -> language" oidc-user-attribute-idp-mapper)
$KCADM update identity-provider/instances/$IDENTITY_PROVIDER_ALIAS/mappers/$MAPPER_ID -r $REALM_NAME -s 'config={"syncMode": "INHERIT", "claim": "language", "user.attribute": "language"}'

#############
# authentication

# in the `browser` flow the identity provider redirector is set as default (= no login screen should be displayed)
EXECUTION_ID=$(getExecution $REALM_NAME browser identity-provider-redirector)
$KCADM create authentication/executions/$EXECUTION_ID/config -r $REALM_NAME -b '{"config":{"defaultProvider":"'$IDENTITY_PROVIDER_ALIAS'"},"alias":"'$IDENTITY_PROVIDER_ALIAS'"}'

We use a new realm for this tutorial called federations. Please note that Keycloak uses this name also in the redirect_uri parameter. For this tutorial the value of this parameter would result in http://localhost:8080/auth/realms/federations/broker/swissid/endpoint. For security reasons, SwissID requires the upfront registration of any redirect_uri values.

REALM_NAME='federations'
createRealm $REALM_NAME

To realize an easy idempotence of this script, we delete first an identity provider with the alias swissid. If it doesn’t exist, our helper function deleteIdentityProvider will ignore the deletion.

IDENTITY_PROVIDER_ALIAS='swissid'
deleteIdentityProvider $REALM_NAME $IDENTITY_PROVIDER_ALIAS

Then we create the new identity provider with the alias swissid and the name SwissID. We define OpenID Connect (oidc) as the federation protocol between the identity broker and the identity provider.

IDENTITY_PROVIDER_ID=$(createIdentityProvider $REALM_NAME $IDENTITY_PROVIDER_ALIAS "SwissID" oidc)

We can now configure all attributes necessary for the SwissID connectivity.

The values for clientId and clientSecret are provided by SwissID upon registration. We use the two variables (CLIENT_ID_ISSUED_BY_SWISSID and LIENT_ID_ISSUED_BY_SWISSID) as placeholders here.
The three urls tokenUrl, jwksUrl and authorizationUrl are used for redirecting the user and for the communications between Keycloak and the SwissID identity provider. In this tutorial we use the integration environment of SwissID (login.int.swissid.ch). Further we specify that Keycloak should validate the JWT signature and grab the public key from an url (useJwksUrl). We configure the transmission of the client_secret within a POST request (client_secret_post) and set the default scope for receiving profile, email and address information.

$KCADM update identity-provider/instances/$IDENTITY_PROVIDER_ALIAS -r $REALM_NAME -s trustEmail=true -s 'config={"validateSignature": "true", "clientId": "'$CLIENT_ID_ISSUED_BY_SWISSID'", "clientSecret" : "'$CLIENT_SECRET_ISSUED_BY_SWISSID'", "tokenUrl": "https://login.int.swissid.ch:443/idp/oauth2/access_token", "useJwksUrl": "true", "jwksUrl": "https://login.int.swissid.ch:443/idp/oauth2/connect/jwk_uri", "authorizationUrl": "https://login.int.swissid.ch:443/idp/oauth2/authorize", "clientAuthMethod": "client_secret_post", "syncMode": "FORCE", "defaultScope": "openid profile email address" }'

The information in the JWT received from an external identity provider can be processed and attached to the current user. Keycloak provides some identity provider mappers out of the box for this purpose. Just by providing a configuration it is possible to map values from an external claim to a user attribute or role, set a hardcoded attribute or role and more.

In the first example below, the mapper reads the claim given_name used by SwissID and sets it as the firstname attribute within Keycloak.

MAPPER_ID=$(createIdentityProviderMapper $REALM_NAME $IDENTITY_PROVIDER_ALIAS "given_name -> firstName" oidc-user-attribute-idp-mapper)
$KCADM update identity-provider/instances/$IDENTITY_PROVIDER_ALIAS/mappers/$MAPPER_ID -r $REALM_NAME -s 'config={"syncMode": "INHERIT", "claim": "given_name", "user.attribute": "firstName"}'

MAPPER_ID=$(createIdentityProviderMapper $REALM_NAME $IDENTITY_PROVIDER_ALIAS "family_name -> lastName" oidc-user-attribute-idp-mapper)
$KCADM update identity-provider/instances/$IDENTITY_PROVIDER_ALIAS/mappers/$MAPPER_ID -r $REALM_NAME -s 'config={"syncMode": "INHERIT", "claim": "family_name", "user.attribute": "lastName"}'

MAPPER_ID=$(createIdentityProviderMapper $REALM_NAME $IDENTITY_PROVIDER_ALIAS "gender -> gender" oidc-user-attribute-idp-mapper)
$KCADM update identity-provider/instances/$IDENTITY_PROVIDER_ALIAS/mappers/$MAPPER_ID -r $REALM_NAME -s 'config={"syncMode": "INHERIT", "claim": "gender", "user.attribute": "gender"}'

MAPPER_ID=$(createIdentityProviderMapper $REALM_NAME $IDENTITY_PROVIDER_ALIAS "language -> language" oidc-user-attribute-idp-mapper)
$KCADM update identity-provider/instances/$IDENTITY_PROVIDER_ALIAS/mappers/$MAPPER_ID -r $REALM_NAME -s 'config={"syncMode": "INHERIT", "claim": "language", "user.attribute": "language"}'

The class org.keycloak.broker.oidc.mappers.UserAttributeMapper is the implementation for above mappings.

Step 2: Configure authentication

All shown CLI calls of this step are coming from the file realm_tutorial_swissid.sh (download)
#!/usr/bin/env bash

REALM_NAME='federations'

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"]'

#############
# identity providers
# swissid
IDENTITY_PROVIDER_ALIAS='swissid'
createIdentityProvider $REALM_NAME $IDENTITY_PROVIDER_ALIAS "SwissID" oidc
$KCADM update identity-provider/instances/$IDENTITY_PROVIDER_ALIAS -r $REALM_NAME -s trustEmail=true -s 'config={"clientId": "'$CLIENT_ID_ISSUED_BY_SWISSID'", "clientSecret" : "'$CLIENT_SECRET_ISSUED_BY_SWISSID'", "tokenUrl": "https://login.int.swissid.ch:443/idp/oauth2/access_token", "validateSignature": "true", "useJwksUrl": "true", "jwksUrl": "https://login.int.swissid.ch:443/idp/oauth2/connect/jwk_uri", "authorizationUrl": "https://login.int.swissid.ch:443/idp/oauth2/authorize", "clientAuthMethod": "client_secret_post", "syncMode": "FORCE", "defaultScope": "openid profile email address" }'
# mappers
MAPPER_ID=$(createIdentityProviderMapper $REALM_NAME $IDENTITY_PROVIDER_ALIAS "given_name -> firstName" oidc-user-attribute-idp-mapper)
$KCADM update identity-provider/instances/$IDENTITY_PROVIDER_ALIAS/mappers/$MAPPER_ID -r $REALM_NAME -s 'config={"syncMode": "INHERIT", "claim": "given_name", "user.attribute": "firstName"}'

MAPPER_ID=$(createIdentityProviderMapper $REALM_NAME $IDENTITY_PROVIDER_ALIAS "family_name -> lastName" oidc-user-attribute-idp-mapper)
$KCADM update identity-provider/instances/$IDENTITY_PROVIDER_ALIAS/mappers/$MAPPER_ID -r $REALM_NAME -s 'config={"syncMode": "INHERIT", "claim": "family_name", "user.attribute": "lastName"}'

MAPPER_ID=$(createIdentityProviderMapper $REALM_NAME $IDENTITY_PROVIDER_ALIAS "gender -> gender" oidc-user-attribute-idp-mapper)
$KCADM update identity-provider/instances/$IDENTITY_PROVIDER_ALIAS/mappers/$MAPPER_ID -r $REALM_NAME -s 'config={"syncMode": "INHERIT", "claim": "gender", "user.attribute": "gender"}'

MAPPER_ID=$(createIdentityProviderMapper $REALM_NAME $IDENTITY_PROVIDER_ALIAS "language -> language" oidc-user-attribute-idp-mapper)
$KCADM update identity-provider/instances/$IDENTITY_PROVIDER_ALIAS/mappers/$MAPPER_ID -r $REALM_NAME -s 'config={"syncMode": "INHERIT", "claim": "language", "user.attribute": "language"}'

#############
# authentication

# in the `browser` flow the identity provider redirector is set as default (= no login screen should be displayed)
EXECUTION_ID=$(getExecution $REALM_NAME browser identity-provider-redirector)
$KCADM create authentication/executions/$EXECUTION_ID/config -r $REALM_NAME -b '{"config":{"defaultProvider":"'$IDENTITY_PROVIDER_ALIAS'"},"alias":"'$IDENTITY_PROVIDER_ALIAS'"}'

By setting a default identity provider for an identity provider redirector, it is possible to hide the Keycloak login screen completely. A user will be redirected straight to the login screen of SwissID.

We first have to get the internal id of the identity provider redirector execution within the authentication flow and can the set the alias of an identity provider (e.g. swissid).

EXECUTION_ID=$(getExecutionId $REALM_NAME browser identity-provider-redirector)
$KCADM create authentication/executions/$EXECUTION_ID/config -r $REALM_NAME -b '{"config":{"defaultProvider":"'$IDENTITY_PROVIDER_ALIAS'"},"alias":"'$IDENTITY_PROVIDER_ALIAS'"}'

Usage

When a user wants to access the account page, the browser will display directly the SwissID login screen:

SwissID Login Flow

The payload of an access token received by Keycloak and issued by SwissID:

{
  "at_hash": "72Pl1ejNRT8U3WLAWIL7Ww",
  "sub": "NldaxfEmY2h64upwgFmusXt12tpTE+bgKPyU9EvDza0=",
  "gender": "male",
  "auditTrackingId": "628bbae1-b4e5-4d53-a59b-6c31c7f1d515-142549498",
  "amr": [
    "urn:swissid:pwd.pwd"
  ],
  "iss": "https://login.int.swissid.ch:443/idp/oauth2",
  "tokenName": "id_token",
  "language": "de_CH",
  "acr": "loa-1",
  "updated_at": 1591282601,
  "azp": "f33c0-3d25c-282f7-6efff",
  "auth_time": 1597657820,
  "exp": 1597661420,
  "iat": 1597657820,
  "email": "es***@inventage.com",
  "address": {},
  "given_name": "Edwin",
  "nonce": "RY1yCfHYCfeHcu7j-Dea6g",
  "aud": "f33c0-3d25c-282f7-6efff",
  "c_hash": "c_O6j7akm3k-5iK5mf05KQ",
  "org.forgerock.openidconnect.ops": "nV1gCOHDwg_nbWY7Y2IGwZd8GCE",
  "s_hash": "adoaj7GgR2in_Z9Bip4CAw",
  "realm": "/sesam",
  "tokenType": "JWTToken",
  "family_name": "***"
}

References