dotnet / AspNetCore.Docs

Documentation for ASP.NET Core
https://docs.microsoft.com/aspnet/core
Creative Commons Attribution 4.0 International
12.59k stars 25.29k forks source link

Add multiple hosted WASM with authn/z guidance #26113

Closed gkfischer closed 2 years ago

gkfischer commented 2 years ago

Hi there, is there a repo that can be cloned with the complete sample? I followed the instructions now two times and always end up with 404 errors and/or "Failed to find a valid digest in the 'integrity' attribute for resource 'https://localhost:5001/_framework/MultipleBlazorApps.Shared.dll' with computed SHA-256 integrity '47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU='. The resource has been blocked.".

[Enter feedback here]


Document Details

Do not edit this section. It is required for docs.microsoft.com ➟ GitHub issue linking.

gkfischer commented 2 years ago

Oh and one thing I forgot to mention in the original post: a sample that includes authentication also would be great

guardrex commented 2 years ago

Hello @gkfischer ...

We can't host many samples because we don't have the bandwidth to maintain them. They must be updated for every release and then they pile up over many years.

The guidance in this topic should be correct, and I regularly work on it and validate it working. If you put up a repro of your failing project in GH, I'll take a look at it. Normally, we recommend the usual public support channels ...

WRT auth, our auth guidance wouldn't be significantly different for the WASM client apps in this multiple-hosted WASM scenario. You'd just need to get the callback addresses to match whatever URLs the app is using.

I'm going to close this issue as not actionable, but I will take a look at your failing repro project if you put it on GH. You can leave a link to it in a comment here and then I'll re-open this while discussing it further.

gkfischer commented 2 years ago

Hi there, Thanks for the swift reply. I can understand the problem with maintaining a full sample.

Here's the repo where I tried to implement the authentication: https://github.com/gkfischer/MultipleBlazorAppsWithAuthentication.git

gkfischer commented 2 years ago

By the way, the Slack link isn't working for me image

guardrex commented 2 years ago

@spboyer ... Shayne, the Slack signup page seems to be broken ...

https://tattoocoder.com/aspnet-slack-sign-up/

guardrex commented 2 years ago

Indeed, it's a bit tricky to get the full config/setup into place. Just hacking around, I added some drop-outs to the client apps' middleware request processing (i.e., !ctx.Request.Path.StartsWithSegments(...)) for requests like /_configuration, /.well-known, /connect, and /Identity because they look like they need to go to the host ASP.NET Core app.

There's perhaps a 2nd client entry to add to IdS config in appsettings.json ...

"MultipleBlazorApps.SecondClient": {
  "Profile": "IdentityServerSPA"
}

... and that all kind'a sort'a gets things almost working in the first client app. Sign in and sign out seem to work, but the 2nd client throws a 'SSL rec too long' error in the browser, and there's a problem with the auth (a 401) for the WeatherForecast requests.

Anyway ... I think my hacking around isn't the best way to proceed. This is obviously a bit complex and tricky to get just right. Let's ask @javiercn ...

Current (non-auth) guidance is at ...

https://docs.microsoft.com/aspnet/core/blazor/host-and-deploy/multiple-hosted-webassembly

gkfischer commented 2 years ago

Thanks for taking care of this Luke

gkfischer commented 2 years ago

If there is a working sample, I can help with drafting the article if you want.

guardrex commented 2 years ago

If we provide guidance, it will probably be in a new section of this doc with the changes to make to the base (non-auth) case.

gkfischer commented 2 years ago

Sure, makes sense. I think anybody at least semi-professional will be looking for authentication in the apps.

guardrex commented 2 years ago

I think I made a little more progress on this based on ...

https://github.com/dotnet/aspnetcore/issues/42072#issuecomment-1152706349

I changed my host IdS config to set an issurer of ...

builder.Services.AddIdentityServer(options =>
    {
        options.IssuerUri = "https://localhost:5001";
    })
    .AddApiAuthorization<ApplicationUser, ApplicationDbContext>();

... placed this bit in there ...

builder.Services.AddAuthentication()
    .AddIdentityServerJwt().AddJwtBearer(o => o.Authority = "https://localhost:5001");

... and updated my CORS to ...

builder.Services.AddCors(options =>
{
    options.AddDefaultPolicy(
        policy =>
        {
            policy.WithOrigins("https://localhost:5001", "https://localhost:5002");
            policy.WithHeaders("authorization");
        });
});

... and then I made my request this way in the FetchData component of the first client app ...

@page "/fetchdata"
@using Microsoft.AspNetCore.Authorization
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication
@using MultipleBlazorApps.Shared
@using System.Net.Http.Headers
@attribute [Authorize]
@inject HttpClient Http
@inject IAccessTokenProvider TokenProvider

<PageTitle>Weather forecast</PageTitle>

