Versatile 2FA Single Sign-On with Keycloak and privacyIDEA

Originally published at: https://www.privacyidea.org/versatile-2fa-single-sign-on-with-keycloak-and-privacyidea/

In a previous article, we wrote about benefits and risks of single sign-on (SSO) in enterprise environments and how a solid 2-factor-authentication solution as provided by privacyIDEA helps reduce risks. In this article we will demonstrate how to setup a SSO system with Keycloak and privacyIDEA. As an example application we integrate the Kolab Groupware Server and setup the Roundcubemail webmailer to authenticate with OpenID Connect (OIDC) and a second factor managed by privacyIDEA. Other popular collaboration platforms such as Tine 2.0 or Open-Xchange and many other applications will also work with Keycloak and privacyIDEA as long as they support at least one standard SSO protocol.

First, we setup three machines with Ubuntu Server 18.04 and provide similar /etc/hosts files to each of them. For a proper setup, Kolab requires a fully qualified domain name. We choose kolab.netknights.it.

127.0.0.1      localhost
127.0.1.1      kolab
192.168.56.200 kolab.netknights.it    kolab
192.168.56.201 pi.netknights.it       pi
192.168.56.202 keycloak.netknights.it keycloak

To put the system into action, one would have to configure DNS and NAT properly so that the server is reachable from the internet. DNS record of type A, AAAA and MX are crucial to do this. You may read about this requirement in the Kolab docs “preparing the system“. For this proof of concept we will not use any logical volumes nor discuss firewall setups or SSL transport layer security.

After updating the initial Ubuntu Server systems, we install privacyIDEA, Keycloak and Kolab following their general installing instructions. We start with Kolab, since in this scenario, we would like to attach the ds-389 LDAP directory delivered by Kolab to our backend, i.e. Keycloak and privacyIDEA.

Kolab 16 Installation

The primary OS supported by Kolab is CentOS, which is also supported by privacyIDEA. However, there are also Kolab and privacyIDEA packages available for Ubuntu 18.04. We will use these for our setup. The install instructions can be found at docs.kolab.org. We summarize them here for convenience

# Add repositories for apt to /etc/sources/ 
~$ echo 'deb http://obs.kolabsys.com/repositories/Kolab:/16/Ubuntu_18.04/ ./ deb-src http://obs.kolabsys.com/repositories/Kolab:/16/Ubuntu_18.04/ ./' \ 
| tee /etc/apt/sources.list.d/kolab.list 
# Add signing key 
~$ wget -q -O- https://ssl.kolabsys.com/community.asc | apt-key add - 
~$ echo -e 'Package: *\nPin: origin obs.kolabsys.com\nPin-Priority: 501' \ 
| tee /etc/apt/preferences.d/kolab 
~$ apt-get update
~$ apt-get install kolab

We let the postfix SMTP service be configured as “Internet with smarthost”. Outbound email will be relayed to another (trusted) mail server, e.g. the one of your ISP. Inbound mail will reach postfix on port 25 if your DNS records are configured correctly. After installation the configuration script is called via

~$ setup-kolab

It will ask for some information and several passwords. The password for the directory manager will be used to first login, so remember it. Also passwords for a the cyrus-imapd administrator, a kolab-service user, the mysql database root user and several database passwords are needed. Note, that all of them are stored in the /etc/kolab/kolab.conf file and only very few are needed for interactive logins.

Kolab comes without a predefined admin user. Only the directory admin is defined which should not be used for user administration. So we login as “cn=Directory Manager” and define a new admin user. In our default setup, the UID is generated from the surname (check the “System” tab), so we choose kolab-admin to differentiate from pi-admin and keycloak-admin later.

On the System tab we set the predefined kolab-admin role for the user to grant him access for user management. Also set a password.

Hit submit to complete the process. Next add another ordinary user to test the second factor login later on. We call this user test-user.

privacyIDEA 3.x Installation

Now, we setup privacyIDEA. Install instructions for the most recent version can be found at readthedocs.privacyidea.io. We install the official Ubuntu packages, specifically the privacyidea-apache2 package. The installation only takes few minutes. After creating the admin user, here called pi-admin, with

~$ pi-manage admin add pi-admin -e pi-admin@localhost

