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

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."

Related

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

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");
};

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);
}
}
}
}

Additionally password protect an ASP.NET Core MVC website hosted as Azure WebApp with existing authentication

I have an existing ASP.NET Core MVC application with ASP.NET Core Identity where I use a combination of signInManager.PasswordSignInAsync and [Authorize] attributes to enforce that a user is logged in to website, has a certain role et cetera. This works fine locally and in an Azure WebApp.
Now, I want to publish a preview version of my application to another Azure WebApp. This time, I want each visitor to enter another set of credentials before anything from the website is being shown. I guess I'd like to have something like an .htaccess / BasicAuthenication equivalent. However, after a user entered the first set of credentials, he should not be logged in since he should need to use the normal login prodecure (just as in the live version which is publicly accessible but this has certain pages which require the user to be logged in). Basically, I just want to add another layer of password protection on top without impacting the currently existing authentication.
Given that I want allow access to anyone with the preview password, the following solutions do not seem to work in my case:
Limit the access to the WebApp as a firewall setting. The client IPs will not be from a certain IP range and they will be dynamically assigned by their ISP.
Use an individual user account with Azure AD in front. This might be my fallback (although I'm not sure on how to implement exactly) but I'd rather not have another set of individual user credentials to take care. The credentials could even be something as simple as preview // preview.
Is there a simple way like adding two lines of codes in the Startup class to achieve my desired second level of password protection?
You can do a second auth via a basic auth, something simple and not too much code. You will need a middleware that will intercept/called after the original authentication is done
Middleware
public class SecondaryBasicAuthenticationMiddleware : IMiddleware
{
//CHANGE THIS TO SOMETHING STRONGER SO BRUTE FORCE ATTEMPTS CAN BE AVOIDED
private const string UserName = "TestUser1";
private const string Password = "TestPassword1";
public async Task InvokeAsync(HttpContext context, RequestDelegate next)
{
//Only do the secondary auth if the user is already authenticated
if (!context.User.Identity.IsAuthenticated)
{
string authHeader = context.Request.Headers["Authorization"];
if (authHeader != null && authHeader.StartsWith("Basic "))
{
// Get the encoded username and password
var encodedUsernamePassword = authHeader.Split(' ', 2, StringSplitOptions.RemoveEmptyEntries)[1]?.Trim();
// Decode from Base64 to string
var decodedUsernamePassword = Encoding.UTF8.GetString(Convert.FromBase64String(encodedUsernamePassword));
// Split username and password
var username = decodedUsernamePassword.Split(':', 2)[0];
var password = decodedUsernamePassword.Split(':', 2)[1];
// Check if login is correct
if (IsAuthorized(username, password))
{
await next.Invoke(context);
return;
}
}
// Return authentication type (causes browser to show login dialog)
context.Response.Headers["WWW-Authenticate"] = "Basic";
// Return unauthorized
context.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
}
else
{
await next.Invoke(context);
}
}
//If you have a db another source you want to check then you can do that here
private bool IsAuthorized(string username, string password) =>
UserName == username && Password == password;
}
In startup -> Configure (make sure you add this after your existing authentication and authorization)
//Enable Swagger and SwaggerUI
app.UseMiddleware<SecondaryBasicAuthenticationMiddleware>(); //can turn this into an extension if you wish
app.UseAuthentication();
app.UseAuthorization();
In Startup -> ConfigureServices register the middleware
services.AddTransient<SecondaryBasicAuthenticationMiddleware>();
And chrome should pop up a basic auth dialog like this

Authentication against local AD in the Angular application

I've been developing an Angular app with .NET Core backend (services). The task is to enable an integrated authentication, i.e. make it work with the local user seamlessly, so I login to my (connected to a local AD) machine once and the web application lets me in without the necessity to login a second time. We've been working with Identity Server 4 and intended to implement this scenario using it.
There is a little documentation on the official website concerning the Windows Authentication (e.g. against Active directory): http://docs.identityserver.io/en/latest/topics/windows.html but it doesn't explain much. As per my info, to make this scenario work the browser utilizes either Kerberos or NTLM. Neither of them is mentioned in the IS4 docs. I'm lacking the understanding of how the local credentials are getting picked up and how IS4 'knows' the user belongs to AD? How I can make sure only the users from a specific domain have access to my app?
I found some working stuff here https://github.com/damienbod/AspNetCoreWindowsAuth but questions remain the same. Even though I was able to get to the app with my local account I don't understand the flow.
I expect the user utilizing the app in the local network to log-in to the app without entering the login/password (once he's already logged in to the Windows). Is this something achievable?
Identity Server is intended to serve as an Identity Provider, if you need to talk with your AD you should see the Federation Gateway architecture they propose using the IAuthenticationSchemeProvider. Where Identity Server acts as an endpoint and talks with your AD.
This is the link:
http://docs.identityserver.io/en/latest/topics/federation_gateway.html
You have the control to programmatically reach your AD and pass the correct credentials to get the authentication. That step should be done in your Identity Server. After you get authenticated you should get redirected to your application again.
About your last question, the answer is yes, if you have your website hosted on an intranet and you have the access to your AD, you don't need to capture your credentials as user input, you can programmatically reach the AD as I said.
Bellow is the code I use to connect with my active directory
On the ExternalController class, you get when you use IdentityServer, you have this:(I don't remember at the top of my head how much I changed from the original code, but you should get the idea)
/// <summary>
/// initiate roundtrip to external authentication provider
/// </summary>
[HttpGet]
public async Task<IActionResult> Challenge(string provider, string returnUrl)
{
if (string.IsNullOrEmpty(returnUrl)) returnUrl = "~/";
// validate returnUrl - either it is a valid OIDC URL or back to a local page
if (Url.IsLocalUrl(returnUrl) == false && _interaction.IsValidReturnUrl(returnUrl) == false)
{
// user might have clicked on a malicious link - should be logged
throw new Exception("invalid return URL");
}
if (AccountOptions.WindowsAuthenticationSchemeName == provider)
{
// windows authentication needs special handling
return await ProcessWindowsLoginAsync(returnUrl);
}
else
{
// start challenge and roundtrip the return URL and scheme
var props = new AuthenticationProperties
{
RedirectUri = Url.Action(nameof(Callback)),
Items =
{
{ "returnUrl", returnUrl },
{ "scheme", provider },
}
};
return Challenge(props, provider);
}
}
private async Task<IActionResult> ProcessWindowsLoginAsync(string returnUrl)
{
// see if windows auth has already been requested and succeeded
var result = await HttpContext.AuthenticateAsync(AccountOptions.WindowsAuthenticationSchemeName);
if (result?.Principal is WindowsPrincipal wp)
{
// we will issue the external cookie and then redirect the
// user back to the external callback, in essence, testing windows
// auth the same as any other external authentication mechanism
var props = new AuthenticationProperties()
{
RedirectUri = Url.Action("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));
// add the groups as claims -- be careful if the number of groups is too large
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(
IdentityServer4.IdentityServerConstants.ExternalCookieAuthenticationScheme,
new ClaimsPrincipal(id),
props);
return Redirect(props.RedirectUri);
}
else
{
// trigger windows auth
// since windows auth don't support the redirect uri,
// this URL is re-triggered when we call challenge
return Challenge(AccountOptions.WindowsAuthenticationSchemeName);
}
}
If you want to use Azure AD, I would recommend you to read this article:
https://damienbod.com/2019/05/17/updating-microsoft-account-logins-in-asp-net-core-with-openid-connect-and-azure-active-directory/
Not sure if it's what you want, but I would use the Active Directory Federation Services to configure an OAuth2 endpoint and obtain the user token in the .Net Core Web App.
Isn't NTLM authentication support limited on non Microsoft browsers?
OAuth2 have the advantage of using only standard technologies.
One way to do it is to have 2 instances of the app deployed.
The first one is configured to use Windows Authentication and the other one uses IS4.
ex:
yoursite.internal.com
yoursite.com
Your local DNS should redirect traffic internally from yoursite.com to yoursite.internal.com
yoursite.internal.com will be the one configured to use AD authentication. You should have a flag in your appsettings.json to indicate if this instance is a AD auth or IS4 auth.
The downside of this solution is that you have to deploy 2 instances

Categories