Skip to content

Build the OpenID Connect sign-in into GrandNode and remove its plugin - #978

Merged
KrzysztofPajak merged 8 commits into
developfrom
feature/oidc-login-in-core
Oct 23, 2026
Merged

KrzysztofPajak merged 8 commits into
developfrom
feature/oidc-login-in-core

Conversation

@KrzysztofPajak

Copy link
Copy Markdown
Member

Builds the OpenID Connect browser sign-in into GrandNode and removes the plugin Authentication.OpenIdConnect
Type: feature

Before running this branch: delete src/Web/Grand.Web/Plugins/Authentication.OpenIdConnect/ (build output of the removed plugin, not tracked). A leftover Authentication.OpenIdConnect.dll registers the OidcLogin scheme a second time and startup fails with "Scheme already exists: OidcLogin".

Issue

The browser sign-in with the identity provider lived in the plugin Authentication.OpenIdConnect, but core already owned everything around it: the resource server, the account links, the provider sessions, SsoOnly, SignInMode = ProviderOnly, break-glass accounts and every OidcLogin setting in appsettings. Core even hard-coded the plugin's system name. As a result, a core security mode (ProviderOnly) worked only while a plugin was installed: uninstalling or disabling the plugin left only break-glass accounts able to sign in. The configuration was also split between appsettings and a plugin settings row that held a single value (DisplayOrder).

OpenID Connect is a protocol, like cookie and JWT bearer authentication, so it belongs in core. Vendor-specific providers (Google, Facebook) stay plugins.

Solution

  • Protocol code moved unchanged (namespaces and comments only) to Grand.Web.Common/Security/Oidc: the handler builder, token refresher, RP-initiated sign-out, back-channel logout endpoint and logout-token validator, profile synchronizer, identity mapper, session data and the OidcLoginProvider.
  • New Grand.Web.Common/Startup/OidcLoginStartup. The provider, the token refresher and the sign-out provider are registered only when OidcResourceServer:Authority and OidcLogin:ClientId are set (OidcLoginConfig.IsConfigured). The HTTP client (no redirects), the logout-token validator (singleton) and the profile synchronizer are always registered, because their endpoints are always mapped. Lifetimes are the same as in the plugin. The startup priority is 502 (the plugin used 10); there is no ordering dependency.
  • Storefront: OidcLoginController (base BaseController as before, so no public-store or closed-store filters were added), the view component, the views Views/OidcLogin/* (themes can now override them) and the models OidcFailedModel/OidcLinkModel are in Grand.Web. The routes are in Grand.Web/Endpoints/OidcLoginEndpointProvider. URLs are byte-identical and never take a language prefix: /signin-oidc, /oidc/login, /oidc/callback, /oidc/link, /oidc/link/complete, /oidc/failed, POST /oidc/backchannel-logout.
  • The method name stays ExternalAuth.OpenIdConnect, so existing account links, the link-id hash and "My account" work unchanged. The Active toggle in Configuration → External authentication still applies.
  • The plugin's configuration page is replaced by a read-only "OpenID Connect" card on Configuration → External authentication. It shows the Authority and the ClientId, and only whether a client secret is set, never the secret. The grid's configure link of the OpenID Connect row opens that card. DisplayOrder moved to appsettings as OidcLogin:DisplayOrder (default 0).
  • OidcSignInModeCheck no longer checks for an installed plugin. Log texts and comments that named the plugin are reworded.
  • A back-channel logout sent to a store without OpenID Connect sign-in is answered like a refused token (400 The logout token was refused), so an outsider cannot tell whether the feature is set up. The log names the reason.
  • Resources: Plugins.ExternalAuth.OpenIdConnect.* became Account.Oidc.* (storefront) and Admin.Plugins.ExternalAuthenticationMethods.Oidc.* (admin card). They are in DefaultLanguage.xml and in Upgrade/en_250.xml, which the existing 2.5 migration imports. No new migration: 2.5 is unreleased.
  • docs/oidc-resource-server.md describes the built-in sign-in, with no plugin installation steps.

Breaking changes

None for released versions: the plugin, its settings and its resource keys exist only on develop (2.5).

For a develop installation that had the plugin installed:

  • Delete src/Web/Grand.Web/Plugins/Authentication.OpenIdConnect/ (see the note at the top).
  • The ExternalAuth.OpenIdConnect entry in App_Data/InstalledPlugins.cfg is harmless and can be removed.
  • The stored row of the deleted OidcLoginSettings class is no longer read; set OidcLogin:DisplayOrder instead.
  • A database already at 2.5 shows raw Account.Oidc.* keys until App_Data/Resources/Upgrade/en_250.xml is imported again in Configuration → Languages → English → Import.

Code: OidcSignInModeCheck.Problem lost its IEnumerable<PluginInfo> parameter, and the plugin types are gone (the moved ones now live in Grand.Web.Common.Security.Oidc).

Testing

Unit tests: the solution builds. Grand.Web.Common.Tests, Grand.Web.Tests, Grand.Web.Admin.Tests, Grand.Business.Authentication.Tests and Grand.Modules.Tests pass. The plugin's tests moved with the code, and new tests cover OidcLoginStartup, the routes, the admin card and the unconfigured back-channel answer. OrderReportServiceTests.ProductsNeverSold_ExcludesSoldProducts (Checkout, not touched here) can fail in a full parallel run and passes alone; a clean develop run shows the same kind of flake.

End to end (done on Keycloak 26.8):

  1. Delete src/Web/Grand.Web/Plugins/Authentication.OpenIdConnect/ and build the solution. Start the store with OidcResourceServer:Enabled=true, OidcResourceServer:Authority=<realm URL>, OidcLogin:ClientId, OidcLogin:ClientSecret and, for the full flow, OidcLogin:SignInMode=ProviderOnly with one OidcLogin:BreakGlassAccounts entry. Expected: no "Scheme already exists" and no "ProviderOnly sign-in mode without a working OpenID Connect login" error in the log.
  2. Configuration → External authentication: the grid lists "OpenID Connect" (tick Active). The card shows the Authority, the ClientId and "set" for the secret. Configuration → Plugins has no OpenID Connect plugin.
  3. Storefront: /login redirects to the provider; signing in there returns to the store signed in. A user with an unverified e-mail ends on /oidc/failed. /admin/login?password=1 signs in the break-glass account only.
  4. Sign out in the store: the browser passes through the provider's end-session endpoint and returns signed out.
  5. Back-channel logout: configure http(s)://<store>/oidc/backchannel-logout at the provider and sign a user out there (Keycloak: Users → Sessions → Sign out). The user's store session ends; the log has Back-channel logout: 1 session(s) ended.
  6. Remove OidcLogin:ClientId and restart. There is no provider button and no grid row, and /oidc/login redirects to /oidc/failed. POST /oidc/backchannel-logout answers 400 The logout token was refused.

Copilot AI balanced review requested due to automatic review settings October 23, 2026 05:23

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Assert.AreEqual(400, context.Response.StatusCode);
//the answer of a refused token: an outsider learns nothing about whether the sign-in is set up
context.Response.Body.Position = 0;
var body = await new StreamReader(context.Response.Body).ReadToEndAsync();
@KrzysztofPajak
KrzysztofPajak merged commit 49e4c32 into develop Oct 23, 2026
9 checks passed
@KrzysztofPajak
KrzysztofPajak deleted the feature/oidc-login-in-core branch October 23, 2026 14:32
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.

2 participants