we login to the UI. privacyIDEA needs to access the LDAP directory provided by Kolab, so we create an LDAP resolver and use the Kolab service account created above via “Config->Users->New ldapresolver”. The dn and password for the kolab-service account can be checked in the file /etc/kolab/kolab.conf. We use the OpenLDAP preset given by privacyIDEA but change the UID type to “dn”. The resolver test buttons help to avoid typos and to check the connection. For the test, we leave TLS to be configured later.

The resolver is added to a new realm kolab_realm at “Config->Realms->Create Realm”.

You should now be able to see the users within privacyIDEA.

We will enroll an HOTP token for test-user with the privacyIDEA App, available from Google Play Store. You may alternatively use the Google Authenticator. Install the app and proceed with the enrollment as given below. The privacyIDEA UI auto-completes the username as you type. The generated QR code must be scanned with the App to complete the enrollment.

  • Enroll a new OTP token…
  • …with the privacyIDEA app.
  • The token is assigned to “test-user”.

The OTP token is now assigned to the user test-user and the privacyIDEA app on your phone should display a six-digits OTP code.

To issue trigger challenges asking for an OTP key on user login, privacyIDEA needs an authorization. Since we do not want our pi-admin password to flow through the wire all the time, we create another, unpriviledged admin user on the privacyIDEA terminal.

~$ pi-manage admin add trigger-admin -e trigger-admin@localhost

The trigger-admin needs a superuser policy to restrict the access. In “Config->Policies”, first create a default superuser policy using the “superuser” template. Add only the pi-admin to the admin field. Then add another policy without template. Name it trigger_admin, select the scope admin, add the action triggerchallenge and add the created trigger-admin to the admin field. Now, we have two admin users. pi-admin has the default superuser access and trigger-admin has only very limited access allowing to trigger the challenge.

As privacyIDEA is now up and running, we proceed with the Keycloak server, which will act as the central element in the SSO environment.

Keycloak 9 Installation

Start the Keycloak installation by downloading the Keycloak standalone server from keycloak.org. General install information is found in the “getting started” guide. A detailed guide how to integrate Keycloak with systemd on Ubuntu Server 18.04 LTS is found here. The necessary steps are summarized below.

~$ sudo apt-get update
~$ sudo apt-get install default-jre-headless
# Install Keycloak 9
~$ mkdir -p /opt/keycloak /etc/keycloak
~$ wget https://downloads.jboss.org/keycloak/9.0.0/keycloak-9.0.0.tar.gz
~$ tar -xvzf keycloak-9.0.0.tar.gz
~$ mv keycloak-9.0.0.tar.gz /opt/keycloak
# Add keycloak user
~$ groupadd keycloak
~$ useradd -r -g keycloak -d /opt/keycloak -s /sbin/nologin keycloak
~$ chown -R keycloak: /opt/keycloak
~$ chmod o+x /opt/keycloak/bin/
# Place config file
~$ cp /opt/keycloak/docs/contrib/scripts/systemd/wildfly.conf /etc/keycloak/keycloak.conf
# Setup systemd files
~$ sed 's/wildfly/keycloak/' /opt/keycloak/docs/contrib/scripts/systemd/launch.sh \
| tee /opt/keycloak/bin/launch.sh
~$ chown keycloak: /opt/keycloak/bin/launch.sh
~$ sed 's/wildfly/keycloak/g' /opt/keycloak/docs/contrib/scripts/systemd/wildfly.service \
| tee /etc/systemd/system/keycloak.service
# Enable and start the daemon
~$ systemctl daemon-reload
~$ systemctl enable keycloak
~$ systemctl start keycloak
~$ systemctl status keycloak
# Add admin user
~$ /opt/keycloak/bin/add-user-keycloak.sh -r master -u keycloak-admin -p <password> 
~$ systemctl restart keycloak

Now you should be greeted by Keycloak at http://192.168.56.202:8080. Login with your created keycloak-admin user. As Keycloak should validate the user logins it has to have access to the user store. In “User Federation”, add an LDAP provider with the following settings. The kolab-service account is used as an unpriviledged bind and again we disable TLS for the test setup.

Hit “Synchronize all users” to pull the users from LDAP to Keycloak. You may enable the periodic sync to keep the Keycloak user store up-to-date.

Important: The keycloak-admin should not be required to provide a second factor to prevent locking the configuration while testing. For this purpose, define a no2fa group in “Groups” and add the keycloak-admin to that group in “Users”.

Next, privacyIDEA is integrated with Keycloak. Following our earlier article on the integration of Django with Keycloak and privacyIDEA, we download the two files PrivacyIDEA-Provider.jar and privacyIDEA.ftl of the most recent release of the privacyIDEA keycloak-provider and install it to Keycloak.