<h1>Weather forecast</h1>

<p>This component demonstrates fetching data from the server.</p>

@if (forecasts == null)
{
    <p><em>Loading...</em></p>
}
else
{
    <table class="table">
        <thead>
            <tr>
                <th>Date</th>
                <th>Temp. (C)</th>
                <th>Temp. (F)</th>
                <th>Summary</th>
            </tr>
        </thead>
        <tbody>
            @foreach (var forecast in forecasts)
            {
                <tr>
                    <td>@forecast.Date.ToShortDateString()</td>
                    <td>@forecast.TemperatureC</td>
                    <td>@forecast.TemperatureF</td>
                    <td>@forecast.Summary</td>
                </tr>
            }
        </tbody>
    </table>
}

@code {
    private WeatherForecast[]? forecasts;

    protected override async Task OnInitializedAsync()
    {

        var requestMessage = new HttpRequestMessage()
        {
            Method = new HttpMethod("GET"),
            RequestUri = new Uri("https://localhost:5000/WeatherForecast")
        };

        var tokenResult = await TokenProvider.RequestAccessToken();

        if (tokenResult.TryGetToken(out var token))
        {
            requestMessage.Headers.Authorization =
                new AuthenticationHeaderValue("Bearer", token.Value);

            var response = await Http.SendAsync(requestMessage);
            var responseStatusCode = response.StatusCode;

            forecasts = await response.Content.ReadFromJsonAsync<WeatherForecast[]>();
        }
    }
}

... and I seem to be getting weather data now in the client app 🎉 ... but only the first client app. The second client app is still failing with an SSL error. Also, the config for the issuer of https://localhost:5001 probably isn't going to work for the second client app, which probably uses an issuer of https://localhost:5002.

This is a TRICKY CONFIG for a silly 🦖. We probably do need Javier's help on this one.

guardrex commented 2 years ago

Someone may have had some success with this scenario here ...

https://github.com/tesar-tech/MultipleBlazorAppsWithAuth

I'll take a look and see if that works.

UPDATE: Yuck! 😄 ... That didn't work out so well. I like my (broken) approach better!

gkfischer commented 2 years ago

I've seen that one, but it's based on .NET 5. I tried to use all the steps listed in the description there for a .NET 6 solution, but couldn't get it working either.

guardrex commented 2 years ago

I discovered my SSL error was merely that I missed the "s" in https://localhost:5002 in the launchSettings.json file, so that was silly. The tip off on that was in an SO answer where someone said that that error can happen if a secure request is made but the server returns unencrypted content.

So, the second client app is working now ... loading, BUT the auth and/or IdS is failing, so I'll work on that a bit more.

guardrex commented 2 years ago

At this point, it looks like the problem with my test app is in the Duende IdS config for permitted redirect URIs ... maybe 😄.

https://docs.duendesoftware.com/identityserver/v6/quickstarts/2_interactive/

That looks like the config for clients to set redirect URIs for the client apps here, but I can't get the API (yet) in the app to light up. They don't provide enough information.

The current (almost working) test app is at https://github.com/guardrex/MultipleBlazorAppsWithAuth. I'll probably return to this later today or within the next few days depending on the workload.

UPDATE: Oh ... well ... I did find the the API 😄 ... Duende.IdentityServer.Models.Client. It was obscured by my namespace for the first client app, which a hosted WASM solution pre-names with Client.

... so I'll be toying with something like ...

using Duende.IdentityServer.Models;

namespace MultipleBlazorApps.Server
{
    public class Config
    {
        public static IEnumerable<Duende.IdentityServer.Models.Client> Clients =>
            new[]
            {
                new Duende.IdentityServer.Models.Client
                {
                  ClientId = "MultipleBlazorApps.Client",
                  //ClientSecrets = {new Secret("SuperSecretPassword".Sha256())},

                  AllowedGrantTypes = GrantTypes.Code,

                  RedirectUris = {"https://localhost:5001/signin-oidc"},
                  FrontChannelLogoutUri = "https://localhost:5001/signout-oidc",
                  PostLogoutRedirectUris = {"https://localhost:5001/signout-callback-oidc"},

                  //AllowOfflineAccess = true,
                  //AllowedScopes = {"openid", "profile", "weatherapi.read"},
                  RequirePkce = true,
                  RequireConsent = true,
                  AllowPlainTextPkce = false
                },
                new Duende.IdentityServer.Models.Client
                {
                  ClientId = "MultipleBlazorApps.SecondClient",
                  //ClientSecrets = {new Secret("SuperSecretPassword".Sha256())},

                  AllowedGrantTypes = GrantTypes.Code,

                  RedirectUris = {"https://localhost:5002/signin-oidc"},
                  FrontChannelLogoutUri = "https://localhost:5002/signout-oidc",
                  PostLogoutRedirectUris = {"https://localhost:5002/signout-callback-oidc"},

                  //AllowOfflineAccess = true,
                  //AllowedScopes = {"openid", "profile", "weatherapi.read"},
                  RequirePkce = true,
                  RequireConsent = true,
                  AllowPlainTextPkce = false
                },
            };
    }
}

