Use multiple identity providers from a Blazor WASM ASP.NET Core App secured using BFF

This post shows how to implement a Blazor WASM UI hosted in an ASP.NET Core application using multiple identity providers to authenticate. Two confidential OpenID Connect code flow clients with PKCE are used to implement the Blazor authentication. The Blazor WASM and the ASP.NET Core application are a single security context. This is implemented using the backend for frontend security architecture.

Code: https://github.com/damienbod/ApiJwtWithTwoSts

Setup

Two identity providers are implemented using OpenIddict with ASP.NET Core Identity. Any OpenID Connect identity provider can be used here. The identity provider implements a confidential client requiring a secret. The OpenID Connect code flow is used to implement the authentication flow.


The Blazor application was setup using the Blazor.BFF.OpenIDConnect.Template which can be installed and used from Visual Studio or the dotnet cmd tool. The template creates an ASP.NET Core application with a WASM view and default definitions for OIDC code flow.

Blazor authentication

The Blazor applicaiton requires two OIDC client implementations. The user of the application can choose the required IDP. The DefaultChallengeScheme is set to “UNKNOWN” and this has no definition. The login or sign-in needs to set the authentication scheme for the challenge. The redirect Url and the logout URL needs to be explicitly set and cannot match a definition from a different challenge.

services.AddAuthentication(options =>
{
    options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
    options.DefaultChallengeScheme = "UNKNOWN";
})
.AddCookie()
.AddOpenIdConnect("T1", options =>
{
    configuration.GetSection("OpenIDConnectSettingsT1").Bind(options);

    options.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
    options.CallbackPath = "/signin-oidc-t1";
    options.SignedOutCallbackPath = "/signout-callback-oidc-t1";
    options.ResponseType = OpenIdConnectResponseType.Code;
    options.SaveTokens = true;
    options.Scope.Add("profile");
    options.GetClaimsFromUserInfoEndpoint = true;
    options.TokenValidationParameters = new TokenValidationParameters
    {
        NameClaimType = "name",
        RoleClaimType = "role"
    };
})
.AddOpenIdConnect("T2", options =>
{
    configuration.GetSection("OpenIDConnectSettingsT2").Bind(options);

    options.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
    options.ResponseType = OpenIdConnectResponseType.Code;
    options.CallbackPath = "/signin-oidc-t2";
    options.SignedOutCallbackPath = "/signout-callback-oidc-t2";
    options.SaveTokens = true;
    options.Scope.Add("profile");
    options.GetClaimsFromUserInfoEndpoint = true;
    options.TokenValidationParameters = new TokenValidationParameters
    {
        NameClaimType = "name",
        RoleClaimType = "role"
    };
});

The AccountController implements the login and the logout requests. The login needs to force the login for the correct scheme. I named my schemes T1 and T2.

[HttpGet("LoginT1")]
public IActionResult T1(string returnUrl)
{
	var redirectUrl = !string.IsNullOrEmpty(returnUrl) ? returnUrl : "/";
	return Challenge(new AuthenticationProperties 
		{ RedirectUri = redirectUrl }, "T1");
}

[HttpGet("LoginT2")]
public IActionResult T2(string returnUrl)
{
	var redirectUrl = !string.IsNullOrEmpty(returnUrl) ? returnUrl : "/";
	return Challenge(new AuthenticationProperties 
		{ RedirectUri = redirectUrl }, "T2");
}

The CallbackPath needs to be unique in the application and a separated definition is required for each identity provider.

Blazor client logout

The logout works a bit like the login. You need to have a unique SignedOutCallbackPath definition for each scheme. I added a new claim to the id_token token from the identity provider. I added this claim in the OpenIddict server. Once the user is authenticated, the idp claim can used to verify which identity provider was used for authentication. The claim is required to logout which sends a request to the correct identity provider.

