Blazor landing page for not authorized users - c#

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

Related

SignalR Cookie Authentication Not Working In Blazor Server

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
{
}

Blazor ServerApp. Windows Authentication in Class?

I'm using the latest VS2019Pro with Core 3.1.
It seems like Blazor ServerApp has real-time code running within the #code{} tags in the .razor pages. So instead of using APIs to provide the data, I was thinking it would make a lot of sense to just create classes and methods to return the data.
The only issue I am facing is being able to use User.Identity.Name in a Class. Usually this is provided in the .razor pages and in Controllers without issue, but how can I (if it's even possible) use the same User.Identity.Name property within classes?
You can use the standard .net core auth: https://learn.microsoft.com/en-us/aspnet/core/blazor/security/?view=aspnetcore-3.1
Basically you define a:
[CascadingParameter] private Task<AuthenticationState> authenticationStateTask { get; set; }
Then you can await it:
private async Task LogUsername()
{
var authState = await authenticationStateTask;
var user = authState.User;
if (user.Identity.IsAuthenticated)
{
_authMessage = $"{user.Identity.Name} is authenticated.";
}
else
{
_authMessage = "The user is NOT authenticated.";
}
}
Edit -----
Something like this??
Base Component:
[CascadingParameter] private Task<AuthenticationState> authenticationStateTask { get; set; }
public String Username {get;set;}
protected override async Task OnInitializedAsync(){
Username = (await authenticationStateTask).User.Identity.Name;
}
Then in your other components:
#inherits BaseCompomnent
#Username
IMyRoleManager _myRoleManager;
private AuthenticationStateProvider _authenticationStateProvider;
public MyRepository(IMyRoleManager myRoleManager, AuthenticationStateProvider authentication)
{
_myRoleManager = myRoleManager;
_authenticationStateProvider = authentication;
}
public async Task<bool> AddRoleToUser(string role)
{
var id = await GetUserIdFromAuthenticationStateProviderAsync();
var result = await _myRoleManager.SetRoleForUser(role, id);
return result;
}
And in the startup file the correct entries have to be there for services.xxx (identity services)

How do I correctly notify the Blazor UI that my user state has changed?

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.

JWT doesn't get stored in ASP.NET Core with Blazor

I followed this tutorial: https://medium.com/#st.mas29/microsoft-blazor-web-api-with-jwt-authentication-part-1-f33a44abab9d
I downloaded the example: https://github.com/StuwiiDev/DotnetCoreJwtAuthentication/tree/Part2
I can see that the token is created but I don't understand how it is or should be saved on the client side as each time I access the SampleDataController, which has the Authorize tag, it returns a 401.
When calling and adding the token using Postman it works.
What am I missing for my user to be authenticated? Doesn't Microsoft.AspNetCore.Authentication.JwtBearer handle the client part (storing the token)?
What am I missing for my user to be authenticated? Doesn't Microsoft.AspNetCore.Authentication.JwtBearer handle the client part (storing the token)?
The JwtBearer runs on server side , it will only validate the authorization header of request, namely Authorization: Bearer your_access_token, and won't care about how you WebAssembly codes runs . So you need send the request with a jwt accessToken . Since the tutorial suggests you should use localStorage , let's store the accessToken with localStorage .
Because WebAssembly has no access to BOM yet, we need some javascript codes served as glue . To do that, add a helper.js under the JwtAuthentication.Client/wwwroot/js/ :
var wasmHelper = {};
wasmHelper.ACCESS_TOKEN_KEY ="__access_token__";
wasmHelper.saveAccessToken = function (tokenStr) {
localStorage.setItem(wasmHelper.ACCESS_TOKEN_KEY,tokenStr);
};
wasmHelper.getAccessToken = function () {
return localStorage.getItem(wasmHelper.ACCESS_TOKEN_KEY);
};
And reference the script in your JwtAuthentication.Client/wwwroot/index.html
<body>
<app>Loading...</app>
<script src="js/helper.js"></script>
<script src="_framework/blazor.webassembly.js"></script>
</body>
Now, let's wrap the javascript codes into C# . Create a new file Client/Services/TokenService.cs:
public class TokenService
{
public Task SaveAccessToken(string accessToken) {
return JSRuntime.Current.InvokeAsync<object>("wasmHelper.saveAccessToken",accessToken);
}
public Task<string> GetAccessToken() {
return JSRuntime.Current.InvokeAsync<string>("wasmHelper.getAccessToken");
}
}
Register this service by :
// file: Startup.cs
services.AddSingleton<TokenService>(myTokenService);
And now we can inject the TokenService into Login.cshtml and use it to save token :
#using JwtAuthentication.Client.Services
// ...
#page "/login"
// ...
#inject TokenService tokenService
// ...
#functions {
public string Email { get; set; } = "";
public string Password { get; set; } = "";
public string Token { get; set; } = "";
/// <summary>
/// response from server
/// </summary>
private class TokenResponse{
public string Token;
}
private async Task SubmitForm()
{
var vm = new TokenViewModel
{
Email = Email,
Password = Password
};
var response = await Http.PostJsonAsync<TokenResponse>("http://localhost:57778/api/Token", vm);
await tokenService.SaveAccessToken(response.Token);
}
}
Let's say you want to send data within FetchData.cshtml
#functions {
WeatherForecast[] forecasts;
protected override async Task OnInitAsync()
{
var token = await tokenService.GetAccessToken();
Http.DefaultRequestHeaders.Add("Authorization",String.Format("Bearer {0} ",token));
forecasts = await Http.GetJsonAsync<WeatherForecast[]>("api/SampleData/WeatherForecasts");
}
}
and the result will be :
Apologies in advance as this is somewhat responding to a previous answer, but I don't have the rep to comment on that.
If it helps anyone else who was similarly looking for a solution to using JWT in a Blazor app, I found #itminus answer incredibly useful, but it also pointed me to another course.
One problem I found was that calling FetchData.cshtml a second time would blow up when it tries to add the Authorization header a second time.
Instead of adding the default header there, I added it to the HttpClient singleton after a successful login (which I believe Blazor creates for you automatically). So changing SubmitForm in Login.cshtml from #itminus' answer.
protected async Task SubmitForm()
{
// Remove any existing Authorization headers
Http.DefaultRequestHeaders.Remove("Authorization");
TokenViewModel vm = new TokenViewModel()
{
Email = Email,
Password = Password
};
TokenResponse response = await Http.PostJsonAsync<TokenResponse>("api/Token/Login", vm);
// Now add the token to the Http singleton
Http.DefaultRequestHeaders.Add("Authorization", string.Format("Bearer {0} ", response.Token));
}
Then I realised, than as I'm building a SPA, so I didn't need to persist the token across requests at all - it's just in attached to the HttpClient.
The following class handle the login process on the client, storing the JWT token in local storage. Note: It is the developer responsibility to store the JWT token, and passes it to the server. The client (Blazor, Angular, etc.) does not do that for him automatically.
public class SignInManager
{
// Receive 'http' instance from DI
private readonly HttpClient http;
public SignInManager(HttpClient http)
{
this.http = http;
}
[Inject]
protected LocalStorage localStorage;
public bool IsAuthenticated()
{
var token = localStorage.GetItem<string>("token");
return (token != null);
}
public string getToken()
{
return localStorage.GetItem<string>("token");
}
public void Clear()
{
localStorage.Clear();
}
// model.Email, model.Password, model.RememberMe, lockoutOnFailure: false
public async Task<bool> PasswordSignInAsync(LoginViewModel model)
{
SearchInProgress = true;
NotifyStateChanged();
var result = await http.PostJsonAsync<Object>("/api/Account", model);
if (result)// result.Succeeded
{
_logger.LogInformation("User logged in.");
// Save the JWT token in the LocalStorage
// https://github.com/BlazorExtensions/Storage
await localStorage.SetItem<Object>("token", result);
// Returns true to indicate the user has been logged in and the JWT token
// is saved on the user browser
return true;
}
}
}
// This is how you call your Web API, sending it the JWT token for // the current user
public async Task<IList<Profile>> GetProfiles()
{
SearchInProgress = true;
NotifyStateChanged();
var token = signInManager.getToken();
if (token == null) {
throw new ArgumentNullException(nameof(AppState)); //"No token";
}
this.http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
// .set('Content-Type', 'application/json')
// this.http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
Profiles = await this.http.GetJsonAsync<Profile[]>("/api/Profiles");
SearchInProgress = false;
NotifyStateChanged();
}
// You also have to set the Startup class on the client as follows:
public void ConfigureServices(IServiceCollection services)
{
// Add Blazor.Extensions.Storage
// Both SessionStorage and LocalStorage are registered
// https://github.com/BlazorExtensions/Storage
**services.AddStorage();**
...
}
// Generally speaking this is what you've got to do on the client. // On the server, you've got to have a method, say in the Account controller, whose function is to generate the JWT token, you've to configure the JWT middleware, to annotate your controllers with the necessary attribute, as for instance:
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
and so on...
Hope this helps...