~$ wget https://github.com/privacyidea/keycloak-provider/releases/download/v0.3/PrivacyIDEA-Provider.jar
~$ wget https://github.com/privacyidea/keycloak-provider/releases/download/v0.3/privacyIDEA.ftl
~$ cp PrivacyIDEA-Provider.jar /opt/keycloak/standalone/deployment/
~$ cp privacyIDEA.ftl /opt/keycloak/themes/base/login/

In Keycloak, the authentication is managed in so called “Authentication Flows”. Copy the default browser-based flow below and rename it to PrivacyIDEA.

Add an execution to “PrivacyIDEA Forms” and choose the installed plugin called PrivacyIDEA from the list.

Delete the unnecessary items in the flow (or set them to disabled), so that only “PrivacyIDEA Forms” and “Cookie” remain. The authentication flow should now look like this:

We set PrivacyIDEA to REQUIRED here, which means that additionally to username and password, the second factor is required for all users. We have to configure the plugin to reach our privacyIDEA server at https://192.168.56.202. We disable SSL-verification for the self-signed certificate here, which you must not do in a productive environment. Members of the no2fa group, defined above will not be asked for their second factor. For issuing the trigger challenge a service account is needed. We use the trigger-admin account created in privacyIDEA earlier.

Set the edited authentication flow as default browser flow in “Bindings”.

So privacyIDEA is now configured to challenge the second factor for every user. The last step is to enable OpenID Connect logins in roundcubemail.

Installation of the Kolab SSO plugin

For the OIDC, Kolab provides the kolab_sso plugin for Roundcubemail which is available on git.kolab.org. Clone the repository and copy the plugin to the Roundcubemail directory to install it.

~$ git clone https://git.kolab.org/diffusion/RPK/roundcubemail-plugins-kolab.git
~$ cp -r roundcubemail-plugins-kolab/plugins/kolab_sso/ /usr/share/roundcubemail/plugins/

Place the default configuration file.

~$ cp /usr/share/roundcubemail/plugins/kolab_sso/config.inc.php.dist /etc/roundcubemail/kolab_sso.inc.php

Apache should redirect host.roundcube/sso to host.roundcube/?_task=login&_action=sso, since keycloak does not support parameters in urls. It will display “Invalid parameter: redirect_uri”. Add the redirect as follows to /etc/apache2/sites-enabled/roundcubemail.conf.

RewriteEngine On
RewriteCond %{REQUEST_URI} ^/roundcubemail
RewriteRule "^sso" "/roundcubemail/?_task=login&_action=sso" [L,QSA]

We proceed on the Keycloak machine and add Roundcubemail as a new OpenID Connect client as given below.

Save the form to access the “Credentials” tab. We will soon need the generated secret again.

The kolab_sso plugin needs the certificate of the Keycloak server during the OpenID Connect authentication process. It is available from the Keycloak management console in the “Realm Settings”.

Add the key and the client secret alongside the token_uri and auth_uri to the kolab_sso.inc.php configuration file. Make sure that the public key copied from keycloak is properly formatted. The IMAP, SMTP and LDAP credentials in the top part of the file are required for accessing the mailbox, sending emails and accessing the server addressbooks. Configure them accordingly.

After successfully testing the ordinary password login with Roundcubemail at http://192.168.56.200/roundcubemail, you may disable the password login to allow only OpenID Connect by setting

 $config['kolab_sso_disable_login'] = true;

in kolab_sso.inc.php.

Test the login

We are now ready to test the OpenID Connect login at Roundcubemail. Navigate once again to http://192.168.56.200/roundcubemail to test the login. You may monitor some log files during the process.

/var/log/privacyidea/privacyidea.log
/var/log/roundcubemail/*
/opt/keycloak/standalone/log/*
  • The Roundcubemail kolab_sso login…
  • gets redirected to Keycloak.
  • After typing the password, the user is prompted to provide a second factor…
  • …which we get in this case from the OTP token in the privacyIDEA app.
  • The correct OTP gets us directly to Roundcubemail webmail.

Conclusion

We demonstrated the integration of privacyIDEA with Keycloak to provide a solid basis to secure your applications with a second factor in a single sign-on (SSO) environment. For maximum flexibility, the system relies on standard protocols such as SAML or OpenID Connect (OIDC). The privacyIDEA keycloak-provider is designed to perfectly fit the two components together, uniting the rich identity management capabilities of Keycloak and the powerful multi factor management of privacyIDEA.

We chose the Roundcubemail webmailer of the Kolab Collaboration Server as an example application. The kolab_sso plugin provided the necessary interface to connect via OIDC to easily enhance security by adding a second factor managed by privacyIDEA. The setup of other popular open collaboration platforms such as Tine 2.0 or Open-Xchange work similarly.

Including additional applications in this setup is very easy as long as they support at least one SSO protocol. These applications do not even to be hosted on your own servers. Nowadays, most cloud-based applications offer both, the possibility to use an external identity provider and to use OIDC. Thus, you can also use remote services with your own user base, defining access-rules to fit your needs.

Thanks for this tutorial, I can get it working for a HOTP token. What I’d really like is to able to use a push token or (even better) to give users the option of a push token or a U2F token depending on which device they’re logging in from. Could you give any guidance on how you could get that kind of a setup working with Keycloak?

Why would using push tokens be any different?
Setup is more complicated (Firebase) but should work the same.

Did you read this?
https://www.privacyidea.org/testing-privacyidea-push-token/

FIDO2 might work with the release of 3.3…
FWIW, I prefer Yubikey AES.

I’ve got the push token working when I access the link ( https://my.host.domain/privacyidea/validate/check?user=myuser&pass=mypin). But when I login with my username and password via Keycloak, I don’t get a push and I do get asked for a passcode…

I haven’t done the setup above…
But simply switching from TOTP/HOTP to push on my other setups (AnyConnect) was seamless.

I’ll try this on the weekend…

I think I figured it out (or at least figured out a workaround). It seems like the Keycloak connector wasn’t so happy when I had privacyidea running at a suburl of my main domain. Once I created a subdomain/virtualhost for it to run on, it seemed to work. (Although unfortunately I’m not getting push to work on my iPad. It’s working great on my android phone, and I can get the push token enrolled on the iPad, but I when I try to login no push arrives at the iPad – even if I disable the android token).

Now have iPad working too (had been maintaining IP filtering on the system while testing from inside the local network, which obviously meant the push via the Apple cloud was getting filtered out). U2F is apparently still a work-in-progress for the Keycloak connector. Which is kind of a bummer (I’ve got a solokey, not a yubikey – don’t know how Keycloak would deal with the yubikey). But it’s more or less working now. Thanks to all.

I don’t think it’s a Keycloak issue.
U2F should be avoided if possible, my understanding is

Unless 3.3 fixes everything

Features:
* New token type: WebAuthn/FIDO2 token (#1468)
* WebAuthn hardware tokens can now be used with privacyIDEA

Yes, Fuck. Look at this: Support U2F · Issue #20 · privacyidea/keycloak-provider · GitHub
Noone from the community issued a pull request, yet.

And I assume equally no work on WebAuthn for the keycloak connector, which looks like it may be more or less replacing U2F in privacyIDEA going forward.

WebAuthn is U2F, its second “incarnation” FIDO2.
WebAuthn is the name given to FIDO2 by W3C after having it certified.

How can one replace the other?

It does look like v3.3 of privacyIDEA separately lists U2F and WebAuthn as kinds of available tokens… (not saying that necessarily corresponds to a meaningful difference).

Actually FIDO2 is the umbrella name which splits into two components “webauthn” which is specified by W3C, and which is the protocol that runs between the webservice and the browser and into the 2nd part, “CTAP”, which is the protocol that runs between the browser and the authenticator device.

U2F is a sub protocol that can be “tunneled” within WebAuthn. But WebAuthn supports a lot of further authentication types, mechanisms, algorithms… WebAuthn basically supports everything, probably brewing coffee.

So yes, it might look as if we could ditch u2f token type in privacyIDEA, because hardware devices, that support U2F also run within WebAuthn. But there might be old token with tokentype U2F enrolled in privacyIDEA, U2F is much simpler to implement on the application side, U2F allows dedicated trusted facets while WebAuthn trusts all subdomains…
…to name a few.

Currently I am not sure, if I would want to do WebAuthn in a controlled environment. It might get interesting if you have a very inhomogenous user base.

I was actually able to achieve what I wanted to enabling WebAuthn directly in Keycloak and then to make that and PrivacyIdea (configured for push) as alternative 2fA mechanisms via a custom “flow” within Keycloak.