Email confirmation in Web API - c#

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.

Related

Encoded Characters in Query Parameters Crash Angular Client

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?

Get User From Usermanger After Login

I'm trying to get a user from the usermanger after External Login, using .Net-Core and IdentityServer4
public async Task<IActionResult> ExternalLoginCallback(string returnUrl = null, string remoteError = null)
{
if (remoteError != null)
{
ErrorMessage = $"Error from external provider: {remoteError}";
return RedirectToAction(nameof(Login));
}
var info = await _signInManager.GetExternalLoginInfoAsync();
if (info == null)
{
return RedirectToAction(nameof(Login));
}
// Sign in the user with this external login provider if the user already has a login.
var result = await _signInManager.ExternalLoginSignInAsync(info.LoginProvider, info.ProviderKey, isPersistent: false, bypassTwoFactor: true);
if (result.Succeeded)
{
var user = await _userManager.GetUserAsync(HttpContext.User);
var serviceProvider = HttpContext.RequestServices;
var context = serviceProvider.GetRequiredService<PublicAstootContext>();
var coinbaseRule = new CoinbaseRule();
await coinbaseRule.UpdateCoinbaseAccountInfo(context, user.Id, info);
//...
}
}
However, even after the login succeeds, when I attempt to get the user from the usermanger, the user is always null.
How can I get the user from the usermanger after my external login?
The call to ExternalLoginSignInAsync does not populate HttpContext.User - it ends up writing an authentication cookie that is read in subsequent requests when attempting to populate HttpContext.User, but not before. In your example, the call to ExternalLoginSignInAsync occurs within the same request as the call to GetUserAsync, which means that HttpContext.User will not represent an authenticated user and so no match will be found.
Instead, you can use UserManager.FindByLoginAsync to get the correct value for user:
var user = await _userManager.FindByLoginAsync(
info.LoginProvider, info.ProviderKey);

ASP MVC ConfirmEmailAsync not working

I register a user, receive a token via email which looks like this:
Please confirm your account by clicking here
I click the link and I can see that the ConfirmEmail method in AccountController fires:
[AllowAnonymous]
public async Task<ActionResult> ConfirmEmail(string userId, string code)
{
if (userId == null || code == null)
{
return View("Error");
}
var result = await UserManager.ConfirmEmailAsync(userId, code);
return View(result.Succeeded ? "ConfirmEmail" : "Error");
}
And that result.Succeeded is true.
Everything appears fine, but when trying to log in after completing this process I get taken to the page telling me my account is locked
Locked out.
This account has been locked out, please try again later.
What couldI be doing wrong? Do I need to manually change the lockout flag in the db? If so, what is the point of the ConfirmEmailAsync method?
ConfirmEmailAsync just sets the EmailConfirmed on the user account record to true. From UserManager (edited for brevity):
public virtual async Task<IdentityResult> ConfirmEmailAsync(TUser user, string token)
{
...
var store = GetEmailStore();
...
await store.SetEmailConfirmedAsync(user, true, CancellationToken);
return await UpdateUserAsync(user);
}
Where GetEmailStore returns the IUserEmailStore (which is implemented by UserStore by default), which sets the flag:
public virtual Task SetEmailConfirmedAsync(TUser user, bool confirmed, CancellationToken cancellationToken = default(CancellationToken))
{
...
user.EmailConfirmed = confirmed;
return Task.CompletedTask;
}
The error you're getting indicated that the LockoutEnabled flag on the user account is true. You can set this to false by calling the SetLockoutEnabledAsync method on the UserManager.
There is also a SupportsUserLockout flag on the UserManager which unlocks accounts by default on creation. In order to set this you will need to create your own UserManager and override this flag to false.
At first, I had challenges getting these to work and after a series of research no success. Finally, I got the root of the problem(s) and fixed them thus sharing my experience. Follow the following process and I am sure it will help.
Step 1
Goto Startup.cs and remove the code below if you have it initialised;
services.Configure<RouteOptions>(options =>
{
options.LowercaseUrls = true;
//options.LowercaseQueryStrings = true; //(comment or remove this line)
});
Step 2 For GenerateEmailConfirmationTokenAsync() / ConfirmEmailAsync()
2a. On registering new user for token generation go as thus;
var originalCode = await userManager.GenerateEmailConfirmationTokenAsync(user);
var code = HttpUtility.UrlEncode(originalCode);
var confirmationLink = Url.Action("ConfirmEmail", "Account",
new { userId = user.Id, token = code }, Request.Scheme);
2b. On receiving confrimationLink for email confirmation, go as thus
var originalCode = HttpUtility.UrlDecode(token);
var result = await userManager.ConfirmEmailAsync(user, originalCode);
if (result.Succeeded)
{
return View(); //this returns login page if successful
}
For GeneratePasswordResetTokenAsync() and ResetPasswordAsync()
a.
var originalCode = await userManager.GeneratePasswordResetTokenAsync(user);
var code = HttpUtility.UrlEncode(originalCode);
var passwordResetLink = Url.Action("ResetPassword", "Account",
new { email = model.Email, token = code }, Request.Scheme);
b.
var orginalCode = HttpUtility.UrlDecode(model.Token);
var result = await userManager.ResetPasswordAsync(user, orginalCode, model.Password);
if (result.Succeeded)
{
return View("ResetPasswordConfirmation");
}

