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.56k stars 10.05k forks source link

Cannot provide a value for property 'AuthenticationService' on type 'Microsoft.AspNetCore.Components.WebAssembly.Authentication.RemoteAuthenticatorView'. There is no registered service of type 'Microsoft.AspNetCore.Components.WebAssembly.Authentication.IRemoteAuthenticationService`1[Microsoft.AspNetCore.Components.WebAssembly.Authentication.RemoteAuthenticationState #51759

Open dlgombert opened 1 year ago

dlgombert commented 1 year ago

Is there an existing issue for this?

Describe the bug

I have no Idea what is causing this, but no matter what when I try and login, I get redirected to an error page reading:

Cannot provide a value for property 'AuthenticationService' on type 'Microsoft.AspNetCore.Components.WebAssembly.Authentication.RemoteAuthenticatorView'. There is no registered service of type 'Microsoft.AspNetCore.Components.WebAssembly.Authentication.IRemoteAuthenticationService1[Microsoft.AspNetCore.Components.WebAssembly.Authentication.RemoteAuthenticationState ` Working on blazor hosted project using netcore 8 rc 2. Be happy to provide code or more information, but this is driving me a little crazy.

Expected Behavior

No response

Steps To Reproduce

No response

Exceptions (if any)

No response

.NET Version

No response

Anything else?

No response

mkArtakMSFT commented 1 year ago

Thanks for contacting us. @halter73 I think you've fixed this already for the upcoming RTM release, haven't you? Could you please link the PR here? Thanks!

halter73 commented 1 year ago

@halter73 I think you've fixed this already for the upcoming RTM release, haven't you? Could you please link the PR here?

Unless you're starting a new project from the Blazor templates, I don't think it's likely I've fixed this issue.

Working on blazor hosted project using netcore 8 rc 2. Be happy to provide code or more information, but this is driving me a little crazy.

That would help us figure out what's going on. Please provide a minimalistic repro project (ideally a GitHub repo) that illustrates the problem.

ghost commented 1 year ago

Hi @dlgombert. We have added the "Needs: Author Feedback" label to this issue, which indicates that we have an open question for you before we can take further action. This issue will be closed automatically in 7 days if we do not hear back from you by then - please feel free to re-open it if you come back to this issue after that time.

dudley810 commented 1 year ago

I have this issues to using Azure Ad. Are there any examples out there for a dotnet 8 hosted webassembly application? I can get the dotnet 8 version to work if I add the dotnet 7 components into the dotnet 8. like the fallback index.html etc. But I really did not want to do that. Let me know if you have some sample code for dotnet 8 thanks.

dlgombert commented 1 year ago

@dudley810 Part of the reason I haven't answered the request for feedback is that I've found that the error is unpredictable. I have had a lot of trouble recreating the issue, and when I tried recreating the project from scratch, I was having so much trouble connecting to Azure that I got fed up and gave up. Here are a couple of things I've noticed:

This has been very frustrating and I couldn't find anything that seemed related anywhere else online. The only thing I ever changed that made any difference was the location of my routes and perhaps pages/layouts.

dudley810 commented 1 year ago

I created a sample for this issue. https://github.com/dudley810/dotnet8identityopenid. @halter73 and @mkArtakMSFT let us know if there is any other information you need. Seems like @dlgombert and I are having the same issue but not sure how similar his project is to mine.

dlgombert commented 1 year ago

@dudley810 probably does a better simpler job of summarizing than I did. Our projects are similar enough and this is definitely the issue.

VultureJD commented 1 year ago

I'm getting the same error when using a similar project structure to @dudley810 but with OIDC rather than MSAL. Server-side login is working fine, but when the WASM loads, it redirects to a blank page and get that error in the console. I've even tried manually injecting the RemoteAuthService and it still fails to render the authentication component. builder.Services.AddScoped<IRemoteAuthenticationService<RemoteAuthenticationState>, RemoteAuthenticationService<RemoteAuthenticationState, RemoteUserAccount, OidcProviderOptions>>();

I'm at a loss and can't figure out how to solve this issue.

