dotnet / aspnetcore

ASP.NET Core is a cross-platform .NET framework for building modern cloud-based web applications on Windows, Mac, or Linux.
https://asp.net
MIT License
35.15k stars 9.92k forks source link

Support Blazor .Net 8 Microsoft.Identity.Web client for Azure clients #51374

Open damienbod opened 10 months ago

damienbod commented 10 months ago

Is there an existing issue for this?

Is your feature request related to a problem? Please describe the problem.

I would like to implement a secure client Microsoft.Identity.Web (no ASP.NET Core Identity) using the new Blazor features.

In RC2, this does not work due to Blazor 8 State Management, required duplicate components and persisting the user state which is stored in a cookie on the client.

Would it be possible to support this for the different rendering modes

Describe the solution you'd like

The application authenticates against Microsoft Entra ID using the Web flow and requires a secret or a certificate. (Single Azure App registration)

OIDC Code flow with PKCE is used, not implicit flow which is the default using Microsoft.Identity.Web

The authentication session is stored in a HTTP only secure cookie.

All Blazor components in the different rendering modes have access to the authentication claims and the user session.

The Anti-forgery cookie is used to access APIs or any post, delete, put requests to the same site server. This can be added to the request using a html component in the form or a HTTP request header using a HttpClient.

No unsecure inline scripts are used in the client.

ASP.NET Core Identity is not used.

UI components can be displayed checking the authorized state

Logout form request uses the Anti-forgery cookie

Additional context

No response

joseph-hungerman commented 8 months ago

I am really struggling with this, as well. There is no good documentation to implement Azure AD in the new template. I have tried using the structure for Blazor Server in NET7 and pulling specific pieces from the NET8 Individual Account solution. I don't want individual accounts, however. I just want to protect the application using single org Entra ID. The closest I have gotten is getting the calls to operate, but now I get a persistent Correlation Error, cookie not found. The cookies exist in the browser set to Strict, even though I set up the project to put Correlation and Nonce cookies to SameSite None with Secure to Always. If the project is running localy, and I change a piece of code, hot reload is triggered, and the app reloads properly with executing the signin. If I shut down and restart, I get the correlation error again. I am posting this here since it seems to be related, but I thought about opening a new issue.

Code (HIDDEN for security):

DI Extension:

`private static void AddAuthentication(IServiceCollection services, ConfigurationManager configuration) { services.AddCascadingAuthenticationState();

services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
    .AddMicrosoftIdentityWebApp(opt =>
    {
        configuration.Bind("AzureAd", opt);
        opt.NonceCookie.SameSite = SameSiteMode.None;
        opt.NonceCookie.SecurePolicy = CookieSecurePolicy.Always;
        opt.CorrelationCookie.SameSite = SameSiteMode.None;
        opt.CorrelationCookie.SecurePolicy = CookieSecurePolicy.Always;
    })
    .EnableTokenAcquisitionToCallDownstreamApi()
    .AddDownstreamApi("HIDDEN", opt =>
    {
        opt.BaseUrl = configuration.IsProduction()
            ? "https://HIDDEN"
            : "https://HIDDEN";
        opt.Scopes = new[] { configuration.GetSection("HIDDEN").Value }!;
    }).AddInMemoryTokenCaches();

}`

Program.cs snippets: `builder.Services.AddControllersWithViews(opt => { var policy = new AuthorizationPolicyBuilder() .RequireAuthenticatedUser() .Build();

    opt.Filters.Add(new AuthorizeFilter(policy));
})
.AddMicrosoftIdentityUI();

builder.Services.AddCascadingAuthenticationState();

builder.Services.AddMicrosoftIdentityConsentHandler();`

app.UseCookiePolicy(new() { MinimumSameSitePolicy = SameSiteMode.None, Secure = CookieSecurePolicy.Always });

app.UseHttpsRedirection();

app.UseStaticFiles();

app.UseAntiforgery();

app.UseAuthorization();

app.MapRazorComponents() .AddInteractiveServerRenderMode() .AddInteractiveWebAssemblyRenderMode() .AddAdditionalAssemblies(typeof(SeeEye.App.Client._Imports).Assembly);

app.MapControllers();

app.UseStatusCodePagesWithRedirects("/StatusCode/{0}");

app.Run();

JeremyLikness commented 8 months ago

We just added the sample for OIDC with Microsoft Entra (Azure AD) ... does this get you what you are looking for/need?

joseph-hungerman commented 8 months ago

Is the Aspire portion required?

joseph-hungerman commented 8 months ago

What package is this extension in? AddHttpForwarderWithServiceDiscovery

I'm still getting the same error (I have tried on Chrome & Firefox):

An unhandled exception occurred while processing the request. AuthenticationFailureException: Correlation failed.

Unknown location

AuthenticationFailureException: An error was encountered while handling the remote login.

Microsoft.AspNetCore.Authentication.RemoteAuthenticationHandler.HandleRequestAsync()

joseph-hungerman commented 8 months ago

OK, so I figured out the cookie issue. The problem was in my effort to make my app secure. I set the Referrer-Policy to "strict-origin-when-cross-origin" which apparently messed everything up. I will re-test your new OIDC example with this change in mind.

joseph-hungerman commented 8 months ago

We just added the sample for OIDC with Microsoft Entra (Azure AD) ... does this get you what you are looking for/need?

I tested the new sample implementation, and it worked well. I think it is a big step forward, but the documentation still needs to be updated. A couple things I noticed:

  1. The sample wraps the router in a CascadingAuthenticationState component, which I thought was out of fashion with .NET8. Should AddCascadingAuthenticationState be added to the server and client projects?
  2. The documentation warns against adding an HTTP context accessor in the Program file, which the template has.
damienbod commented 8 months ago

Hi @JeremyLikness @halter73

Thanks for the example. I tried this and it does not work for me. After the authentication is complete, the client AuthenticationStateProvider gets updated with no state (after a small delay) and the authenticated session is reset.

Greetings Damien

halter73 commented 8 months ago

Thanks for the example. I tried this and it does not work for me. After the authentication is complete, the client AuthenticationStateProvider gets updated with no state (after a small delay) and the authenticated session is reset.

Is this something you see while running the BlazorWebOidc sample without modification? Do you have a stack trace for whatever is constructing the client's PersistentAuthenticationStateProvider constructor the second time? This shouldn't hapen since it's registered as a singleton.

damienbod commented 8 months ago

Hi @halter73 Thanks for your reply. Almost. I just switched out the OpenID Connect client to use a local STS and removed the aspire stuff.

I create the trace later.

damienbod commented 5 months ago

@halter73 Got around to looking at this. I did not disable the Microsoft claims mappings. When the claims renaming is disabled, everything works. I will create a PR to update the docs