Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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
1 task done
dlgombert opened this issue Oct 30, 2023 · 25 comments
Labels
area-blazor Includes: Blazor, Razor Components
Milestone

Comments

@dlgombert
Copy link

dlgombert commented Oct 30, 2023

Is there an existing issue for this?

  • I have searched the existing issues

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

@dotnet-issue-labeler dotnet-issue-labeler bot added the area-blazor Includes: Blazor, Razor Components label Oct 30, 2023
@mkArtakMSFT
Copy link
Member

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
Copy link
Member

@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.

@halter73 halter73 added the Needs: Author Feedback The author of this issue needs to respond in order for us to continue investigating this issue. label Oct 31, 2023
@ghost
Copy link

ghost commented Oct 31, 2023

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
Copy link

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
Copy link
Author

dlgombert commented Nov 3, 2023

@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:

  • When I first started working on this project I think I was using a different template. Maybe it was different in 8 rc1? The template has the layouts, the pages, and the routes in the client project. When I do it like that, is when the issue is sporadic.
  • I tried to emulate how the current template is (which is hell of a lot better in other aspects imo), and when I do it that way the issue is constant.
  • The error might be related to the refresh token in some way. Several times I've had it show up directly after seeing a refresh token error in the developer console in Edge.
  • Coupled with the above, it seems to happen more directly after I've restarted the project in Visual Studio.
  • I'm not using Identity and I'm using Azure only for authentication, and then my local db almost entirely for authorization.

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.

@ghost ghost added Needs: Attention 👋 This issue needs the attention of a contributor, typically because the OP has provided an update. and removed Needs: Author Feedback The author of this issue needs to respond in order for us to continue investigating this issue. labels Nov 3, 2023
@dudley810
Copy link

dudley810 commented Nov 5, 2023

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
Copy link
Author

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

@VultureJD
Copy link

VultureJD commented Nov 6, 2023

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
Copy link
Member

halter73 commented Nov 6, 2023

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

@halter73 halter73 removed the Needs: Attention 👋 This issue needs the attention of a contributor, typically because the OP has provided an update. label Nov 6, 2023
@halter73 halter73 removed their assignment Nov 6, 2023
@dudley810
Copy link

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

@halter73
Copy link
Member

halter73 commented Nov 6, 2023

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
Copy link

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
Copy link
Author

@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
Copy link
Member

halter73 commented Nov 7, 2023

UserInfo, PersistingAuthenticationStateProvider (server), and PersistentAuthenticationStateProvider (client) in dudley810/dotnet8identityopenid#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
Copy link
Member

halter73 commented Nov 7, 2023

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 dudley810/dotnet8identityopenid#1. Thanks for pointing out that we need add a "Prerendering with authentication" section back as part of this.

@dlgombert
Copy link
Author

@halter73 Thank you very much.

@jonhilt
Copy link

jonhilt commented Nov 7, 2023

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 :)

@mkArtakMSFT mkArtakMSFT added this to the Discussions milestone Nov 7, 2023
@VultureJD
Copy link

VultureJD commented Nov 12, 2023

@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
Copy link

VultureJD commented Nov 15, 2023

@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:

  • setting CallbackPath to local reference (e.g. /authentication/login-callback)
  • setting CorrelationCookie.Path, NonceCookie.Path to the local reference with other paths (e.g. /app/authentication/login-callback)
  • configuring the OnRedirectToIdentityProvider event to set the context.ProtocolMessage.RedirectUri to the FQDN of the authentication/login-callback endpoint
  • setting base href in App.razor to the local path of the app (e.g. /app/)

@VultureJD
Copy link

@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
Copy link
Member

halter73 commented Dec 8, 2023

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
Copy link

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
Copy link
Member

On the client, singletons are still per-user.

@PlagueHO
Copy link

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
Copy link

JochenMSFT commented Jan 7, 2024

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

@dotnet-policy-service dotnet-policy-service bot added the pending-ci-rerun When assigned to a PR indicates that the CI checks should be rerun label Feb 6, 2024
@wtgodbe wtgodbe removed the pending-ci-rerun When assigned to a PR indicates that the CI checks should be rerun label Feb 6, 2024
@dotnet-policy-service dotnet-policy-service bot added the pending-ci-rerun When assigned to a PR indicates that the CI checks should be rerun label Feb 6, 2024
@wtgodbe wtgodbe removed the pending-ci-rerun When assigned to a PR indicates that the CI checks should be rerun label Feb 13, 2024
@dotnet dotnet deleted a comment from dotnet-policy-service bot Feb 13, 2024
@dotnet dotnet deleted a comment from dotnet-policy-service bot Feb 13, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-blazor Includes: Blazor, Razor Components
Projects
None yet
Development

No branches or pull requests

9 participants