Razor Engine: URL generation in template of & generates & - c#

I have an ASP.NET Core application which use Identity for user management.
I have a MailProvider which prepares an email to send via smtp for account activation purposes:
// ...
var activationMailViewModel = new ActivationMailViewModel()
{
LastName = user.LastName,
FirstName= user.FirstName,
UserName = userName,
Email = user.Email,
CompanyName = companyName,
Url = url.Action("ConfirmEmail", "Account", new { Area = "", code = token, userId = user.Id }, request.Scheme)
};
// ...
var result = Engine.Razor.RunCompile(new LoadedTemplateSource($"Activation", path), "SendUserAccountCreation" + Guid.NewGuid(), null, activationMailViewModel);
// ...
var mailMessage = new MailMessage(
new MailAddress("kyc#vente-privee.com", "KYC-vente-privee.com"),
new MailAddress(user.Email, userName))
{
Subject = GetGlobalString("ActivationMail_Subject", cultureCode),
BodyEncoding = System.Text.Encoding.UTF8,
SubjectEncoding = System.Text.Encoding.UTF8
};
<p>ACTIVATE MY ACCOUNT</p>
However it seems that the link generated can be interpreted in two different ways due to the transcription of the character & into & in the query string of the url.
http://base.url.net/Account/ConfirmEmail?code=CfDJ8PtYvJr8Ve1GnxXJykedIzKTQDg%2FTXBwV6NmIYMy8Gi7yUbqZagGbZRacKSFrE717h%2FGjmm6l8QA3knPPgxyNnM1vxe3wb6KnFsGtZUOMTas7QhX1MW5dE4cU5sorA99Dz03zV8ldVMOMP5BGfUrts2nNQqbs8dNLPNgupdkNzaWa4q6fM5u9E99CzRcFjAn7nnd57Ht3IIREAqz6lqufFYo469%2BN2VJxmNJJ1p6OAvO6dMJ9M%2Fzdz3xkpBajJbxRw%3D%3D**&**userId=21e4673c-f121-417f-9837-7f5b234f6f01
http://base.url.net/Account/ConfirmEmail?code=CfDJ8PtYvJr8Ve1GnxXJykedIzKTQDg%2FTXBwV6NmIYMy8Gi7yUbqZagGbZRacKSFrE717h%2FGjmm6l8QA3knPPgxyNnM1vxe3wb6KnFsGtZUOMTas7QhX1MW5dE4cU5sorA99Dz03zV8ldVMOMP5BGfUrts2nNQqbs8dNLPNgupdkNzaWa4q6fM5u9E99CzRcFjAn7nnd57Ht3IIREAqz6lqufFYo469%2BN2VJxmNJJ1p6OAvO6dMJ9M%2Fzdz3xkpBajJbxRw%3D%3D**&**userId=21e4673c-f121-417f-9837-7f5b234f6f01
Which can be problematic for the my AccountController:
[Authorize]
public class AccountController : BaseController
{
// GET: /Account/ConfirmEmail
[AllowAnonymous]
public async Task<ActionResult> ConfirmEmail(string code, string userId)
{
if (code == null || userId == null)
{
return View("Error");
}
// Rest of the code... not relevant to the question
}
If the browser / mail client interprets & as & then the userId will be set to null and the account / email cannot be confirmed.
For example:
On ProtonMail: the link on which I can click leads to an address which use & in the query string, which is just fine.
On Gmail: & and hence the link does not confirm the email.
However in both email providers, the plain text shows that the url has been generated with the Razor engine is: &.
What is the best strategy so that my users do not end up with a link that does not work.

Seems the issue was about the raw formatting which was not applied to the email.
The answer given here on SO:
When doing e-mails, I use the RazorEngineService in RazorEngine.Templating, e.g. in my case, it looks like this:
using RazorEngine.Templating;
RazorEngineService.Create().RunCompile(html, ...)
Assuming you are using the same assembly, #Html.Raw does NOT exist with this usage. I > was finally able to get raw HTML output by doing this in my e-mails:
#using RazorEngine.Text
#(new RawString(Model.Variable))

Related

asp.net core Identity ConfirmEmailModel does not bind values correctly OnGetAsync

I have a .net core Blazor project where I have scaffolded Identity into the project. I have customized IdentityUser to use integer IDs instead of the default.
I have run into a strange problem with the functionality of confirming email via the default
https://localhost:44348/identity/account/manage/email page
this built-in page has a button which posts to OnPostSendVerificationEmailAsync as below:
public async Task<IActionResult> OnPostSendVerificationEmailAsync()
{
var user = await _userManager.GetUserAsync(User);
if (user == null)
{
return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
}
if (!ModelState.IsValid)
{
await LoadAsync(user);
return Page();
}
var userId = await _userManager.GetUserIdAsync(user);
var email = await _userManager.GetEmailAsync(user);
var code = await _userManager.GenerateEmailConfirmationTokenAsync(user);
code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code));
var callbackUrl = Url.Page(
"/Account/ConfirmEmail",
pageHandler: null,
values: new { area = "Identity", userId = userId, code = code },
protocol: Request.Scheme);
await _emailSender.SendEmailAsync(
email,
"Confirm your email", HtmlEncoder.Default.Encode(callbackUrl));
//await _emailSender.SendEmailAsync(
// email,
// "Confirm your email",
// $"Please confirm your account by <a href='{HtmlEncoder.Default.Encode(callbackUrl)}'>clicking here</a>.");
StatusMessage = "Verification email sent. Please check your email.";
return RedirectToPage();
}
I have made slight modifications to the function however those changes should not affect the default mechanism of call-back URL generation. basically, this generates an email confirmation URL and sends an email to the indicated user account.
I receive the following generated URL to the mailbox:
https://localhost:44348/Identity/Account/ConfirmEmail?userId=1&code=Q2ZESjhQcXM4RmJpR2xGT3I2NW41SUkxT0pXWnI5WlE5TUlhUndzSi9aQnA1ZGN4OTc5ZS9UOFFiR2xranhidFBWWTU2c3AxZ25peUYvamFaVUVhbG5ac3ArcHh5WkVNMVczUHVDQVBqMVdXUUFFeGdwU1FWZXo1eUxSVHZUU3dwb2RpUGVpTFhCTzhJSjhHR3ZpckVYNUZQMVJrUGhUU1FQOE52TnVWdU9jYVBKbXIyTkY4V05NVVhrVXVid2xQRFJzSTFRPT0
however, the problem is that the target ConfirmEmail does not bind userId and code correctly. the code part is null.
any bright ideas on the elephant in the room that I am unable to see here will be greatly appreciated. thank you.
however, the code is null
apparently, the problem is with the format of the BODY element of the email being sent. OK, step-by-step now.
this piece of code uses encoder which encodes the URL to follow the rules as per this discussion
await _emailSender.SendEmailAsync(
email,
"Confirm your email",
$"Please confirm your account by <a href='{HtmlEncoder.Default.Encode(callbackUrl)}'>clicking here</a>.");
part of what encoder does is it turns the ampersand into this: &
according to this thread
The solution to this problem is setting the body as "html" and not "plain" because the email content is HTML and the callback URL generates HTML content which is sent as a plain email to the user.
If you set the content as HTML the '&' will be converted to '&' and the email will display the correct link.
i suppose they leave little option for people who would like to stick to PLAIN email body rather than HTML.

