Skip to content

feat(api): example OIDC single sign-on implementation - #1387

Closed
Foowy wants to merge 11 commits into
ShokoAnime:masterfrom
Foowy:feature/oidc-auth
Closed

feat(api): example OIDC single sign-on implementation#1387
Foowy wants to merge 11 commits into
ShokoAnime:masterfrom
Foowy:feature/oidc-auth

Conversation

@Foowy

@Foowy Foowy commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

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.

  • Adds Settings.Oidc (disabled by default, config-file only — not exposed in the settings UI/API)
  • OidcAuthController/api/v3/Auth/Oidc/Challenge and /Callback, authorization-code flow with PKCE against the provider's discovery document
  • Sign-in requires an explicit prior link (GET /Auth/Oidc/Link / POST /Auth/Oidc/Unlink) — no auto-matching by username/email
  • Optional OidcSettings.AutoCreateUsers (default off) provisions a new local account on first sign-in
  • Minted token expiry matches the OIDC ID token's own exp claim; Unlink invalidates all tokens minted under that provider link
  • ServerStatus.OidcEnabled lets the WebUI conditionally show an SSO button without revealing provider details
  • docs/oidc-sso-setup.md walks through IdP registration and account linking

Security notes

  • PKCE (S256); issuer pinned to configured Authority rather than trusting the discovery document's self-declared issuer
  • redirect_uri built from an explicit OidcSettings.PublicUrl setting, not derived from request headers
  • returnUrl validated via Url.IsLocalUrl before being carried through signed state (closes an open-redirect path)
  • Discovery document cached per-authority with refresh-and-retry on validation failure, so IdP key rotation doesn't cause a sign-in outage
  • Unique index on JMMUser.ExternalAuthID (all DB backends) closes a concurrent-link race

Test plan

  • dotnet build Shoko.Server.sln — 0 errors
  • dotnet test Shoko.Tests/Shoko.Tests.csproj — 517 passed, 0 failed
  • Manual end-to-end test against a real OIDC provider (feedback on the flow itself would help shape this first)

Rebase history

  • 2026-08-16 16:41 UTC — rebased onto master @ 4f0e3c52b (11 commits), now at 666a8063f
  • 2026-08-23 16:23 UTC — rebased onto upstream/master @ b6fdba59b (11 commits), now at 99d56cc1a

@Foowy
Foowy force-pushed the feature/oidc-auth branch from c5b841f to d7532f5 Compare July 15, 2026 00:27
@Foowy

Foowy commented Jul 15, 2026

Copy link
Copy Markdown
Contributor Author

and back to appeasing the sonarqube gods

@Foowy
Foowy force-pushed the feature/oidc-auth branch from abce272 to a011a04 Compare July 15, 2026 11:04

@revam revam left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A few comments so far.

Comment thread Shoko.Server/API/v3/Controllers/OidcAuthController.cs Outdated
Comment thread Shoko.Server/API/v3/Controllers/OidcAuthController.cs Outdated
Comment thread Shoko.Server/API/v3/Controllers/OidcAuthController.cs Outdated
Comment thread Shoko.Server/API/v3/Controllers/OidcAuthController.cs Outdated
Comment thread Shoko.Server/API/v3/Controllers/OidcAuthController.cs Outdated
Comment thread Shoko.Server/API/v3/Controllers/OidcAuthController.cs Outdated
Comment thread Shoko.Server/API/v3/Controllers/OidcAuthController.cs Outdated
@Foowy
Foowy force-pushed the feature/oidc-auth branch from a011a04 to 82b3448 Compare July 16, 2026 17:20
Comment thread Shoko.Server/API/v3/Controllers/OidcAuthController.cs Outdated
@Foowy
Foowy force-pushed the feature/oidc-auth branch 2 times, most recently from 476f510 to bce8d62 Compare July 18, 2026 16:35
Comment thread Shoko.Server/API/v3/Controllers/OidcAuthController.cs Outdated
Comment thread Shoko.Server/API/v3/Controllers/OidcAuthController.cs Outdated
Comment thread Shoko.Server/API/v3/Controllers/OidcAuthController.cs
Comment thread Shoko.Server/API/v3/Controllers/OidcAuthController.cs Outdated
Comment thread Shoko.Server/API/v3/Controllers/OidcAuthController.cs
Comment thread Shoko.Server/API/v3/Controllers/OidcAuthController.cs
@Foowy
Foowy force-pushed the feature/oidc-auth branch 3 times, most recently from 19d1857 to 3dad450 Compare July 29, 2026 14:53
@Foowy
Foowy force-pushed the feature/oidc-auth branch 4 times, most recently from b9e70c2 to ea4003d Compare August 10, 2026 00:46
@Foowy

Foowy commented Aug 10, 2026

Copy link
Copy Markdown
Contributor Author

Added documentation and further changes to match current specs and safeguards

@Foowy
Foowy force-pushed the feature/oidc-auth branch 3 times, most recently from 599d4cc to 666a806 Compare August 16, 2026 16:34
@Foowy
Foowy force-pushed the feature/oidc-auth branch from 666a806 to 99d56cc Compare August 23, 2026 16:25
Foowy added 4 commits August 25, 2026 13:08
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
Foowy added 7 commits August 25, 2026 13:08
…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
Foowy force-pushed the feature/oidc-auth branch from 99d56cc to 46b6c28 Compare August 25, 2026 13:08
@sonarqubecloud

Copy link
Copy Markdown

@Foowy Foowy closed this Sep 4, 2026
@Foowy
Foowy deleted the feature/oidc-auth branch September 4, 2026 13:50
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.

3 participants