9 min read

Combining Keycloak, LDAP, and Nextcloud (With Discord SSO!)

For most of the past couple months my energies at Eyebeam have been focused on helping move Undersco.re to a more scalable user management and authentication model. Previously, we'd just been using Keycloak with a Discord identity provider. This provided a relatively seamless single sign-on experience, but had a number of drawbacks, most importantly:

  • Running through the Discord IDP, without being able to synchronize with guilds, meant all users had to be a member of Underscore's Discord to use SSO.
  • This process didn't allow for real one-to-one account syncing across different apps, particularly NextCloud, which serves as the core of Underscore. Instead, it would create separate user accounts in apps like NextCloud, which used Keycloak + Discord for authentication but wouldn't reflect changes in Keycloak's user management.
  • This also created a pretty strong dependency on Discord, which we want to avoid in order to allow potential integrations with other identity providers.

To remedy these problems, after a fair bit of poking around, we ultimately settled on using LLDAP as our single source of truth for users and user groups, effectively making Keycloak a middleware solution for managing authentication, sessioning, and SSO. Long-term, we hope to be able to combine something like Discord role synchronization with LLDAP, so that individual organizations can use Discord for user management (as described in this GitHub issue). Right now, though, our userbase and number of organizations is small enough that manually updating our LLDAP database isn't a huge lift.

This process involved a huge amount of trial and error, and I am by no means a system administrator or DevOps specialist, but we do have a working version of this model. I'll do my best to explain what I think is happening at each step, but I welcome any feedback on improving this process or points where I might be misunderstanding what's happening.

LLDAP

Our process mostly follows the Docker setup instructions on LLDAP's Github. You can use the following docker-compose.yml to run it locally, making sure to mkdir data and chown 1000:1000 data in the same directory you're running docker-compose up:

version: '3'

volumes:
  lldap_data:
    driver: local

services:
  lldap:
    image: nitnelave/lldap:stable
    # Change this to the user:group you want.
    user: "1000:1000"
    ports:
      # For LDAP
      - "3890:3890"
      # For the web front-end
      - "17170:17170"
    volumes:
      # "lldap_data:/data"
      # Alternatively, you can mount a local folder
      - "./:/data"
    environment:
      - LLDAP_JWT_SECRET=REPLACE_WITH_RANDOM
      - LLDAP_LDAP_USER_PASS=REPLACE_WITH_PASSWORD
      - LLDAP_LDAP_BASE_DN=dc=example,dc=com

If all goes well, you should be able to log in to lldap at localhost:17170 and create some users and groups:

Discord

You'll need to create a Discord developer account and create an OAuth2 application.

Keycloak

Start a keycloak docker container, making sure to include the keycloak-discord IDP .jar file in the providers folder. You can use the following `docker-compose.yml` file, making sure to mkdir keycloak_data/providers/ and include the .jar file in the providers folder:

  keycloak:
    image: quay.io/keycloak/keycloak:legacy
    container_name: keycloak
    ports:
      - "8081:8081"
    environment:
      - DB_VENDOR=POSTGRES
      - DB_ADDR=postgresql
      - DB_DATABASE=${KEYCLOAK_DATABASE_NAME}
      - DB_USER=${KEYCLOAK_DATABASE_USER}
      - DB_SCHEMA=public
      - DB_PASSWORD=${KEYCLOAK_DATABASE_PASSWORD}
      - KEYCLOAK_USER=${KEYCLOAK_ADMIN_USER}
      - KEYCLOAK_PASSWORD=${KEYCLOAK_ADMIN_PASSWORD}
      - KEYCLOAK_ADMIN=${KEYCLOAK_ADMIN_USER}
      - KEYCLOAK_ADMIN_PASSWORD=${KEYCLOAK_ADMIN_PASSWORD}      
      - PROXY_ADDRESS_FORWARDING=true
      - KC_HOSTNAME=localhost
      - PUID=1000
      - PGID=998
    volumes:
      - './keycloak_data/providers:/opt/jboss/keycloak/providers'
    depends_on:
      - postgresql

In the User Federation section, follow the instructions for setting up LLDAP with keycloak. To run with your local LDAP, it should look like this:

To sync groups, you'll also need to add the groups mapper: your settings should look like this:

You can now set up a Discord IDP: hit Identity providers > Discord, and include the Client ID & Client Secret from the Discord app page:

NextCloud

Finally, let's set up our NextCloud! We're using the LinuxServer docker-compose as a base:

---
version: "2.1"
services:
  nextcloud:
    image: lscr.io/linuxserver/nextcloud:latest
    container_name: nextcloud
    environment:
      - PUID=1000
      - PGID=1000
      - TZ=Europe/London
    volumes:
      - /home/{YOUR_USER}/nextcloud:/config
    ports:
      - 80:80
      - 443:443
    extra_hosts:
      - "host.docker.internal:host-gateway"
    restart: unless-stopped

Once your nextcloud container is up and running, we'll want to make sure it can talk to the LLDAP container to pull data. You can get your container names by running docker container ls:

Mine are nextcloud and lldap_lldap_1.

Now create a network for docker to connect them:

docker network create lldap-bridge

And connect both containers:

docker network connect lldap-bridge nextcloud

docker network connect lldap-bridge lldap_lldap_1

Now they should be able to address each other by their container names.

In Nextcloud, search for "LDAP user and group backend" and enable it:

Then go to Settings > LDAP/AD integration, and follow the instructions from LLDAP. Your settings should look like this - note that we're using the LLDAP container's name so we can address it directly through our bridge network.

(In the groups section, be sure to add any groups you want to sync in "Only from these groups").

If you verify your settings and get "configuration OK", then go to "Users" in Nextcloud, you should see all your users from LLDAP!

Now, let's set up NextCloud as a client in KeyCloak for authentication with Discord. In the Keycloak admin console, go to Clients and hit Create client. Your settings should look something like this:

content/images/2022/10/image-2.png

Go to "Advanced" settings for the client and make sure to use RS256 for your ID token signature algorithm and Use refresh tokens

Now, finally, let's enable OIDC. Go back to Nextcloud, go to Apps, and search for OpenID Connect Login, then enable it.

