SSO Integrationintermediate15 min readJune 28, 2026

Nextcloud SSO with Keycloak using OIDC

Run Nextcloud in Docker Compose behind an nginx TLS proxy and connect it to Keycloak SSO using the Social Login app. Covers full stack setup, Keycloak client config, manual endpoint configuration, attribute mapping, and exclusive SSO enforcement.

KT

KeycloakPro Team

KeycloakPro Team

Introduction

Nextcloud supports Keycloak SSO through the Social Login app (sociallogin), which provides a "Custom OpenID Connect" provider type alongside its built-in social providers (Google, GitHub, etc).

This guide covers a self-hosted Nextcloud running in Docker Compose, behind an nginx TLS reverse proxy, connected to Keycloak 24+. Both must run on HTTPS — Keycloak refuses HTTP redirect URIs, and a secure context is required for the login flow to work correctly.

Unlike some OIDC integrations, Social Login has no discovery-URL field — you configure each Keycloak endpoint (authorize, token, userinfo, logout) individually through the Administration UI. There's no occ CLI command for provider creation with this app, so all provider configuration happens in the UI.

Prerequisites

  • Keycloak 24+ with HTTPS
  • Docker and Docker Compose
  • A domain pointed at your host (e.g. dev.nextcloud.io) with valid TLS certs — for local development, see "Local development HTTPS" below for a self-signed CA setup
  • openssl (only needed for the local CA path)
  • sudo access (only needed to edit /etc/hosts and trust the local CA)
  • Keycloak admin access

Local development HTTPS (optional)

If dev.nextcloud.io isn't a real public domain — just a name you're using on your own machine — you need three things before docker compose up -d will give you a working HTTPS site: a /etc/hosts entry pointing the domain at 127.0.0.1, a locally-trusted TLS certificate for it, and nginx config to serve that cert. The scripts below handle the first two; nginx config for ./nginx/conf.d is up to your reverse-proxy setup.

Add these as scripts/add-host.sh, scripts/generate-certs.sh, and scripts/setup.sh in your project directory:

scripts/add-host.sh — adds an idempotent, clearly-marked /etc/hosts entry:

#!/usr/bin/env bash
set -euo pipefail

DOMAIN="dev.nextcloud.io"
IP="127.0.0.1"
MARKER="# nextcloud-sso-setup"
HOSTS_FILE="/etc/hosts"
ENTRY="${IP} ${DOMAIN} ${MARKER}"

if grep -qE "^[^#]*[[:space:]]dev\.nextcloud\.io([[:space:]]|\$)" "${HOSTS_FILE}"; then
  echo "✔ ${DOMAIN} already present in ${HOSTS_FILE} — nothing to do."
  exit 0
fi

echo "▶ Adding '${ENTRY}' to ${HOSTS_FILE} (requires sudo)..."
echo "${ENTRY}" | sudo tee -a "${HOSTS_FILE}" > /dev/null
echo "✔ Added host entry."

scripts/generate-certs.sh — generates a local CA and a SAN-compliant server certificate signed by it (SAN is mandatory; browsers reject certs without it):

#!/usr/bin/env bash
set -euo pipefail

DOMAIN="dev.nextcloud.io"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
CERT_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)/certs"

CA_KEY="${CERT_DIR}/ca.key"
CA_CRT="${CERT_DIR}/ca.crt"
SRV_KEY="${CERT_DIR}/${DOMAIN}.key"
SRV_CSR="${CERT_DIR}/${DOMAIN}.csr"
SRV_CRT="${CERT_DIR}/${DOMAIN}.crt"

LEAF_DAYS=825   # browser-enforced maximum
CA_DAYS=3650

FORCE="${1:-}"
mkdir -p "${CERT_DIR}"

if [[ -f "${SRV_CRT}" && "${FORCE}" != "--force" ]]; then
  echo "✔ Certificate already exists: ${SRV_CRT}"
  echo "  Run '$(basename "$0") --force' to regenerate."
  exit 0
fi