halter73 commented 1 year ago

Thanks for the repro @dudley810. It looks like this issue is trying to use types like RemoteAuthenticatorView from Microsoft.AspNetCore.Components.WebAssembly.Authentication in components that can be server rendered. This is not supported.

Since we want to be able to authenticate the user during server-side rendering, it's better to use cookies rather than JwtBearer and MSAL.js. I opened a PR is at https://github.com/dudley810/dotnet8identityopenid/pull/1 to use AddMicrosoftIdentityWebApp which uses cookies like you would for other server rendered UI stacks (e.g. MVC and Razor pages) instead of AddMicrosoftIdentityWebApi.

Of course, we still need to be able to flow authentication state from the server to the client for client-side rendering. To do this, the PR defines custom AuthenticationStateProvider's on both the server and client. They utilize PersistentComponentState to serialize and deserialize the authentication state as render modes transition.

If you need the JWT token, you can use OpenIdConnectOptions.SaveTokens and then access it from the HttpContext like so:

var authResult = await context.AuthenticateAsync(); // This is cached if the user already authenticated
var accessToken = authResult.Properties?.GetTokenValue("access_token");

You theoretically could then flow the access token to the client to have it make requests with it directly, but that's strongly discouraged:

DO NOT send access tokens that were issued to the middle tier to any other party. Access tokens issued to the middle tier are intended for use only by that middle tier.

Security risks of relaying access tokens from a middle-tier resource to a client (instead of the client getting the access tokens themselves) include:

  • Increased risk of token interception over compromised SSL/TLS channels.
  • Inability to satisfy token binding and Conditional Access scenarios requiring claim step-up (for example, MFA, Sign-in Frequency).
  • Incompatibility with admin-configured device-based policies (for example, MDM, location-based policies).

https://learn.microsoft.com/entra/identity-platform/v2-oauth2-on-behalf-of-flow#middle-tier-access-token-request

You can however use this access token from your API controllers and follow the backend for frontend or BFF pattern. https://learn.microsoft.com/en-us/azure/architecture/patterns/backends-for-frontends

dudley810 commented 1 year ago

@halter73 - Where can you get the 8.0.100-rtm.23519.30 to install?

halter73 commented 1 year ago

https://dotnetbuilds.azureedge.net/public/Sdk/8.0.100-rtm.23519.30/dotnet-sdk-8.0.100-rtm.23519.30-win-x64.exe

Or you can use one of the install scripts at https://learn.microsoft.com/en-us/dotnet/core/tools/dotnet-install-script

VultureJD commented 1 year ago

Thanks @halter73. I implemented your solution with OIDC and it's now working as expected. Is this something that can be added into the docs? As part of the .NET 7 docs, there is a section that mentions pre-rendering not being supported with auth. This section has been removed in the .NET 8 version but hasn't been replaced with anything else. There is also nothing mentioned about InteractiveAuto and auth either that I could find. e.g. In .NET 7 docs: https://learn.microsoft.com/en-us/aspnet/core/blazor/security/webassembly/additional-scenarios?view=aspnetcore-7.0#prerendering-with-authentication

dlgombert commented 1 year ago

@halter73 Can the userinfo be implemented as a remoteuseraccount class? Or can this class be used as a substitution for the remote useraccount class and persisted instead of directly accessing the claimsprincipal?

halter73 commented 1 year ago

UserInfo, PersistingAuthenticationStateProvider (server), and PersistentAuthenticationStateProvider (client) in https://github.com/dudley810/dotnet8identityopenid/pull/1 are substitutes for RemoteUserAccount, RemoteAuthenticationService, RemoteAuthenticatorView, etc...

The RemoteAuthenticationService is designed to be used exclusively from WebAssembly whereas the PersistentComponentState-based PersistingAuthenticationStateProvider also works while prerendering the component.

halter73 commented 1 year ago

Thanks @halter73. I implemented your solution with OIDC and it's now working as expected. Is this something that can be added into the docs?

