Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Verified Commit 83489d7a authored by Nicolas Gelot's avatar Nicolas Gelot
Browse files

feat: add oidc login

parent 0f22e099
Loading
Loading
Loading
Loading
Loading
+29 −0
Original line number Diff line number Diff line
@@ -71,3 +71,32 @@ OBJECTSTORE_S3_USEPATH_STYLE=true

OBJECTSTORE_S3_AUTOCREATE=
OBJECTSTORE_S3_OBJECT_PREFIX=

# Keycloak
KEYCLOAK_HOSTNAME=localhost
KEYCLOAK_ADMIN_USER=admin
KEYCLOAK_ADMIN_PASSWORD=admin
KEYCLOAK_DB_HOST=db
KEYCLOAK_DB_NAME=keycloak
KEYCLOAK_DB_USER=keycloak
KEYCLOAK_DB_PASSWORD=123456
KEYCLOAK_REALM=murena
KEYCLOAK_NEXTCLOUD_CLIENT_ID=nextcloud
KEYCLOAK_NEXTCLOUD_CLIENT_SECRET=nextcloud-secret
KEYCLOAK_NEXTCLOUD_REDIRECT_BASE=http://localhost:8000

# OIDC app (oidc_login)
OIDC_PROVIDER_URL=http://keycloak:8080/realms/murena/.well-known/openid-configuration
OIDC_CLIENT_ID=nextcloud
OIDC_CLIENT_SECRET=nextcloud-secret
OIDC_DEFAULT_CLIENT_ID=
OIDC_SCOPE=openid profile email
OIDC_UNIQUE_ID_CLAIM=preferred_username
OIDC_LOGIN_BUTTON_NAME=Sign in with Keycloak
OIDC_AUTO_REDIRECT=false
OIDC_USE_ID_TOKEN=false
OIDC_HIDE_LOGIN_FORM=false
OIDC_REDIR_FALLBACK=true
OIDC_END_SESSION_REDIRECT=true
OIDC_LOGOUT_URL=http://localhost:8000/apps/oidc_login/oidc
OIDC_CODE_CHALLENGE_METHOD=
+11 −0
Original line number Diff line number Diff line
@@ -53,6 +53,17 @@ Use this mode when integrating or patching apps, adapting entrypoints, or testin
- Need more juice? hook the stack to managed databases or caches by updating the relevant env variables, no rebuild required.
- Object storage ready: set the S3-compatible env vars (AWS S3, MinIO, etc.) to offload files without touching the image.
- Syslog and Sentry endpoints are prewired—just tweak the env vars if you need to point them somewhere else.
- Optional patches and helper scripts can be switched on/off with the same env-driven approach—no Dockerfile edits required.

## Local Keycloak SSO

- `docker-compose.local.yml` also spins up Keycloak + Postgres on http://localhost:8383 so you can test the `oidc_login` app end-to-end.
- Default realm/client settings live in `.env` (`KEYCLOAK_*` variables); tweak them before running `docker compose up`.
- The helper service `keycloak-init` executes `config/keycloak/init.sh`, which auto-creates the realm and the Nextcloud confidential client (redirect URIs, secret, web origins).
- Once the bootstrap succeeds, the init script drops a sentinel file inside the shared Keycloak volume so subsequent restarts skip the provisioning step automatically (delete it if you need to re-run the init).
- `hooks.d/before-starting/20-configure-oidc.sh` reads the `OIDC_*` env vars at every boot and configures the `oidc_login` app via `occ`, so updating `.env` is all you need to re-point Nextcloud.
- Enable the plugin with `docker compose -f docker-compose.local.yml exec nextcloud su -s /bin/sh www-data -c "php occ app:enable oidc_login"` if it isn’t already active.
- Set `OIDC_AUTO_REDIRECT=1` and `OIDC_HIDE_LOGIN_FORM=1` in `.env` if you want Nextcloud to immediately redirect users to Keycloak instead of showing the legacy password screen.

## Coding conventions

+79 −0
Original line number Diff line number Diff line
#!/bin/sh
set -euo pipefail

KEYCLOAK_URL="${KEYCLOAK_URL:-http://keycloak:8080}"
REALM="${KEYCLOAK_REALM:-murena}"
CLIENT_ID="${KEYCLOAK_NEXTCLOUD_CLIENT_ID:-nextcloud}"
CLIENT_SECRET="${KEYCLOAK_NEXTCLOUD_CLIENT_SECRET:-nextcloud-secret}"
REDIRECT_BASE="${KEYCLOAK_NEXTCLOUD_REDIRECT_BASE:-http://localhost:8000}"
SENTINEL_PATH="${KEYCLOAK_SENTINEL_PATH:-/opt/keycloak/data/.murena-oidc-init}"
ADMIN_USER="${KEYCLOAK_ADMIN_USER:?Missing KEYCLOAK_ADMIN_USER}"
ADMIN_PASSWORD="${KEYCLOAK_ADMIN_PASSWORD:?Missing KEYCLOAK_ADMIN_PASSWORD}"

