Angular Azure AD B2C Password reset policy after password changed logs into app and then shows login page - azure-ad-b2c

I am using Azure AD B2C with my Angular 11 application.
I am trying to use forgot password policy and it is working fine except after password is changed, it logs me into application and displays me my landing page and a fraction of second redirects to login page. Not sure what i am doing wrong.
Can anyone please help me how to resolve this?
this.msalBroadcastService.msalSubject$
.pipe(
filter((msg: EventMessage) => msg.eventType === EventType.LOGIN_FAILURE || msg.eventType === EventType.ACQUIRE_TOKEN_FAILURE),
takeUntil(this._destroying$)
)
.subscribe((result: EventMessage) => {
// Checking for the forgot password error. Learn more about B2C error codes at
// https://learn.microsoft.com/azure/active-directory-b2c/error-codes
if (result.error && result.error.message.indexOf('AADB2C90118') > -1) {
let resetPasswordFlowRequest: RedirectRequest | PopupRequest = {
authority: b2cPolicies.authorities.resetPassword.authority,
scopes: [],
};
this.login(resetPasswordFlowRequest);
};
});
login(userFlowRequest?: RedirectRequest | PopupRequest) {
if (this.msalGuardConfig.interactionType === InteractionType.Popup) {
if (this.msalGuardConfig.authRequest) {
this.authService.loginPopup({ ...this.msalGuardConfig.authRequest, ...userFlowRequest } as PopupRequest)
.subscribe((response: AuthenticationResult) => {
this.authService.instance.setActiveAccount(response.account);
});
} else {
this.authService.loginPopup(userFlowRequest)
.subscribe((response: AuthenticationResult) => {
this.authService.instance.setActiveAccount(response.account);
});
}
} else {
if (this.msalGuardConfig.authRequest) {
this.authService.loginRedirect({ ...this.msalGuardConfig.authRequest, ...userFlowRequest } as RedirectRequest);
} else {
this.authService.loginRedirect(userFlowRequest);
}
}
}

I was trying to do the same thing the other day using user flows, not custom policy as per business requirement, However, I found out that after the password reset is finished, it redirects back to the app, but it can not sign in back using returned information through the URL. Not sure, if it is a bug or what, but it still tries to do that and timeout.
I would suggest after the password reset is successful, try to force logout e.g. remove MSAL stored information, and redirect to the login or landing page.
If I find a better solution, I will post it for you.

I have done a bit of research on this. I found out, once the password has been reset, the user is supposed to log in again. If you want to get the user logged in after the password has been reset, you need to authenticate the user in the API and return back the token. Here is the code for that:
public async Task<OAuthResult> AuthenticateCredentialsAsync(string username, string password)
{
var oauthEndpoint = new Uri(new Uri($"{_b2cSettings.Authority}{_b2cSettings.SignInROPC}/"), "oauth2/v2.0/token");
using (var client = new HttpClient())
{
var result = await client.PostAsync(oauthEndpoint, new FormUrlEncodedContent(new[]
{
new KeyValuePair<string, string>("client_id", _b2cSettings.ClientId),
new KeyValuePair<string, string>("grant_type", "password"),
new KeyValuePair<string, string>("client_info", "1"),
new KeyValuePair<string, string>("username", username),
new KeyValuePair<string, string>("password", password),
new KeyValuePair<string, string>("scope", $"openid {_b2cSettings.ClientId} offline_access "),
new KeyValuePair<string, string>("client_secret", _b2cSettings.ClientSecret),
new KeyValuePair<string, string>("response_type", "token id_token")
}));
if (result.StatusCode != HttpStatusCode.OK)
throw new AuthenticationFailedException(await result.Content.ReadAsStringAsync());
var content = await result.Content.ReadAsStringAsync();
var authResult = JsonConvert.DeserializeObject<OAuthResult>(content);
return authResult;
}
}
public class OAuthResult
{
public string Token_Type { get; set; }
public string Scope { get; set; }
public int Expires_In { get; set; }
public string Access_Token { get; set; }
public string Id_Token { get; set; }
public string Refresh_Token { get; set; }
public string Client_Info { get; set; }
}
NOTE: You need to create an ROPC policy in the Azure AD B2C.
Another option is to logout user after the password reset and redirection happens back to the App:
this.loggedInSuccess$ = new ReplaySubject<boolean>();
this.msalBroadcastService.msalSubject$
.pipe(
filter((msg: EventMessage) => msg.eventType === EventType.LOGIN_SUCCESS || msg.eventType === EventType.ACQUIRE_TOKEN_SUCCESS)
)
.subscribe((result) => {
const payload: IdTokenClaims = <AuthenticationResult>result.payload;
if (payload.idTokenClaims?.tfp === b2cPolicies.names.forgotPassword) {
return this.msalService.logout();
} else if (payload.idTokenClaims?.tfp === b2cPolicies.names.signIn) {
this.loggedInSuccess$.next(true);
this.msalService.instance.setActiveAccount(payload.account);
}
return result;
});
If you have any issues, let me know, and I can post the full code.
Furthermore, you can handle more things as per your app requirements using the MSAL events:
https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-angular/docs/v2-docs/events.md

