feat(api): example OIDC single sign-on implementation - #1387
Closed
Foowy wants to merge 11 commits into
Closed
Conversation
Contributor
Author
|
and back to appeasing the sonarqube gods |
revam
reviewed
Jul 16, 2026
revam
reviewed
Jul 16, 2026
Foowy
force-pushed
the
feature/oidc-auth
branch
2 times, most recently
from
July 18, 2026 16:35
476f510 to
bce8d62
Compare
revam
reviewed
Jul 19, 2026
revam
reviewed
Jul 19, 2026
revam
reviewed
Jul 19, 2026
revam
reviewed
Jul 19, 2026
revam
reviewed
Jul 19, 2026
revam
reviewed
Jul 19, 2026
Foowy
force-pushed
the
feature/oidc-auth
branch
3 times, most recently
from
July 29, 2026 14:53
19d1857 to
3dad450
Compare
Foowy
force-pushed
the
feature/oidc-auth
branch
4 times, most recently
from
August 10, 2026 00:46
b9e70c2 to
ea4003d
Compare
Contributor
Author
|
Added documentation and further changes to match current specs and safeguards |
Foowy
force-pushed
the
feature/oidc-auth
branch
3 times, most recently
from
August 16, 2026 16:34
599d4cc to
666a806
Compare
Foowy
force-pushed
the
feature/oidc-auth
branch
from
August 23, 2026 16:25
666a806 to
99d56cc
Compare
Adds an optional OIDC SSO login flow, additive to existing local login, with account-linking, hardened redirect/nonce handling, and settings integration.
…xpiry Fixes an account-takeover vector where first-time SSO sign-in linked to any existing local account matching the SSO preferred_username or verified email. Sign-in now only resolves an account that was already explicitly linked via a new authenticated `Link` endpoint; a matching `Unlink` endpoint is also added. - Removed username/email auto-matching on sign-in - Added `GET /Auth/Oidc/Link` (authenticated) and `POST /Auth/Oidc/Unlink` - Prefixed `ExternalAuthID` with the authority to prevent collisions if `Settings.Oidc.Authority` is ever changed - Dropped the local username from the WebUI redirect fragment - Simplified the `returnUrl`/`WebUIPublicPath` fallback logic - Matched the minted API token's expiry to the OIDC ID token's own `exp` claim instead of a fixed 30-day window
Extends BaseController like the rest of the v3 controllers instead of a bespoke User property override, per review. Also collapses the duplicated "is OIDC enabled" guard and provider-configuration try/catch that lived separately in StartAuthorizeAsync and Callback into one GetEnabledConfigurationAsync helper, addressing SonarCloud's duplication finding.
…rors - `Callback` now checks `claims is null` alongside `validationError` instead of asserting non-null with `!` - Token device-name key now includes the upstream subject, not just the authority - `Unlink` invalidates all tokens minted for that user under the linked provider, parsed from the stored `ExternalAuthID` prefix - Both `BadRequest` error paths in `Callback` now redirect to the WebUI like every other error case - Added opt-in `OidcSettings.AutoCreateUsers` (default off): sign-in provisions a new local account from the subject with a random password when no link exists, hard-aborting if the username is taken
…findings Validated returnUrl on Challenge/Link with Url.IsLocalUrl before it flows through the signed state payload — an unvalidated absolute returnUrl was an open-redirect path that could exfiltrate the minted API token via the Callback's URL fragment. Logged the discovery-document fetch exception in GetEnabledConfigurationAsync instead of swallowing it. Added a unique index on JMMUser.ExternalAuthID across all three DB backends to close a race window between concurrent Link() calls. Removed redundant null-forgiving operators flagged by SonarCloud (S8970) in OidcAuthController and AuthTokensRepository.
Addressed four findings from a security review of the OIDC example implementation: - Added PKCE (`S256`) to the authorization-code exchange — required by OAuth 2.1 for all clients, confidential or not, and closes the code-interception gap even with a client secret in play. - `GetProviderConfigurationAsync` now keeps one long-lived `ConfigurationManager` per configured authority instead of instantiating a new one (and re-fetching the discovery document) on every single `Challenge`/`Callback` call. - `ValidIssuer` is now pinned to `Settings.Oidc.Authority` rather than trusting whatever issuer the discovery document self-declares; `GetEnabledConfigurationAsync` rejects the exchange up front if the document's `issuer` doesn't match the configured authority. - Added `OidcSettings.PublicUrl`, an explicit admin-configured base URL used to build the fixed `redirect_uri`, replacing the previous derivation from the request's `Host`/`X-Forwarded-Proto` headers.
Covers IdP client registration, the settings-server.json fields (including the new PublicUrl), account linking, and the design rationale behind the hardening in the previous commit — PKCE, issuer pinning, discovery-document caching, and the fixed redirect_uri.
Caching the ConfigurationManager per authority (previous commit) dropped an implicit benefit of the old per-request instantiation: a JWKS key rotation on the IdP used to be picked up immediately, now it wouldn't be until the manager's own refresh interval elapsed, breaking every login in between. `Callback` now forces a refresh and retries validation once before giving up, matching what `OpenIdConnectHandler` does internally for the same scenario.
Worked step-by-step examples for Authentik, Keycloak, and Authelia client registration, plus a "Further reading" section linking the underlying specs (OIDC Core, PKCE RFC 7636, discovery RFC 8414) and each provider's own documentation, so a reader isn't left to trust the guide blind or guess at unfamiliar terms.
Replaced the two `!` null-forgiving uses on `settings.Authority` and `settings.PublicUrl` with explicit `ArgumentException.ThrowIfNullOrWhiteSpace` guards. Both values are already guaranteed non-null by `GetEnabledConfigurationAsync`'s early-return check, but that guarantee doesn't cross the method boundary for the compiler's nullable analysis — the guard makes the invariant explicit and gives the compiler (and Sonar) a real narrowing to reason about instead of a suppressed warning.
Foowy
force-pushed
the
feature/oidc-auth
branch
from
August 25, 2026 13:08
99d56cc to
46b6c28
Compare
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.



Summary
Example implementation of optional OIDC single sign-on, offered as a discussion starting point — not opinion-locked. See ShokoAnime/ShokoServer#1386 for the broader SSO conversation. Opened as draft specifically to be picked apart before considering it mergeable.
Settings.Oidc(disabled by default, config-file only — not exposed in the settings UI/API)OidcAuthController—/api/v3/Auth/Oidc/Challengeand/Callback, authorization-code flow with PKCE against the provider's discovery documentGET /Auth/Oidc/Link/POST /Auth/Oidc/Unlink) — no auto-matching by username/emailOidcSettings.AutoCreateUsers(default off) provisions a new local account on first sign-inexpclaim;Unlinkinvalidates all tokens minted under that provider linkServerStatus.OidcEnabledlets the WebUI conditionally show an SSO button without revealing provider detailsdocs/oidc-sso-setup.mdwalks through IdP registration and account linkingSecurity notes
S256); issuer pinned to configuredAuthorityrather than trusting the discovery document's self-declared issuerredirect_uribuilt from an explicitOidcSettings.PublicUrlsetting, not derived from request headersreturnUrlvalidated viaUrl.IsLocalUrlbefore being carried through signed state (closes an open-redirect path)JMMUser.ExternalAuthID(all DB backends) closes a concurrent-link raceTest plan
dotnet build Shoko.Server.sln— 0 errorsdotnet test Shoko.Tests/Shoko.Tests.csproj— 517 passed, 0 failedRebase history
master@4f0e3c52b(11 commits), now at666a8063fupstream/master@b6fdba59b(11 commits), now at99d56cc1a