KCADM="/opt/keycloak/bin/kcadm.sh"

if [ -f "${SENTINEL_PATH}" ]; then
  echo "[keycloak-init] Sentinel found (${SENTINEL_PATH}). Keycloak already configured. Exiting."
  exit 0
fi

echo "[keycloak-init] Waiting for Keycloak at ${KEYCLOAK_URL}..."
until "${KCADM}" config credentials --server "${KEYCLOAK_URL}" --realm master --user "${ADMIN_USER}" --password "${ADMIN_PASSWORD}" >/dev/null 2>&1; do
  printf '.'
  sleep 5
done
printf "\n[keycloak-init] Connected to Keycloak.\n"

if ! "${KCADM}" get "realms/${REALM}" >/dev/null 2>&1; then
  echo "[keycloak-init] Creating realm ${REALM}."
  "${KCADM}" create realms -s "realm=${REALM}" -s enabled=true -s registrationAllowed=false
else
  echo "[keycloak-init] Realm ${REALM} already exists."
fi

fetch_client_uuid() {
  "${KCADM}" get clients -r "${REALM}" -q "clientId=${CLIENT_ID}" --fields id,clientId |
    sed -n 's/.*"id"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n 1
}

CLIENT_UUID="$(fetch_client_uuid)"

if [ -z "${CLIENT_UUID}" ]; then
  echo "[keycloak-init] Creating client ${CLIENT_ID}."
  "${KCADM}" create clients -r "${REALM}" \
    -s "clientId=${CLIENT_ID}" \
    -s "name=client_${CLIENT_ID}" \
    -s enabled=true \
    -s protocol=openid-connect \
    -s publicClient=false \
    -s standardFlowEnabled=true \
    -s directAccessGrantsEnabled=false \
    -s serviceAccountsEnabled=false \
    -s "secret=${CLIENT_SECRET}"
  CLIENT_UUID="$(fetch_client_uuid)"
else
  echo "[keycloak-init] Client ${CLIENT_ID} already exists, updating."
fi

if [ -z "${CLIENT_UUID}" ]; then
  echo "[keycloak-init] ERROR: unable to determine client UUID for ${CLIENT_ID}." >&2
  exit 1
fi

SANITIZED_BASE="${REDIRECT_BASE%/}"
REDIRECT_URIS="[\"${SANITIZED_BASE}/apps/oidc_login/*\",\"${SANITIZED_BASE}/index.php/apps/oidc_login/*\"]"
WEB_ORIGINS="[\"${SANITIZED_BASE}\"]"

"${KCADM}" update "clients/${CLIENT_UUID}" -r "${REALM}" \
  -s "name=client_${CLIENT_ID}" \
  -s "secret=${CLIENT_SECRET}" \
  -s "redirectUris=${REDIRECT_URIS}" \
  -s "webOrigins=${WEB_ORIGINS}" \
  -s "baseUrl=${SANITIZED_BASE}" \
  -s "attributes.\"post.logout.redirect.uris\"=${SANITIZED_BASE}/"

echo "[keycloak-init] Client ${CLIENT_ID} configured (realm ${REALM})."

mkdir -p "$(dirname "${SENTINEL_PATH}")"
touch "${SENTINEL_PATH}"
echo "[keycloak-init] Wrote sentinel ${SENTINEL_PATH} to skip future runs."
+26 −0
Original line number Diff line number Diff line
#!/bin/bash
set -euo pipefail

run_psql() {
  psql -v ON_ERROR_STOP=1 -U "${POSTGRES_USER}" "$@"
}

echo "Starting Keycloak DB init..."

if ! run_psql -tAc "SELECT 1 FROM pg_roles WHERE rolname = '${KEYCLOAK_DB_USER}'" | grep -q 1; then
  run_psql -c "CREATE USER ${KEYCLOAK_DB_USER} WITH PASSWORD '${KEYCLOAK_DB_PASSWORD}';"
  echo "Created user '${KEYCLOAK_DB_USER}'."
else
  echo "User '${KEYCLOAK_DB_USER}' already exists."
fi

if ! run_psql -tAc "SELECT 1 FROM pg_database WHERE datname = '${KEYCLOAK_DB_NAME}'" | grep -q 1; then
  run_psql -c "CREATE DATABASE ${KEYCLOAK_DB_NAME} OWNER ${KEYCLOAK_DB_USER};"
  echo "Created DB '${KEYCLOAK_DB_NAME}'."
else
  echo "DB '${KEYCLOAK_DB_NAME}' already exists."
fi

run_psql -c "GRANT ALL PRIVILEGES ON DATABASE ${KEYCLOAK_DB_NAME} TO ${KEYCLOAK_DB_USER};"