Related

Identityserver4 windows authentication failed on IIS but working in VS

I am trying to implement Identityserver4 (version 4.0.0) with windows authentication. While running on visual studio its working correctly. When I deploy this to IIS windows popup is showing continuously (401 status) after entering credentials. Below is my code . I also tried to deploy Duende Software's sample source also but getting the same result. I think there is some configuration missing from my end. Kindly help me.
Program.cs
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.UseSerilog()
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
});
launchSettings.json
"windowsAuthentication": true,
ExternalController.cs
public async Task<IActionResult> Challenge(string scheme, string returnUrl)
{
if (string.IsNullOrEmpty(returnUrl)) returnUrl = "~/";
if(scheme == "Windows")
{
return await ChallengeWindowsAsync(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");
}
// start challenge and roundtrip the return URL and scheme
var props = new AuthenticationProperties
{
RedirectUri = Url.Action(nameof(Callback)),
Items =
{
{ "returnUrl", returnUrl },
{ "scheme", scheme },
}
};
return Challenge(props, scheme);
}
//ChallengeWindowsAsync
private async Task<IActionResult> ChallengeWindowsAsync(string returnUrl)
{
// see if windows auth has already been requested and succeeded
var result = await HttpContext.AuthenticateAsync("Windows");
if (result?.Principal is WindowsPrincipal wp)
{
// we will issue the external cookie and then redirect the
// user back to the external callback, in essence, treating windows
// auth the same as any other external authentication mechanism
var props = new AuthenticationProperties()
{
RedirectUri = Url.Action("Callback"),
Items =
{
{ "returnUrl", returnUrl },
{ "scheme", "Windows" },
}
};
var id = new ClaimsIdentity("Windows");
// the sid is a good sub value
id.AddClaim(new Claim(JwtClaimTypes.Subject, wp.FindFirst(ClaimTypes.PrimarySid).Value));
// the account name is the closest we have to a display 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
var wi = wp.Identity as WindowsIdentity;
// translate group SIDs to display names
var groups = wi.Groups.Translate(typeof(NTAccount));
var roles = groups.Select(x => new Claim(JwtClaimTypes.Role, x.Value));
id.AddClaims(roles);
await HttpContext.SignInAsync(
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("Windows");
}
}
IIS Configuration
Windows authentication is enabled

After Logout, login with the same user credentials is not working with Mongo Realm

I am using below code to log out the the current logged in user
public async Task Logout()
{
SecureStorage.RemoveAll();
await RealmMain.realmApp.CurrentUser.LogOutAsync();
}
Then, I use below code to sign in back again.
public async Task<bool> LoginWithCredential(Action<string> error)
{
try {
var credentials = Credentials.EmailPassword(userId, pass);
var user = await RealmMain.realmApp.LogInAsync(credentials);
return user != null;
}
catch (Exception ex){
SecureStorage.RemoveAll();
Console.WriteLine(ex);
error(exceptionText);
return false;
}
}
RealmMain Class is like this below.
public sealed class RealmMain
{
private const string AppId = "*************";
public static App realmApp = App.Create(AppId);
public SyncConfiguration ConfigForSync
{
get
{
var temp = new SyncConfiguration(realmApp.CurrentUser.Id, realmApp.CurrentUser)
{
// EncryptionKey = AppContext.GetBytes(AppContext.DbKey)
};
Console.WriteLine(temp.EncryptionKey);
return temp;
}
}
public static RealmMain Instance { get; } = new RealmMain();
private RealmMain()
{
}
}
Problem here is - When is log out and then try to sign in with the same user credential.
I get below error.
"Realms.Sync.Exceptions.AppException: Unknown: must authenticate first
at Realms.Sync.App.LogInAsync (Realms.Sync.Credentials credentials)"
If I use some different user to sign in after logging out.
I get this.
In nutshell Logout and then login is not working for me, I have to quit the app to make it work every time.
Any suggestion to solve this issue would be appreciated.

ASP.NET Identity: UserManager.PasswordHasher.VerifyHashedPassword keeps failing

I'm using ASP.NET Identity 2 with Entity Framework 5 (because our Oracle data provider does not support EF6). For some reason the password verification by means of UserManager.PasswordHasher.VerifyHashedPassword keeps failing.
My UserStore class contains:
public Task SetPasswordHashAsync(IccmUser user, string passwordHash)
{
IPasswordHasher hasher = new PasswordHasher();
var t = Task.Run(() => {
user.PasswordHash = hasher.HashPassword(passwordHash);
});
return t;
}
The (obviously hashed) password is stored in the database. Thus, this code seems to work just fine.
My AccountController does the password verification like this:
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Login(SignInModel model, string returnUrl)
{
if (ModelState.IsValid) {
// This fails:
//var user = await UserManager.FindAsync(model.UserName, model.Password);
// Thus: do it step by step.
PasswordVerificationResult result = PasswordVerificationResult.Failed;
// Step 1: find user.
IccmUser user = await UserManager.FindByNameAsync(model.UserName);
if (user == null) {
ModelState.AddModelError("", "Couldn't find the user.");
} else {
// Step 2: validate password
result = UserManager.PasswordHasher.VerifyHashedPassword(user.PasswordHash, model.Password);
if (result != PasswordVerificationResult.Success) {
ModelState.AddModelError("", "The password is not valid.");
} else {
// Step 3: sign-in user.
await SignInAsync(user, model.RememberMe);
return Redirect(returnUrl);
}
}
}
// If we got this far, something failed, redisplay form
return View(model);
}
The VerifyHashedPassword() in step 2 always returns Failed. Both parameters (PasswordHash and Password) are passed in correctly.
Any pointers to what I'm missing are greatly appreciated.
The error is in the UserStore implementation. SetPasswordHashAsync() is not supposed to hash the password. Instead, it receives the hashed password from UserManager.CreateAsync(). Thus, the following change in UserStore does the trick:
public Task SetPasswordHashAsync(IccmUser user, string passwordHash)
{
return Task.FromResult(user.PasswordHash = passwordHash);
}
Sorry for the noise.

How to delete user with UserManager in mvc5

I'm using mvc5, and everything about user account management I do with UserManager. It works good with roles, claims, etc. But I didn't find how to delete user with UserManager. Is there a way to delete user with UserManager? I can create Database context with dbset and then delete it from this context, but I don't want create dbcontext, userclass, etc. for one delete method.
I had issues with the above answer, though I was able to work out what's wrong. I kept getting a cascading error. Basically the user was being deleted without the role being deleted. DeleteAsync was not doing that for me (I have the latest build of Identity Framework). Ended up passing both the userid and role into my code, deleting the user from the role, then deleting the user. Seems to work fine.
[HttpPost, ActionName("Delete")]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Delete(string id, string role)
{
// Check for for both ID and Role and exit if not found
if (id == null || role == null)
{
return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
}
// Look for user in the UserStore
var user = UserManager.Users.SingleOrDefault(u => u.Id == id);
// If not found, exit
if (user == null)
{
return HttpNotFound();
}
// Remove user from role first!
var remFromRole = await UserManager.RemoveFromRoleAsync(id, role);
// If successful
if (remFromRole.Succeeded)
{
// Remove user from UserStore
var results = await UserManager.DeleteAsync(user);
// If successful
if (results.Succeeded)
{
// Redirect to Users page
return RedirectToAction("Index", "Users", new {area = "Dashboard"});
}
else
{
return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
}
}
else
{
return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
}
}
Delete was not supported in UserManager in 1.0, but its supported in the upcoming 2.0 release, and in the current 2.0 nightly builds if you want to preview the changes early.
Using the updated asp.net identity I have the following code:
public UserManagerController()
: this(new UserManager<User>(new UserStore<User>(new ApplicationDbContext())))
{
}
public UserManagerController(UserManager<User> userManager)
{
UserManager = userManager;
}
public UserManager<User> UserManager { get; private set; }
public async Task<ActionResult> Delete (string id)
{
var userContext = new ApplicationDbContext();
var user = UserManager.Users.SingleOrDefault(u => u.Id == id);
var userStore = new UserStore<User>(userContext);
await UserManager.DeleteAsync(user);
// var userManager = new UserManager<User>(userStore);
// await userManager.DeleteAsync(user);
return RedirectToAction("Index");
}
This one now deletes the user. It is also no need to delete from UserRoles table as that is taken care of by UserManager.DeleteAsync(user).
Hope this helps a few. I spent some time figuring out why I got some errors.
Trond

ASP.net Identity Disable User

Using the new ASP.net Identity in MVC 5, How do we disable a user from logging in? I don't want to delete them, maybe just disable their account for a time period.
Does anyone have any ideas on this as I don't see a status column or anything on the ASPNetUsers table.
await userManager.SetLockoutEnabledAsync(applicationUser.Id, true);
await userManager.SetLockoutEndDateAsync(DateTime.Today.AddYears(10));
Update: As CountZero points out, if you're using v2.1+, then you should try and use the lockout functionality they added first, before trying the solution below. See their blog post for a full sample: http://blogs.msdn.com/b/webdev/archive/2014/08/05/announcing-rtm-of-asp-net-identity-2-1-0.aspx
Version 2.0 has the IUserLockoutStore interface that you can use to lockout users, but the downside is that there is no OOB functionality to actually leverage it beyond the pass-through methods exposed by the UserManager class. For instance, it would be nice if it would actually increment the lockout count as a part of the standard username/password verification process. However, it's fairly trivial to implement yourself.
Step #1: Create a custom user store that implements IUserLockoutStore.
// I'm specifying the TKey generic param here since we use int's for our DB keys
// you may need to customize this for your environment
public class MyUserStore : IUserLockoutStore<MyUser, int>
{
// IUserStore implementation here
public Task<DateTimeOffset> GetLockoutEndDateAsync(MyUser user)
{
//..
}
public Task SetLockoutEndDateAsync(MyUser user, DateTimeOffset lockoutEnd)
{
//..
}
public Task<int> IncrementAccessFailedCountAsync(MyUser user)
{
//..
}
public Task ResetAccessFailedCountAsync(MyUser user)
{
//..
}
public Task<int> GetAccessFailedCountAsync(MyUser user)
{
//..
}
public Task<bool> GetLockoutEnabledAsync(MyUser user)
{
//..
}
public Task SetLockoutEnabledAsync(MyUser user, bool enabled)
{
//..
}
}
Step #2: Instead of UserManager, use the following class in your login/logout actions, passing it an instance of your custom user store.
public class LockingUserManager<TUser, TKey> : UserManager<TUser, TKey>
where TUser : class, IUser<TKey>
where TKey : IEquatable<TKey>
{
private readonly IUserLockoutStore<TUser, TKey> _userLockoutStore;
public LockingUserManager(IUserLockoutStore<TUser, TKey> store)
: base(store)
{
if (store == null) throw new ArgumentNullException("store");
_userLockoutStore = store;
}
public override async Task<TUser> FindAsync(string userName, string password)
{
var user = await FindByNameAsync(userName);
if (user == null) return null;
var isUserLockedOut = await GetLockoutEnabled(user);
if (isUserLockedOut) return user;
var isPasswordValid = await CheckPasswordAsync(user, password);
if (isPasswordValid)
{
await _userLockoutStore.ResetAccessFailedCountAsync(user);
}
else
{
await IncrementAccessFailedCount(user);
user = null;
}
return user;
}
private async Task<bool> GetLockoutEnabled(TUser user)
{
var isLockoutEnabled = await _userLockoutStore.GetLockoutEnabledAsync(user);
if (isLockoutEnabled == false) return false;
var shouldRemoveLockout = DateTime.Now >= await _userLockoutStore.GetLockoutEndDateAsync(user);
if (shouldRemoveLockout)
{
await _userLockoutStore.ResetAccessFailedCountAsync(user);
await _userLockoutStore.SetLockoutEnabledAsync(user, false);
return false;
}
return true;
}
private async Task IncrementAccessFailedCount(TUser user)
{
var accessFailedCount = await _userLockoutStore.IncrementAccessFailedCountAsync(user);
var shouldLockoutUser = accessFailedCount > MaxFailedAccessAttemptsBeforeLockout;
if (shouldLockoutUser)
{
await _userLockoutStore.SetLockoutEnabledAsync(user, true);
var lockoutEndDate = new DateTimeOffset(DateTime.Now + DefaultAccountLockoutTimeSpan);
await _userLockoutStore.SetLockoutEndDateAsync(user, lockoutEndDate);
}
}
}
Example:
[AllowAnonymous]
[HttpPost]
public async Task<ActionResult> Login(string userName, string password)
{
var userManager = new LockingUserManager<MyUser, int>(new MyUserStore())
{
DefaultAccountLockoutTimeSpan = /* get from appSettings */,
MaxFailedAccessAttemptsBeforeLockout = /* get from appSettings */
};
var user = await userManager.FindAsync(userName, password);
if (user == null)
{
// bad username or password; take appropriate action
}
if (await _userManager.GetLockoutEnabledAsync(user.Id))
{
// user is locked out; take appropriate action
}
// username and password are good
// mark user as authenticated and redirect to post-login landing page
}
If you want to manually lock someone out, you can set whatever flag you're checking in MyUserStore.GetLockoutEnabledAsync().
You can have a new class, which should be derived from IdentityUser class. YOu can add a boolean property in the new class and can use this new property of take care per check for login process. I also done it pretty well. I might wanna take a look at : blog
UserManager.RemovePasswordAsync("userId") will effectively disable a user. If the user has no password he will not be able to log in. You will need to set a new password to enable the user again.

Resources