Firstly, I'm sorry if this topic has already been discussed. I couldn't find what I wanted online so that's why I'm doing this.
The title explains it well, HttpContextAccessor is null when I try to access it.
I registered the service in Program.cs like this :
builder.Services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
builder.Services.AddScoped<CustomStateProvider>();
builder.Services.AddScoped<AuthenticationStateProvider>(s => s.GetRequiredService<CustomStateProvider>());
I'm using it to save user data inside cookies on authentification.
I injected IHttpContextAccessor inside the CustomStateProvider class :
[Inject]
public IHttpContextAccessor HttpContextAccessor { get; set; }
However when I launch the app I get System.NullReferenceException on this line :
var cookie = HttpContextAccessor.HttpContext.Request.Cookies["CurrentUser"];
Here's the full CustomStateProvider class :
public class CustomStateProvider : AuthenticationStateProvider
{
[Inject]
public IHttpContextAccessor HttpContextAccessor { get; set; }
private readonly IAuthService _authService;
private CurrentUser _currentUser;
public CustomStateProvider(IAuthService authService)
{
this._authService = authService;
}
public override async Task<AuthenticationState> GetAuthenticationStateAsync()
{
var identity = new ClaimsIdentity();
try
{
var userInfo = GetCurrentUser();
if (userInfo.IsAuthenticated)
{
var claims = new[] { new Claim(ClaimTypes.Name, _currentUser.UserName) }.Concat(_currentUser.Claims.Select(c => new Claim(c.Key, c.Value)));
identity = new ClaimsIdentity(claims, "Server authentication");
}
}
catch (HttpRequestException ex)
{
Console.WriteLine("Request failed:" + ex);
}
return new AuthenticationState(new ClaimsPrincipal(identity));
}
public async Task Login(ConnexionModel loginParameters)
{
_authService.Login(loginParameters);
// No error - Login the user
var user = _authService.GetUser(loginParameters.UserName);
_currentUser = user;
var cookieOptions = new CookieOptions
{
Expires = DateTime.Now.AddDays(7),
HttpOnly = true
};
HttpContextAccessor.HttpContext.Response.Cookies.Append("CurrentUser", JsonConvert.SerializeObject(_currentUser), cookieOptions);
NotifyAuthenticationStateChanged(GetAuthenticationStateAsync());
}
public async Task Logout()
{
_currentUser = null;
HttpContextAccessor.HttpContext.Response.Cookies.Delete("CurrentUser");
NotifyAuthenticationStateChanged(GetAuthenticationStateAsync());
}
public async Task Register(InscriptionModel registerParameters)
{
_authService.Register(registerParameters);
// No error - Login the user
var user = _authService.GetUser(registerParameters.UserName);
_currentUser = user;
var cookieOptions = new CookieOptions
{
Expires = DateTime.Now.AddDays(7),
HttpOnly = true
};
HttpContextAccessor.HttpContext.Response.Cookies.Append("CurrentUser", JsonConvert.SerializeObject(_currentUser), cookieOptions);
NotifyAuthenticationStateChanged(GetAuthenticationStateAsync());
}
public CurrentUser GetCurrentUser()
{
if (_currentUser != null && _currentUser.IsAuthenticated)
{
return _currentUser;
}
else
{
var cookie = HttpContextAccessor.HttpContext.Request.Cookies["CurrentUser"];
if (cookie != null)
{
return JsonConvert.DeserializeObject<CurrentUser>(cookie);
}
}
return new CurrentUser();
}
}
}
I think I didn't register the service properly, I tried messing with Program.cs but couldn't solve the problem. I'm pretty new to Blazor so I don't really know much about this kind of stuff.
Thank you for your help.
IHttpContext on Blazor Server is only available on the first Http request when loading a Blazor Server application. After that all connections to the server is handled via SignalR. Your HttpContext will most likely be null after this. Depending on what version of Blazor Server you're running you need to find the first file that is rendered in your application tree.
You could persist the user details into a cookie however, since Blazor Server is stateful, I would suggest you persist it via a cascading parameter throughout the application life cycle.
In my version of Blazor Server that I'm developing an app in I have two files:
_Host.cshtml
App.razor
_Host.cshtml is where I can access the IHttpContext where it is not null, I store all those values into a User object and persist it through the application via a cascading parameter. In my instance I'm using Windows Authentication, but you'd be able to use this methodology for any type of authentication.
_Host.cshtml
#page "/"
#namespace oms.net.Pages
#addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
#using Core.Identity.User
#using Microsoft.AspNetCore.Components.Web
#inject IUserRepository User
#{
Layout = null;
}
#{
var userObject = await User.GetUserDetailsAsync(User.GetCurrentUserName());
if(userObject != null)
{
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title></title>
<base href="~/" />
<component type="typeof(HeadOutlet)" render-mode="ServerPrerendered" />
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.4.1/css/bootstrap.min.css">
</head>
<body>
//this is key for persisting the user object as a cascading parameter
<component type="typeof(App)" render-mode="ServerPrerendered" param-UserObject = "#userObject"/>
<div id="blazor-error-ui">
<environment include="Staging,Production">
An error has occurred. This application may no longer respond until reloaded.
</environment>
<environment include="Development">
An unhandled exception has occurred. See browser dev tools for details.
</environment>
Reload
<a class="dismiss">🗙</a>
</div>
<script src="_framework/blazor.server.js"></script>
</body>
</html>
}
else
{
<b>Invalid user, contact support. The user may need to be added to []</b>
<i>Your Active Directory username may not match your produce pro credentials. If this is the case, please inform IT.</i>
}
}
App.razor
#using Core.Identity.User
#using Core.Identity.Models
<CascadingAuthenticationState>
<CascadingValue Value="userObject">
<Router AppAssembly="#typeof(Program).Assembly">
<Found Context="routeData">
<RouteView RouteData="#routeData" DefaultLayout="#typeof(MainLayout)" />
</Found>
<NotFound>
<LayoutView Layout="#typeof(MainLayout)">
<p></p>
</LayoutView>
</NotFound>
</Router>
</CascadingValue>
</CascadingAuthenticationState>
#code {
[Parameter]
public UserModel userObject { get; set; }
}
Example for accessing cascading parameter in a Razor component:
[CascadingParameter]
private UserModel User { get; set; }
Here is a more detailed explanation I created: https://www.raynorsystems.com/null-ihttpcontextaccessor-blazor-server/
Related
I have API calls utility in my blazor web project. I have added condition where if I get unauthorized response from API, I am throwing the unauthorized error and trying to catch it in program.cs file so I can redirect user to login page. while throwing error blazor engine returning error in browser.
Utitlity.cs
public async Task<CurrentResponse> GetAsync(IHttpClientFactory _httpClient, string url)
{
try
{
var request = new HttpRequestMessage(HttpMethod.Get, url);
request.Headers.Clear();
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", GetClaimValue(CustomClaimTypes.AccessToken));
var client = _httpClient.CreateClient("FSMAPI");
HttpResponseMessage httpResponseMessage = await client.SendAsync(request);
if(httpResponseMessage.StatusCode == System.Net.HttpStatusCode.Unauthorized)
{
ManageUnAuthorizedError();
}
CurrentResponse response = JsonConvert.DeserializeObject<CurrentResponse>(httpResponseMessage.Content.ReadAsStringAsync().Result);
return response;
}
catch (Exception exc)
{
throw exc;
}
}
private void ManageUnAuthorizedError(/*IHttpClientFactory _httpClient*/)
{
throw new UnauthorizedAccessException(HttpStatusCode.Unauthorized.ToString());
}
Program.cs
app.UseExceptionHandler(c => c.Run(async context =>
{
var exception = context.Features
.Get<IExceptionHandlerPathFeature>()
.Error;
var response = new { error = exception.Message };
if(exception.Message == HttpStatusCode.Unauthorized.ToString())
{
context.Response.Redirect("/Login");
}
}));
Here's a possible solution based on the information you've provided in the question.
You need to interact with the UI through Services
A notification service:
public class NeedToAuthenticateService
{
public string ErrorMessage { get; set; } = string.Empty;
public event EventHandler? AuthenticationRequired;
public void NotifyAuthenticationRequired()
=> AuthenticationRequired?.Invoke(this, new EventArgs());
}
This is a "simple" emulation of your API call done through a service that interfaces with the NeedToAuthenticateService and raises the AuthenticationRequired event.
public class APIReaderService
{
private NeedToAuthenticateService needToAuthenticateService;
public APIReaderService(NeedToAuthenticateService needToAuthenticateService)
{
this.needToAuthenticateService = needToAuthenticateService;
}
public void GetData()
{
// If you get an error
needToAuthenticateService.ErrorMessage = "You need to log in!";
needToAuthenticateService.NotifyAuthenticationRequired();
}
}
A simple demo Login page showing the message.
#page "/Logon"
<h3>Logon</h3>
#inject NeedToAuthenticateService needToAuthenticateService
<div class="p-3">
#this.needToAuthenticateService.ErrorMessage
</div>
#code {
}
A modified MainLayout page which registers and event handler with NeedToAuthenticateService and triggers a navigate event when AuthenticationRequired is raised.
#inherits LayoutComponentBase
#inject NeedToAuthenticateService needToAuthenticateService
#inject NavigationManager NavManager
#implements IDisposable
<PageTitle>BlazorApp1</PageTitle>
<div class="page">
<div class="sidebar">
<NavMenu />
</div>
<main>
<div class="top-row px-4">
About
</div>
<article class="content px-4">
#Body
</article>
</main>
</div>
#code {
protected override void OnInitialized()
=> this.needToAuthenticateService.AuthenticationRequired += GoToLogIn;
private void GoToLogIn(object? sender, EventArgs e)
=> NavManager.NavigateTo("/Logon");
public void Dispose()
=> this.needToAuthenticateService.AuthenticationRequired -= GoToLogIn;
}
And finally the registered services in Program
builder.Services.AddSingleton<WeatherForecastService>();
builder.Services.AddScoped<APIReaderService>();
builder.Services.AddScoped<NeedToAuthenticateService>();
Here I have simple private chat application in blazor server signalR where user logins, addfriend and chat with that friend.
For login I have used AspNetCore.Identity.
Now, the problem is that the application works perfectly fine in local machine but as I run application in ngrok() there is an error saying Error: System.Net.Http.HttpRequestException: Response status code does not indicate success: 401 (Unauthorized).ngrok is a cross-platform application that enables developers to expose a local development server to the Internet.
The software makes locally-hosted web server appear to be hosted on a subdomain of ngrok.com, meaning that no public IP or domain name on the local machine is needed
Below is what have done to authenticate user to signlar hub
In Host.cshtml
#{
var cookie = HttpContext.Request.Cookies[".AspNetCore.Identity.Application"];
}
<body>
#* Pass the captured Cookie to the App component as a paramter*#
<component type="typeof(App)" render-mode="Server" param-Cookie="cookie" />
</body>
In App.razor
#inject CookiesProvider CookiesProvider
#* code omitted here... *#
#code{
[Parameter]
public string Cookie { get; set; }
protected override Task OnInitializedAsync()
{
// Pass the Cookie parameter to the CookiesProvider service
// which is to be injected into the Chat component, and then
// passed to the Hub via the hub connection builder
CookiesProvider.Cookie = Cookie;
return base.OnInitializedAsync();
}
}
CookiesProvider.cs
public class CookiesProvider
{
public string Cookie { get; set; }
}
In webchat.razor
#attribute [Authorize]
#* code omitted here... *#
#code{
protected override async Task OnInitializedAsync()
{
var container = new CookieContainer();
var cookie = new Cookie()
{
Name = ".AspNetCore.Identity.Application",
Domain = "localhost",
Value = CookiesProvider.Cookie
};
container.Add(cookie);
hubConnection = new HubConnectionBuilder()
.WithUrl(NavigationManager.ToAbsoluteUri("/chathub"), options =>
{
// Pass the security cookie to the Hub.
options.Cookies = container;
}).Build();
await hubConnection.StartAsync();
}
}
In hub
[Authorize()]
public class ChatHub : Hub
{
}
Goal:
I would like to have a kind of landing page if the user is not logged in (basically all other pages should be locked without the need of the [Authorize] attribute on all pages).
Setup:
Blazor WASM
ASP.NET Hosted (with IdentityServer authorization)
Code:
I have rewritten the MainLayout.razor to redirect all not authorized requests to my redirect handler
<NotAuthorized>
<RedirectToLogin />
</NotAuthorized>
My RedirectToLogin.razor contains the landing page named Index.razor and the RemoteAuthenticatorView for auth requests
#inject NavigationManager Navigation
#using Microsoft.AspNetCore.Components.WebAssembly.Authentication
#if (!string.IsNullOrEmpty(action))
{
<RemoteAuthenticatorView Action="#action" />
}
else
{
<div>
Landing page...<br />
Login<br />
register
</div>
}
My RedirectToLogin.razor.cs does listen to location changes and forwards authentication request to the RemoteAuthenticatorView
public partial class RedirectToLogin : IDisposable
{
[CascadingParameter] private Task<AuthenticationState> AuthenticationStateTask { get; set; }
[Inject] private NavigationManager NavigationManager { get; set; }
string action = "";
protected override async Task OnInitializedAsync()
{
NavigationManager.LocationChanged += LocationChanged;
}
public void Dispose()
{
NavigationManager.LocationChanged -= LocationChanged;
}
async void LocationChanged(object sender, LocationChangedEventArgs e)
{
action = "";
var authenticationState = await AuthenticationStateTask;
if (authenticationState?.User?.Identity is not null)
{
var url = Navigation.ToBaseRelativePath(Navigation.Uri);
if (!authenticationState.User.Identity.IsAuthenticated)
{
if (url == "authentication/logged-out")
{
NavigationManager.NavigateTo("", true);
return;
}
if (url.Contains("authentication"))
{
var index = url.IndexOf("authentication") + 15;
if (url.Contains("?"))
action = url.Substring(index, url.IndexOf('?') - index);
else
action = url.Substring(index);
}
this.StateHasChanged();
}
}
}
}
The Problem:
The whole system works fine for basically all authentication requests with the exception of login callbacks.
Instead of loading the authorized view it still shows the landing page.
You need to refresh the page or click a second time on the login button to be redirected to the authorized view.
I tried to navigate the user manually on navigation change with login callback in url or when the user is authorized, but nothing seems to work.
Have you any idea why this behavior occurs and/or how this can be fixed?
Please comment if there's an easier way to accomplish my goal. I didn't find anything on the net and tried my best.
The problem is you are passing everything relating to authentication to this landing page. You need to have two separate pages for what you are trying to do, one landing page for unauthorized users and one for the authorized ones.
To change this, you'll need to update your Program.cs file and set the AuthenticationPaths under builder.Services.AddApiAuthorization. Here's an example:
builder.Services.AddApiAuthorization(options =>
{
options.AuthenticationPaths.LogInPath = "auth/login";
options.AuthenticationPaths.LogInCallbackPath = "auth/login-callback";
options.AuthenticationPaths.LogInFailedPath = "auth/login-failed";
options.AuthenticationPaths.LogOutPath = "auth/logout";
options.AuthenticationPaths.LogOutCallbackPath = "auth/logout-callback";
options.AuthenticationPaths.LogOutFailedPath = "auth/logout-failed";
options.AuthenticationPaths.LogOutSucceededPath = "auth/logged-out";
options.AuthenticationPaths.ProfilePath = "auth/profile";
options.AuthenticationPaths.RegisterPath = "auth/register";
options.AuthenticationPaths.RemoteProfilePath = "/profile";
options.AuthenticationPaths.RemoteRegisterPath = "/register";
});
Then the Authentication.razor page:
#page "/auth/{action}"
#using Microsoft.AspNetCore.Components.WebAssembly.Authentication
<RemoteAuthenticatorView Action="#Action">
<LoggingIn></LoggingIn>
<CompletingLoggingIn></CompletingLoggingIn>
<LogInFailed>Failed to log in.</LogInFailed>
<LogOut></LogOut>
<LogOutFailed>Failed to log out.</LogOutFailed>
<CompletingLogOut></CompletingLogOut>
<LogOutSucceeded><RedirectToLogin ReturnUrl="Dashboard"/></LogOutSucceeded>
<UserProfile></UserProfile>
<Registering></Registering>
</RemoteAuthenticatorView>
#code{
[Parameter] public string Action { get; set; }
}
For more info on the RemoteAuthenticatorView, check out this Microsoft documentation: https://learn.microsoft.com/en-us/aspnet/core/blazor/security/webassembly/additional-scenarios?view=aspnetcore-5.0#customize-app-routes
and just below that section: https://learn.microsoft.com/en-us/aspnet/core/blazor/security/webassembly/additional-scenarios?view=aspnetcore-5.0#customize-the-authentication-user-interface
I am using the .Net Core Angular template with individual accounts:
dotnet new angular -au individual
And I am adding an external Microsoft login provider thus:
services.AddAuthentication()
.AddMicrosoftAccount(config => {
config.ClientId = "***REDACTED***";
config.ClientSecret = "***REDACTED***";
config.SaveTokens = true;
})
.AddIdentityServerJwt();
Then, I create a page with the [Authorized] attribute, and I try to retrieve the Microsoft access_token but it always comes up as null. This is how the page is built:
Test.cshtml
#page
#model TestModel
<partial name="_LoginPartial" />
<h1>hi</h1>
<div>
<p>Access Token</p>
<pre>
#Model.AccessToken
</pre>
<p>ID Token</p>
<pre>
#Model.IdToken
</pre>
</div>
Test.cshtml.cs
[Authorize]
public class TestModel : PageModel {
private readonly UserManager<ApplicationUser> _userManager;
public string AccessToken;
public string IdToken;
public TestModel(UserManager<ApplicationUser> userManager) {
_userManager = userManager;
}
public async Task OnGetAsync() {
if (!User.Identity.IsAuthenticated) {
return;
}
var user = await _userManager.GetUserAsync(User);
AccessToken = await _userManager.GetAuthenticationTokenAsync(user, "Microsoft", "access_token");
IdToken = await _userManager.GetAuthenticationTokenAsync(user, "Microsoft", "id_token");
}
}
As mentioned, I only ever get null values for AccessToken and IdToken - what am I doing wrong?
P.S. I also tried await HttpContext.GetTokenAsync("Microsoft", "access_token") - it also returns null.
Answering my own question after days of research, which culminated in me cloning the repos at https://github.com/dotnet/aspnetcore and at https://github.com/identityserver/identityserver4 and tracking the target through the code...
The solution is to add a call to SigninManager.UpdateExternalAuthenticationTokensAsync to the Account/ExternalLogin callback handler (method OnGetCallbackAsync), after verifying that the external login is successful, i.e. after the call to _signInManager.ExternalLoginSignInAsync thus:
// Sign in the user with this external login provider if the user already has a login.
var result = await _signInManager.ExternalLoginSignInAsync(
info.LoginProvider,
info.ProviderKey,
isPersistent: false,
bypassTwoFactor : true);
if (result.Succeeded) {
await _signInManager.UpdateExternalAuthenticationTokensAsync(info); // <-- This
_logger.LogInformation(
"{Name} logged in with {LoginProvider} provider.",
info.Principal.Identity.Name,
info.LoginProvider);
return LocalRedirect(returnUrl);
}
I am currently learning asp.net core and blazor, and have come across an issue with little documentation. I have a Server side Blazor app and am re doing authentication to use local storage and the ServerAuthenticationStateProvider. This code is based off this guide, here is my current implementation of the state provider:
MyAuthenticationStateProvider.cs
namespace BlazorApp
{
public class MyAuthenticationStateProvider : ServerAuthenticationStateProvider
{
private readonly HttpClient _httpClient;
private readonly ILocalStorageService _localStorage;
public MyAuthenticationStateProvider(HttpClient httpClient, ILocalStorageService localStorage)
{
_httpClient = httpClient;
_localStorage = localStorage;
}
public override async Task<AuthenticationState> GetAuthenticationStateAsync()
{
var savedToken = await _localStorage.GetItemAsync<string>("authToken");
if (string.IsNullOrWhiteSpace(savedToken))
{
return new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity()));
}
var user = new ClaimsPrincipal(new ClaimsIdentity(ParseClaimsFromJwt(savedToken), "jwt"));
return new AuthenticationState(user);
}
public void MarkUserAsAuthenticated(string token)
{
var authenticatedUser = new ClaimsPrincipal(new ClaimsIdentity(ParseClaimsFromJwt(token), "jwt"));
var authState = Task.FromResult(new AuthenticationState(authenticatedUser));
NotifyAuthenticationStateChanged(authState);
}
LoginControl.cs
#page "/loginControl"
#inject IAuthService AuthService
#inject NavigationManager NavigationManager
<AuthorizeView>
<Authorized>
<b>Hello, #context.User.Identity.Name!</b>
<a class="ml-md-auto btn btn-primary"
href="logout?returnUrl=/"
target="_top">Logout</a>
</Authorized>
<Authorizing>
<b>Authentication in progress</b>
</Authorizing>
<NotAuthorized>
<input type="text"
placeholder="Email"
#bind="#email" />
<input type="password"
placeholder="Password"
#bind="#password" />
<button class="ml-md-auto btn btn-primary"
#onclick="#createSession">
Login
</button>
</NotAuthorized>
</AuthorizeView>
#code {
string email = "";
string password = "";
async void createSession()
{
var loginRequest = new LoginRequest
{
Email = email,
Password = password
};
await AuthService.Login(loginRequest);
}
}
I would expect that after the NotifyAuthenticationStateChanged(AuthState) is called, my login UI would refresh and the <Authorized> content to display. However my UI still shows the <NotAuthorized> content. Did I miss something to do with dispatching to the main thread?? I am Very new to all this but my Mentor mentioned something to do with this possibly having to do with being a background thread not telling the UI to re-render.
Really simple. All you need to do is:
StateHasChanged();
I just built a login control yesterday, so here are some bonus things you might want to know:
My login control has this:
<Login OnLogin="LoginComplete"></Login>
/// <summary>
/// This method returns the LoginResponse object
/// </summary>
/// <param name="loginResponse"></param>
private void LoginComplete(LoginResponse loginResponse)
{
// if the login was successful
if (loginResponse.Success)
{
// Set the player
player = loginResponse.Player;
// refresh the UI
this.StateHasChanged();
}
}
And in your control to invoke the LoginResponse delegate
// Get the loginResponse
LoginResponse loginResponse = await PlayerService.Login(emailAddress, password);
// if the loginResponse exists
if (NullHelper.Exists(loginResponse))
{
// set the player
player = loginResponse.Player;
// Perform the Login
await OnLogin.InvokeAsync(loginResponse);
}
I solved the problem by redirecting the user with the navigation manager and forcing the redirecting (by doing this the app is forced to change the authState).
I only did that because StateChanged() wans't working.
NaviMngr.NavigateTo("/", true);
I went about this wrong, server side blazor already implements AuthenticationStateProvider, so all I need to do is to implement something that sets the user.identity.isauthenticated, I am using cookies and a jet token to do this.