From d19aef28eb033a21cabe115597d8599e31650a45 Mon Sep 17 00:00:00 2001 From: Eric Meehan Date: Fri, 23 Oct 2026 20:28:35 -0400 Subject: [PATCH] Clients --- README.md | 51 +++++++++++++++++++++++++++++++----- playbooks/deploy-eom.yml | 56 ++++++++++++++++++++++++++++++++++------ tasks/main.yml | 17 ++++++++++++ tests/validate.py | 30 ++++++++++++++------- 4 files changed, 130 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index 90e8c6e..b00918b 100755 --- a/README.md +++ b/README.md @@ -22,10 +22,49 @@ Ansible Vault-encrypted vars file: - `openldap_readonly_password`: existing LDAP search-account password. - `matrix_dex_oidc_client_id` and `matrix_dex_oidc_client_secret`: existing MAS client. - `postgresql_admin_password`: needed only if deliberately enabling DB management. -- Optional `www_dex_client_secret`: enables the website client and its separate - `www-ldap`/`entryUUID` connector, without changing legacy identities. It must - match the website's `DEX_CLIENT_SECRET`. Leave absent to preserve Matrix-only - behavior. Review and declare any other deployed clients before rollout. +- `www_dex_client_secret`: matches the website's `DEX_CLIENT_SECRET`. +- `gitea_dex_client_secret`, `discourse_dex_client_secret`, + `peertube_dex_client_secret`, `mastodon_dex_client_secret`, and + `nextcloud_dex_client_secret`: one distinct confidential-client secret per app. + +The site playbook now registers all seven clients explicitly in +`dex_static_clients` and disables the Matrix convenience client to avoid a +duplicate. None of these secrets is optional unless you deliberately remove its +client from your inventory/playbook. Existing secrets must be preserved, not +regenerated on each deploy. No real secrets are included in source control. + +### Application configuration required after client registration + +All applications use issuer **https://dex.eom.dev/** and discovery document +**https://dex.eom.dev/.well-known/openid-configuration**, with `openid profile +email` scopes (and `groups` only when used). Configure the matching ID/secret in +each service. Client creation in Dex **does not enable SSO in the application**. + +| Application | Client ID | Exact callback / prerequisite | +| --- | --- | --- | +| Matrix Auth Service | Existing `matrix_dex_oidc_client_id` | Original `https://mas.eom.dev/upstream/callback/01K96AQEZKKABW34PY3R6BVNJ4` retained | +| www | `www-membership` | `https://www.eom.dev/members/oidc/callback/` | +| Gitea | `gitea` | `https://gitea.eom.dev/user/oauth2/dex/callback`; add OpenID Connect authentication source named **dex** | +| Discourse | `discourse` | `https://discourse.eom.dev/auth/oidc/callback`; enable discourse-openid-connect and confirm callback for your installed plugin version | +| PeerTube | `peertube` | `https://peertube.eom.dev/plugins/auth-openid-connect/router/code-cb`; install/configure official auth-openid-connect plugin | +| Mastodon | `mastodon` | `https://mastodon.eom.dev/auth/auth/openid_connect/callback`; retained from its configured OIDC_REDIRECT_URI; verify installed OIDC implementation supports that route | +| Nextcloud | `nextcloud` | `https://nextcloud.eom.dev/index.php/apps/user_oidc/code`; install **user_oidc**, not the OIDC provider app or oidc_login | + +Callback paths are not interchangeable. Gitea's source name affects its URL; +Nextcloud's admin screen supplies the exact callback, which can omit index.php +when URL rewriting is enabled. If using **oidc_login** instead, explicitly replace +the callback with its `/apps/oidc_login/oidc` route rather than enabling both by +guesswork. The existing Mastodon deployment points to Google; changing it to Dex +is a separate app-side migration, not another set of automatically active OIDC +environment variables. Keep a tested administrator fallback and avoid automatic +email-only account linking when migrating existing users. + +The website's `www-ldap` connector uses entryUUID while the legacy `ldap` +connector uses uid. Those produce different subjects for the same directory +entry: do not switch existing accounts between connectors without an explicit +linking/migration plan. The legacy connector still filters for Dex Users; merely +registering an application does not bypass this restriction or grant paid room +access. Each relying application must implement its own authorization policy. Run the playbook on the controller with its kubeconfig and this role available in `ANSIBLE_ROLES_PATH`. Load your encrypted variables with Ansible's `--extra-vars` @@ -70,8 +109,8 @@ The site playbook maps legacy inventory variables explicitly: | `openldap_readonly_password` | `dex_ldap_bind_password` | | `matrix_dex_oidc_client_id` / `matrix_dex_oidc_client_secret` | Existing Matrix Auth Service client | -The site playbook enables the existing Matrix client. Its original -redirect URI is explicit in `dex_matrix_redirect_uris`; override to match your +The site playbook explicitly includes the existing Matrix client. Its original +redirect URI is explicit in `dex_static_clients`; override to match your actual MAS configuration. Other clients previously added directly to Helm must be declared in `dex_static_clients` before running this role. Helm values are reset to this role's complete values: undeclared manual Helm customizations are diff --git a/playbooks/deploy-eom.yml b/playbooks/deploy-eom.yml index 3e68183..7ba3968 100755 --- a/playbooks/deploy-eom.yml +++ b/playbooks/deploy-eom.yml @@ -46,14 +46,54 @@ dex_ldap_group_user_attribute: cn dex_ldap_group_filter: "(objectClass=groupOfUniqueNames)" - dex_matrix_client_enabled: true - dex_matrix_redirect_uris: - - https://mas.eom.dev/upstream/callback/01K96AQEZKKABW34PY3R6BVNJ4 - - # Optional website integration. Set www_dex_client_secret in Vault to enable; - # it must equal the membership application's DEX_CLIENT_SECRET. - dex_static_clients: "{{ [{'id': 'www-membership', 'name': 'eom.dev Membership', 'secret': www_dex_client_secret, 'redirectURIs': ['https://www.eom.dev/members/oidc/callback/']}] if (www_dex_client_secret | default('') | length > 0) else [] }}" - dex_additional_connectors: "{{ [_dex_eom_www_connector] if (www_dex_client_secret | default('') | length > 0) else [] }}" + # All clients are declared once below; don't also emit the convenience client. + dex_matrix_client_enabled: false + # Credentials are separate Vault/inventory variables, shared only with the + # corresponding application. No secret is generated or rotated by this play. + dex_static_clients: + - id: "{{ matrix_dex_oidc_client_id }}" + name: Matrix Auth Service + secret: "{{ matrix_dex_oidc_client_secret }}" + redirectURIs: + - https://mas.eom.dev/upstream/callback/01K96AQEZKKABW34PY3R6BVNJ4 + - id: www-membership + name: eom.dev Membership + secret: "{{ www_dex_client_secret }}" + redirectURIs: + - https://www.eom.dev/members/oidc/callback/ + - id: gitea + name: Gitea + secret: "{{ gitea_dex_client_secret }}" + # Authentication source in Gitea must be named exactly "dex". + redirectURIs: + - https://gitea.eom.dev/user/oauth2/dex/callback + - id: discourse + name: Discourse + secret: "{{ discourse_dex_client_secret }}" + # discourse-openid-connect strategy name is oidc. + redirectURIs: + - https://discourse.eom.dev/auth/oidc/callback + - id: peertube + name: PeerTube + secret: "{{ peertube_dex_client_secret }}" + # Requires peertube-plugin-auth-openid-connect. + redirectURIs: + - https://peertube.eom.dev/plugins/auth-openid-connect/router/code-cb + - id: mastodon + name: Mastodon + secret: "{{ mastodon_dex_client_secret }}" + # Matches OIDC_REDIRECT_URI in the existing Mastodon deployment. + redirectURIs: + - https://mastodon.eom.dev/auth/auth/openid_connect/callback + - id: nextcloud + name: Nextcloud + secret: "{{ nextcloud_dex_client_secret }}" + # This chooses the user_oidc app, not oidc_login or the OIDC provider app. + # Override from its admin screen if your installation rewrites index.php. + redirectURIs: + - https://nextcloud.eom.dev/index.php/apps/user_oidc/code + dex_additional_connectors: + - "{{ _dex_eom_www_connector }}" _dex_eom_www_connector: type: ldap id: www-ldap diff --git a/tasks/main.yml b/tasks/main.yml index c786aa4..8daa194 100755 --- a/tasks/main.yml +++ b/tasks/main.yml @@ -13,6 +13,23 @@ fail_msg: Configure database/LDAP credentials, client secrets and explicit database chart version if managing PostgreSQL. no_log: true +- name: Validate explicit OIDC clients + ansible.builtin.assert: + that: + - item.id | default('') | length > 0 + - item.name | default('') | length > 0 + - item.redirectURIs | default([]) | length > 0 + - item.public | default(false) or ((item.secret | default('') | length > 0) != (item.secretEnv | default('') | length > 0)) + fail_msg: Every confidential client needs an ID, name, exact callback URI and either a secret or secretEnv, but not both. + loop: "{{ dex_static_clients }}" + no_log: true + +- name: Reject duplicate explicit client IDs + ansible.builtin.assert: + that: + - dex_static_clients | map(attribute='id') | unique | length == dex_static_clients | length + no_log: true + - name: Ensure Dex namespace kubernetes.core.k8s: state: present diff --git a/tests/validate.py b/tests/validate.py index ed71276..c6c3825 100755 --- a/tests/validate.py +++ b/tests/validate.py @@ -8,31 +8,41 @@ from kubernetes_validate import validate from ansible.parsing.dataloader import DataLoader from ansible.template import Templar -# Resolve the site's actual workflow variables with fixture credentials only; -# verify both Matrix-only migration and opt-in website integration. +# Resolve the site's actual workflow with fixture credentials only. defaults = yaml.safe_load(Path('defaults/main.yml').read_text()) site = yaml.safe_load(Path('playbooks/deploy-eom.yml').read_text())[0]['vars'] -for website_enabled in (False, True): +for fixture_run in (1,): variables = { **defaults, **site, 'dex_admin_password': 'fixture-db', 'openldap_readonly_password': 'fixture-ldap', 'matrix_dex_oidc_client_id': 'matrix-fixture', 'matrix_dex_oidc_client_secret': 'fixture-matrix', + 'www_dex_client_secret': 'fixture-www', + 'gitea_dex_client_secret': 'fixture-gitea', + 'discourse_dex_client_secret': 'fixture-discourse', + 'peertube_dex_client_secret': 'fixture-peertube', + 'mastodon_dex_client_secret': 'fixture-mastodon', + 'nextcloud_dex_client_secret': 'fixture-nextcloud', } - if website_enabled: - variables['www_dex_client_secret'] = 'fixture-www' resolved = Templar(loader=DataLoader(), variables=variables).template(site) assert resolved['dex_issuer'] == 'https://dex.eom.dev/' assert resolved['dex_postgres_password'] == 'fixture-db' assert resolved['dex_ldap_group_user_attribute'] == 'cn' assert resolved['dex_theme_enabled'] is True assert resolved['dex_postgres_manage'] is False - assert bool(resolved['dex_static_clients']) == website_enabled - assert bool(resolved['dex_additional_connectors']) == website_enabled - if website_enabled: - assert resolved['dex_static_clients'][0]['secret'] == 'fixture-www' - assert resolved['dex_additional_connectors'][0]['config']['userSearch']['idAttr'] == 'entryUUID' + clients = {client['id']: client for client in resolved['dex_static_clients']} + assert len(clients) == len(resolved['dex_static_clients']) == 7 + assert resolved['dex_matrix_client_enabled'] is False + assert clients['matrix-fixture']['redirectURIs'] == ['https://mas.eom.dev/upstream/callback/01K96AQEZKKABW34PY3R6BVNJ4'] + assert clients['www-membership']['secret'] == 'fixture-www' + assert clients['gitea']['redirectURIs'] == ['https://gitea.eom.dev/user/oauth2/dex/callback'] + assert clients['discourse']['redirectURIs'] == ['https://discourse.eom.dev/auth/oidc/callback'] + assert clients['peertube']['redirectURIs'] == ['https://peertube.eom.dev/plugins/auth-openid-connect/router/code-cb'] + assert clients['mastodon']['redirectURIs'] == ['https://mastodon.eom.dev/auth/auth/openid_connect/callback'] + assert clients['nextcloud']['redirectURIs'] == ['https://nextcloud.eom.dev/index.php/apps/user_oidc/code'] + assert all(client['secret'] and len(client['redirectURIs']) == 1 for client in clients.values()) + assert resolved['dex_additional_connectors'][0]['config']['userSearch']['idAttr'] == 'entryUUID' values = yaml.safe_load(Path('/tmp/dex-values.yml').read_text()) config = values['config']