Email confirmation fails

I need to confirm the user's email by link but ASP.Net Identity fails on this action. Below the code is how I generate a token:
string code = await UserManager.GenerateEmailConfirmationTokenAsync(user.Id); // Return Opd+0yNG++ZXFpRS19A8j8OgI7dzCAKTYWotHYvuu0nyYsH4SIQS+bHwkbmqQDlwvGAy5fyxxUsu4yIzHwF+PD6QNPU+PwBnIDUGMC9++FkMlqegqHVKpA59qvbokfI0yByYqLoZfD1EUpWExddDN0BN/SVNSgOKlyzd928k+k4O2/TnfSf/JFj8x1NUKuaj
And how I try to check and confirm it:
[AllowAnonymous]
public async Task<ActionResult> ConfirmEmail(string userId, string code)
{
if (userId == null || code == null)
{
Logger.Error(string.Format("userId: {0}, code: {1}", userId, code));
return View("Error");
}
// args values: userId: b96bf253-62e1-4c5e-bf5f-5bc527df9fd9 code: Opd 0yNG ZXFpRS19A8j8OgI7dzCAKTYWotHYvuu0nyYsH4SIQS bHwkbmqQDlwvGAy5fyxxUsu4yIzHwF PD6QNPU PwBnIDUGMC9 FkMlqegqHVKpA59qvbokfI0yByYqLoZfD1EUpWExddDN0BN/SVNSgOKlyzd928k k4O2/TnfSf/JFj8x1NUKuaj
var result = await UserManager.ConfirmEmailAsync(userId, code);
if (result.Succeeded) // This is false
{
return View("ConfirmEmail");
}
Logger.Error(string.Concat<string>(result.Errors)); // Wrong marker
return View("Error");
}
There are no null-values I checked in the debugger. Why can it fail?
P.S. I use Npgsql if it is important and all works perfect except this.
When the + sign is lost, it indicates an UrlEncoding issue. + represents a whitespace and if you want to preserve it, you should encode your code content and then decode it in your ConfirmEmail method right before calling UserManager.ConfirmEmailAsync.
When encoded, + becomes %2B and vice versa when decoded.
Use HttpUtility.UrlEncode() and HttpUtility.UrlDecode() from the System.Web namespace.
Firstly you must be determine token is correct or not.
On your generate token, note that token and check your database after that.
string code = await UserManager.GenerateEmailConfirmationTokenAsync(user.Id);
var callbackUrl = Url.Action("ConfirmEmail", "Account", new { userId = user.Id, code = code }, protocol: Request.Url.Scheme);
UserManager.EmailService = new EmailService();
await UserManager.SendEmailAsync(user.Id, "Confirm your account", "Please confirm your account by clicking here");
Also this is my email service for check.
public class EmailService : IIdentityMessageService
{
public System.Threading.Tasks.Task SendAsync(IdentityMessage message)
{
// Plug in your email service here to send an email.
var mail = new MailMessage
{
Subject = message.Subject,
Body = message.Body,
IsBodyHtml = true
};
mail.To.Add(message.Destination);
mail.From = new MailAddress("me#mail.com","me");
var smtp = new SmtpClient
{
Host = "smtp.mail.com",
Port = 25,
UseDefaultCredentials = false,
Credentials = new System.Net.NetworkCredential("me#mail.com", "password"),
EnableSsl = true
};
// Enter seders User name and password
smtp.Send(mail);
return System.Threading.Tasks.Task.FromResult(0);
}
When you get email check link, it must be match with callbackurl. When you click url this method should be call.
[AllowAnonymous]
public async Task<ActionResult> ConfirmEmail(string userId, string code)
{
if (userId == null || code == null)
return View("Error");
var result = await UserManager.ConfirmEmailAsync(userId, code);
return View(result.Succeeded ? "ConfirmEmail" : "Error");
}
In this method you must be see code variable is match with your code at your identity table.
Also there is important problem with Identity Framework. There is no dependency injection container or etc. on MVC out of the box. You must be create your own IoC. I always prefer structuremap to do this. Because lifetime manager better than others(ninject, unity etc.). Also I'm using current user object for reduce request to database or session manager.
public class CurrentUser : ICurrentUser
{
private readonly ApplicationDbContext _context;
private readonly IIdentity _identity;
private ApplicationUser _user;
public CurrentUser(IIdentity identity, ApplicationDbContext context)
{
_identity = identity;
_context = context;
}
public ApplicationUser User
{
get { return _user ?? (_user = _context.Users.Find(_identity.GetUserId())); }
}
}
In my MVC registry ( please check structuremap docs), i register everything that i need on
public class MvcRegistry : Registry
{
public MvcRegistry()
{
For<BundleCollection>().Use(BundleTable.Bundles);
For<RouteCollection>().Use(RouteTable.Routes);
For<IIdentity>().Use(() => HttpContext.Current.User.Identity);
For<IUserStore<ApplicationUser>>().Use<UserStore<ApplicationUser>>();
For<DbContext>().Use(() => new ApplicationDbContext());
For<IAuthenticationManager>().Use(() => HttpContext.Current.GetOwinContext().Authentication);
For<HttpSessionStateBase>().Use(() => new HttpSessionStateWrapper(HttpContext.Current.Session));
For<HttpContextBase>().Use(() => new HttpContextWrapper(HttpContext.Current));
For<HttpServerUtilityBase>().Use(() => new HttpServerUtilityWrapper(HttpContext.Current.Server));
}
}
With this I always use same object for same user also i prevent object reference problem and tight coupling problems as well.

ASP.Net Identity 2 - custom response from OAuthAuthorizationServerProvider

This question is continuation of my previous one: ASP.Net Identity 2 login using password from SMS - not using two-factor authentication
I've build my custom OAuthAuthorizationServerProvider to support custom grant_type.
My idea was to create grant_type of sms that will allow user to generate one-time access code that will be send to his mobile phone and then user as password when sending request with grant_type of password.
Now after generating, storing and sending via SMS that password I'd like to return custom response, not token from my GrantCustomExtension.
public override async Task GrantCustomExtension(OAuthGrantCustomExtensionContext context)
{
const string allowedOrigin = "*";
context.OwinContext.Response.Headers.Add("Access-Control-Allow-Origin", new[] {allowedOrigin});
if (context.GrantType != "sms")
{
context.SetError("invalid_grant", "unsupported grant_type");
return;
}
var userName = context.Parameters.Get("username");
if (userName == null)
{
context.SetError("invalid_grant", "username is required");
return;
}
var userManager = context.OwinContext.GetUserManager<ApplicationUserManager>();
ApplicationUser user = await userManager.FindByNameAsync(userName);
if (user == null)
{
context.SetError("invalid_grant", "user not found");
return;
}
var generator = new TotpSecurityStampBasedTokenProvider<ApplicationUser, string>();
await userManager.UpdateSecurityStampAsync(user.Id);
var accessCode = await generator.GenerateAsync("SMS", userManager, user);
var accessCodeExpirationTime = TimeSpan.FromMinutes(5);
var result = await userManager.AddAccessCode(user, accessCode, accessCodeExpirationTime);
if(result.Succeeded)
{
Debug.WriteLine("Login code:"+accessCode);
//here I'll send login code to user phone via SMS
}
//return 200 (OK)
//with content type="application/json; charset=utf-8"
//and custom json content {"message":"code send","expires_in":300}
//skip part below
ClaimsIdentity oAuthIdentity = await user.GenerateUserIdentityAsync(userManager, "SMS");
var ticket = new AuthenticationTicket(oAuthIdentity, null);
context.Validated(ticket);
}
How can I stop generating token and return custom response from OAuthAuthorizationServerProvider?
I'm aware of two methods: TokenEndpoint, TokenEndpointResponse, but I'd like to override whole response, not just token.
EDIT:
For now I'm creating temporary ClaimsIdentity in GrantCustomExtension using code below:
var ci = new ClaimsIdentity();
ci.AddClaim(new Claim("message","send"));
ci.AddClaim(new Claim("expires_in", accessCodeExpirationTime.TotalSeconds.ToString(CultureInfo.InvariantCulture)));
context.Validated(ci);
and I'm overriding TokenEndpointResponse:
public override Task TokenEndpointResponse(OAuthTokenEndpointResponseContext context)
{
if (context.TokenEndpointRequest.GrantType != "sms") return base.TokenEndpointResponse(context);
//clear response containing temporary token.
HttpContext.Current.Response.SuppressContent = true;
return Task.FromResult<object>(null);
}
This has two issues: when calling context.Validated(ci); I'm saying this is a valid user, but instead I'd like to response information that I've send access code via SMS.
HttpContext.Current.Response.SuppressContent = true; clears response, but I'd like to return something instead of empty response.
This is more of a workaround then a final solution, but I believe it is the most reliable way of solving your issue without rewriting tons of code from the default OAuthAuthorizationServerProvider implementation.
The approach is simple: use a Owin middleware to catch token requests, and overwrite the response if an SMS was sent.
[Edit after comments] Fixed the code to allow the response body to be buffered and changed as per this answer https://stackoverflow.com/a/36414238/965722
Inside your Startup.cs file:
public void Configuration(IAppBuilder app)
{
var tokenPath = new PathString("/Token"); //the same path defined in OAuthOptions.TokenEndpointPath
app.Use(async (c, n) =>
{
//check if the request was for the token endpoint
if (c.Request.Path == tokenPath)
{
var buffer = new MemoryStream();
var body = c.Response.Body;
c.Response.Body = buffer; // we'll buffer the response, so we may change it if needed
await n.Invoke(); //invoke next middleware (auth)
//check if we sent a SMS
if (c.Get<bool>("sms_grant:sent"))
{
var json = JsonConvert.SerializeObject(
new
{
message = "code send",
expires_in = 300
});
var bytes = Encoding.UTF8.GetBytes(json);
buffer.SetLength(0); //change the buffer
buffer.Write(bytes, 0, bytes.Length);
//override the response headers
c.Response.StatusCode = 200;
c.Response.ContentType = "application/json";
c.Response.ContentLength = bytes.Length;
}
buffer.Position = 0; //reset position
await buffer.CopyToAsync(body); //copy to real response stream
c.Response.Body = body; //set again real stream to response body
}
else
{
await n.Invoke(); //normal behavior
}
});
//other owin middlewares in the pipeline
//ConfigureAuth(app);
//app.UseWebApi( .. );
}
And inside your custom grant method:
// ...
var result = await userManager.AddAccessCode(user, accessCode, accessCodeExpirationTime);
if(result.Succeeded)
{
Debug.WriteLine("Login code:"+accessCode);
//here I'll send login code to user phone via SMS
}
context.OwinContext.Set("sms_grant:sent", true);
//you may validate the user or set an error, doesn't matter anymore
//it will be overwritten
//...
I would recommend to have a look at this answer :
https://stackoverflow.com/a/24090287/2508268
public override Task TokenEndpoint(OAuthTokenEndpointContext context)
{
foreach (KeyValuePair<string, string> property in context.Properties.Dictionary)
{
context.AdditionalResponseParameters.Add(property.Key, property.Value);
}
return Task.FromResult<object>(null);
}

Categories

Resources