Blazor WASM standalone project reauthentication for multiple browser tabs after logout - authentication

I have implemented authorization with JWT auth on our blazor WASM project. All was well - logging in and logging out...until
Scenario:
I noticed that if I duplicated my 'logged in (authenticated identity user) tab', now having two valid authorized user tabs open. In the first tab I log out: I am re-directed to the login page, the stored token is gone, everything seems ok - but when I go to the second tab and click to go to a new page(within the tab) I am able to do so. When I debug and check the auth state.. it still has the validated identity user!
Expected Results:
When I logout in the first autenticated tab, I expect the auth state of the second tab to also be deauthenticated.
Error Messages:
Currently none
What I have tried:
I searched around for someone solving the same thing for Blazor WASM standalone apps. I did come across this guy's video where he describes most people are using the AuthenticationStateProvider wrong by injecting it directly instead of using a cascading parameter. He actually then demonstates the exact issue I am having - but then proceeds to solve it with a Balzor Server class library(using RevalidateServerAuthenticationStateProvider with some custom code on the ValidateAuthenticationStateAsync function)! Not a WASM solution! Link to his video: https://www.youtube.com/watch?v=42O7rECc87o&list=WL&index=37&t=952s - 13:23 he demonstrates the issue I have attempted to depict.
Code of my logout sequence:
To start - when you click the logout button - I redirect to a logout page. We will start here:
Logout.Razor.cs
[CascadingParameter]
public Task<AuthenticationState> AuthenticationStateTask { get; set; }
protected override async Task OnInitializedAsync()
{
await AuthService.Logout();
var authState = await AuthenticationStateTask;
var user = authState.User;
if (!user.Identity.IsAuthenticated)
{
NavManager.NavigateTo("/");
}
}
You can see you then are waiting for the AuthService to log you out.
AuthService.cs
private readonly AuthenticationStateProvider _authStateProvider;
public async Task Logout()
{
await _localStorage.RemoveItemAsync("authToken");
await ((CustomAuthStateProvider)_authStateProvider).NotifyUserLogout();
_client.DefaultRequestHeaders.Authorization = null;
}
Then you are waiting on the CustomAuthStateProvider:AuthenticationStateProdiver to notify that the state has changed.
CustomAuthStateProvider.cs
_anonymous = new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity()));
public async Task NotifyUserLogout()
{
var authState = Task.FromResult(_anonymous);
NotifyAuthenticationStateChanged(authState);
}
At this point in the active page we are logged out and redirected to the login page.
I was hoping there is some browser session option for the authstate I am missing or there is some microsoft documentation of handling this situation on WASM projects - but my peanut brain can't seem to find it.
Additional Information: I am purely only using the AuthenticationStateProdiver for a custom login.
I am not using OidcAuthentication or MsalAuthentication services.
This is a standlone WASM app with a completely decoupled API. All .net6 living on azure.
Looking forward to see if anyone else has this issue!
BR,
MP

Related

Blazor Server and SignalR and Azure AD