Now, in the volume mounted by your NextCloud docker container (if you're following this tutorial exactly, it should be /home/{YOUR_USER}/nextcloud), go to www/nextcloud/config/ and add the following to config.php. DO NOT make these changes in the root-level config.php - OpenID connect will not register them, and will likely throw a "Curl error 3" when you try to log in. Make sure to enter your client secret from Keycloak, and note that we use host.docker.internal to connect to Keycloak's endpoints.

    // Some Nextcloud options that might make sense here
    'allow_user_to_change_display_name' => false,
    'lost_password_link' => 'disabled',

    // URL of provider. All other URLs are auto-discovered from .well-known
    'oidc_login_provider_url' => 'http://host.docker.internal:8081/realms/testrealm',

    // Client ID and secret registered with the provider
    'oidc_login_client_id' => 'nextcloud',
    'oidc_login_client_secret' => '{YOUR_SECRET}',

    // Automatically redirect the login page to the provider
    'oidc_login_auto_redirect' => true,

    // Redirect to this page after logging out the user
    'oidc_login_logout_url' => 'http://localhost:8081/realms/testrealm/protocol/openid-connect/logout',

    // If set to true the user will be redirected to the
    // logout endpoint of the OIDC provider after logout
    // in Nextcloud. After successfull logout the OIDC
    // provider will redirect back to 'oidc_login_logout_url' (MUST be set).
    'oidc_login_end_session_redirect' => true,

    // Quota to assign if no quota is specified in the OIDC response (bytes)
    //
    // NOTE: If you want to allow NextCloud to manage quotas, omit this option. Do not set it to
    // zero or -1 or ''.
    'oidc_login_default_quota' => '1000000000',

    // Login button text
    'oidc_login_button_text' => 'Log in with OpenID',

    // Hide the NextCloud password change form.
    'oidc_login_hide_password_form' => false,

    // Use ID Token instead of UserInfo
    'oidc_login_use_id_token' => false,

    'oidc_login_attributes' => array (
        'id' => 'preferred_username',
        'mail' => 'email',
        'ldap_uid' => 'preferred_username',
    ),

    // Default group to add users to (optional, defaults to nothing)
    'oidc_login_default_group' => 'oidc',

    // Use external storage instead of a symlink to the home directory
    // Requires the files_external app to be enabled
    'oidc_login_use_external_storage' => false,

    // Set OpenID Connect scope
    'oidc_login_scope' => 'openid profile',

    // Run in LDAP proxy mode
    // In this mode, instead of creating users of its own, OIDC login
    // will get the existing user from an LDAP database and only
    // perform authentication with OIDC. All user data will be derived
    // from the LDAP database instead of the OIDC user response
    //
    // The `id` attribute in `oidc_login_attributes` must return the
    // "Internal Username" (see expert settings in LDAP integration)
    'oidc_login_proxy_ldap' => true,

    // Disable creation of new users from OIDC login
    'oidc_login_disable_registration' => true,

    // Fallback to direct login if login from OIDC fails
    // Note that no error message will be displayed if enabled
    'oidc_login_redir_fallback' => false,

    // Use an alternative login page
    // This page will be php-included instead of a redirect if specified
    // In the example below, the PHP file `login.php` in `assets`
    // in nextcloud base directory will be included
    // Note: the PHP variable $OIDC_LOGIN_URL is available for redirect URI
    // Note: you may want to try setting `oidc_login_logout_url` to your
    // base URL if you face issues regarding re-login after logout
    'oidc_login_alt_login_page' => 'assets/login.php',

    // For development, you may disable TLS verification. Default value is `true`
    // which should be kept in production
    'oidc_login_tls_verify' => false,

    // If you get your groups from the oidc_login_attributes, you might want
    // to create them if they are not already existing, Default is `false`.
    'oidc_create_groups' => false,

    // Enable use of WebDAV via OIDC bearer token.
    'oidc_login_webdav_enabled' => false,

    // Enable authentication with user/password for DAV clients that do not
    // support token authentication (e.g. DAVx⁵)
    'oidc_login_password_authentication' => false,

    // The time in seconds used to cache public keys from provider.
    // The default value is 1 day.
    'oidc_login_public_key_caching_time' => 86400,

    // The minimum time in seconds to wait between requests to the jwks_uri endpoint.
    // Avoids that the provider will be DoSed when someone requests with unknown kids.
    // The default is 10 seconds.
    'oidc_login_min_time_between_jwks_requests' => 10,

    // The time in seconds used to cache the OIDC well-known configuration from the provider.
    // The default value is 1 day.
    'oidc_login_well_known_caching_time' => 0,

    // If true, nextcloud will download user avatars on login.
    // This may lead to security issues as the server does not control
    // which URLs will be requested. Use with care.
    'oidc_login_update_avatar' => false,

Refresh localhost, and if everything went according to plan, it should take you directly to the Keycloak login screen!

This SSO method can also be used with other applications - for example, we use Dashy for our "login" page, and the session is shared across different apps connected to Keycloak, so people are able to go directly from Dashy to NextCloud.

Bonus: Dashy Homepage

We follow the basic docker-compose.yml setup from Dashy's docs, creating a config folder with a dashy-config.yml file for customization  in the same folder we're running Dashy in.

docker-compose.yml

---
# Welcome to Dashy! To get started, run `docker compose up -d`
# You can configure your container here, by modifying this file
version: "3.8"
services:
  dashy:
    container_name: Dashy

    # Pull latest image from DockerHub
    image: lissy93/dashy

    # To build from source, replace 'image: lissy93/dashy' with 'build: .'
    # build: .

    # Or, to use a Dockerfile for your archtecture, uncomment the following
    # context: .
    # dockerfile: ./docker/Dockerfile-arm32v7

    # You can also use an image with a different tag, or pull from a different registry, e.g:
    # image: ghcr.io/lissy93/dashy or image: lissy93/dashy:arm64v8

    # Pass in your config file below, by specifying the path on your host machine
    volumes:
      - ./config/dashy-config.yml:/app/public/conf.yml
      # - /path/to/item-icons:/app/public/item-icons

    # Set port that web service will be served on. Keep container port as 80
    ports:
      - 4000:80

    # Set any environmental variables
    environment:
      - NODE_ENV=production
    # Specify your user ID and group ID. You can find this by running `id -u` and `id -g`
    #  - UID=1000
    #  - GID=1000

    # Specify restart policy
    restart: unless-stopped

    # Configure healthchecks
    healthcheck:
      test: ['CMD', 'node', '/app/services/healthcheck']
      interval: 1m30s
      timeout: 10s
      retries: 3
      start_period: 40s

config/dashy-config.yml

appConfig:  
  auth:    
    enableKeycloak: true    
    keycloak:
      serverUrl: 'http://localhost:8081'      
      realm: '{YOUR_REALM}'      
      clientId: 'dashy'
pageInfo: 
  title: My Dashboard
sections:
  - name: My Section
    items:
      - title: NextCloud
        url: https://localhost/apps/dashboard

Go into keycloak and create a client named "Dashy" with the following settings:

Now, if you restart Dashy, you should be able to log in through KeyCloak and then enter NextCloud directly, as it automatically checks KeyCloak to see if you have an active session!

Sources