Closed dhindrik closed 9 months ago
I Had the same problem (authorizing with local Keycloak server).
If I manually logged in (calling HttpContext.ChallengeAsync("oidc")
and then visited the page with the Authorize
, everything works fine.
If I visited the page with the Authorize
attribute before being authenticated, I got the same error message as OP (instead of being redirected to the Login page as configures in Routes.razor
.
It turned out the DefaultChallengeScheme
I configured in the AddAuthentication
options was no the same string as the one I used in my AddOpenIdConnect
call.
When that was corrected, everything worked fine.
Same trouble here, in NET 7.0 was used startup.cs:
services.AddScoped<AuthenticationStateProvider, CustomAuthenticationStateProvider>();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseAntiforgery(); // <-- NEW in net8.0
// modified endpoints for net8.0:
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
endpoints.MapRazorComponents<Pages.App>().AddInteractiveServerRenderMode()
.AddAdditionalAssemblies(new System.Reflection.Assembly[] { typeof(NavBar).Assembly });
});
Everything worked, redirect to login also.
Routes.razor net8.0 and 7.0
<CascadingAuthenticationState>
<StartupLoadingScreen>
<Router AppAssembly="@typeof(Program).Assembly" AdditionalAssemblies="new[] { typeof(NavBar).Assembly }">
<Found Context="routeData">
<AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" >
<NotAuthorized>
**<E1W.Pages.RedirToLogin/>**
</NotAuthorized>
</AuthorizeRouteView>
....
but in NET 8.0 it fails - when you enter direct url(any different page then login) into the browser and user is not authorized.
Also related-more detail to someones else:
Temporary solution - but also bug: https://github.com/dotnet/aspnetcore/issues/52317#issuecomment-1830673284
So if it is the case, please provide updated "documentation", for net 8.0 (minimalised setup):
I have the same issue.
If have the following AuthorizationRequirement
and handler to check if the logged in user has a certain claim:
public class RequireClaimPresent : IAuthorizationRequirement
{
public RequireClaimPresent(string claimType)
{
ClaimType = claimType;
}
public string ClaimType { get; set; }
}
public class RequireClaimPresentHandler : AuthorizationHandler<RequireClaimPresent>
{
protected override Task HandleRequirementAsync(
AuthorizationHandlerContext context, RequireClaimPresent requirement)
{
bool hasClaim = context.User.Claims.Any(claim => claim.Type == requirement.ClaimType);
if (hasClaim)
{
context.Succeed(requirement);
}
return Task.CompletedTask;
}
}
I add it to my services using:
builder.Services.AddAuthorization(x => x.AddPolicy("MyClaimType", policy => policy.AddRequirements(new RequireClaimPresent("MyClaimType"))));
services.AddSingleton<IAuthorizationHandler, RequireClaimPresentHandler>();
When using it in an AutorizedView
, it works:
<CascadingAuthenticationState>
<AuthorizeView Policy="MyClaimType">
You are authorized!
</AuthorizeView>
</CascadingAuthenticationState>
I use this change the page depending on user rights.
However, sometimes I want non-authorized users to be unable to access the page at all, also not via a direct url.
For this I attempt to use the Authorize
attribute at the top of the page:
@attribute [Authorize(Policy = "MyClaimType")]
As soon as I add the attribute, I get the error specified above:
An unhandled exception occurred while processing the request.
InvalidOperationException: No authenticationScheme was specified, and there was no DefaultChallengeScheme found. The default schemes can be set using either AddAuthentication(string defaultScheme) or AddAuthentication(Action<AuthenticationOptions> configureOptions).
Microsoft.AspNetCore.Authentication.AuthenticationService.ChallengeAsync(HttpContext context, string scheme, AuthenticationProperties properties)
I'm using Blazor in .NET 8.
Before I used Blazor in .NET 6 and there verything just worked (although many details in the program.cs
have changed since then so it is hard to rule out if it due to one of these things).
If someone spots a mistake please let me know. Otherwise I hope this problem gets fixed soon.
There appear to be a couple issues going on here. The two most recent comments from @yablos and @Stefan13-13 seem to be related #52063 which basically comes down to #46996 changing the way routing works for MapRazorComponents
vs the old MapBlazorHub
/MapFallbackToPage("/_Host")
.
Since MapRazorComponents
now uses endpoint routing, authentication and authorization is first handled by the UseAuthentication
and UseAuthorization
middleware and any associated authentication schemes and authorization policies.
This means that if a Blazor component requires authentication, but the client is unauthenticated, the authorization middleware will ask the authentication scheme to "challenge" the client. When using cookie-based authentication schemes, this will redirect you to CookieAuthenticationOptions.LoginPath before any custom, blazor-specific AuthenticationStateProvider
gets a chance to run or any AuthorizeView
or AuthorizeRouteView
gets a chance to render.
As long as the request is authenticated, meaning the HttpContext.User.Identity.IsAuthenticated
is true and HttpContext.User.Identity.Name
is not null when the authorization middleware runs, then the rest of Blazor authentication should continue as normal if you're doing server rendering since AddInteractiveServerComponents
adds the ServerAuthenticationStateProvider
which copies the authentication state from the HttpContext
or HubContext
via SetAuthenticationState
.
I know that you may not want cookie auth, but cookies are the best way to transparently capture and preserve authentication state across requests even those that don't involve Blazor. You don't need to use Identity to use basic authentication cookies. If you follow the guidance at https://learn.microsoft.com/en-us/aspnet/core/security/authentication/cookie, you can create custom /login
endpoint that allows you to programmatically define any ClaimsPrincipal
you want and configure things like refresh and expiration.
If all your endpoints really are Blazor endpoints, you're free to register a custom IAuthorizationMiddlewareResultHandler
that causes the authentication middleware to skip any "challenge" that would otherwise be initiated for unauthenticated requests to endpoints that require authorization as suggested in #52063 and the "**Temporary solution" mentioned earlier, but I wouldn't recommend it in the even any non-Blazor endpoints get added at a later time not realizing the built-in security usually implemented by the authorization middleware is being bypassed. This seems like a security hole waiting to happen.
I think your best bet is to rely on standard authentication schemes and policies that work for Blazor and any other kind of ASP.NET Core endpoint.
As for the original issue, I'm not sure how an authentication scheme was ever specified without a call to .AddNegotiate()
as demonstrated in https://learn.microsoft.com/en-us/aspnet/core/security/authentication/windowsauth?view=aspnetcore-6.0&tabs=visual-studio#iisiis-express or some other call to add an authentication handler. For me, I see the "No authenticationScheme was specified" when I run dhindrik/Blazor8AuthAzure locally with IIS Express without even needing to publish to Azure.
I see it on Azure too, but it seems expected to me when no authentication handler is registered. Does Azure App Service register an authentication handler automatically with it's SiteExtensions? I thought if you tried to add an Identity provider to your App Service via the portal, it tried to authenticate every request regardless of how the app was configured in termps of the [Authorize]
attribute. Is that not the case? @dhindrik Can you provide a sample of the .NET 7 application that does work?
How can I use HttpContext.SignInAsync
(Cookies Auth) in a Blazor Web App with @rendermode="InteractiveServer"
? HttpContext
is always null
.
You cannot call HttpContext.SignInAsync
directly with @rendermode="InteractiveServer"
, because code could be running in the context of a SignalR WebSocket connection which has already sent response headers. Cookie sign in needs the client to make a new request in order to send its Set-Cookie
header.
Fortunately, you can create a minimal endpoint accepting a form post that calls SignInAsync
and redirects back to your component. The template already does something similar for logout since NavMenu.razor
, which contains the logout form, can be rendered interactively. The "/Logout" endpoint below sends the Set-Cookie
header via SignInAsync
as part of a 302 redirect response landing the browser back at the returnUrl
where it started.
For login, the template is configured to always render Login.razor non-interactively with this logic in App.razor
, so it can use a <EditForm>
rather than a plain <form>
and count on the HttpContext
being available as a [CascadingParameter]
.
But if you really want to render the login form on an interactive component, nothing is stopping you from taking the same approach the template does with minimal endpoint for logout and use it for login as well.
@halter73 could you please provide an example of how you would implement a login and registration page using this approach? because I can't figure out how I can get all the form submissions, validations, and redirects to work together as they should. I am probably just incompetent but some help would be appreciated!
Is this still broken?
I'm trying to use my DuendeIdentityServer to log into a Blazor app and it redirects to Identity to do the login but then ends up in an infinite loop as the page requiring authorization just starts the OIDC flow again.
Yes ... still same issue on .net 8
Anyone looking into .NET 8 Auth with Server-side Blazor should read carefully @halter73 explanation. It's helped me very much to setup a simple login.
Is there an existing issue for this?
Describe the bug
We have upgraded our Blazor site to .NET 8. After we deployed it to Azure we get this exception:
InvalidOperationException: No authenticationScheme was specified, and there was no DefaultChallengeScheme found. The default schemes can be set using either AddAuthentication(string defaultScheme) or AddAuthentication(Action<AuthenticationOptions> configureOptions).
In Azure we have Authentication enabled with Microsoft as provider. It worked fine with .NET 7 and it also works fine with .NET 8 if we just changes target framework. If we rewrite it in the new style, with App, Routes, MapRazorComponents etc we get the exception.
We can also reproduce it with a new app (se reproduction repo.
After a week of trying to isolate the problem we realized it is about the Authorize attribute we have in the base component. The exception is only thrown when we have the base component for a component with a page directive.
Expected Behavior
Site should work as it does with .NET 7.
Steps To Reproduce
Repo: https://github.com/dhindrik/Blazor8AuthAzure
Exceptions (if any)
No response
.NET Version
8.0.100
Anything else?
No response