if [[ ! -f "${CA_KEY}" || "${FORCE}" == "--force" ]]; then
  openssl genrsa -out "${CA_KEY}" 4096
  openssl req -x509 -new -nodes -sha256 -days "${CA_DAYS}" \
    -key "${CA_KEY}" -out "${CA_CRT}" \
    -subj "/C=US/O=Local Dev/CN=Local Dev CA"
fi

openssl genrsa -out "${SRV_KEY}" 2048

EXT_FILE="$(mktemp)"
trap 'rm -f "${EXT_FILE}"' EXIT
cat > "${EXT_FILE}" <<EOF
basicConstraints = CA:FALSE
keyUsage         = digitalSignature, keyEncipherment
extendedKeyUsage = serverAuth
subjectAltName   = DNS:${DOMAIN}
EOF

openssl req -new -key "${SRV_KEY}" -out "${SRV_CSR}" \
  -subj "/C=US/O=Local Dev/CN=${DOMAIN}"

openssl x509 -req -in "${SRV_CSR}" \
  -CA "${CA_CRT}" -CAkey "${CA_KEY}" -CAcreateserial \
  -out "${SRV_CRT}" -days "${LEAF_DAYS}" -sha256 \
  -extfile "${EXT_FILE}"

rm -f "${SRV_CSR}"
chmod 600 "${SRV_KEY}" "${CA_KEY}"
chmod 644 "${SRV_CRT}" "${CA_CRT}"

echo "✔ Certificates written to ${CERT_DIR}/"

scripts/setup.sh — orchestrates .env creation, certs, hosts entry, and docker compose up -d in one shot:

#!/usr/bin/env bash
set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"

command -v openssl >/dev/null 2>&1 || { echo "✖ openssl is required."; exit 1; }
command -v docker  >/dev/null 2>&1 || { echo "✖ docker is required.";  exit 1; }

if [[ ! -f "${ROOT_DIR}/.env" ]]; then
  echo "▶ Creating .env from .env.example — EDIT THE PASSWORDS."
  cp "${ROOT_DIR}/.env.example" "${ROOT_DIR}/.env"
fi

bash "${SCRIPT_DIR}/generate-certs.sh"
bash "${SCRIPT_DIR}/add-host.sh"

( cd "${ROOT_DIR}" && docker compose up -d )

echo "✔ Done. Open https://dev.nextcloud.io"
echo "  (trust certs/ca.crt first to avoid browser warnings)"

Make them executable and run the orchestrator:

chmod +x scripts/*.sh
./scripts/setup.sh

Then trust the local CA so your browser stops warning about it:

# macOS
sudo security add-trusted-cert -d -r trustRoot \
  -k /Library/Keychains/System.keychain certs/ca.crt

Linux equivalent: copy certs/ca.crt into /usr/local/share/ca-certificates/, then run sudo update-ca-certificates. For browser-specific trust (Firefox, Chrome on Linux), import it via the browser's certificate settings instead, since the system store isn't always read by browsers there.

To remove this local setup later:

docker compose down -v
sudo sed -i '' '/nextcloud-sso-setup/d' /etc/hosts   # macOS; on Linux, drop the '' after -i
rm -rf certs/

Step 0 — Stand up the Nextcloud stack

Before any OIDC config, get Nextcloud itself running.

Create a project directory and cd into it — this is where docker-compose.yml, .env, and any supporting folders (certs/, nginx/conf.d/) will live:

mkdir nextcloud-sso-setup
cd nextcloud-sso-setup

A minimal production-leaning stack looks like this. Save it as docker-compose.yml in that directory:

# docker-compose.yml
services:
  db:
    image: mariadb:11.4
    container_name: nextcloud-db
    restart: unless-stopped
    command: >
      --transaction-isolation=READ-COMMITTED
      --log-bin=binlog
      --binlog-format=ROW
    environment:
      MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
      MYSQL_DATABASE: ${MYSQL_DATABASE}
      MYSQL_USER: ${MYSQL_USER}
      MYSQL_PASSWORD: ${MYSQL_PASSWORD}
    volumes:
      - db_data:/var/lib/mysql
    networks:
      - nextcloud
    healthcheck:
      test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]
      interval: 10s
      timeout: 5s
      retries: 10

  redis:
    image: redis:7-alpine
    container_name: nextcloud-redis
    restart: unless-stopped
    networks:
      - nextcloud

  nextcloud:
    image: nextcloud:31-apache
    container_name: nextcloud-app
    restart: unless-stopped
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_started
    environment:
      MYSQL_HOST: db
      MYSQL_DATABASE: ${MYSQL_DATABASE}
      MYSQL_USER: ${MYSQL_USER}
      MYSQL_PASSWORD: ${MYSQL_PASSWORD}
      REDIS_HOST: redis
      NEXTCLOUD_ADMIN_USER: ${NEXTCLOUD_ADMIN_USER}
      NEXTCLOUD_ADMIN_PASSWORD: ${NEXTCLOUD_ADMIN_PASSWORD}
      OVERWRITEPROTOCOL: https
      OVERWRITECLIURL: https://${NEXTCLOUD_DOMAIN}
      TRUSTED_PROXIES: ${TRUSTED_PROXIES}
    volumes:
      - nextcloud_data:/var/www/html
    networks:
      - nextcloud

  # Background jobs every 5 min via the image's built-in cron loop
  cron:
    image: nextcloud:31-apache
    container_name: nextcloud-cron
    restart: unless-stopped
    entrypoint: /cron.sh
    volumes:
      - nextcloud_data:/var/www/html
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_started
    networks:
      - nextcloud

  nginx:
    image: nginx:alpine
    container_name: nextcloud-nginx
    restart: unless-stopped
    depends_on:
      - nextcloud
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx/conf.d:/etc/nginx/conf.d:ro
      - ./certs:/etc/nginx/certs:ro
    networks:
      - nextcloud

volumes:
  db_data:
  nextcloud_data:

networks:
  nextcloud:
    driver: bridge
    ipam:
      config:
        - subnet: 172.28.0.0/16

Create a .env.example alongside it so anyone cloning the setup knows exactly what to fill in:

# .env.example
# Copy to .env and change every value before starting the stack.

# --- MariaDB ---
MYSQL_ROOT_PASSWORD=change-me-root-password
MYSQL_DATABASE=nextcloud
MYSQL_USER=nextcloud
MYSQL_PASSWORD=change-me-nextcloud-password

# --- Nextcloud admin (created on first boot) ---
NEXTCLOUD_ADMIN_USER=admin
NEXTCLOUD_ADMIN_PASSWORD=change-me-admin-password

# --- Domain / proxy ---
NEXTCLOUD_DOMAIN=your-domain.example.com
# Docker network subnet — must match docker-compose.yml; used as Nextcloud TRUSTED_PROXIES
TRUSTED_PROXIES=172.28.0.0/16

From inside the project directory:

cp .env.example .env
docker compose up -d

Then edit .env with real values before (or right after) bringing the stack up — Compose reads .env automatically since it sits next to docker-compose.yml.

Key points specific to this setup:

  • OVERWRITEPROTOCOL: https and OVERWRITECLIURL are required — without them, Nextcloud generates http:// URLs internally even though nginx terminates TLS, which breaks the OIDC redirect URI matching in Step 2.
  • TRUSTED_PROXIES must include the nginx container's network range (or service name resolution) so Nextcloud trusts the forwarded headers from the reverse proxy.
  • The cron service shares the same app volume as nextcloud and runs Nextcloud's background jobs — this matters later for token housekeeping and provisioning sync, not just for OIDC, but it's easy to skip when focused on auth.

Step 1 — Install the Social Login app

Install Nextcloud's Social Login app, which supports custom OpenID Connect providers (including Keycloak) alongside the usual social logins:

docker compose exec --user www-data nextcloud php occ app:install sociallogin

Equivalent UI path: top-right menu → Apps → search for Social Login → Install.

Once installed, it appears under Administration settings → Social login.

Nextcloud App Store showing the Social Login app with an Install button
Social Login can also be installed from the App Store UI if you prefer a GUI path

If the App Store UI fails to load apps or downloads stall partway through, this is often a Docker network MTU mismatch rather than a Nextcloud problem — see Troubleshooting below. Installing via occ app:install instead avoids the App Store CDN download path entirely, since it pulls the app differently — worth trying first if you've hit MTU issues.

Step 2 — Create the Keycloak confidential client

In the Keycloak admin console:

  1. Clients → Create client
  2. Client type: OpenID Connect
  3. Client ID: nextcloud
  4. Click Next

Capability config:

  • Client authentication: ON (confidential client — Nextcloud stores the secret server-side)
  • Standard flow: ON
  • Direct access grants: OFF
  • Click Next

Login settings:

  • Root URL: https://dev.nextcloud.io
  • Valid redirect URIs: https://dev.nextcloud.io/apps/sociallogin/custom_oidc/keycloakpro
  • Valid post logout redirect URIs: https://dev.nextcloud.io/
  • Web origins: https://dev.nextcloud.io
Keycloak admin console showing the nextcloud client with Client authentication ON and Valid redirect URIs set to the Social Login custom OIDC callback path
Social Login's redirect path includes the internal name you choose in Step 3 — here, keycloakpro

The redirect path for Social Login is /apps/sociallogin/custom_oidc/<internal-name>, where <internal-name> matches whatever you set as the Internal name field in Step 3. This is different from user_oidc's fixed /apps/user_oidc/code path — if you change the internal name later, you must update the redirect URI in Keycloak to match.

2.1 — Copy the client secret

Clients → nextcloud → Credentials → copy the client secret.

Keycloak admin console Credentials tab for the nextcloud client showing the Client secret field and copy button
Copy the client secret — you need it in Step 3

2.2 — Confirm the email mapper is present

Keycloak includes an email claim by default via the built-in email scope, but it's worth checking before you rely on it for account provisioning.

  1. Clients → nextcloud → Client scopes → nextcloud-dedicated → Mappers
  2. Confirm a mapper for email exists in the list

If the claim is missing, add one: Add mapper → By configuration → User Property → Property: email, Token Claim Name: email.

Step 3 — Configure the OIDC provider in Nextcloud (Social Login)

Unlike apps that use a single discovery URL, Social Login has no discovery-URL field — you provide each Keycloak endpoint manually. Go to Administration settings → Social login, scroll to Custom OpenID Connect, and click + to add a provider:

FieldValue
Internal namekeycloakpro (any unique identifier, no spaces — this also becomes part of the redirect URI)
TitleSSO (text shown on the login button)
Authorize urlhttps://<keycloak-host>/realms/<realm>/protocol/openid-connect/auth
Token urlhttps://<keycloak-host>/realms/<realm>/protocol/openid-connect/token
Display name claim (optional)name
User info URL (optional)https://<keycloak-host>/realms/<realm>/protocol/openid-connect/userinfo
Logout URL (optional)https://<keycloak-host>/realms/<realm>/protocol/openid-connect/logout
Client Idnextcloud
Client Secret(paste from Step 2.1)
Scopeopenid email profile
Nextcloud Social login admin page showing the Custom OpenID Connect form filled in with Internal name, Title, Authorize url, Token url, Client Id, and Client Secret fields
Social Login requires each Keycloak endpoint individually — there's no single discovery URL

Click Save.

Where these URLs come from: every Keycloak realm exposes them at a predictable path under /realms/<realm>/protocol/openid-connect/. You can confirm the exact values by checking your realm's discovery document directly: https://<keycloak-host>/realms/<realm>/.well-known/openid-configuration — even though Social Login doesn't consume that document automatically, it's the fastest way to copy the right endpoint values without typos.

3.1 — Attribute mapping

Social Login doesn't expose a separate "unique identifier" mapping field — it derives the Nextcloud user from the OIDC sub claim by default, which is stable and never changes even if a Keycloak username or email is later updated. The Display name claim field above only controls what's shown as the display name (commonly name or preferred_username) — it does not affect how the account is matched.

3.2 — Optional: hide the default login form

Back at the top of the Social login settings page, Hide social login controls whether the username/password form is hidden in favor of just the SSO button(s).

Leave this off until you've fully confirmed Keycloak login works end-to-end — same caution as Step 5's exclusive-SSO setting. If something is misconfigured and you've already hidden local login, you can lock yourself out of the admin panel.

Step 4 — Test the login

Open a private browser window and go to https://dev.nextcloud.io. A Login with SSO button (or whatever title you set in Step 3) appears on the login page.

Nextcloud login page showing the username and password form alongside a Login with SSO button
The SSO button appears once the provider is saved in Step 3

Click the button. Keycloak handles authentication:

Keycloak login page displayed in the browser during the Nextcloud SSO authorization flow
Keycloak handles the credential check — Nextcloud never sees the user's password

After login, Nextcloud provisions the account from the Keycloak token claims and shows the home dashboard:

Nextcloud home dashboard after successful Keycloak SSO login showing the user's files and the Keycloak user name in the top-right profile menu
First login creates the Nextcloud account — subsequent logins update the display name and email from Keycloak

Step 5 — Disable local login (optional)

Once SSO is working, you can hide the username/password form so Keycloak is the only visible login path. In Administration settings → Social login, check Hide social login... actually this checkbox is misleadingly named — it hides the native login form section, not the social providers. To confirm the exact behavior for your Nextcloud version before relying on it in production, test in a private window after toggling it.

For a harder enforcement (blocking local password auth entirely, not just hiding the UI), use:

docker compose exec --user www-data nextcloud php occ config:app:set sociallogin disable_password_confirmation --value=1

Test any local-login restriction thoroughly in a staging environment before applying to production, and keep a Keycloak admin account that can recover access to the Nextcloud admin panel. Locking out local login without a tested recovery path is the most common way to get stuck out of your own instance.

Troubleshooting common issues

"Invalid redirect URI" from Keycloak

The redirect URI Nextcloud sends must match the Valid redirect URIs field exactly. For Social Login, the path is /apps/sociallogin/custom_oidc/<internal-name> — the <internal-name> must exactly match what you entered in Step 3's Internal name field, including case. Check Keycloak's error page for the URI that was actually sent, then match it.

Login button missing after saving the provider

The sociallogin app may not be enabled, or the provider form didn't save correctly. Confirm via docker compose exec --user www-data nextcloud php occ app:list | grep sociallogin, or check Administration → Apps. Also double check you clicked Save after filling in the Custom OpenID Connect fields — the form does not autosave.

Account provisioning succeeds but the user cannot log in

This is uncommon with Social Login since it relies on the stable sub claim by default. If it happens, check that the Authorize url, Token url, and Client Id all still match what's configured in Keycloak — a realm rename or client ID change will break the link even though sub itself is stable.

Endpoint URLs are reachable from the browser but not from Nextcloud's server

Social Login's token exchange happens server-to-server, not through the browser. In a Docker setup this usually means the nextcloud container can't resolve or reach the Keycloak host — check container DNS/network config, not just firewall rules on the host.

App Store downloads stall or partially fail

If installing sociallogin (or any app) hangs or fails partway through a download, check for a Docker network MTU mismatch — common on Windows/WSL setups. Symptoms: connections to the app store CDN start fine but truncate. Fix by setting an explicit MTU on the Docker network (commonly 1400–1450) in your Compose network config or daemon settings.

Pasting multi-line CLI commands breaks them

If a multi-line occ or shell command (using \ continuations) is pasted from a Windows-side editor, CRLF line endings can break the continuation, causing each line to run as a separate, invalid command. Always run CLI setup commands as a single line where possible, or run dos2unix on any script file before executing it.

Production checklist

  • HTTPS on both Nextcloud and Keycloak
  • OVERWRITEPROTOCOL and OVERWRITECLIURL set correctly when behind a reverse proxy
  • TRUSTED_PROXIES includes the proxy's network range
  • Redirect URI matches /apps/sociallogin/custom_oidc/<internal-name> exactly, including the internal name
  • Client secret stored in Nextcloud's database, not in source control
  • Confirmed the email mapper exists on the Keycloak client scope
  • Tested with a fresh Keycloak user who has no prior Nextcloud account
  • Local-login restriction (if used) tested in a staging environment before production
  • At least one Keycloak admin account available for Nextcloud recovery access

Need help integrating Nextcloud with Keycloak?

We deliver production-ready Nextcloud + Keycloak integrations in 1–3 weeks.

Fixed-price, zero vendor lock-in, full source code ownership.

See integration packages