Implementing Azure AD (Microsoft.Identity.Web) ontop of Microsoft.AspNetCore.Identity.EntityFrameworkCore (Local Users) - c#

My Web Application (.NET 6) was originally built using Microsoft.AspNetCore.Identity.EntityFrameworkCore to handle user accounts locally. Now I need to add support for Microsoft Azure AD. Through researching I found the Microsoft.Identity.Web library which appears to make implementing Azure AD extremely simple, and I was able to get a basic implementation working rather quickly, however I ran into issues as soon as I began working on the "post-redirect" tasks.
These Steps WORK correctly
User Browses to site
User Clicks "Login with Work Account"
User is Redirected to Microsoft
User Logs in
User is Redirected back to my Application
The problem starts at this point, when my controller method is hit, everything is null as if there is no user account information, no claims nothing. It almost appears as if the user just clicked the refresh button.
For testing purposes, I created the following method in the AccountsController to handle the user redirect
[AllowAnonymous]
[HttpGet]
public async Task<IActionResult> HandleLoginRedirect(string token = null)
{
var userAccount = User.Identity;
var userClaims = User.Identity as System.Security.Claims.ClaimsIdentity;
//You get the user's first and last name below:
var name = userClaims?.FindFirst("name")?.Value;
return View("SelectBuilding");
}
When the user is redirected they hit this function correctly, however token, userAccount and userClaims are all null. So I am unsure how I am supposed to lookup the users local account and/or create a local account for them if they don't have one? Shouldn't I be able to see atleast the users Name and/or Email address?
I did find that I can add an Event Handler "OnTokenValidated" that appears to be fired before the AccountController method is fired, and inside that event handler I can see several pieces of information including the user name & email address.
Here is an example of the OnTokenValidated event, in this event the tenantId is correctly set, and if I inspect context.SecurityToken.Claims I can see ~15 claims for the user account.
azure.Events.OnTokenValidated = async context =>
{
string tenantId = context.SecurityToken.Claims.FirstOrDefault(x => x.Type == "tid" || x.Type == "http://schemas.microsoft.com/identity/claims/tenantid")?.Value;
Console.WriteLine("Token Validated");
};

Related

How to Invalidate AspNetCore.Identity.Application Cookie after user log out