I am working on a web application using Blazor Server .Net 5. On my index page, I need to show the number of online users that logged into the website through Azure AD.
First, the user hits the web, and it gets redirected to Azure AD. Once the user is Authenticated in AD he/she will land on the index page. I want to show number of online users inside the app. I started using SignalR, but I am getting a very weird Error.
I am using SingalR client lib
First I created the
PeoplHub : Hub{
public async Task SendMessage(string user, string message)
{
await Clients.All.SendAsync("ReceiveMessage", user, message);
}
}
Then in my Index.razor I have created
hubConnection = new HubConnectionBuilder()
.WithUrl(NavigationManager.ToAbsoluteUri("/chathub"))
.Build();
hubConnection.On<string, string>("ReceiveMessage", (user, message) =>
{
var encodedMsg = $"{user}: {message}";
messages.Add(encodedMsg);
InvokeAsync(StateHasChanged);
});
await hubConnection.StartAsync();
I have also Implemented the IAsyncDisposal
public async ValueTask DisposeAsync()
{
if (hubConnection is not null)
{
await hubConnection.DisposeAsync();
}
}
in my startup I implemented
services.AddResponseCompression(opts =>
{
opts.MimeTypes = ResponseCompressionDefaults.MimeTypes.Concat(
new[] { "application/octet-stream" });
});
app.UseResponseCompression();
endpoints.MapHub<PeopleHub>("/peoplehub");
When I run the app, I get this error message
An unhandled exception occurred while processing the request.
JsonReaderException: '<' is an invalid start of a value. LineNumber: 2 | BytePositionInLine: 0.
System.Text.Json.ThrowHelper.ThrowJsonReaderException(ref Utf8JsonReader json, ExceptionResource resource, byte nextByte, ReadOnlySpan<byte> bytes)
InvalidDataException: Invalid negotiation response received.
Microsoft.AspNetCore.Http.Connections.NegotiateProtocol.ParseResponse(ReadOnlySpan<byte> content)
After researching on this issue. I found some useful information. We don't know the known issue, you can create a support ticket and ask for help.
It turns out that there is a known issue breaking SignalR Hubs with Blazor Server and Microsoft Identity.
And I also find official engineer said they don't plan to make improvements in this area given that we haven't seen many customers hitting it.
Related Issue:
blazor server signalr JsonReaderException
Workaround
ASP.NET Core Blazor Server additional security scenarios
Adding on to the answer by Jason Pan.
A quick way to validate the authorization is the problem.
Since I knew my code worked without Authorization in a dotnet 7 app
and this error was seen when I moved the code into my production code (dotnet 6)
where we use authorization with Azure AD
I ran a test with "AllowAnymous" on the hub.
[AllowAnonymous()] //TODO: authorize...
public class SignalrHub : Hub
{
and everything works as expected.
Next : follow the workaround as posted by Jason

Windows authentication fail with "401 Unauthorized"

I have a MVC client accessing a Web API protected by IDS4. They all run on my local machine and hosted by IIS. The app works fine when using local identity for authentication. But when I try to use Windows authentication, I keep getting "401 Unauthorized" error from the dev tool and the login box keeps coming back to the browser.
Here is the Windows Authentication IIS setting
and enabled providers
It's almost like that the user ID or password was wrong, but that's nearly impossible because that's the domain user ID and password I use for logging into the system all the time. Besides, according to my reading, Windows Authentication is supposed to be "automatic", which means I will be authenticated silently without a login box in the first place.
Update
I enabled the IIS request tracing and here is the result from the log:
As you can see from the trace log item #29, the authentication (with the user ID I typed in, "DOM\Jack.Backer") was successful. However, some authorization item (#48) failed after that. And here is the detail of the failed item:
What's interesting is that the ErrorCode says that the operation (whatever it is) completed successfully, but still I received a warning with a HttpStatus=401 and a HttpReason=Unauthorized. Apparently, this is what failed my Windows Authentication. But what is this authorization about and how do I fix it?
In case anyone interested - I finally figured this one out. It is because the code that I downloaded from IndentityServer4's quickstart site in late 2020 doesn't have some of the important pieces needed for Windows authentication. Here is what I had to add to the Challenge function of the ExternalController class
and here is the ProcessWindowsLoginAsync function
private async Task<IActionResult> ProcessWindowsLoginAsync(string returnUrl)
{
var result = await HttpContext.AuthenticateAsync(AccountOptions.WindowsAuthenticationSchemeName);
if (result?.Principal is WindowsPrincipal wp)
{
var props = new AuthenticationProperties()
{
RedirectUri = Url.Action(nameof(Callback)),
Items =
{
{ "returnUrl", returnUrl },
{ "scheme", AccountOptions.WindowsAuthenticationSchemeName },
}
};
var id = new ClaimsIdentity(AccountOptions.WindowsAuthenticationSchemeName);
id.AddClaim(new Claim(JwtClaimTypes.Subject, wp.Identity.Name));
id.AddClaim(new Claim(JwtClaimTypes.Name, wp.Identity.Name));
if (AccountOptions.IncludeWindowsGroups)
{
var wi = wp.Identity as WindowsIdentity;
var groups = wi.Groups.Translate(typeof(NTAccount));
var roles = groups.Select(x => new Claim(JwtClaimTypes.Role, x.Value));
id.AddClaims(roles);
}
await HttpContext.SignInAsync(IdentityConstants.ExternalScheme, new ClaimsPrincipal(id), props);
return Redirect(props.RedirectUri);
}
else
{
return Challenge(AccountOptions.WindowsAuthenticationSchemeName);
}
}
Now my windows authentication works with no issues.

How can I invoke callback while login in Google in .net core?

I am coding a third-party login-in with Google.
Here is the tutorial of Microsoft:
https://learn.microsoft.com/en-us/aspnet/core/security/authentication/social/google-logins?view=aspnetcore-3.0
I did what it said by using the Secret Manager and AddAuthentication
When I click the Login button on the website, it will redirect to google login successfully.
Now the next step is the invoke the callback. However, I don't know how to invoke it after login.
The tutorial of Microsoft only shows these two class:
https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.authentication.google.googleoptions?view=aspnetcore-2.2
https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.authentication.remoteauthenticationoptions.callbackpath?view=aspnetcore-2.2
But does not say something more about that yet. I even don't know how to use it and I can't find any other tutorial about it.
Would you please tell me how can I invoke the callback? Thank you.
You can use like below. Login user on External login method then get the result with confirmlogin method.
[HttpGet]
public async Task<object> ExternalLogin()
{
string redirectUrl = "api/account/ConfirmLogin&Provider=Google";
await AuthenticationProperties properties =
SignInManager.ConfigureExternalAuthenticationProperties("Google", redirectUrl);
return new ChallengeResult(provider, properties);
}
[HttpGet]
public async Task ConfirmLoginAsync(string Provider)
{
/*
make your checks
*/
Request.HttpContext.Response.Redirect(url);
}
Also you need to register your url like :
registerUrl
I hope it helps.

How to prevent multiple login in SAAS application?

What I need to do
I'm developing an application using ASP.NET CORE and I actually encountered a problem using the Identity implementation.
In the official doc infact there is no reference about the multiple session, and this is bad because I developed a SaaS application; in particular a user subscribe a paid plan to access to a specific set of features and him can give his credentials to other users so they can access for free, this is a really bad scenario and I'll lose a lot of money and time.
What I though
After searching a lot on the web I found many solutions for the older version of ASP.NET CORE, so I'm not able to test, but I understood that the usually the solution for this problem is related to store the user time stamp (which is a GUID generated on the login) inside the database, so each time the user access to a restricted page and there are more session (with different user timestamp) the old session will closed.
I don't like this solution because an user can easily copy the cookie of the browser and share it will other users.
I though to store the information of the logged in user session inside the database, but this will require a lot of connection too.. So my inexperience with ASP.NET CORE and the lack of resource on the web have sent me in confusion.
Someone could share a generic idea to implement a secure solution for prevent multiple user login?
I've created a github repo with the changes to the default .net core 2.1 template needed to only allow single sessions. https://github.com/xKloc/IdentityWithSession
Here is the gist.
First, override the default UserClaimsPrincipalFactory<IdentityUser> class with a custom one that will add your session to the user claims. Claims are just a key/value pair that will be stored in the user's cookie and also on the server under the AspNetUserClaims table.
Add this class anywhere in your project.
public class ApplicationClaimsPrincipalFactory : UserClaimsPrincipalFactory<IdentityUser>
{
private readonly UserManager<IdentityUser> _userManager;
public ApplicationClaimsPrincipalFactory(UserManager<IdentityUser> userManager, IOptions<IdentityOptions> optionsAccessor) : base(userManager, optionsAccessor)
{
_userManager = userManager;
}
public async override Task<ClaimsPrincipal> CreateAsync(IdentityUser user)
{
// find old sessions and remove
var claims = await _userManager.GetClaimsAsync(user);
var session = claims.Where(e => e.Type == "session");
await _userManager.RemoveClaimsAsync(user, session);
// add new session claim
await _userManager.AddClaimAsync(user, new Claim("session", Guid.NewGuid().ToString()));
// create principal
var principal = await base.CreateAsync(user);
return principal;
}
}
Next we will create an authorization handler that will check that the session is valid on every request.
The handler will match the session claim from the user's cookie to the session claim stored in the database. If they match, the user is authorized to continue. If they don't match, the user will get a Access Denied message.
Add these two classes anywhere in your project.
public class ValidSessionRequirement : IAuthorizationRequirement
{
}
public class ValidSessionHandler : AuthorizationHandler<ValidSessionRequirement>
{
private readonly UserManager<IdentityUser> _userManager;
private readonly SignInManager<IdentityUser> _signInManager;
public ValidSessionHandler(UserManager<IdentityUser> userManager, SignInManager<IdentityUser> signInManager)
{
_userManager = userManager ?? throw new ArgumentNullException(nameof(userManager));
_signInManager = signInManager ?? throw new ArgumentNullException(nameof(signInManager));
}
protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, ValidSessionRequirement requirement)
{
// if the user isn't authenticated then no need to check session
if (!context.User.Identity.IsAuthenticated)
return;
// get the user and session claim
var user = await _userManager.GetUserAsync(context.User);
var claims = await _userManager.GetClaimsAsync(user);
var serverSession = claims.First(e => e.Type == "session");
var clientSession = context.User.FindFirst("session");
// if the client session matches the server session then the user is authorized
if (serverSession?.Value == clientSession?.Value)
{
context.Succeed(requirement);
}
return;
}
}
Finally, just register these new classes in start up so they get called.
Add this code to your Startup class under the ConfigureServices method, right below services.AddDefaultIdentity<IdentityUser>()
.AddEntityFrameworkStores<ApplicationDbContext>();
// build default authorization policy
var defaultPolicy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.AddRequirements(new ValidSessionRequirement())
.Build();
// add authorization to the pipe
services.AddAuthorization(options =>
{
options.DefaultPolicy = defaultPolicy;
});
// register new claims factory
services.AddScoped<IUserClaimsPrincipalFactory<IdentityUser>, ApplicationClaimsPrincipalFactory>();
// register valid session handler
services.AddTransient<IAuthorizationHandler, ValidSessionHandler>();
You can use UpdateSecurityStamp to invalidate any existing authentication cookies. For example:
public async Task<IActionResult> Login(LoginViewModel model)
{
var user = await _userManager.FindByEmailAsync(model.Email);
if (user == null)
{
ModelState.AddModelError(string.Empty, "Invalid username/password.");
return View();
}
if (await _userManager.ValidatePasswordAsync(user, model.Password))
{
await _userManager.UpdateSecurityStampAsync(user);
var result = await _signInManager.SignInAsync(user, isPersistent: false);
// handle `SignInResult` cases
}
}
By updating the security stamp will cause all existing auth cookies to be invalid, basically logging out all other devices where the user is logged in. Then, you sign in the user on this current device.
Best way is to do something similar to what Google, Facebook and others do -- detect if user is logging in from a different device. For your case, I believe you would want to have a slight different behavior -- instead of asking access, you'll probably deny it. It's almost like you're creating a license "per device", or a "single tenant" license.
This Stack Overflow thread talks about this solution.
The most reliable way to detect a device change is to create a
fingerprint of the browser/device the browser is running on. This is a
complex topic to get 100% right, and there are commercial offerings
that are pretty darn good but not flawless.
Note: if you want to start simple, you could start with a Secure cookie, which is less likely to be exposed to cookie theft via eavesdropping. You could store a hashed fingerprint, for instance.
There are some access management solutions (ForgeRock, Oracle Access Management) that implement this Session Quota functionality. ForgeRock has a community version and its source code is available on Github, maybe you can take a look at how it is implemented there. There is also a blog post from them giving a broad view of the functionality (https://blogs.forgerock.org/petermajor/2013/01/session-quota-basics/)
If this is too complex for your use case, what I would do is combine the "shared memory" approach that you described with an identity function, similar to what Fabio pointed out in another answer.

AspNet Core External Authentication with Both Google and Facebook

I am trying to implement the Form-Authentication in ASP.Net Core with Both Google and Facebook Authentications. I followed some tutorials and after some struggles, I managed to make it work both.
However, the problem is that I cannot use both authentications for the same email.
For example, my email is 'ttcg#gmail.com'.
I used Facebook authentication to log in first... Registered my email and it worked successfully and put my record into 'dbo.ASPNetUsers' table.
Then I logged out, clicked on Google Authentication to log in. It authenticated successfully, but when I tried to register it keeps saying that my email is already taken.
I tried to do the same thing for other online websites (Eg, Stackoverflow). I used the same email for both Google and Facebook and the website knows, I am the same person and both my login / claims are linked even though they come from different places (Google & Facebook).
I would like to have that feature in my website and could you please let me know how could I achieve that.
In theory, it should put another line in 'dbo.AspNetUserLogins' and should link the same UserId with multiple logins.
Do I need to implement my own SignInManager.SignInAsync method to achieve that feature? Or am I missing any configuration?
You need to link your Facebook external login to your Google external login with your email by using UserManager.AddLoginAsync, you cannot register twice using the same adresse if you use the adresse as login.
Check out the Identity sample on Identity github repo.
https://github.com/aspnet/Identity/blob/dev/samples/IdentitySample.Mvc/Controllers/ManageController.cs
To link external login to a user, the Manae controller expose methods LinkLogin and LinkLoginCallback
LinkLogin requests a redirect to the external login provider to link a login for the current user
LinkLoginCallback processes the provider response
//
// POST: /Manage/LinkLogin
[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult LinkLogin(string provider)
{
// Request a redirect to the external login provider to link a login for the current user
var redirectUrl = Url.Action("LinkLoginCallback", "Manage");
var properties = _signInManager.ConfigureExternalAuthenticationProperties(provider, redirectUrl, _userManager.GetUserId(User));
return Challenge(properties, provider);
}
//
// GET: /Manage/LinkLoginCallback
[HttpGet]
public async Task<ActionResult> LinkLoginCallback()
{
var user = await GetCurrentUserAsync();
if (user == null)
{
return View("Error");
}
var info = await _signInManager.GetExternalLoginInfoAsync(await _userManager.GetUserIdAsync(user));
if (info == null)
{
return RedirectToAction(nameof(ManageLogins), new { Message = ManageMessageId.Error });
}
var result = await _userManager.AddLoginAsync(user, info);
var message = result.Succeeded ? ManageMessageId.AddLoginSuccess : ManageMessageId.Error;
return RedirectToAction(nameof(ManageLogins), new { Message = message });
}