Yes. That's what I'm working on as part of #49668 and why had code ready to quickly open a PR at https://github.com/dudley810/dotnet8identityopenid/pull/1. Thanks for pointing out that we need add a "Prerendering with authentication" section back as part of this.

dlgombert commented 1 year ago

@halter73 Thank you very much.

jonhilt commented 1 year ago

Thanks for the repro @dudley810. It looks like this issue is trying to use types like RemoteAuthenticatorView from Microsoft.AspNetCore.Components.WebAssembly.Authentication in components that can be server rendered. This is not supported.

Since we want to be able to authenticate the user during server-side rendering, it's better to use cookies rather than JwtBearer and MSAL.js. I opened a PR is at dudley810/dotnet8identityopenid#1 to use AddMicrosoftIdentityWebApp which uses cookies like you would for other server rendered UI stacks (e.g. MVC and Razor pages) instead of AddMicrosoftIdentityWebApi.

Of course, we still need to be able to flow authentication state from the server to the client for client-side rendering. To do this, the PR defines custom AuthenticationStateProvider's on both the server and client. They utilize PersistentComponentState to serialize and deserialize the authentication state as render modes transition.

This explanation is pure gold! Easily the clearest (and most succinct) explanation I've seen of the preferred flow for auth when using components that need to support static server-side rendering. Will be great to see this sort of thing in the docs come next week :)

VultureJD commented 1 year ago

@halter73 After getting it to run locally with the above solution, it now won't auth in when hosted as an Azure WebApp, sitting behind AppGateway. The issue seems to be setting the RedirectUri whereas for our other on-prem Blazor apps we have been able to use CallbackPath. We've narrowed it down to the RedirectUri is POSTing to the Authentication component when it should be a GET. I know this isn't really related to the above, but do you have any suggestions?

This is what the AddAuth code currently looks like:

            services.AddAuthentication(options =>
            {
                options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
                options.DefaultChallengeScheme = authScheme;
            })
            .AddCookie(o => o.SessionStore = new MemoryCacheTicketStore())
            .AddOpenIdConnect(authScheme, options =>
            {
                options.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
                options.Authority = configuration["IdentityAddress"];
                options.ClientId = configuration["BzAppName"];
                options.ResponseType = "code";
                options.Scope.Add("openid");
                options.Scope.Add("profile");
                //options.CallbackPath = "/authentication/login-callback"; This works for on-prem services, but in Azure redirects to appname.azurewebsites.net + CallbackPath
                options.GetClaimsFromUserInfoEndpoint = true;
                options.SaveTokens = false;
                options.Events = new OpenIdConnectEvents
                {
                    OnRedirectToIdentityProvider = context =>
                    {
                        context.ProtocolMessage.RedirectUri = _redirectUri; //This is the FQDN of the AppGw route to the Authentication component
                        return Task.CompletedTask;
                    },
                    OnTokenValidated = context => OnTokenValidated(context, configuration)
                };
                options.ClaimActions.MapUniqueJsonKey(ClaimTypes.Role, "role");
                options.ClaimActions.Remove("name");
                options.ClaimActions.MapUniqueJsonKey(ClaimTypes.Name, "name");
                options.ClaimActions.MapJsonKey("permission", "permission");
            });

Should note that for Blazor WASM Standalone apps running as WebApps sitting behind AppGateway in Azure, we don't need to configure any of the above and is just: builder.Services.AddOidcAuthentication(options => builder.Configuration.Bind("oidc", options.ProviderOptions));

VultureJD commented 1 year ago

@halter73 After getting it to run locally with the above solution, it now won't auth in when hosted as an Azure WebApp, sitting behind AppGateway. The issue seems to be setting the RedirectUri whereas for our other on-prem Blazor apps we have been able to use CallbackPath. We've narrowed it down to the RedirectUri is POSTing to the Authentication component when it should be a GET. I know this isn't really related to the above, but do you have any suggestions?