I'll pick back up with this later. The latest complaint/concern/problem seems to be the redirect URIs, and that might be the right direction with .AddInMemoryClients(Config.Clients).

Also ... so I don't forget this ... it looks like all of that can be read from config with something like ...

builder.Services.AddIdentityServer(options =>
    {
        options.IssuerUri = "https://localhost:5001";
    })
    .AddInMemoryClients(builder.Configuration.GetSection("IdentityServer:Clients"))
    .AddApiAuthorization<ApplicationUser, ApplicationDbContext>();

... or go the C# way ...

builder.Services.AddIdentityServer(options =>
    {
        options.IssuerUri = "https://localhost:5001";
    })
    .AddInMemoryClients(Config.Clients)
    .AddApiAuthorization<ApplicationUser, ApplicationDbContext>();

... but if this turns out to be the right direction, I favor the app settings way.

... and note ...

gkfischer commented 2 years ago

Again, thanks a lot Luke for investigating and the progress you make.

guardrex commented 2 years ago

UPDATE

guardrex commented 2 years ago

OoooooooooK ... Now, we're cook'in! 🍻

I may have licked it with my 🦖 RexHack'ins™ 🙈😄.

I have login and API access for both WASM client apps now. The trick was not to set the issuer the following way (or peg it to the port 5001 URI, which seems philosophically wrong anyway) ...

builder.Services.AddIdentityServer(options =>
{
    options.IssuerUri = "https://localhost:5000";
})

REMOVED

That permits ...

I need to clean up a few nits and bits ... and ...

There's a (minor) annoyance here. The second client app is showing this banner ...

Capture

... so I'll need to research that a bit further.

I'm going to work on this further on Thursday morning. If it all seems good, I'll float it to the PU for review and feedback of the base implementation case. If they like it (or FIX 🙈 my crappy code and then they like it 😆), then we'll probably proceed with a new topic section.

guardrex commented 2 years ago

Found the following ...

Add a comment to indicate that the authority needs to be configured when missing https://github.com/dotnet/aspnetcore/pull/19366

... and I just noticed that when I created by second client, I did so from the project template with auth ... it left the following in appsettings.json ...

{
  "Local": {
    "Authority": "https://login.microsoftonline.com/",
    "ClientId": "33333333-3333-3333-33333333333333333"
  }
}

... and that generated the markup for the banner in the app's Index component ...

<div class="alert alert-warning" role="alert">
    Before authentication will function correctly, you must configure your provider details in <code>Program.cs</code>
</div>

... so I 🔪 the appsettings.json file and 🔪 the <div> ... and that's the end of that little problem! 😄

guardrex commented 2 years ago

~I just updated ...~

~https://github.com/guardrex/MultipleBlazorAppsWithAuth~

~... with the latest ✨ magic bits ✨.~

REMOVED! This is an unsupported docs scenario at this time. The API supports Identity that might work with Duende and the multiple hosted WASM demo shown in the topic, but we aren't able to provide guidance on it at this time.

gkfischer commented 2 years ago

You're a ✨. thanks a lot.

gkfischer commented 2 years ago

One more thing: I noticed that a lot, and I mean a real lot, of exceptions, are thrown. Some about a wrong payload, which is nothing much to worry about I guess as the authorization is working. But there are thousands of these: image Roughly 5.000 exceptions after starting the app, clicking "Weather forecast" and doing a login (all of which is working properly)

guardrex commented 2 years ago

I'm not seeing any exceptions like that. Research it further on the PU's repo ...

https://github.com/dotnet/aspnetcore/issues?q=is%3Aissue+An+existing+connection+was+forcibly+closed+by+the+remote+host.+

guardrex commented 2 years ago

UPDATE: I've made some final touches to the GH repo sample, and I've opened an internal discussion about proceeding with this. I'm asking management some process and review questions for this if we're going to proceed with writing this up. I'll report back in a bit on what they decide.

guardrex commented 2 years ago

UPDATE: I just had to strike my last remark and comments that I would leave the testing solution up in my GH repo. This is too personally risky for me to get involved with. I won't be able to get MS or Duende support for it at all, and I don't want to put anything up that might be wrong and then lead to compromised security, even if I wouldn't ultimately be charged by someone with a liability claim or held legally responsible for any damages. It's just not a risk that I'm personally willing to take.

The product unit is going to take a look at the whole Blazor security model in the next few years. They might provide a multiple-hosting WASM solution approach that includes new authn/z features that make this type of app hosting scenario much simpler and safer.