I am having trouble invalidating .AspNetCore.Identity.Application cookie in ASP.NET Core Identity once the user log out.
Once user clicks on log out below code will execute.
public async Task<IActionResult> Logout(LogoutInputModel model)
{
// build a model so the logged out page knows what to display
LoggedOutViewModel loggedOutViewModel = await BuildLoggedOutViewModelAsync(model.LogoutId);
_logger.LogInformation($"loggedOutViewModel : {JsonConvert.SerializeObject(loggedOutViewModel)}");
if (User?.Identity.IsAuthenticated == true)
{
// delete local authentication cookie
await _norskTakstSignInManager.SignOutAsync();
//clear cookies
var appCookies = Request.Cookies.Keys;
foreach (var cookie in appCookies)
{
Response.Cookies.Delete(cookie);
}
// raise the logout event
await _events.RaiseAsync(new UserLogoutSuccessEvent(User.GetSubjectId(), User.GetDisplayName()));
}
// check if we need to trigger sign-out at an upstream identity provider
if (loggedOutViewModel.TriggerExternalSignout)
{
// build a return URL so the upstream provider will redirect back
// to us after the user has logged out. this allows us to then
// complete our single sign-out processing.
string url = Url.Action("Logout", new { logoutId = loggedOutViewModel.LogoutId });
// this triggers a redirect to the external provider for sign-out
return SignOut(new AuthenticationProperties { RedirectUri = url }, loggedOutViewModel.ExternalAuthenticationScheme);
}
return View("LoggedOut", loggedOutViewModel);
}
This successfully clears all the cookies in the browser, however, if I grab the value of the cookie named ".AspNetCore.Identity.Application" prior to signing out, then add it back in on to the browser, then i can log in to the application without entering user credentials.
I tested few flows setting up cookie expiration time in different ways but non of them seem to work correctly.
I want to know way to invalidate the cookie without just clearing to resolve this issue.Then user should not be able to enter cookie manually and log in to the system. Any help is hugly appreciated. Thank you.
That's by design... one thing you can do is try updating the user's security stamp after logout, using UserManager.UpdateSecurityStampAsync.
This way the cookie's security stamp won't match the one in the database and the cookie will no longer be valid (however, no other cookie issued to that user will, even if they haven't "signed out"... so if a user has several sessions opened, all of those cookies will stop being valid, not just the one you signed out).
Identity doesn't track specific user sessions (it just validates the cookie against the user, and if it matches, it matches). If you want to be able to selectively remove sessions, you'll have to track them yourself
For me the best security practice is save every login and logout in one record with an unique random ID as GUID, then save this "id session" into the claims, and check this everytime the user access, if the ID in the claim is correct to that session.

Authentication/Authorization in ASP.NET Core using Enterprise SingleSignon page and Microsoft SignInManager

Presently I am working on an authentication issue in one of my ASP.NET Core(3.0) application.
To give some background, we have to use an Enterprise Single sign on page to authenticate the users. Once the user got authenticated it redirects back to our application along with user name in a HTTP header called "SM_USER". Then using that information we load corresponding Claim information from DB using Microsoft SignInManger. The application is working fine if the user is accessing the root but it couldn't able to access if they are trying to navigate a specific page directly, like http://website/Controller/Index.
I am suspecting that we may have implemented it wrongly so would like to know how should the below scenario to be implemented?
Our users first get authenticated using an enterprise Login Page(Single sign on) then redirected to our application. user information available on HTTP Headers and the corresponding claims information is available in our DB which we need to use for authorization purpose(we wanted to use Microrosoft SignInManger to load them).
I found the issue after researching it throughly so sharing here in case it useful for someone.
we observed whenever a user try to access a page if the user is not authenticated then it is redirecting Login page(Observed that decorating them with [Authorize] attribute is causing this), where We are using this Login page for local development purpose.
so when the user get redirected to login page and if the environment is not development then below code is executed on the Get method, which takes care of signing the user and creating UserPrincipal. Then after that we redirecting to the page the user requested.
if (!_signInManager.IsSignedIn(User))
{
string userName = HttpContext.Request.Headers["SM_USER"].ToString();
if (userName.Length > 0)
{
var user = await _userManager.FindByNameAsync(userName);
if (user != null)
{
var claimsPrincipal = await _signInManager.CreateUserPrincipalAsync(user);
await _signInManager.Context.SignInAsync(IdentityConstants.ApplicationScheme,
claimsPrincipal,
new AuthenticationProperties { IsPersistent = true });
if (string.IsNullOrEmpty(returnUrl)) //returnUrl is a parameter get passed by the system.
{
return RedirectToAction("<Action>", "<Controller>");
}
else
{
return Redirect(returnUrl);
}
}
}
}

Prompt user for additional information during an Open Id Connect event?

Using asp.net Core, Mvc and OpenIdConnect, is it possible to prompt an authenticated user for additional information during the ODIC authentication process, and then redirect back to the originally-desired page?
To give a concrete example: in our system one person, represented by an email address, can have multiple user ids that they may wish to operate under. Assume my email address is tregan#domain.com, and I have 3 user ids to choose from: treganCat, treganDog, treganMouse. When I hit a Controller action that is decorated with the [Authorize] attribute I first go through OpenIdConnect authentication, and one of the claims returned is an email address.
Using that email address, I want the application to prompt me to select the identity that I want to run under (treganDog, treganCat, or treganMouse).
From there, I want the application to take the user id that I selected, interrogate a database for the roles that go along with the selected user id, and load those roles as claims to my identity.
Finally, I want the application to send me on to my desired page (which is the protected Controller method that I originally attempted to visit).
Is this possible?
I'm using an Owin Startup class; the code below "works" except for the fictional line "var identityGuid = [return value from the prompt];" ("fictional" because it represents what I would like to occur, but in fact a series of redirects would be needed).
My example below uses the OnTicketReceived event, but that selection is arbitrary, I would be willing to do this in any event.
services.AddAuthentication(authenticationOptions =>
{
authenticationOptions.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
authenticationOptions.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie()
.AddOpenIdConnect(openIdConnectOptions =>
{
openIdConnectOptions.Authority = Configuration["PingOne:Authority"];
openIdConnectOptions.CallbackPath = "/Callback";
openIdConnectOptions.ClientId = Configuration["PingOne:ClientId"];
openIdConnectOptions.ClientSecret = Configuration["PingOne:ClientSecret"];
openIdConnectOptions.ResponseType = "code";
openIdConnectOptions.Events.OnTicketReceived = (ticketReceivedContext) =>
{
var emailClaim =
ticketReceivedContext.Principal.Claims.FirstOrDefault(o =>
o.Type == ClaimTypes.Email);
string emailAddress = emailClaim.Value;
//here is where I would like to prompt the user to select an identity based on the email address
//the selected identity is represented by a guid
var identityGuid = [return value from the prompt];
var roles = new MyRepository(myContext).GetRolesForUserId(identityGuid);
var claims = new List<Claim>();
foreach (string role in roles)
{
claims.Add(new Claim(ClaimTypes.Role, role));
}
ticketReceivedContext.Principal.AddIdentity(new ClaimsIdentity(claims));
return Task.CompletedTask;
};
});
This is impersonation where there is a real user and you need to identify the impersonated user after login.
You will need to complete the login first, return to the app and configure the principal. Then render a UI and receive the selected choice.
You then need your UI to call the back end and tell it to update claims in the auth cookie. Not sure if you'll get this to work though - the impersonated user may need separate storage - such as a second cookie.
This highlights that it can be useful to separate the token / credential the UI receives from the claims the back end works with.
I use the below design a lot for REST APIs that serve UIs directly - though it may be overkill for your solution:
https://authguidance.com/2017/10/03/api-tokens-claims/
I think what I want to do is simply not possible without either figuring out a way to do it inside PingOne or writing my own IdentityServer and taking care of the extra steps there.
I decided to instead write a custom middleware that fires after the Authentication middleware, as described in this SO question: In asp.net core, why is await context.ChallengeAsync() not working as expected?

Forgot Password in ADB2C - (AADB2C90118), How to run a specific user flow?

Problem
I am trying to handle the 'Reset password' user-flow in my application. Upon clicking the 'Forgot Password' link, the OnRemoteFailure OpenId event is successfully triggered, successfully redirecting to the specified url 'Home/ResetPassword', but instead or redirecting to the ADB2C reset password screen, it's redirecting back to the sign-in/sign-up page.
Background
The sign-up/sign-in policy works successfully but as per Microsoft docs: https://learn.microsoft.com/en-us/azure/active-directory-b2c/active-directory-b2c-reference-policies:
" A sign-up or sign-in user flow with local accounts includes a Forgot password? link on the first page of the experience. Clicking this link doesn't automatically trigger a password reset user flow.
Instead, the error code AADB2C90118 is returned to your application. Your application needs to handle this error code by running a specific user flow that resets the password. To see an example, take a look at a simple ASP.NET sample that demonstrates the linking of user flows. "
Active Directory B2C Settings
UserFlows
Code
OpenIdEvent
protected virtual Task OnRemoteFailure(RemoteFailureContext context)
{
context.HandleResponse();
// Handle the error code that Azure AD B2C throws when trying to reset a password from the login page
// because password reset is not supported by a "sign-up or sign-in policy"
if (context.Failure is OpenIdConnectProtocolException && context.Failure.Message.Contains("AADB2C90118"))
{
// If the user clicked the reset password link, redirect to the reset password route
context.Response.Redirect("/Home/ResetPassword");
}
else if (context.Failure is OpenIdConnectProtocolException && context.Failure.Message.Contains("access_denied"))
{
context.Response.Redirect("/");
}
else
{
context.Response.Redirect("/Home/Error?message=" + WebUtility.UrlEncode(context.Failure.Message));
}
return Task.FromResult(0);
}
HomeController
public IActionResult ResetPassword()
{
var redirectUrl = Url.Action(nameof(HomeController.Index), "Home");
var properties = new AuthenticationProperties { RedirectUri = redirectUrl };
properties.Items[AzureADB2COptionsExtended.PolicyAuthenticationProperty] = _adb2cOptions.ResetPasswordPolicyId;
return Challenge(properties, AzureADB2CDefaults.AuthenticationScheme);
}
A lot of examples I found use OWIN... There is very limited documentation on ASP.Net Core 2.2 w/ ADB2C.
The Sign-up-sign-in policy now has built-in support for password resets without a second "password-reset" user flow. It is quite confusing with all the documentation and samples out there but this is the latest docs and it works for us!
https://learn.microsoft.com/en-us/azure/active-directory-b2c/force-password-reset?pivots=b2c-user-flow
Normally, the ResetPassword flow you configured in your appsettings.json is called automatically when using the Microsoft.Identity.Web package. In your case B2C_1_SSPR. That means you must define a custom user flow with this Id.
(I guess SSPR = self-service password reset)
The only thing you need in this default case is call the following in your Startup:
services.AddMicrosoftIdentityWebAppAuthentication(Configuration, "ActiveDirectoryB2C");
This works like a charm.
However, you decided to deal with all this stuff by yourself and not to use the Microsft.Identity.Web library for all processing.
In this case you are able to handle Password-reset by yourself.
But let's have a look at the Microsft.Identity.Web. They integrated an AccountController that handles the processing of the B2C custom flows (in this case the ResetPassword action).
The exception handling for AADB2C90118 can be found in the source code of the Identity package:
if (isOidcProtocolException && message.Contains(ErrorCodes.B2CForgottenPassword))
{
// If the user clicked the reset password link, redirect to the reset password route
context.Response.Redirect($"{context.Request.PathBase}/MicrosoftIdentity/Account/ResetPassword/{SchemeName}");
}
Answer that was posted in the question:
For anyone else having the same or similar issue, make sure to keep an eye out on the OpenIdConnectEvents. We have been experimenting with ADB2C/OpenID and had test code. This code was obviously invalid.
protected virtual Task OnRedirectToIdentityProvider(RedirectContext context)
{
string policy = "";
context.Properties.Items.TryGetValue(AzureADB2COptionsExtended.PolicyAuthenticationProperty, out policy);
if (!string.IsNullOrEmpty(policy) && !policy.ToLower().Equals(_adb2cOptions.DefaultPolicy.ToLower()))
{
context.ProtocolMessage.Scope = OpenIdConnectScope.OpenId;
context.ProtocolMessage.ResponseType = OpenIdConnectResponseType.IdToken;
context.ProtocolMessage.IssuerAddress = context.ProtocolMessage.IssuerAddress.ToLower().Replace(_adb2cOptions.DefaultPolicy.ToLower(), policy.ToLower());
}
return Task.FromResult(0);
}
See the latest "Self-Service Password Reset" policy (custom or non-custom) from Microsoft docs.
From the doc:
"The new password reset experience is now part of the sign-up or sign-in policy. When the user selects the Forgot your password? link, they are immediately sent to the Forgot Password experience. Your application no longer needs to handle the AADB2C90118 error code, and you don't need a separate policy for password reset."

Deleted User logged in even after deleting on the other broswer

var query = from p in AdminModelContext.Users
where p.UserName == model.UserName && p.Password == encryptPassword
&& p.IsDeleted == false
select p;
IList<Users> userList = query.ToList();
if (userList.Count() > 0)
{
FormsAuthentication.SetAuthCookie(model.UserName, model.RememberMe);
if (CheckUrl(returnUrl))
{
return Redirect(returnUrl);
}
SetRoleForUser(userList[0].RolesId);
LoggerService.Info(string.Format("Login Successful for the user : {0}",
model.UserName));
return RedirectToAction("Index", "Home");
}
I am using the following code to Login through my website. The problem that I am facing is when I logged in with a user on a specific browser and at the same time login with a different user on a different browser, and then I delete the user(logged in on the other browser). Still I am able to navigate through the pages with the deleted user logged in.
I am not finding a fair solution to put authentication logic on every page. My website is in MVC model and using Form based authentication.
Please suggest how can I put the logged in user session validation and achieve this.
None of the answers so far actually acknowledge the question.
Lets look at the control flow:
User A enters log in page, supplies valid credentials
User A is issued Ticket A.
User B enters site, supplies valid credentials.
User B is issued Ticket B.
User B then revokes User A's access CREDENTIALS.
At this point nothing happens to Ticket A. Because the Ticket is independent on the credentials. When Ticket A expires they will then be required to present their credentials and it will fail login.
So what you've noticed that kicking a live user out of your site is actually pretty hard. As you've realized the ONLY solution is to have authentication logic on EVERY request. That unfortunately is really heavy.
In the login system I built I handled this aspect by creating 2 tickets, 1 ticket that's stored in the Forms Auth ticket as normal that has a big duration, and a ticket that's stored in HttpRuntime.Cache, I set the cache expiration to 15 minutes on this ticket.
On every page request I check to see whether a user has a ticket in the cache (based off their Forms Auth ticket information), at this point if they have no ticket I do a user data refresh and poll the user database. If the user has become suspended or deleted they will be logged out at then.
Using this method I know that my site can disable a user and within 15 minutes that user will be barred from the site. If I want them immediately barred I can just cycle the app config to clear the cache and FORCE it to happen.
Normally if you have the [Authorize] attribute defined for an Controller or an Action the Authentication is checked on every post back.
The build in MembershipProvider handles all that for you. But it seems you are using your own user Database. Then you have to implement your own MembershipProvider, IPrincipal and MembershipUser and have this added to your Web.config replacing the default one.
More you'll find here how to implement your own MembershipProvider: http://msdn.microsoft.com/en-us/library/f1kyba5e.aspx
My suggestion is to create an empty MVC project and have a look at the default authentication mechanism. And if your building a new Application with a new Database, try to use the default authentication.
Your validateUser function in your own MembershipProvider could look like this.
public override bool ValidateUser(string username, string password)
{
bool isValid = false;
bool isApproved = false;
string pwd = "";
using (AdminModelContext db = new AdminModelContext())
{
var user = db.Users.FirstOrDefault(u => u.UserName == username);
if (user != null)
{
pwd = user.Password;
isApproved = user.IsApproved;
if (CheckPassword(password, pwd))
{
if (isApproved)
{
isValid = true;
user.LastLoginDate = DateTime.Now;
user.LastActivityDate = DateTime.Now;
try
{
db.SubmitChanges();
}
catch (Exception ex)
{
Console.WriteLine(ex);
}
}
}
else
{
UpdateFailureCount(username, "password");
}
}
}
return isValid;
}
I see the problem now. I dont know how this works in MVC, but by using Authenticate_Request, you can validate if the user is still valid. The business logic may also double check if the user is still valid. But as far as I know, there is no way of iterating all the open sessions and killing the requierd ones, even in that case, the authorization cookie should be double checked on Session_Start event.
Another options is adding a global invalidated_users list on the application, and then checking that user against the invalid list. This list should only contain users that are invalidated after the application has restarted.
Link for Reading All Users Session:
http://weblogs.asp.net/imranbaloch/archive/2010/04/05/reading-all-users-session.aspx

Categories