[ValidateAntiForgeryToken]
[Authorize]
[HttpPost("Logout")]
public IActionResult Logout()
{
	var authProperties = new AuthenticationProperties
	{
		RedirectUri = "/"
	};

	// custom claims added to idp, you need to 
	// implement something on your idp for this
	var usedT1ForAuthn = User.Claims.Any(
		idpClaim => idpClaim.Type == "idp" && idpClaim.Value == "T1");
	var usedT2ForAuthn = User.Claims.Any(
		idpClaim => idpClaim.Type == "idp" && idpClaim.Value == "T2");

	if(usedT1ForAuthn) 
		return SignOut(authProperties, 
		CookieAuthenticationDefaults.AuthenticationScheme, "T1");

	if (usedT2ForAuthn)
		return SignOut(authProperties, 
		CookieAuthenticationDefaults.AuthenticationScheme, "T2");

	return SignOut(authProperties, 
	CookieAuthenticationDefaults.AuthenticationScheme);
}

The custom “idp” is also used to display identity provider in the Blazor WASM UI.

<Authorized>
	<strong>@context?.User?.Identity?.Name idp: 
		@context?.User?.Claims.FirstOrDefault(c => c.Type == "idp")?.Value
	</strong>
	<form method="post" action="api/Account/Logout">
		<AntiForgeryTokenInput />
		<button class="btn btn-link" type="submit">Signout</button>
	</form>
</Authorized>

The user can login using different identity providers.

The name claim and the identity provider are displayed in the UI.

Notes

Using multiple identity providers in ASP.NET Core is a simple implementation once you understand the scheme concept and the authentication flow events used. If using the one line wrappers authentication clients from the different providers, you need to override the default implementation hidden inside these libraries. Most of these packages provide extension methods and contracts for this, but sometimes it is more simple to use the standard ASP.NET Core implementation. This works for any conform OpenID Connect server.

Links

https://documentation.openiddict.com/configuration/claim-destinations.html

https://learn.microsoft.com/en-us/aspnet/core/security/authentication/claims

https://www.nuget.org/packages/Blazor.BFF.OpenIDConnect.Template

7 comments

  1. […] Use multiple identity providers from a Blazor WASM ASP.NET Core App secured using BFF [#.NET #ASP.NET Core #dotnet #OAuth2 #Security #Web #aspnetcore #OIDC #OpenId connect] […]

  2. […] Use multiple identity providers from a Blazor WASM ASP.NET Core App secured using BFF (Damien Bowden) […]

  3. Greg Wruck's avatar

    Great article. I have implemented and am experimenting with it. It solves an awful lot of problems. I’m just trying to get my head around how this works in a multi-user environment. Am I correct in assuming that the web server hosts the “single page” _Host file and it will maintain the HttpContext for each user that has logged in since we can’t do it in the Blazor Wasm client context? Are you able to provide some commentary around this?

    1. damienbod's avatar

      Hi greg, thanks, yes, the session is stored in a cookie. This is used then to apply the session as required. It is like any other server rendered application then.

      Greetings Damien

  4. Greg Wruck's avatar

    Thanks for response. The conceptual problem I had was how the UserController “knew” which user was making the request in the line public IActionResult GetCurrentUser() => Ok(CreateUserInfo(User)); from the Github repository. After a bit more research, I found that it is using the information passed with the cookie in the http pipeline.

  5. seanterry42's avatar
    seanterry42 · · Reply

    Damien, would you consider doing one of these for an auto render mode Blazor app in .NET 8? I haven’t been able to get an OIDC identity provider working without using Identity, which feels like overkill-and-a-half.

    1. damienbod's avatar

      Hi Sean

      Not yet, I have not been able to get this to run in a stable and secure way. I’m waiting a bit for Microsoft to release a template. Using ASP.NET Core Identity is not an option for 99% of my solutions. Until this, I use the existing as it is and just update to .NET 8

      Greetings Damien

Leave a reply to Dew Drop – February 14, 2023 (#3879) – Morning Dew by Alvin Ashcraft Cancel reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.