This is what the AddAuth code currently looks like:

            services.AddAuthentication(options =>
            {
                options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
                options.DefaultChallengeScheme = authScheme;
            })
            .AddCookie(o => o.SessionStore = new MemoryCacheTicketStore())
            .AddOpenIdConnect(authScheme, options =>
            {
                options.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
                options.Authority = configuration["IdentityAddress"];
                options.ClientId = configuration["BzAppName"];
                options.ResponseType = "code";
                options.Scope.Add("openid");
                options.Scope.Add("profile");
                //options.CallbackPath = "/authentication/login-callback"; This works for on-prem services, but in Azure redirects to appname.azurewebsites.net + CallbackPath
                options.GetClaimsFromUserInfoEndpoint = true;
                options.SaveTokens = false;
                options.Events = new OpenIdConnectEvents
                {
                    OnRedirectToIdentityProvider = context =>
                    {
                        context.ProtocolMessage.RedirectUri = _redirectUri; //This is the FQDN of the AppGw route to the Authentication component
                        return Task.CompletedTask;
                    },
                    OnTokenValidated = context => OnTokenValidated(context, configuration)
                };
                options.ClaimActions.MapUniqueJsonKey(ClaimTypes.Role, "role");
                options.ClaimActions.Remove("name");
                options.ClaimActions.MapUniqueJsonKey(ClaimTypes.Name, "name");
                options.ClaimActions.MapJsonKey("permission", "permission");
            });

Should note that for Blazor WASM Standalone apps running as WebApps sitting behind AppGateway in Azure, we don't need to configure any of the above and is just: builder.Services.AddOidcAuthentication(options => builder.Configuration.Bind("oidc", options.ProviderOptions));

Actually my apologies. I had added back in the Authentication.razor to try and resolve, but ended back at the original exception in OP title. The error is a 404 when routing to /authentication/login-callback. It is routing properly through AppGateway, but the WebApp itself is throwing a 404 on /authentication/login-callback. It's odd that this works locally though but won't run in Azure. 😞

Edit: Tried hosting as a Linux and Windows WebApp. Both have the same error. Edit 2: The only thing that works is making the root azurewebsites URI the redirectUri, however then the browser obviously redirects away from the domain to yourapp.azurewebsites.net which isn't what we want to happen.

Final edit for anyone that comes across this. Solved it by:

VultureJD commented 1 year ago

@halter73 Any reason the PersistentAuthenticationStateProviders are Singletons and not Scoped? Especially given the PersistingComponentState is only persisting on a static key? builder.Services.AddSingleton<AuthenticationStateProvider, PersistentAuthenticationStateProvider>();

halter73 commented 11 months ago

Especially given the PersistingComponentState is only persisting on a static key?

I'm not sure I follow this. The persisted "UserInfo" state can only be read once after the wasm runtime starts. Any subsequent attempts to construct the PersistingComponentState will result in it returning the defaultUnauthenticatedTask, so singleton seems natural to me. Although, I'm not super familiar with how scopes are used in Blazor client projects. Usually I work with per-request or ephemeral scopes on the server.

VultureJD commented 11 months ago

The persisted "UserInfo" state can only be read once after the wasm runtime starts. Any subsequent attempts to construct the PersistingComponentState will result in it returning the defaultUnauthenticatedTask, so singleton seems natural to me.

I'm more intrigued as .AddOidc() adds the Remote Auth State Provider as a scoped rather than a singleton within the Blazor Client layer. Just conscious that if the PersistentAuthenticationStateProvider uses a static key and is a singleton, what would happen if two users persisted their state and then tried to retrieve it? Would there be a bleeding of claims? Without validation of identity on the client side, could this be a security issue?

I'm also not super familiar with how the PersistentCompnentState is handled under the hood when there are multiple requests at the same time, so my understanding may be way off.

halter73 commented 11 months ago

On the client, singletons are still per-user.

PlagueHO commented 11 months ago

Thanks for the repro @dudley810. It looks like this issue is trying to use types like RemoteAuthenticatorView from Microsoft.AspNetCore.Components.WebAssembly.Authentication in components that can be server rendered. This is not supported.

