Blazor Server applications offer a robust platform for building interactive web UIs with .NET. When it comes to authentication, while ASP.NET Core provides excellent built-in support for standards like OpenID Connect (OIDC), real-world scenarios often involve integrating with older, legacy Identity Providers (IdPs). These IdPs might not fully adhere to modern specifications, lack features like discovery endpoints, or require specific custom parameter handling, making the standard middleware challenging to use directly.
This article provides a comprehensive guide to implementing a custom AuthenticationStateProvider
in a Blazor Server application to handle an OAuth 2.0 Authorization Code Flow with PKCE (Proof Key for Code Exchange). This approach grants you the fine-grained control necessary to interact successfully with legacy IdPs that may have non-standard behaviors, ensuring secure and effective authentication.
We will cover the creation of the PKCE parameters, redirection to the IdP, handling the callback, managing tokens, and populating the user’s ClaimsPrincipal
. The focus will be on the flexibility a custom solution provides when dealing with the idiosyncrasies of older authentication systems.
Core Concepts Refresher
Before diving into the implementation, let’s briefly revisit the key components:
- Blazor Server: Your application logic runs on the server, with UI updates pushed to the client via SignalR. Authentication state is managed server-side. For more details, see Blazor hosting models.
AuthenticationStateProvider
: A Blazor service that provides the current user’s authentication state (ClaimsPrincipal
). Customizing it allows defining how Blazor understands who is logged in. Learn more from the ASP.NET Core documentation onAuthenticationStateProvider
.- OAuth 2.0 Authorization Code Flow: A standard flow where the user authenticates with an IdP, which then provides an authorization code to your application. Your application exchanges this code for an access token (and potentially a refresh token).
- PKCE (Proof Key for Code Exchange): An extension to the Authorization Code Flow that enhances security by preventing authorization code interception attacks. It involves a client-generated
code_verifier
and a transformedcode_challenge
. - Legacy IdP: An older Identity Provider that might not fully support modern standards (e.g., OIDC discovery, standard PKCE parameter names, standard claim types), requiring a more tailored client implementation.
ClaimsPrincipal
: Represents the authenticated user, containing a collection of claims. See the ClaimsPrincipal Class documentation.
Project Setup and Prerequisites
Ensure you have a Blazor Server project. You’ll primarily be working with C# and ASP.NET Core services. For communication with the IdP, HttpClient
will be used, typically managed via IHttpClientFactory
.
Add necessary NuGet packages if not already present (though most are part of the default Blazor Server template):
Microsoft.AspNetCore.Components.Authorization
System.Net.Http.Json
(for convenience withHttpClient
)
You will also need configuration details for your legacy IdP. Store these securely, for example, in appsettings.json
for development, as described in the ASP.NET Core Configuration documentation:
|
|
1. PKCE Helper Utilities
PKCE requires generating a code_verifier
(a random string) and a code_challenge
(typically a SHA256 hash of the verifier, Base64 URL-encoded).
Create a utility class for this:
|
|
This class provides methods to create the necessary PKCE parameters. The code_verifier
will need to be stored temporarily (server-side) between initiating the login and handling the callback.
2. Token Storage Service
In Blazor Server, tokens are sensitive and must be stored securely on the server, associated with the user’s circuit/session. For simplicity, we’ll create a scoped service to hold tokens. In a production scenario, you might consider more robust storage like IDistributedCache
if sessions can span multiple server instances.
|
|
This service will be registered as scoped in Program.cs
. Each user circuit will get its own instance.
3. Implementing the Custom AuthenticationStateProvider
This is the core of our solution. Let’s call it LegacyIdPAuthenticationStateProvider
.
|
|
Important Note on _pendingCodeVerifier
and _pendingState
: The use of static
fields for _pendingCodeVerifier
and _pendingState
is a simplification for this example and is not safe for concurrent users in a production Blazor Server application. Each user’s PKCE state must be isolated. Consider using ASP.NET Core Session state or IDistributedCache
with unique keys tied to the user’s interaction flow.
4. Registering Services in Program.cs
|
|
The /auth/callback
endpoint is crucial. It receives the authorization code from the IdP and triggers the token exchange process.
5. UI Integration (Login/Logout Controls)
In your MainLayout.razor
or a dedicated login component. Ensure App.razor
wraps the Router
with <CascadingAuthenticationState>
.
|
|
And a simple page to display user claims, protected by the [Authorize]
attribute:
Pages/UserProfile.razor:
|
|
Addressing Legacy IdP Quirks
The primary benefit of a custom AuthenticationStateProvider
is its adaptability:
- Non-Standard Endpoints/Parameters: You control the exact URLs and parameters sent to the IdP in
InitiateLogin
andHandleAuthCallbackAsync
. - Token Response Variations: The
HandleAuthCallbackAsync
method parses the JSON response. AdjustJsonSerializer.Deserialize
orTryGetProperty
calls for IdP-specific token property names. - Claim Mapping: Manually create
Claim
objects from the token or UserInfo response, mapping proprietary claim names (e.g.,legacy_user_id
) to standardClaimTypes
. - PKCE Flexibility: If the IdP expects
code_challenge_method=PLAIN
(not recommended) or has unusualcode_verifier
requirements,PkceUtil
and request parameters can be adapted. - Client Secrets with PKCE: Your
tokenRequestBody
can includeclient_secret
if the IdP mandates it for confidential clients (like Blazor Server) even when using PKCE. Store secrets securely.
Debugging Tips
- Logging: Add extensive logging in your
LegacyIdPAuthenticationStateProvider
and the/auth/callback
endpoint. - Browser Developer Tools: Use the Network tab to trace redirects, inspect parameters, and view IdP responses.
- IdP Logs: If accessible, IdP logs are invaluable for diagnosing server-side IdP errors.
- Test Incrementally: Verify each step: authorization redirect, callback, code exchange, token parsing, and finally claim population.
Conclusion
Implementing a custom AuthenticationStateProvider
in Blazor Server, while more involved than using built-in middleware, offers unparalleled flexibility for integrating with OAuth 2.0 / PKCE flows, especially when dealing with legacy Identity Providers. By taking control of each step – from PKCE generation and IdP redirection to token exchange and claims processing – you can effectively navigate non-standard behaviors and ensure secure authentication for your application.
Remember to handle tokens securely on the server, implement robust PKCE state management (avoiding static fields for per-user state in production), and adapt the claim mapping logic to match your specific legacy IdP’s conventions. This custom approach empowers you to bring modern security practices like PKCE to systems that might not fully support the latest standards out-of-the-box.