How to Display a Status Message on the Next Page after a Redirect with .net Framework?

I am adding a form to my .net web forms application and I want to redirect the user to another page but display a status message after the redirect such as 'Your articles has been submitted successfully'.
Any good ways of doing this?
I was thinking of doing it with sessions and a user control but wanted to see if there is an easier way.
Thought about code like this:
User control codebehind:
public String SessionName { get; set; }
public String Message
{
get
{
if (Session[SessionName] == null)
return String.Empty;
return Session[SessionName].ToString();
}
}
protected void Page_Unload(object sender, EventArgs e)
{
Session[SessionName] = null;
}
User control markup:
<% if (!String.IsNullOrEmpty(Message))
{%>
<div>
<%= Message %>
</div>
<%} %>
No, saving it in session and then reading on another page is the way to go.
What if your redirection included a query-string parameter that the destination page recognized and selected the right message? That avoids session altogether.
Landed on this post when trying to figure out how to pass the default StatusMessage field you get in net6 web apps (Razor Pages in this case).
I got it working with session as suggested above. However, I realised that in my case it was utterly unnecessary: there is a [TempData] attribute you can stick on the StatusMessage property of one page that will store it in memory (I'm guessing) and delete it after you read it.
Therefore, all you need to do is have the field defined in both the caller page and the receiving page and just set it on the caller page - the receiving page will find the value you set on the caller when you try to read it on the html side.
Next time you try to read it it will have been deleted (so you don't keep showing the same message over and over).
Example.
Caller page post call:
public class Create : PageModel
{
private readonly IMyService _service;
public Create(IMyService service)
{
_service = service;
}
[TempData] public string StatusMessage { get; set; } = string.Empty;
public async Task<IActionResult> OnPostAsync()
{
var model = new SomeModel();
try
{
await _service.Create(model);
}
catch(Exception e)
{
StatusMessage = $"Error: {e.Message}";
return Page();
}
StatusMessage = $"Model created successfully";
return RedirectToPage("/SomeModels/Index");
}
}
Receiving page:
public class Index : PageModel
{
[TempData] public string StatusMessage { get; set; } = string.Empty;
public readonly List<ExerciseCategory> Models;
public async Task<IActionResult> OnGet()
{
var models = await _service.Get();
Models.AddRange(models);
return Page();
}
}

Categories

Resources