Skip to content

oidc: multiple redirect URIs per client + goose migrations#749

Merged
cyberb merged 5 commits into
masterfrom
oidc-multi-redirect-uri
Jun 5, 2026
Merged

oidc: multiple redirect URIs per client + goose migrations#749
cyberb merged 5 commits into
masterfrom
oidc-multi-redirect-uri

Conversation

@cyberb
Copy link
Copy Markdown
Member

@cyberb cyberb commented Jun 4, 2026

Why

Apps like audiobookshelf need more than one redirect URI per OIDC client — a web callback (/auth/openid/callback) and a mobile-app callback (/auth/openid/mobile-redirect). The client model stored a single redirect_uri, so web SSO and the official mobile app couldn't both work against one client.

What changed

1. Multiple redirect URIs, normalized. They now live in their own table oidc_redirect_uri (client_id, redirect_uri) — one row per URI, no delimiter, so a redirect URI may contain any character (commas/sub-delims are legal in URLs per RFC 3986). Clients() returns RedirectURIs []string; the Authelia template ranges over them. /oidc/register accepts a repeated redirect_uri form field, so single-value callers are unchanged and new callers can pass several.

2. Migrations moved to goose. The hand-rolled idempotent migrator is replaced by goose (pure-Go, works with modernc.org/sqlite, CGO_ENABLED=0). Existing steps become ordered Go migrations (v1–v5); they stay idempotent so a pre-goose device — which has the tables but no goose_db_version ledger — upgrades cleanly. v6 creates oidc_redirect_uri, backfills from the legacy oidc_client.redirect_uri, and drops that column.

Is it breaking for existing devices? No.

  • goose finds no goose_db_version on a legacy DB, runs v1–v6; the create/alter steps are idempotent (guarded if not exists / column-existence checks), so existing tables/rows are untouched.
  • v6 migrates each existing client's single redirect_uri into the new table, then drops the column — data is preserved.
  • The single-value redirect_uri form field still works, so the current golib client and every shipped app register unchanged.
  • platform.db lives in $SNAP_DATA, so a failed platform refresh rolls the DB back with the revision snapshot — the dropped column returns together with the older binary.

Tests (use the real migration code)

  • TestMigrator_MigratesLegacyRedirectUriIntoTableAndDropsColumn: MigrateTo(5) builds the pre-change schema via the actual migrations → insert a legacy client row → Migrate() to latest → assert the row moved to oidc_redirect_uri and oidc_client.redirect_uri is gone.
  • Existing custom_proxy legacy-upgrade + prod-row-preservation tests still pass (goose runs the idempotent steps over partial legacy schemas).
  • Multiple-URI round-trip + a comma-bearing redirect URI preserved as one URI.
  • config/auth/rest green locally. (Unrelated identification/network/backup failures are Android/Termux dev-sandbox only: hardware-ID, netlink routes, useradd/os/user.)

Follow-up (separate PRs)

  • golib: slice-based RegisterOIDCClient helper (platform already accepts repeated values).
  • audiobookshelf: register web + mobile callbacks so the official Android app gets SSO.

cyberb added 3 commits June 4, 2026 21:01
Apps such as audiobookshelf need more than one redirect URI per OIDC client
(a web callback plus a mobile-app callback). The client registration stored and
emitted exactly one.

- oidc_client gains a nullable redirect_uris column (idempotent ALTER, default
  ''); the legacy redirect_uri column is kept and still populated with the first
  URI, so existing rows and a downgrade keep working.
- Clients() returns RedirectURIs, falling back to [redirect_uri] when the new
  column is empty; the Authelia template ranges over the list.
- /oidc/register accepts a repeated redirect_uri form field, so the existing
  single-value callers are unchanged and new callers can pass several.

Backward compatible: no manual migration; existing single-redirect clients
render identically and re-populate redirect_uris on their next registration.
A comma is a legal unencoded sub-delimiter in a URL path/query (RFC 3986), so a
redirect URI may contain one; splitting on comma could corrupt it. A literal
space can never appear in a URL, so store the list space-separated and split on
whitespace. Added a test with a comma-bearing redirect URI.
Replace the hand-rolled idempotent migrator with pressly/goose (pure Go, works
with modernc.org/sqlite, CGO_ENABLED=0) so migrations are versioned and the boot
path can run to the latest version. The existing steps (config, oidc_client,
custom_proxy + its https/authelia columns) become ordered Go migrations; they
stay idempotent so a pre-goose device with no goose_db_version table upgrades
cleanly.

Multiple redirect URIs per OIDC client now live in their own oidc_redirect_uri
table (client_id, redirect_uri) — no delimiter, so a redirect URI may contain
any character (commas are legal in URLs). Migration v6 creates the table,
backfills from the legacy oidc_client.redirect_uri, and drops that column.

Tests use the real migration code via MigrateTo(version): bring the DB to the
pre-change schema, insert a legacy client row, migrate to latest, and assert the
row moved to oidc_redirect_uri and the column is gone. Existing custom_proxy
legacy-upgrade tests still pass (goose runs the idempotent steps over partial
legacy schemas).
@cyberb cyberb changed the title oidc: support multiple redirect URIs per client oidc: multiple redirect URIs per client + goose migrations Jun 4, 2026
cyberb added 2 commits June 5, 2026 07:10
The appcenter 'files' search now also surfaces Nextcloud because its store
description changed to 'Access & share your files from any device.' and the
filter intentionally matches description (unit-tested via 'photos' -> PhotoPrism).
That is store-metadata drift, not a UI regression, so the stale stable #2884
baseline no longer matches. Point the skip-build at the current latest stable
build so the visual-diff step skips the outdated baseline until stable rebuilds.
- authelia: read the redirect URI value in range instead of indexing twice
- drop TestOIDC_RedirectURIContainingCommaIsPreserved (moot now that URIs live
  in a row-per-URI table with no delimiter)
- add TestMigrator_UpgradesPreGooseDbWithoutVersionTable: simulate a pre-goose
  device (legacy oidc_client with redirect_uri, no goose_db_version), run
  Migrate(), assert the row is moved to oidc_redirect_uri and the column dropped
@cyberb cyberb merged commit 9652096 into master Jun 5, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant