Encoded Characters in Query Parameters Crash Angular Client - c#

When a user requests a password reset, a reset url will be send by email:
https://example.com/reset-password?id=206dd3f4-8248-4e7a-bd90-16585a0cc165&code=CfDJ8KegIZmThRtPj1kp523h7xyoNB5wCUt%2BvSf7e53lGbvQoE7xagL4mnLODLy0v6WCC91fiPoN9TRreDVT8SJvDABKX5X3KG8LT1XZqqrlilWC5kbiTmWYKUfWju7DrsZ1DUuEvvJLAqNpbqI%2Fgkiu0vb3PGkinIbygMMBJufllU4TY5qcOjzQIQo06eNo1KR6L1lwT9Vap0TE5rr%2FuYIyt2lRIJQGk9pBUg2qQuiolDl4
This url consists of a user id and a generated token by Identity Core;
[HttpPost("forgot")]
[AllowAnonymous]
public async Task<IActionResult> ForgotPassword([FromBody]ForgotPasswordViewModel model)
{
if (ModelState.IsValid)
{
var user = await _userManager.FindByEmailAsync(model.Email);
if (user == null || !(await _userManager.IsEmailConfirmedAsync(user)))
{
// Don't reveal that the user does not exist or is not confirmed
return Ok();
}
var token = await _userManager.GeneratePasswordResetTokenAsync(user);
var queryParams = new Dictionary<string, string>()
{
{"id", user.Id },
{"token", token }
};
var callbackUrl = ResetPasswordCallbackLink(queryParams); // <--- Creates the url
await _emailSender.SendResetPasswordAsync(model.Email, callbackUrl);
_logger.LogInformation($"User: {model.Email} forgot password");
return Ok();
}
return Ok();
}
This token is crashing the client (Angular 6+) side because of the encoded characters in the token %2B (+) and %2F (/).
Without these encoded characters, the page renders perfectly and the parameters can be read with;
this.id = this.route.snapshot.paramMap.get('id');
this.token = this.route.snapshot.paramMap.get('token');
UPDATE
Navigating to a random existing page like /login?id=123%2B will also generate a 'InternalError: too much recursion' by RxJs. http://prntscr.com/mrspvf So it probably is an architectural issue?

Related

How to specific "Always redirect to external provider when login" in IdentityServer4?

When I want to login via Facebook for example for the first time, everything works. But, after a logout and an attempt to re-login in the same provider, redirection does not occur, and the system logs in using the previous account. How to specify "Always redirect"?
[HttpGet]
public async Task<IActionResult> Callback()
{
// read external identity from the temporary cookie
var result =
await HttpContext.AuthenticateAsync(IdentityServerConstants.ExternalCookieAuthenticationScheme);
if (result?.Succeeded != true)
{
throw new Exception("External authentication error");
}
// lookup our user and external provider info
var (user, provider, providerUserId, claims) = await FindUserFromExternalProvider(result);
// retrieve return URL
var returnUrl = result.Properties.Items["returnUrl"] ?? "~/";
if (user == null)
{
user = await AutoProvisionUser(provider, providerUserId, claims);
}
if (user.Email == null)
{
return RedirectToAction("EnterEmail", new { provider, providerUserId, returnUrl });
}
var additionalLocalClaims = new List<Claim>();
var localSignInProps = new AuthenticationProperties();
ProcessLoginCallback(result, additionalLocalClaims, localSignInProps);
// issue authentication cookie for user
var principal = await _signInManager.CreateUserPrincipalAsync(user);
additionalLocalClaims.AddRange(principal.Claims);
var name = principal.FindFirst(JwtClaimTypes.Name)?.Value ?? user.Id;
var isuser = new IdentityServerUser(user.Id)
{
DisplayName = name,
IdentityProvider = provider,
AdditionalClaims = additionalLocalClaims
};
await HttpContext.SignInAsync(isuser, localSignInProps);
// delete temporary cookie used during external authentication
await HttpContext.SignOutAsync(IdentityServerConstants.ExternalCookieAuthenticationScheme);
// check if external login is in the context of an OIDC request
var context = await _interaction.GetAuthorizationContextAsync(returnUrl);
await _events.RaiseAsync(new UserLoginSuccessEvent(provider, providerUserId, user.Id, name, true, context?.Client.ClientId));
if (context != null)
{
if (context.IsNativeClient())
{
// The client is native, so this change in how to
// return the response is for better UX for the end user.
return this.LoadingPage("Redirect", returnUrl);
}
}
return Redirect(returnUrl);
}
Logout:
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Logout(LogoutInputModel model)
{
// build a model so the logged out page knows what to display
var vm = await BuildLoggedOutViewModelAsync(model.LogoutId);
vm.TriggerExternalSignout = true;
if (User?.Identity.IsAuthenticated == true)
{
// delete local authentication cookie
await _signInManager.SignOutAsync();
await HttpContext.SignOutAsync(IdentityServerConstants.ExternalCookieAuthenticationScheme);
// raise the logout event
await _events.RaiseAsync(new UserLogoutSuccessEvent(User.GetSubjectId(), User.GetDisplayName()));
}
else
{
vm.TriggerExternalSignout = false;
}
// check if we need to trigger sign-out at an upstream identity provider
if (vm.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 = vm.LogoutId });
// this triggers a redirect to the external provider for sign-out
return SignOut(new AuthenticationProperties { RedirectUri = url }, vm.ExternalAuthenticationScheme);
}
return View("LoggedOut", vm);
}
I tried clear cookie, but nothing changed. Then, i tried specify "prompt":"login" in AuthentificationProperties - noting changed.

Email confirmation in Web API

I have Web API application and I want to implement email confirmation.
Now I have method that takes address -- the client host which will be in callback url uriBuilder and will be opened by the user from mail:
public async Task<IdentityResult> RegisterAsync(string email, string userName, string password, string address)
{
var user = new ApplicationUser { Email = email, UserName = userName };
var result = await _userManager.CreateAsync(user, password);
if (result.Succeeded)
{
var code = await _userManager.GenerateEmailConfirmationTokenAsync(user);
var uriBuilder = new UriBuilder(address) { Port = -1 };
var query = HttpUtility.ParseQueryString(uriBuilder.Query);
query["userId"] = user.Id;
query["code"] = code;
uriBuilder.Query = query.ToString();
await _emailService.SendEmailAsync
(
user.Email,
"Email confirmation",
$"Confirm the registration by clicking on the <a href='{uriBuilder}'>link</a>."
);
}
return result;
}
Then on the client side will be POST call to the API:
public async Task<bool> ConfirmEmailAsync(string userId, string code)
{
var user = await _userManager.FindByIdAsync(userId);
if (user == null)
{
throw new UserNotFoundException();
}
var result = await _userManager.ConfirmEmailAsync(user, code);
return result.Succeeded;
}
This is so that user do not interact with API directly.
Is it ok to pass the host address in the request? If not, what should I do if there are several clients? In case of one client I can move it to config.
Yeap, it is ok to pass the host as parameter in the request.
I suppose a user should be in your scenario associated with specific host/address. You can store in the Db the host associated with the user and get that on the RegisterAsync method.

ASP.Net Identity “Invalid token” on password reset with * in password

We get the invalid token error messages when a user tries to reset his password on the reset password screen after entering the new password. Normally this works just fine for everyone even with some special character like #. We have now a case where someone puts in * in his new password on the reset pw screen, gets this error message just because of this special character.
I've tried hours of research now to find a solution to why this happens but with no luck. I've found this solution here which has an issue with special characters in the username but we don't have that issue. There is only an issue with that special character in the password. As we are already in production we can't just disallow that character in passwords.
Someone got a clue?
Generating the token controller method:
[HttpPost]
[AllowAnonymous]
public async Task<ActionResult> ForgotPassword(ForgotPasswordViewModel model)
{
if (ModelState.IsValid)
{
var user = await _userManager.FindByNameAsync(model.Email.ToLower());
if (user == null || !(await _userManager.IsEmailConfirmedAsync(user.UserName)))
{
// Don't reveal that the user does not exist or is not confirmed
return View("ForgotPasswordConfirmation");
}
// For more information on how to enable account confirmation and password reset please visit http://go.microsoft.com/fwlink/?LinkID=320771
// Send an email with this link
var code = await _userManager.GeneratePasswordResetTokenAsync(user.UserName);
code = HttpUtility.UrlEncode(code);
var callbackUrl = Url.Action("ResetPassword", "Account", new { userId = user.UserName, code = code }, protocol: Request.Url.Scheme);
await _emailService.CreateResetPasswordEmailAsync(user, callbackUrl);
return RedirectToAction("ForgotPasswordConfirmation", "Account");
}
// If we got this far, something failed, redisplay form
return View(model);
}
Reset password controller method:
[HttpPost]
[AllowAnonymous]
public async Task<ActionResult> ResetPassword(ResetPasswordViewModel model)
{
if (!ModelState.IsValid)
{
return View(model);
}
var user = await _userManager.FindByNameAsync(model.Email.ToLower());
if (user == null)
{
// Don't reveal that the user does not exist
return RedirectToAction("ResetPasswordConfirmation", "Account");
}
var result = await _userManager.ResetPasswordAsync(user.UserName, HttpUtility.UrlDecode(model.Code), model.Password);
if (result.Succeeded)
{
return RedirectToAction("ResetPasswordConfirmation", "Account");
}
AddErrors(result);
return View();
}
The problem is that you are double encoding the reset token. Here:
var code = await _userManager.GeneratePasswordResetTokenAsync(user.UserName);
code = HttpUtility.UrlEncode(code); //<--problem is this line
var callbackUrl = Url.Action("ResetPassword", "Account",
new { userId = user.UserName, code = code }, protocol: Request.Url.Scheme);
you encode the token and then Url.Action will do it again. So the solution is to not encode manually and let MVC handle it for you - just remove the second line here.
Also, on the other end, there's now no need to decode again, so your code there will be:
var result = await _userManager.ResetPasswordAsync(user.UserName,
model.Code, model.Password);

"Invalid token" error while reset password, ASP.Net Identity 2.2.0

I'm developing a MVC5 ASP.Net application.
I'm using Identity 2.2.0 for authentication.
Everything is OK, but I can't reset my password, because of Invalid Token error.
The following are ResetPassword related actions in Account controller.
[AllowAnonymous]
public ActionResult ForgotPassword()
{
return View();
}
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<ActionResult> ForgotPassword(ForgotPasswordViewModel model)
{
if (!ModelState.IsValid) return View(model);
ApplicationUser userModel = await UserManager.FindByNameAsync(model.Email);
if (userModel == null)
ModelState.AddModelError("", "The user doesn't exist");
if (userModel != null && !await UserManager.IsEmailConfirmedAsync(userModel.Id))
ModelState.AddModelError("", "The user email isn't confirmed");
if (!ModelState.IsValid) return View();
var user = _userService.GetUserByEmail(model.Email);
// For more information on how to enable account confirmation and password reset please visit http://go.microsoft.com/fwlink/?LinkID=320771
// Send an email with this link
string websiteTitle = StaticAssests.WebsiteTitle;
string emailContext = _settingService.GetValue(SettingNames.ResetPasswordMailFormat);
string code = await UserManager.GeneratePasswordResetTokenAsync(userModel.Id);
string callbackUrl = Url.Action("ResetPassword", "Account", new { userId = userModel.Id, code }, Request.Url.Scheme);
emailContext = emailContext.Replace("{userfullname}", user.FullName);
emailContext = emailContext.Replace("{websitetitle}", websiteTitle);
emailContext = emailContext.Replace("{websitedomain}", StaticVariables.WebsiteDomain);
emailContext = emailContext.Replace("{username}", userModel.UserName);
emailContext = emailContext.Replace("{resetpasswordurl}", callbackUrl);
emailContext = emailContext.Replace("{date}", new PersianDateTime(DateTime.Now).ToLongDateTimeString());
await UserManager.SendEmailAsync(userModel.Id, string.Format("Reset password {0}", websiteTitle), emailContext);
return RedirectToAction("ForgotPasswordConfirmation", "Account");
}
[AllowAnonymous]
public ActionResult ForgotPasswordConfirmation()
{
return View();
}
[AllowAnonymous]
public ActionResult ResetPassword(string code)
{
return code == null ? View("Error") : View();
}
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<ActionResult> ResetPassword(ResetPasswordViewModel model)
{
if (!ModelState.IsValid) return View(model);
ApplicationUser userModel = await UserManager.FindByNameAsync(model.Email);
if (userModel == null)
{
ModelState.AddModelError("", "The user doesn't exist");
return View();
}
// Invalid Token error
IdentityResult result = await UserManager.ResetPasswordAsync(userModel.Id, model.Code, model.Password);
if (result.Succeeded)
{
return RedirectToAction("ResetPasswordConfirmation", "Account");
}
AddErrors(result);
return View();
}
I've checked the followings:
1. Resetting email send successfully.
2. GeneratePasswordResetTokenAsync run without any problem. and the generated code with it is the same with code argument in ResetPassword action.
IdentityConfig:
public class ApplicationUserManager : UserManager<ApplicationUser, int>
{
public ApplicationUserManager(IUserStore<ApplicationUser, int> userStore) : base(userStore)
{
}
public static ApplicationUserManager Create(IdentityFactoryOptions<ApplicationUserManager> options, IOwinContext context)
{
ApplicationUserManager applicationUserManager = new ApplicationUserManager(new ApplicationUserStore());
//ApplicationUserManager applicationUserManager = new ApplicationUserManager(context.Get<ApplicationUser>());
//new ApplicationUserManager(new UserStore<UserModel>(context.Get<ApplicationDbContext>()));
// Configure validation logic for usernames
//applicationUserManager.UserValidator = new UserValidator<UserIdentityModel, int>(applicationUserManager)
//{
// AllowOnlyAlphanumericUserNames = false,
// RequireUniqueEmail = true,
//};
applicationUserManager.PasswordValidator = new MyMinimumLengthValidator(6);
applicationUserManager.UserValidator = new MyUserModelValidator();
applicationUserManager.PasswordHasher = new MyPasswordHasher();
// Configure user lockout defaults
applicationUserManager.UserLockoutEnabledByDefault = true;
applicationUserManager.DefaultAccountLockoutTimeSpan = TimeSpan.FromMinutes(5);
applicationUserManager.MaxFailedAccessAttemptsBeforeLockout = 5;
// Register two factor authentication providers. This application uses Phone and Emails as a step of receiving a code for verifying the user
// You can write your own provider and plug in here.
applicationUserManager.RegisterTwoFactorProvider("PhoneCode",
new PhoneNumberTokenProvider<ApplicationUser, int>
{
MessageFormat = "Your security code is: {0}"
});
applicationUserManager.RegisterTwoFactorProvider("EmailCode",
new EmailTokenProvider<ApplicationUser, int>
{
Subject = "Security code",
BodyFormat = "your security code is {0}"
});
applicationUserManager.EmailService = new EmailService();
applicationUserManager.SmsService = new SmsService();
IDataProtectionProvider dataProtectionProvider = options.DataProtectionProvider;
if (dataProtectionProvider != null)
{
applicationUserManager.UserTokenProvider =
new DataProtectorTokenProvider<ApplicationUser, int>(dataProtectionProvider.Create("ASP.NET Identity"));
}
return applicationUserManager;
}
}
What's wrong?
Update:
I've changed User Id from string to int.
I know this is an old question, but I've run into this on two projects and both times the issue was the exact same thing. Perhaps this answer might help others. The token being passed includes special characters that cannot be used as is within a URL string without causing problems.
When passing the token to the front end UI, be sure to URLEncode the token, something like this:
var callbackUrl =
new Uri(string.Format("{0}/resetpassword?userId={1}&code={2}",
ConfigurationManager.AppSettings["websiteUrl"], user.Id,
WebUtility.UrlEncode(token)));
When the token is passed back into the back end, Decode the token before passing it to the password ResetPassword function:
var result = await this.AppUserManager.ResetPasswordAsync(appUser.Id, WebUtility.UrlDecode(resetPasswordBindingModel.ConfirmCode),
resetPasswordBindingModel.NewPassword);
Both of the projects where I had this issue were running HTML/MVC/JavaScript on the front end and the validations were being done over ASP.NET WebAPI 2.x.
One other note: UrlDecode, for some reason, can't properly decode the plus symbol and you get a space in the token instead of the '+'. The trick I used is to just use a string replace to convert any spaces to + signs. It's not ideal, but I've not had a problem with it.

How to receive auth failure from ASP.NET MVC5+Identity server on .NET client?

i'm using ASP.NET MVC5 with the latest Identity and Signalr on the server and have .NET client app. Currently i have working auth logic implemented but i don't get it how can i get auth failure in .NET desktop client?
Here is my .NET desktop client auth code:
private static async Task<bool> AuthenticateUser(string siteUrl, string email, string password)
{
try
{
var handler = new HttpClientHandler { CookieContainer = new CookieContainer() };
using (var httpClient = new HttpClient(handler))
{
var loginUrl = siteUrl + "Account/Login";
_writer.WriteLine("Sending http GET to {0}", loginUrl);
var response = await httpClient.GetAsync(loginUrl);
var content = await response.Content.ReadAsStringAsync();
_verificationToken = ParseRequestVerificationToken(content);
content = _verificationToken + "&UserName="+email+"&Password="+password+"&RememberMe=false";
_writer.WriteLine("Sending http POST to {0}", loginUrl);
response = await httpClient.PostAsync(loginUrl, new StringContent(content, Encoding.UTF8, "application/x-www-form-urlencoded"));
content = await response.Content.ReadAsStringAsync();
_verificationToken = ParseRequestVerificationToken(content);
_connection.CookieContainer = handler.CookieContainer;
return true;
}
}
catch (Exception ex)
{
Logger.Log(ex, "Auth");
return false;
}
}
where _connection is a hub connection which receives cookie needed for hub auth. The problem is that httpCLient.PostAsync() always return valid result and i don't get it how i can implement auth failure detection.
Here is server login code:
// POST: /Account/Login
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Login(LoginViewModel model, string returnUrl)
{
if (ModelState.IsValid)
{
var user = await UserManager.FindAsync(model.UserName, model.Password);
if (user != null)
{
await SignInAsync(user, model.RememberMe);
return RedirectToLocal(returnUrl);
}
else
{
ModelState.AddModelError("", "Invalid username or password.");
}
}
// If we got this far, something failed, redisplay form
return View(model);
}
On failure it just adds error string on the page.
Please advice what is the better way to implement auth result.
This is strange that i got no single answer for this question. I've come to an intermediate solution:
Add unique hidden tags for login and index pages (on failure login page is displayed again, on success - index page)
<div style="display: none;" id="#SharedData.Constants.INDEX_PAGE_TAG"></div>
In .NET client check content string for the specific tag presence.
I don't think this the preformance-wise solution but at least it works...

Categories

Resources