Since we want to be able to authenticate the user during server-side rendering, it's better to use cookies rather than JwtBearer and MSAL.js. I opened a PR is at dudley810/dotnet8identityopenid#1 to use AddMicrosoftIdentityWebApp which uses cookies like you would for other server rendered UI stacks (e.g. MVC and Razor pages) instead of AddMicrosoftIdentityWebApi.

Of course, we still need to be able to flow authentication state from the server to the client for client-side rendering. To do this, the PR defines custom AuthenticationStateProvider's on both the server and client. They utilize PersistentComponentState to serialize and deserialize the authentication state as render modes transition.

If you need the JWT token, you can use OpenIdConnectOptions.SaveTokens and then access it from the HttpContext like so:

var authResult = await context.AuthenticateAsync(); // This is cached if the user already authenticated
var accessToken = authResult.Properties?.GetTokenValue("access_token");

You theoretically could then flow the access token to the client to have it make requests with it directly, but that's strongly discouraged:

DO NOT send access tokens that were issued to the middle tier to any other party. Access tokens issued to the middle tier are intended for use only by that middle tier. Security risks of relaying access tokens from a middle-tier resource to a client (instead of the client getting the access tokens themselves) include:

  • Increased risk of token interception over compromised SSL/TLS channels.
  • Inability to satisfy token binding and Conditional Access scenarios requiring claim step-up (for example, MFA, Sign-in Frequency).
  • Incompatibility with admin-configured device-based policies (for example, MDM, location-based policies).

https://learn.microsoft.com/entra/identity-platform/v2-oauth2-on-behalf-of-flow#middle-tier-access-token-request

You can however use this access token from your API controllers and follow the backend for frontend or BFF pattern. https://learn.microsoft.com/en-us/azure/architecture/patterns/backends-for-frontends

First up: Thank you to @halter73 for the fantastic description of the issue and @dudley810 for the solution/repro (it mostly worked for me). And thanks to everyone else for context/suggestions. I got the solution working with Entra ID External ID (CIAM) after a few tweaks. Still need to make the LoginLogoutEndpointRouteBuilderExtensions.GetAuthProperties() generate the returnURL to be HTTPS when running in Azure Container Apps.

However, I had a general concern that I wanted to get thoughts/opinions on:

My understanding is that because the authentication is being performed by the Blazor Server the bearer token is only available there. Copying the token to the WASM client is a big no. Normally, the client would have used PKCE flow to get its own token.

This isn't a problem for authenticating to APIs hosted by the Blazor Server (as it's using cookie auth). But it would not be possible the Blazor Client to authenticate to an API hosted in another service that was using the same IDP (with delegated API permissions granted). The only way this could be safely done would be to use a BFF pattern and get the Blazor Server to perform any API calls to downstream APIs that require auth. Is my understanding correct - and if so, does that mean there isn't a secure way to call the authenticated APIs from the WASM client (no token from a PKCE flow available on the client)?

One last question I have is @halter73, @mkArtakMSFT: Is this likely to get an OOTB approach from the .NET team in future? It feels like there is a gap in the .NET 8 Blazor Web App (hybrid) story with OpenID auth: the docs seem to omit it and there is no Microsoft Identity option when creating a new Blazor Web App from the VS template (only the "Individual Accounts" option). It seems that this is a "known gap", but perhaps a solution is coming?

I'm really trying to decide if I should just move completely over to WASM Standalone and move 100% onto the client (and accept some of the limitations) or just live with making all downstream API calls from the server backend (not ideal)?

JochenMSFT commented 10 months ago

I have this issues to using Azure Ad. Are there any examples out there for a dotnet 8 hosted webassembly application? I can get the dotnet 8 version to work if I add the dotnet 7 components into the dotnet 8. like the fallback index.html etc. But I really did not want to do that. Let me know if you have some sample code for dotnet 8 thanks.

coming back to this, @mkArtakMSFT it would be great if we can add examples for dotnet 8 hosted webassembly application using the "blazor web app" VS template as recommended in the docs how to ingrate Entra / OIDC Identity Logins .. for standalone webassemply templates (blazorwasm) they are available ..or directly integrate that option in the VS blazor web app template:

image