Open dlgombert opened 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 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.
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.
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.
@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.
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.
@dudley810 probably does a better simpler job of summarizing than I did. Our projects are similar enough and this is definitely the issue.
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.
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).
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 - Where can you get the 8.0.100-rtm.23519.30 to install?
Or you can use one of the install scripts at https://learn.microsoft.com/en-us/dotnet/core/tools/dotnet-install-script
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
@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?
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.
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.
@halter73 Thank you very much.
Thanks for the repro @dudley810. It looks like this issue is trying to use types like
RemoteAuthenticatorView
fromMicrosoft.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 ofAddMicrosoftIdentityWebApi
.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 utilizePersistentComponentState
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 :)
@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));
@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:
@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>();
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.
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 thedefaultUnauthenticatedTask
, 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.
On the client, singletons are still per-user.
Thanks for the repro @dudley810. It looks like this issue is trying to use types like
RemoteAuthenticatorView
fromMicrosoft.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 ofAddMicrosoftIdentityWebApi
.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 utilizePersistentComponentState
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).
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)?
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:
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