Skip to content

Hand identity to an OIDC provider - #127

Merged
ChrisonSimtian merged 1 commit into
developfrom
feat/oidc
Aug 23, 2026
Merged

Hand identity to an OIDC provider#127
ChrisonSimtian merged 1 commit into
developfrom
feat/oidc

Conversation

@ChrisonSimtian

Copy link
Copy Markdown
Contributor

Closes #48 — the last open issue, and the last item on the README's "not there yet" list.

No IAuthenticationProvider port, deliberately

The issue proposed one. The shipped local-auth work decided against it and that reasoning still holds: local credentials are a verification concern that fits a port (ILocalCredentialStore + IPasswordHasher), while OIDC is a redirect/token protocol owned end to end by framework middleware. A port around AddOpenIdConnect would have exactly one implementation and abstract nothing. So the pluggable thing stays the scheme, chosen in the composition root, and both land on the same cookie and ClaimsPrincipal — nothing downstream of sign-in changed.

Two independent checks on who gets in

Your identity blueprint already states the principle — authenticated must never imply authorised, because the Plex source auto-enrols friends. This adopts it rather than restating it:

  1. The provider decides who may obtain a token for the application at all.
  2. Krautwatch re-checks the groups claim against Auth:Oidc:RequiredGroups in OnTokenValidated, and fails the sign-in rather than admitting a principal that then bounces off every page.

RequiredGroups defaults to empty — admitting anyone the provider authenticated — because this has to work against IdPs with no group model, and one homelab's access tiers must not become a precondition for the feature.

The three details that would otherwise read as bugs

  • /login challenges instead of rendering a password form. A form that cannot work is worse than a redirect.
  • /logout is federated. Clearing only our cookie leaves the provider session up, so the next visit signs straight back in — which looks like logout is broken rather than like SSO working.
  • First-run setup stops demanding an administrator. SetupStateHandler meant "no local admin exists", which under OIDC is true forever: the wizard would never complete, /login would claim the instance was unconfigured, and startup would log a setup link leading to a step nobody can finish. It now takes a LocalAdminRequirement, absent meaning required, so nothing else changes.

RequiredGroups also accepts "A,B", because configuration binds lists only from indexed keys and a compose .env has nowhere to put an array. Silently ignoring the string form would leave an operator believing they had restricted access when they had not.

A regression this also fixes

#122 broke the *arr env bootstrap, and it shipped in v0.6.0. My diff there inserted the bootstrap call under the if (authProvider is "local") guard that belonged to the setup-link log:

 if (authProvider is "local")
-    await LogSetupLinkIfRequiredAsync(app);
+    await BootstrapArrInstancesAsync(app);
+await LogSetupLinkIfRequiredAsync(app);

So on oidc or none the bootstrap silently did nothing, and the setup link was logged where it leads nowhere. Both corrected, with the reason recorded in a comment so it does not come back.

Verified

Twelve tests over the two rules that are ours — group admission (member, non-member, no requirement, casing, empty claim) and the setup-state change — plus the comma-separated parsing. The protocol itself is framework middleware and not ours to test. PageAuthorizationSpecs still green: Login/Logout/Setup remain the only anonymous pages.

Auth__Provider and the four Auth__Oidc__* settings are AppHost parameters, so they appear in the generated compose and .env; confirmed by regenerating the bundle. README and self-hosting guide updated with the redirect URI, scopes, and why the second check is worth setting.

./build.sh Test green: 512 passing. Plan in docs/plans/2026-08-23 - oidc authentication.md.

Not tested against a live IdP yet — the Authentik registration is a separate PR on the Homelab repo, and I will report the end-to-end result rather than assume it.

Auth:Provider = oidc was the one value that did nothing. It now adds the OpenID
Connect handler beside the existing cookie, so pages, [Authorize] and the
cascading auth state are untouched — both schemes land on the same principal.

Access is checked twice on purpose: the provider decides who may get a token, and
Auth:Oidc:RequiredGroups re-checks the groups claim here. Neither layer relies on
the other being right. Empty admits anyone the provider authenticated, so IdPs
with no group model still work.

/login challenges instead of showing a password box that cannot work, /logout is
federated so the next visit does not sign straight back in, and first-run setup
stops demanding an administrator that will never exist.

Also fixes a regression from #122, released in v0.6.0: that diff moved the *arr
env bootstrap under the `local` provider guard meant for the setup link, so the
bootstrap silently did nothing unless local auth was configured.

Closes #48

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@ChrisonSimtian ChrisonSimtian added the enhancement New feature or request label Aug 23, 2026
@ChrisonSimtian
ChrisonSimtian merged commit 6b46171 into develop Aug 23, 2026
1 check passed
@ChrisonSimtian
ChrisonSimtian deleted the feat/oidc branch August 23, 2026 04:53
@ChrisonSimtian ChrisonSimtian mentioned this pull request Aug 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Pluggable authentication: an auth port with a simple built-in provider (standalone) and OIDC as the real target

1 participant