Moonglade has supported two authentication approaches for a long time: a built-in local administrator account and Microsoft Entra ID. The Entra ID integration worked well, especially for blogs hosted on Azure, but it also introduced an architectural limitation: the application understood a particular identity provider instead of the protocol behind it.

In PR #1001, I replaced the Entra-specific authentication path with a standards-based OpenID Connect implementation.

This does not mean Moonglade no longer supports Entra ID. Entra ID remains supported—as an OpenID Connect provider. The important change is that Moonglade is no longer designed around Azure identity alone.

Why Change a Working Authentication System?

The previous design used Microsoft.Identity.Web and configuration concepts specific to Entra ID, including tenant ID, instance, and domain.

That was convenient, but it made one identity provider part of the application’s architecture. Adding support for another provider would likely have required another authentication mode, another configuration model, and more provider-specific branches.

The new design reduces the authentication choices to two application-level modes:

  • Local
  • OpenIdConnect

Moonglade now cares about standard OIDC concepts such as authority, client ID, scopes, callback paths, and claims. Provider-specific details stay at the identity provider.

This creates a cleaner boundary: Moonglade defines what it needs from an identity system, while the OIDC provider decides how users authenticate.

A Provider-Neutral Configuration

The old Authentication:EntraID section has been replaced with Authentication:OpenIdConnect:

{
  "Authentication": {
    "Provider": "OpenIdConnect",
    "OpenIdConnect": {
      "Authority": "https://identity.example.com/",
      "ClientId": "moonglade",
      "CallbackPath": "/signin-oidc",
      "SignedOutCallbackPath": "/signout-callback-oidc",
      "NameClaimType": "name",
      "Scopes": [ "openid", "profile", "email" ],
      "AllowedSubjects": []
    }
  }
}

The client secret is intentionally excluded from the configuration file. It should come from a secure external source, such as an environment variable:

Authentication__OpenIdConnect__ClientSecret

Any provider used with Moonglade must publish discovery metadata over HTTPS and support the authorization code flow with PKCE.

Using the Standard ASP.NET Core OIDC Handler

The implementation now uses Microsoft.AspNetCore.Authentication.OpenIdConnect directly. The OIDC handler performs the external authentication, while Moonglade continues to use an encrypted application cookie for its local session.

The essential registration looks like this:

services.AddAuthentication(options =>
{
    options.DefaultScheme =
        CookieAuthenticationDefaults.AuthenticationScheme;
    options.DefaultChallengeScheme =
        BlogAuthSchemas.OpenIdConnect;
})
.AddCookie(
    CookieAuthenticationDefaults.AuthenticationScheme,
    ConfigureApplicationCookie)
.AddOpenIdConnect(BlogAuthSchemas.OpenIdConnect, options =>
{
    options.Authority = oidc.Authority;
    options.ClientId = oidc.ClientId;
    options.ClientSecret = oidc.ClientSecret;

    options.ResponseType = OpenIdConnectResponseType.Code;
    options.UsePkce = true;
    options.RequireHttpsMetadata = true;

    options.SignInScheme =
        CookieAuthenticationDefaults.AuthenticationScheme;
    options.MapInboundClaims = false;
    options.SaveTokens = false;
});

A few choices here are deliberate.

Authorization code flow with PKCE avoids the older implicit-flow model. HTTPS metadata is mandatory, claim names are kept in their original OIDC form, and access or refresh tokens are not persisted in the application cookie. Moonglade needs an authenticated identity, not long-term access to the provider’s APIs.

Sign-out now clears the Moonglade cookie and also invokes the OIDC provider’s logout endpoint when one is advertised in its metadata.

Authentication Is Not Authorization

The most important part of this change is not the replacement of a package. It is the separation of two questions:

  1. Who is the user?
  2. Is this user allowed to administer the blog?

A successful OIDC sign-in answers only the first question. It must not automatically grant access to the Admin Portal.

Moonglade now has one administrator policy named MoongladeAdministrator. Its behavior depends on the configured authentication mode:

