From 02ce26055faaf169a2554b93c9328eec83d7023a Mon Sep 17 00:00:00 2001 From: Aleksandr Albekov Date: Sun, 8 Nov 2026 16:16:24 +0100 Subject: [PATCH] Add the Nextcloud OIDC client and the cloud-users group Nextcloud signs people in through Ory. The group decides who gets in, and the admin page has to offer it for it to be assignable at all. Co-Authored-By: Claude Opus 5.5 --- README.md | 5 +++-- compose.yml | 1 + deploy/k8s/ui.yaml | 1 + e2e/test.env | 1 + kustomization.yaml | 1 + ory/hydra/clients/nextcloud.json | 13 +++++++++++++ ui/src/lib/groups.ts | 2 +- 7 files changed, 21 insertions(+), 3 deletions(-) create mode 100644 ory/hydra/clients/nextcloud.json diff --git a/README.md b/README.md index 6403c6c..68959ab 100644 --- a/README.md +++ b/README.md @@ -44,12 +44,13 @@ Groups decide access. Each OIDC client names one group in `metadata.required_gro | `site-admins` | The bulbashenko.com admin panel | | `mail-users` | Mail clients and the Stalwart web UI | | `vpn-users` | Joining the VPN mesh (Headscale) | +| `cloud-users` | Nextcloud (files, calendars, contacts) | ## Deploy on Kubernetes `kustomization.yaml` at the root is a Kustomize base built from the same `ory/` files that `compose.yml` mounts: Deployments for Kratos, Hydra and Keto (each runs its migration as an init container), Oathkeeper and the UI, the `hydra-clients` Job, Traefik routes (`auth-public` on the `web` entrypoint, `auth-admin` for `users.int` behind the mesh and Oathkeeper), and NetworkPolicies that keep the admin APIs inside the namespace (see Security). Service names match the compose stack, so the configs' internal URLs work unchanged. -The deploying side provides the namespace, the image tag and a Secret `auth` with `KRATOS_DSN`, `HYDRA_DSN`, `KETO_DSN`, `KRATOS_SECRETS_{COOKIE,DEFAULT,CIPHER,PAGINATION}`, `HYDRA_SECRETS_{SYSTEM,COOKIE,PAGINATION}`, `SMTP_CONNECTION_URI`, `SMTP_SENDER` and `OIDC_{SITE,HEADSCALE,FLUXWEB,GRAFANA}_CLIENT_SECRET`, plus a ConfigMap `internal-ca` (key `ca.crt`) with the CA of the Postgres server certificate, mounted at `/etc/internal-ca` so the DSNs can use `sslmode=verify-full&sslrootcert=/etc/internal-ca/ca.crt`. bulbashenko.com deploys it from [bulbashenko-com/infra](https://github.com/bulbashenko-com/infra), pinned to one commit of this repo; `.github/workflows/image.yml` builds `ghcr.io/bulbashenko-com/auth-ui:` once `test` passes on main and then opens the deploy PR `deploy/auth` in infra. +The deploying side provides the namespace, the image tag and a Secret `auth` with `KRATOS_DSN`, `HYDRA_DSN`, `KETO_DSN`, `KRATOS_SECRETS_{COOKIE,DEFAULT,CIPHER,PAGINATION}`, `HYDRA_SECRETS_{SYSTEM,COOKIE,PAGINATION}`, `SMTP_CONNECTION_URI`, `SMTP_SENDER` and `OIDC_{SITE,HEADSCALE,FLUXWEB,GRAFANA,NEXTCLOUD}_CLIENT_SECRET`, plus a ConfigMap `internal-ca` (key `ca.crt`) with the CA of the Postgres server certificate, mounted at `/etc/internal-ca` so the DSNs can use `sslmode=verify-full&sslrootcert=/etc/internal-ca/ca.crt`. bulbashenko.com deploys it from [bulbashenko-com/infra](https://github.com/bulbashenko-com/infra), pinned to one commit of this repo; `.github/workflows/image.yml` builds `ghcr.io/bulbashenko-com/auth-ui:` once `test` passes on main and then opens the deploy PR `deploy/auth` in infra. ## Deploy on Coolify @@ -72,7 +73,7 @@ The deploying side provides the namespace, the image tag and a Secret `auth` wit | `KRATOS_CIPHER_SECRET` | exactly 32 characters: `openssl rand -hex 16` | | `SMTP_CONNECTION_URI` | `smtps://noreply%40example.com:@host.docker.internal:465/?server_name=mail.example.com` | | `SMTP_SENDER` | `noreply@example.com` | - | `OIDC_SITE_CLIENT_SECRET`, `OIDC_HEADSCALE_CLIENT_SECRET`, `OIDC_FLUXWEB_CLIENT_SECRET`, `OIDC_GRAFANA_CLIENT_SECRET` | `scripts/new-client-secret.sh`; the app gets the same value | + | `OIDC_SITE_CLIENT_SECRET`, `OIDC_HEADSCALE_CLIENT_SECRET`, `OIDC_FLUXWEB_CLIENT_SECRET`, `OIDC_GRAFANA_CLIENT_SECRET`, `OIDC_NEXTCLOUD_CLIENT_SECRET` | `scripts/new-client-secret.sh`; the app gets the same value | Optional: `AUTH_LOG_LEVEL` (`info`), `AUTH_RATE_LIMIT_PER_CLIENT` (10 per minute), `AUTH_RATE_LIMIT_TOTAL` (120 per minute). For another domain, change the domains in `ory/*/*.yml`, `ory/hydra/clients/` and the defaults of `AUTH_HOST`, `AUTH_PUBLIC_URL` and `AUTH_ADMIN_HOST`. diff --git a/compose.yml b/compose.yml index 2ff79d2..55bbfed 100644 --- a/compose.yml +++ b/compose.yml @@ -266,6 +266,7 @@ services: OIDC_HEADSCALE_CLIENT_SECRET: ${OIDC_HEADSCALE_CLIENT_SECRET:?} OIDC_FLUXWEB_CLIENT_SECRET: ${OIDC_FLUXWEB_CLIENT_SECRET:?} OIDC_GRAFANA_CLIENT_SECRET: ${OIDC_GRAFANA_CLIENT_SECRET:?} + OIDC_NEXTCLOUD_CLIENT_SECRET: ${OIDC_NEXTCLOUD_CLIENT_SECRET:?} volumes: - ./ory/hydra/clients:/app/clients:ro # Plain-text secrets of the confidential clients; Hydra stores only their hashes. diff --git a/deploy/k8s/ui.yaml b/deploy/k8s/ui.yaml index fb8c6ea..9a6128b 100644 --- a/deploy/k8s/ui.yaml +++ b/deploy/k8s/ui.yaml @@ -87,6 +87,7 @@ spec: - { name: OIDC_HEADSCALE_CLIENT_SECRET, valueFrom: { secretKeyRef: { name: auth, key: OIDC_HEADSCALE_CLIENT_SECRET } } } - { name: OIDC_FLUXWEB_CLIENT_SECRET, valueFrom: { secretKeyRef: { name: auth, key: OIDC_FLUXWEB_CLIENT_SECRET } } } - { name: OIDC_GRAFANA_CLIENT_SECRET, valueFrom: { secretKeyRef: { name: auth, key: OIDC_GRAFANA_CLIENT_SECRET } } } + - { name: OIDC_NEXTCLOUD_CLIENT_SECRET, valueFrom: { secretKeyRef: { name: auth, key: OIDC_NEXTCLOUD_CLIENT_SECRET } } } resources: requests: { cpu: 10m, memory: 48Mi } securityContext: diff --git a/e2e/test.env b/e2e/test.env index 2af421f..9e7170a 100644 --- a/e2e/test.env +++ b/e2e/test.env @@ -17,6 +17,7 @@ OIDC_SITE_CLIENT_SECRET=test-site-secret OIDC_HEADSCALE_CLIENT_SECRET=test-headscale-secret OIDC_FLUXWEB_CLIENT_SECRET=test-fluxweb-secret OIDC_GRAFANA_CLIENT_SECRET=test-grafana-secret +OIDC_NEXTCLOUD_CLIENT_SECRET=test-nextcloud-secret # The suite signs in many times a minute from one machine; the rate-limit test checks the per-client limit. AUTH_RATE_LIMIT_PER_CLIENT=60 AUTH_RATE_LIMIT_TOTAL=1000 diff --git a/kustomization.yaml b/kustomization.yaml index bcf6994..73f2bd6 100644 --- a/kustomization.yaml +++ b/kustomization.yaml @@ -44,6 +44,7 @@ configMapGenerator: - ory/hydra/clients/flux-web.json - ory/hydra/clients/grafana.json - ory/hydra/clients/headscale.json + - ory/hydra/clients/nextcloud.json - ory/hydra/clients/stalwart-webui.json - ory/hydra/clients/thunderbird.json - name: keto-config diff --git a/ory/hydra/clients/nextcloud.json b/ory/hydra/clients/nextcloud.json new file mode 100644 index 0000000..93ff6a3 --- /dev/null +++ b/ory/hydra/clients/nextcloud.json @@ -0,0 +1,13 @@ +{ + "client_id": "nextcloud", + "client_name": "Nextcloud (files, calendars, contacts)", + "client_secret": "${OIDC_NEXTCLOUD_CLIENT_SECRET}", + "token_endpoint_auth_method": "client_secret_post", + "grant_types": ["authorization_code", "refresh_token"], + "response_types": ["code"], + "scope": "openid offline_access profile email groups", + "redirect_uris": ["https://cloud.bulbashenko.com/apps/user_oidc/code"], + "post_logout_redirect_uris": ["https://cloud.bulbashenko.com/"], + "skip_consent": true, + "metadata": { "required_group": "cloud-users" } +} diff --git a/ui/src/lib/groups.ts b/ui/src/lib/groups.ts index 0ae5e10..8f526e0 100644 --- a/ui/src/lib/groups.ts +++ b/ui/src/lib/groups.ts @@ -2,7 +2,7 @@ import "server-only" import { permissions, relationshipsRead, relationshipsWrite } from "./ory" /** The groups that exist. Membership lives only in Keto: Group:#members@User:. */ -export const GROUPS = ["infra-admins", "site-admins", "mail-users", "vpn-users"] as const +export const GROUPS = ["infra-admins", "site-admins", "mail-users", "vpn-users", "cloud-users"] as const export type Group = (typeof GROUPS)[number] export const isGroup = (value: string): value is Group => (GROUPS as readonly string[]).includes(value)