echo "Keycloak DB and user initialized successfully."
+64 −0
Original line number Diff line number Diff line
@@ -9,9 +9,13 @@ services:
      - ONLYOFFICE_DB_NAME=${ONLYOFFICE_DB_NAME}
      - ONLYOFFICE_DB_USER=${ONLYOFFICE_DB_USER}
      - ONLYOFFICE_DB_PASSWORD=${ONLYOFFICE_DB_PASSWORD}
      - KEYCLOAK_DB_NAME=${KEYCLOAK_DB_NAME}
      - KEYCLOAK_DB_USER=${KEYCLOAK_DB_USER}
      - KEYCLOAK_DB_PASSWORD=${KEYCLOAK_DB_PASSWORD}
    volumes:
      - db:/var/lib/postgresql/data
      - ./config/postgres/init-onlyoffice.sh:/docker-entrypoint-initdb.d/10-onlyoffice.sh:ro
      - ./config/postgres/init-keycloak.sh:/docker-entrypoint-initdb.d/20-keycloak.sh:ro
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${DB_USER} -d ${DB_NAME}"]
      interval: 10s
@@ -45,6 +49,21 @@ services:
      context: .
      dockerfile: slim.Dockerfile
      target: nextcloud
    environment:
      - OIDC_PROVIDER_URL=${OIDC_PROVIDER_URL}
      - OIDC_CLIENT_ID=${OIDC_CLIENT_ID}
      - OIDC_CLIENT_SECRET=${OIDC_CLIENT_SECRET}
      - OIDC_DEFAULT_CLIENT_ID=${OIDC_DEFAULT_CLIENT_ID}
      - OIDC_SCOPE=${OIDC_SCOPE}
      - OIDC_UNIQUE_ID_CLAIM=${OIDC_UNIQUE_ID_CLAIM}
      - OIDC_LOGIN_BUTTON_NAME=${OIDC_LOGIN_BUTTON_NAME}
      - OIDC_AUTO_REDIRECT=${OIDC_AUTO_REDIRECT}
      - OIDC_USE_ID_TOKEN=${OIDC_USE_ID_TOKEN}
      - OIDC_HIDE_LOGIN_FORM=${OIDC_HIDE_LOGIN_FORM}
      - OIDC_REDIR_FALLBACK=${OIDC_REDIR_FALLBACK}
      - OIDC_END_SESSION_REDIRECT=${OIDC_END_SESSION_REDIRECT}
      - OIDC_LOGOUT_URL=${OIDC_LOGOUT_URL}
      - OIDC_CODE_CHALLENGE_METHOD=${OIDC_CODE_CHALLENGE_METHOD}
    depends_on:
      syslog:
        condition: service_started
@@ -94,12 +113,57 @@ services:
    networks:
      - worker-network

  keycloak:
    image: quay.io/keycloak/keycloak:26.0
    command: start-dev --http-port=8080 --hostname=${KEYCLOAK_HOSTNAME} --hostname-strict=false
    restart: unless-stopped
    environment:
      - KC_DB=postgres
      - KC_DB_URL_HOST=${KEYCLOAK_DB_HOST}
      - KC_DB_URL_DATABASE=${KEYCLOAK_DB_NAME}
      - KC_DB_USERNAME=${KEYCLOAK_DB_USER}
      - KC_DB_PASSWORD=${KEYCLOAK_DB_PASSWORD}
      - KEYCLOAK_ADMIN=${KEYCLOAK_ADMIN_USER}
      - KEYCLOAK_ADMIN_PASSWORD=${KEYCLOAK_ADMIN_PASSWORD}
      - KEYCLOAK_FRONTEND_URL=http://localhost:8383
      - KEYCLOAK_ADMIN_URL=http://localhost:8383
    depends_on:
      db:
        condition: service_healthy
    ports:
      - "8383:8080"
    volumes:
      - keycloak_data:/opt/keycloak/data
    networks:
      - worker-network
  
  keycloak-init:
    image: quay.io/keycloak/keycloak:26.0
    entrypoint: ["/bin/sh", "/init/keycloak-init.sh"]
    restart: "no"
    environment:
      - KEYCLOAK_ADMIN_USER=${KEYCLOAK_ADMIN_USER}
      - KEYCLOAK_ADMIN_PASSWORD=${KEYCLOAK_ADMIN_PASSWORD}
      - KEYCLOAK_REALM=${KEYCLOAK_REALM}
      - KEYCLOAK_NEXTCLOUD_CLIENT_ID=${KEYCLOAK_NEXTCLOUD_CLIENT_ID}
      - KEYCLOAK_NEXTCLOUD_CLIENT_SECRET=${KEYCLOAK_NEXTCLOUD_CLIENT_SECRET}
      - KEYCLOAK_NEXTCLOUD_REDIRECT_BASE=${KEYCLOAK_NEXTCLOUD_REDIRECT_BASE}
    depends_on:
      keycloak:
        condition: service_started
    volumes:
      - ./config/keycloak/init.sh:/init/keycloak-init.sh:ro
      - keycloak_data:/opt/keycloak/data
    networks:
      - worker-network

volumes: !override
  db:
  nextcloud-config:
  nextcloud-data:
  onlyoffice_data:
  onlyoffice_logs:
  keycloak_data:

networks:
  proxy-network:
Loading