services.AddAuthorizationBuilder()
    .AddPolicy(BlogAuthSchemas.AdministratorPolicy, policy =>
    {
        policy.RequireAuthenticatedUser();

        if (authentication.Provider == AuthenticationProvider.Local)
        {
            policy.RequireRole("Administrator");
            return;
        }

        var allowedSubjects = oidc.AllowedSubjects
            .ToHashSet(StringComparer.Ordinal);

        policy.RequireAssertion(context =>
            context.User.FindAll("sub")
                .Any(claim => allowedSubjects.Contains(claim.Value)));
    });

Local accounts continue to use the existing administrator role. OIDC identities are checked against an explicit allowlist of sub claims.

The same policy protects Admin Razor Pages, administrative API controllers, and UI elements that expose administrative actions. This keeps the authorization rule centralized instead of scattering provider checks throughout the Web layer.

Why Authorize with sub?

OIDC providers commonly return claims such as email address, display name, or preferred username. These values are useful for presentation, but they are poor authorization keys: they may change, be reassigned, or have different semantics across providers.

The sub claim is the subject identifier defined by OpenID Connect. Within the issuer boundary validated by the OIDC handler, it provides the stable identifier needed for an administrator allowlist.

Moonglade therefore does not grant administrator access based on email or display name. Every administrator must have an exact sub value in AllowedSubjects.

Some providers use pairwise subject identifiers, so changing the provider or client registration may produce a different sub for the same person. This is expected and must be considered during migration.

Bootstrapping the First Administrator

A new deployment starts safely with an empty allowlist:

"AllowedSubjects": []

Users can still complete OIDC authentication, but nobody can access the Admin Portal or administrative APIs.

To discover the first administrator identity, Moonglade provides:

/auth/identity

An authenticated OIDC user can open this endpoint to retrieve their own issuer, subject, and display name. The endpoint does not grant any permission and cannot inspect another user’s identity.

The administrator can then copy the returned subject into the configuration:

"AllowedSubjects": [
  "administrator-subject-1"
]

After restarting the application and signing in again, the administrator policy can authorize that identity.

When local authentication is configured, /auth/identity returns HTTP 404 because the endpoint is relevant only to the OIDC bootstrap process.

Failing Fast on Invalid Configuration

External authentication problems are often discovered only when someone tries to sign in. Moonglade now validates its OIDC configuration during startup instead.

The validator checks that:

  • The authority is an absolute HTTPS URL.
  • Client ID and client secret are present.
  • Callback paths are application-relative.
  • The scopes include openid.
  • The name claim type is configured.
  • The subject allowlist contains no blank entries.

This uses ASP.NET Core options validation with ValidateOnStart(). A deployment with incomplete authentication configuration fails immediately with a useful error instead of starting successfully with a broken sign-in flow.

Entra ID Still Works

Existing Entra ID deployments can continue using Microsoft identities through Entra’s standard OIDC endpoints. For a single-tenant blog, the authority should use the tenant-specific v2 endpoint:

https://login.microsoftonline.com/{tenant-id}/v2.0

The existing application client ID can usually be retained, but the application needs a Web client secret and registered sign-in and sign-out callback URLs.

No database migration is required. This is an application configuration and authentication architecture change.

A Smaller Dependency and a Larger Possibility

The goal of this migration was not to replace Microsoft Entra ID with another identity provider. It was to stop making any single provider part of Moonglade’s core authentication design.

Entra ID remains an excellent choice, particularly for Azure-hosted deployments. It now participates through the same standard protocol boundary as other compatible OIDC providers.

The resulting architecture is easier to understand:

  • OIDC authenticates the user.
  • Moonglade’s cookie maintains the application session.
  • A centralized policy authorizes administrators.
  • Stable subject identifiers define who may manage the blog.

By designing around a standard instead of a vendor, Moonglade keeps its existing Azure capabilities while gaining the freedom to work beyond Azure.