Sendgrid C# bulk email X-SMTPAPI header not working

I am trying to send email with SendGrid to multiple recipients in an ASP.Net C# web application
According to the SendGrid documentation I need to add X-SMTPAPI header to my message in JSON formatted string. I do so, for first check I just added a hand-typed string before building my json email list progamatically here is my code:
string header = "{\"to\": [\"emailaddress2\",\"emailaddress3\"], \"sub\": { \"%name%\": [\"Ben\",\"Joe\"]},\"filters\": { \"footer\": { \"settings\": { \"enable\": 1,\"text/plain\": \"Thank you for your business\"}}}}";
string header2 = Regex.Replace(header, "(.{72})", "$1" + Environment.NewLine);
var myMessage3 = new SendGridMessage();
myMessage3.From = new MailAddress("emailaddress1", "FromName");
myMessage3.Headers.Add("X-SMTPAPI", header2);
myMessage3.AddTo("emailaddress4");
myMessage3.Subject = "Test subject";
myMessage3.Html = "Test message";
myMessage3.EnableClickTracking(true);
// Create credentials, specifying your user name and password.
var credentials = new NetworkCredential(ConfigurationManager.AppSettings["xxxxx"], ConfigurationManager.AppSettings["xxxxx"]);
// Create an Web transport for sending email.
var transportWeb = new Web(credentials);
// Send the email, which returns an awaitable task.
transportWeb.DeliverAsync(myMessage3);
But it just seems to ignore my header, and sends the email to the one email "emailaddress4" used in "addto".
According the documentation if the header JSON is parsed wrongly, then SendGrid sends an email about the error to the email address set in "FROM" field, but I get no email about any error.
Anyone got any idea?
For me using the latest 9.x c# library the only way I could solve this was by using the MailHelper static functions like this:
var client = new SendGridClient(HttpClient, new SendGridClientOptions { ApiKey = _sendGridApiKey, HttpErrorAsException = true });
SendGridMessage mailMsg;
var recipients = to.Split(',').Select((email) => new EmailAddress(email)).ToList();
if (recipients.Count() > 1)
{
mailMsg = MailHelper.CreateSingleEmailToMultipleRecipients(
new EmailAddress(from),
recipients,
subject,
"",
body);
}
else
{
mailMsg = MailHelper.CreateSingleEmail(
new EmailAddress(from),
recipients.First(),
subject,
"",
body);
}
if (attachment != null)
{
mailMsg.AddAttachment(attachment.Name,
attachment.ContentStream.ToBase64(),
attachment.ContentType.MediaType);
}
var response = await client.SendEmailAsync(mailMsg).ConfigureAwait(false);
if (response.IsSuccessStatusCode)
{
_log.Trace($"'{subject}' email to '{to}' queued");
return true;
}
else {
throw new HttpRequestException($"'{subject}' email to '{to}' not queued");
}
I'm not sure why you wouldn't recieve any errors at your FROM address, but your JSON contains the following flaws:
, near the end makes the string invalid json
spaces around the first % in %name%, that might make sendgrid think it's an invalid substitution tag
if you use the X-SMTPAPI header to specify multiple recipients, you are not supposed to add a standard SMTP TO using AddTo().
Besides that, you didn't wrap the header at 72 characters (see the example in the documentation).
I figured that however the X-SMTPAPI documentation talks about passing the header as JSON, the API itself expects it as a parameter, containing Ienumerable string. So the working code is:
var myMessage3 = new SendGridMessage();
myMessage3.From = new MailAddress("email4#email.com", "Test Sender");
myMessage3.AddTo("email2#email.com");
myMessage3.Subject = "Új klubkártya regisztrálva";
myMessage3.Html = "Teszt üzenet";
myMessage3.EnableClickTracking(true);
/* SMTP API
* ===================================================*/
// Recipients
var addresses = new[]{
"email2#email.com", "email3#email.com"
};
//string check = string.Join(",", addresses);
myMessage3.Header.SetTo(addresses);
// Create credentials, specifying your user name and password.
var credentials = new NetworkCredential(ConfigurationManager.AppSettings["xxxxxxx"], ConfigurationManager.AppSettings["xxxxxxxxx"]);
// Create an Web transport for sending email.
var transportWeb = new Web(credentials);
// Send the email, which returns an awaitable task.
transportWeb.DeliverAsync(myMessage3);

asp.net web api server side talking back to angularjs

I am developing web site in angular js, .net web api 2, visual studio 2013, and c#.
My url navigation is purely driven by angularjs.
Upon password reset, I email a link to the user that looks like this:
http://localhost:3458/api/Account/ResetPasswordEmail?userId=xxxxxxxxxxxxxxxx9049a8e3-fb90-400a-b73e-78cadc16ae40&code=BVEssssssssssssssssssswT9FsJ3QncMLaPclhLZVUHpHifX8wmG7f3ZrlxDlwkkmcMNccdXz8jdEuGdHM1FJ4WdBu9Yxu9VG43DxamBrasdfasdfasdfbdasdfvgGrqwJxRoJ%2FSCDkOrbV3RupmUaoTgRmebwb1ymBZwkd891G3q6SW%2F%2FTDwOQ7qrkzkAUYtjcwd%2FTH4jNNCzIYmMXF%2BkMF26mBM4Osgc%2Bi%2BO0So41%2Fpp3yK%2BDvEtNCPA%3D%3D&newPassword=xxxxxxxxxx
User receives the email and clicks on the link..
On the server side, following code is executed:
//
// GET: /Account/ResetPasswordEmail
// GET api/Account/ResetPasswordEmail
[Route("ResetPasswordEmail", Name = "ResetPasswordEmail")]
[AllowAnonymous]
public HttpResponseMessage GetResetPasswordEmail(string userId, string code, string newPassword)
{
string errmsg = string.Empty;
try
{
if (userId == null || code == null)
{
errmsg = "Either email address or other necessary parameter is null.";
throw new Exception(errmsg);
}
var result = UserManager.ResetPasswordAsync(userId, code, newPassword);
var response = Request.CreateResponse(HttpStatusCode.Moved);
string fullyQualifiedUrl = Request.RequestUri.GetLeftPart(UriPartial.Authority);
response.Headers.Location = new Uri(fullyQualifiedUrl);
return response;
}
catch(Exception ex)
{
return null;
}
}
The password is reset successfully...
However ...
I want to give visual feedback on the web page that password was or was not reset successfully. In other words, in this situation how would my web api talk back to the angular js code to render a UI. Would I resort to some sort of asp.net mvc type of code?
In your website project's controller folder need to add action which where user will land from email
Then call api from that action if success then return success view in that action

Identity password reset token is invalid

I'm writting MVC 5 and using Identity 2.0.
Now I m trying to reset password. But i always getting "invalid token" error for reset password token.
public class AccountController : Controller
{
public UserManager<ApplicationUser> UserManager { get; private set; }
public AccountController()
: this(new UserManager<ApplicationUser>(new UserStore<ApplicationUser>(new ApplicationDbContext())))
{
}
and i set DataProtectorTokenProvider,
public AccountController(UserManager<ApplicationUser> userManager)
{
//usermanager config
userManager.PasswordValidator = new PasswordValidator { RequiredLength = 5 };
userManager.EmailService = new IddaaWebSite.Controllers.MemberShip.MemberShipComponents.EmailService();
var provider = new Microsoft.Owin.Security.DataProtection.DpapiDataProtectionProvider();
userManager.UserTokenProvider = new Microsoft.AspNet.Identity.Owin.DataProtectorTokenProvider<ApplicationUser>(provider.Create("UserToken"))
as IUserTokenProvider<ApplicationUser, string>;
UserManager = userManager;
}
i generate password reset before sending mail
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<ActionResult> ManagePassword(ManageUserViewModel model)
{
if (Request.Form["email"] != null)
{
var email = Request.Form["email"].ToString();
var user = UserManager.FindByEmail(email);
var token = await UserManager.GeneratePasswordResetTokenAsync(user.Id);
//mail send
}
}
i click link in mail and i'm getting passwordreset token and using
var result = await UserManager.ResetPasswordAsync(model.UserId, model.PasswordToken, model.NewPassword);
the result always false and it says "Invalid Token".
Where should i fix ?
UserManager.GeneratePasswordResetTokenAsync() very often returns string that contains '+' characters. If you pass parameters by query string, this is the cause ('+' character is a space in query string in URL).
Try to replace space characters in model.PasswordToken with '+' characters.
[HttpPost]
[ValidateAntiForgeryToken]
publicasync Task<ActionResult> ManagePassword(ManageUserViewModel model)
{
if (Request.Form["email"] != null)
{
var email = Request.Form["email"].ToString();
var user = UserManager.FindByEmail(email);
var token = await UserManager.GeneratePasswordResetTokenAsync(user.Id);
//before send mail
token = HttpUtility.UrlEncode(token);
//mail send
}
}
And on password reset action decode token HttpUtility.UrlDecode(token);
I have found that the 'Invalid Token' error also occurs when the SecurityStamp column is NULL for the user in the AspNetUsers table in the database.
The SecurityStamp won't be NULL with the out-of-the-box MVC 5 Identity 2.0 code, however a bug had been introduced in our code when doing some customization of the AccountController that cleared out the value in the SecurityStamp field.
Many answers here URLEncode the token before sending to get around the fact that the token (being a base 64 encoded string) often contains the '+' character. Solutions must also take into account that the token ends with '=='.
I was struggling with this issue & it turns out many users within a large organisation were using Scanmail Trustwave Link Validator(r) which was not symmetrically encoding and decoding URLEncoded stings in the email link (at the time of writing).
The easiest way was to use Mateusz Cisek's answer and send a non URLEncoded token and simply replace the space characters back to +. In my case this was done in an angular SPA so the Javascript becomes $routeParams.token.replace(/ /g,'+').
The caveat here will be if using AJAX to send the token and rolling your own query string parsing algorithm - many examples split each parameter on '=', which will of course not include the '==' at the end of the token. Easy to work around by using one of the regex solutions or looking for the 1st '=' only.

How do I implement password reset with ASP.NET Identity for ASP.NET MVC 5.0?

Microsoft is coming up with a new Membership system called ASP.NET Identity (also the default in ASP.NET MVC 5). I found the sample project, but this is not implemented a password reset.
On password reset topic just found this Article: Implementing User Confirmation and Password Reset with One ASP.NET Identity – Pain or Pleasure, not help for me, because do not use the built-in password recovery.
As I was looking at the options, as I think we need to generate a reset token, which I will send to the user. The user can set then the new password using the token, overwriting the old one.
I found the IdentityManager.Passwords.GenerateResetPasswordToken / IdentityManager.Passwords.GenerateResetPasswordTokenAsync(string tokenId, string userName, validUntilUtc), but I could not figure out what it might mean the tokenId parameter.
How do I implement the Password Reset in ASP.NET with MVC 5.0?
I get it: The tokenid is a freely chosen identity, which identifies a password option. For example,
1. looks like the password recovery process, step 1
(it is based on: https://stackoverflow.com/a/698879/208922)
[HttpPost]
[ValidateAntiForgeryToken]
[AllowAnonymous]
//[RecaptchaControlMvc.CaptchaValidator]
public virtual async Task<ActionResult> ResetPassword(
ResetPasswordViewModel rpvm)
{
string message = null;
//the token is valid for one day
var until = DateTime.Now.AddDays(1);
//We find the user, as the token can not generate the e-mail address,
//but the name should be.
var db = new Context();
var user = db.Users.SingleOrDefault(x=>x.Email == rpvm.Email);
var token = new StringBuilder();
//Prepare a 10-character random text
using (RNGCryptoServiceProvider
rngCsp = new RNGCryptoServiceProvider())
{
var data = new byte[4];
for (int i = 0; i < 10; i++)
{
//filled with an array of random numbers
rngCsp.GetBytes(data);
//this is converted into a character from A to Z
var randomchar = Convert.ToChar(
//produce a random number
//between 0 and 25
BitConverter.ToUInt32(data, 0) % 26
//Convert.ToInt32('A')==65
+ 65
);
token.Append(randomchar);
}
}
//This will be the password change identifier
//that the user will be sent out
var tokenid = token.ToString();
if (null!=user)
{
//Generating a token
var result = await IdentityManager
.Passwords
.GenerateResetPasswordTokenAsync(
tokenid,
user.UserName,
until
);
if (result.Success)
{
//send the email
...
}
}
message =
"We have sent a password reset request if the email is verified.";
return RedirectToAction(
MVC.Account.ResetPasswordWithToken(
token: string.Empty,
message: message
)
);
}
2 And then when the user enters the token and the new password:
[HttpPost]
[ValidateAntiForgeryToken]
[AllowAnonymous]
//[RecaptchaControlMvc.CaptchaValidator]
public virtual async Task<ActionResult> ResetPasswordWithToken(
ResetPasswordWithTokenViewModel
rpwtvm
)
{
if (ModelState.IsValid)
{
string message = null;
//reset the password
var result = await IdentityManager.Passwords.ResetPasswordAsync(
rpwtvm.Token,
rpwtvm.Password
);
if (result.Success)
{
message = "the password has been reset.";
return RedirectToAction(
MVC.Account.ResetPasswordCompleted(message: message)
);
}
else
{
AddErrors(result);
}
}
return View(MVC.Account.ResetPasswordWithToken(rpwtvm));
}
Skeleton proposal to sample project on github, if anyone needs it may be tested.The E-mail sending not yet written, possibly with the addition soon.
Seems like a lot of trouble...
What advantage does the above give over:
the user clicking a 'Recover Account' link
this sends an 64 byte encoded string of a datetime ticks value (call it psuedo-hash) in an email
click the link back in the email to a controller/action route that
matches email and it's source server to psuedo-hash, decrypts the psuedo-hash, validates the time since sent and
offers a View for the user to set a new password
with a valid password, the code removes the old user password and assigns the new.
Once complete, successful or not, delete the psuedo-hash.
With this flow, at no time do you EVER send a password out of your domain.
Please, anyone, prove to me how this is any less secure.

Categories