I'm writing api for the application
The task is to redirect to the profile page on the client after confirming the email. While email is not confirmed, I can not give access token to api.
At the moment a crutch lives on the client: username and password
temporarily stored locally, so that after the customer can make a hidden sign in.(what is not safe)
How can this problem be solved?
I have one not very nice idea: after registration give a access token and after on for each request check - emailConfirmed(bool)
But - perform an additional request each time ...
Small code example from back:
ApplicationOAuthProvider:
public override async Task GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context)
{
var userManager = context.OwinContext.GetUserManager<ApplicationUserManager>();
ApplicationUser user = await userManager.FindAsync(context.UserName, context.Password);
var a = await userManager.FindByIdAsync(context.ClientId);
if (user == null)
{
context.SetError("invalid_grant", "Username or Password is incorrect.");
return;
}
if (!user.EmailConfirmed)
{
context.SetError("invalid_grant", "Email not confirmed.");
return;
}
ClaimsIdentity oAuthIdentity = await user.GenerateUserIdentityAsync(userManager,
OAuthDefaults.AuthenticationType);
ClaimsIdentity cookiesIdentity = await user.GenerateUserIdentityAsync(userManager,
CookieAuthenticationDefaults.AuthenticationType);
AuthenticationProperties properties = CreateProperties(user.UserName);
AuthenticationTicket ticket = new AuthenticationTicket(oAuthIdentity, properties);
context.Validated(ticket);
context.Request.Context.Authentication.SignIn(cookiesIdentity);
}
Send code on email:
[AllowAnonymous]
[HttpPost]
[Route("Users/{userId}/emailConfirmation")]
public async Task<IHttpActionResult> SendConfirmationCode(string userId)
{
if (userId == null)
{
return BadRequest("UserId were not transferred");
}
var user = await UserManager.FindByIdAsync(userId);
if (user == null)
{
return Content(HttpStatusCode.NotFound, "User not found");
}
var code = await UserManager.GenerateEmailConfirmationTokenAsync(user.Id);
var url = $"{clientBaseUrl}confirm_email?userId={user.Id}&code={WebUtility.UrlEncode(code)}";
await MailHelper.SendConfirmationLinkOnEmail(url, user.Email);
return Content(HttpStatusCode.OK, new { Message = "Confirmation code was sent on email" });
}
Email confirmation by code:
[AllowAnonymous]
[HttpPost]
[Route("Users/{userId}/emailConfirmationCode")]
public async Task<IHttpActionResult> ConfirmEmail(string userId, [FromBody]EmailConfirmationDTO input)
{
if (userId == null || input == null)
{
return BadRequest("UserId or code were not transferred");
}
try
{
var isConfirmed = await UserManager.IsEmailConfirmedAsync(userId);
if (isConfirmed)
{
return Content(HttpStatusCode.Forbidden, new { Message = "This user already confirmed email" });
}
var result = await UserManager.ConfirmEmailAsync(userId, input.Code);
if (result.Succeeded)
{
var user = Db.Users.FirstOrDefaultAsync(u => u.Id == userId);
return Content(HttpStatusCode.OK, new { responseObj = user });
}
}
catch (Exception e)
{
// ignored
}
return Content(HttpStatusCode.InternalServerError, new { Message = "Error confirmation." });
}
ConfigureAuth:
OAuthOptions = new OAuthAuthorizationServerOptions
{
TokenEndpointPath = new PathString("/Token"),
Provider = new ApplicationOAuthProvider(PublicClientId),
AuthorizeEndpointPath = new PathString("/api/Account/ExternalLogin"),
AccessTokenExpireTimeSpan = TimeSpan.FromDays(5),
#if (!DEBUG)
AllowInsecureHttp = false
#else
AllowInsecureHttp = true
#endif
};
Related
I want to make an email confirmation callback link from asp.net core web API repository class,
This is easy when done in the controller of the API, but I am having trouble doing this from the repository class.
I keep getting this error:
Could not find an IRouter associated with the ActionContext. If your application is using endpoint routing then you can get a IUrlHelperFactory with dependency injection and use it to create a UrlHelper, or use Microsoft.AspNetCore.Routing.LinkGenerator.
this is what I have so far:
this is my repository code for registration:
public async Task<(bool IsSuccess, string ErrorMessage)> Register(RegisterDTO model)
{
if (model != null)
{
var user = _mapper.Map<ApplicationUser>(model);
var result = await _userManager.CreateAsync(user, model.Password);
if (result.Succeeded)
{
await _userManager.AddToRoleAsync(user, "Admin");
var code = await _userManager.GenerateEmailConfirmationTokenAsync(user);
string _scheme = _urlHelper.ActionContext.HttpContext.Request.Scheme;
//var callbackurl = _urlHelper.Link("ConfirmEmail", new { email =
user.Email, code = code });
var callbackurl =
_urlHelper.Action("ConfirmEmail",nameof(AccountController), new { email = user.Email, code = code }, _scheme);
var mailresult = _mailSender.SendEmail(model.Email, "Please confirm your
account by clicking here: link");
return (true, "Account created successfully");
}
return (false, "Eror occured while creating your account");
}
return (false, "Please provide the user data");
}
Account Controller
[HttpGet("{token}/{email}")]
public async Task<IActionResult> ConfirmEmail(string token, string email)
{
var result = await _repository.ConfirmEmail(token, email);
if (result.IsSuccess)
return Ok(result.ErrorMessage);
else
return BadRequest(result.ErrorMessage);
}
I managed to get it to work.
see the code below:
enter code here
public async Task<(bool IsSuccess, string ErrorMessage)>
Register(RegisterDTO model)
{
if (model != null)
{
var user = _mapper.Map<ApplicationUser>(model);
var result = await _userManager.CreateAsync(user, model.Password);
if (result.Succeeded)
{
await _userManager.AddToRoleAsync(user, "Admin");
var _urlHelper = _urlHelperFactory.GetUrlHelper(
_actionContextAccessor.ActionContext);
var code = await _userManager.
GenerateEmailConfirmationTokenAsync(user);
string _scheme = _urlHelper.ActionContext.
HttpContext.Request.Scheme;
var callbackurl = _urlHelper.Action(action:
nameof(AccountController.ConfirmEmail),
controller: "Account", values:
new { email = user.Email, code = code },
protocol: _scheme);
var mailresult = _mailSender.SendEmail(model.Email, "Please
confirm your account by clicking here: <a href=\"" +
callbackurl + "\">link</a>");
if (mailresult.IsSuccess)
{
return (true, "Account created successfully, Please visit
your email to confirm your email");
}
else
{
return (true, $"Account created successfully,
{mailresult.ErrorMessage}");
}
}
return (false, result.Errors.ToString());
}
return (false, "Please provide the user data");
}
In my controller I hard-coded a username and password. Now I want to allow the user to login with his/her Azure AD username and password.
public IActionResult Login([FromBody] LoginModel user)
{
if (user == null)
{
return BadRequest("Invalid client request");
}
if (user.UserName == "johndoe" && user.Password == "def#MKK")
{
var secretKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("justAsecretKey"));
var signinCredentials = new SigningCredentials(secretKey, SecurityAlgorithms.HmacSha256);
var claims = new List<Claim>
{
new Claim(ClaimTypes.Name, user.UserName)
};
var token = new JwtSecurityToken(
audience: "http://api.azurewebsites.net",
issuer: "http://api.azurewebsites.net",
claims: claims,
expires: DateTime.Now.AddMinutes(60),
signingCredentials: signinCredentials
);
var results = new
{
token = new JwtSecurityTokenHandler().WriteToken(token),
expiration = token.ValidTo
};
return Ok(results);
}
else
{
return Unauthorized();
}
}
My Solution:
I got rid of the hardcoded User
Added Credentials validation
Provide the Azure AD Configurations
Set the scopes/api permissions
AcquireTokenByUsernamePassword
Return the Tokens and other info when succesful
Otherwise return BadRequest or Unauthorized
public ActionResult Login([FromBody] UserModel user)
{
if (user == null)
{
var results = new
{
Message = "Invalid client request"
};
return BadRequest(results);
}
if (user.UserName != "" && user.Password != "")
{
try
{
string clientId = Configuration["AzureAd:ClientId"];
string tenantId = Configuration["AzureAd:TenantId"];
string authority = Configuration["AzureAd:Authority"];
string[] scopes = new string[] { "User.Read", "Directory.AccessAsUser.All", "User.Invite.All" };
IPublicClientApplication app = PublicClientApplicationBuilder.Create(clientId)
.WithAuthority(authority)
.WithTenantId(tenantId)
.Build();
var securePassword = new SecureString();
foreach (char c in user.Password.ToCharArray()) // you should fetch the password
securePassword.AppendChar(c); // keystroke by keystroke
var tokens = app.AcquireTokenByUsernamePassword(scopes, user.UserName, securePassword).ExecuteAsync().Result;
graphClient = new GraphServiceClient(
new DelegateAuthenticationProvider(x =>
{
x.Headers.Authorization = new AuthenticationHeaderValue(
"Bearer", tokens.AccessToken);
return Task.FromResult(0);
}));
var results = new
{
ExpiresOn = tokens.ExpiresOn.LocalDateTime,
AccessToken = tokens.AccessToken,
Token = tokens.IdToken,
Environment = tokens.Account.Environment,
AccountId = tokens.Account.HomeAccountId
};
return Ok(results);
}
catch (Exception ex)
{
var results = new
{
Message = ex.Message
};
return Unauthorized(results);
}
}
else
{
var results = new
{
Message = "Username or password isn't valid."
};
return Unauthorized(results);
}
}
This API is intended for a mobile application. The goal is to let the user confirm the email upon registration. When the user registers, a confirmation link is generated and sent over the email. I've done it the same way in a MVC project, it worked fine, but in a Web API project looks like it ain't gonna cut.
Now when the user clicks that link, the respective action method should be hit and do the job.
The only problem is, the ConfirmEmail action method is just not getting triggered when clicking the confirmation link although it looked fine.
Here are the main configurations which might help
MVC service configuration
services.AddMvc(options =>
{
options.EnableEndpointRouting = true;
options.Filters.Add<ValidationFilter>();
})
.AddFluentValidation(mvcConfiguration => mvcConfiguration.RegisterValidatorsFromAssemblyContaining<Startup>())
.SetCompatibilityVersion(Microsoft.AspNetCore.Mvc.CompatibilityVersion.Version_3_0);
Identity Service
public async Task<AuthenticationResult> RegisterAsync(string email, string password)
{
var existingUser = await _userManager.FindByEmailAsync(email);
if(existingUser != null)
{
return new AuthenticationResult { Errors = new[] { "User with this email address exists" } };
}
// generate user
var newUser = new AppUser
{
Email = email,
UserName = email
};
// register user in system
var result = await _userManager.CreateAsync(newUser, password);
if (!result.Succeeded)
{
return new AuthenticationResult
{
Errors = result.Errors.Select(x => x.Description)
};
}
// when registering user, assign him user role, also need to be added in the JWT!!!
await _userManager.AddToRoleAsync(newUser, "User");
// force user to confirm email, generate token
var token = await _userManager.GenerateEmailConfirmationTokenAsync(newUser);
// generate url
var confirmationLink = _urlHelper.Action("ConfirmEmail", "IdentityController",
new { userId = newUser.Id, token = token }, _httpRequest.HttpContext.Request.Scheme);
// send it per email
var mailresult =
await _emailService.SendEmail(newUser.Email, "BingoApp Email Confirmation",
$"Please confirm your account by <a href='{HtmlEncoder.Default.Encode(confirmationLink)}'>clicking here</a>.");
if (mailresult)
return new AuthenticationResult { Success = true };
else
return new AuthenticationResult { Success = false, Errors = new List<string> { "Invalid Email Address"} };
}
Controller
[HttpPost(ApiRoutes.Identity.Register)]
public async Task<IActionResult> Register([FromBody] UserRegistrationRequest request)
{
if (!ModelState.IsValid)
{
return BadRequest(new AuthFailedResponse
{
Errors = ModelState.Values.SelectMany(x => x.Errors.Select(xx => xx.ErrorMessage))
});
}
// register the incoming user data with identity service
var authResponse = await _identityService.RegisterAsync(request.Email, request.Password);
if (!authResponse.Success)
{
return BadRequest(new AuthFailedResponse
{
Errors = authResponse.Errors
});
}
// confirm registration
return Ok();
}
[HttpGet]
public async Task<IActionResult> ConfirmEmail(string userId, string token)
{
if (userId == null || token == null)
{
return null;
}
var user = await _userManager.FindByIdAsync(userId);
if (user == null)
{
return null;
}
var result = await _userManager.ConfirmEmailAsync(user, token);
if (result.Succeeded)
{
await _emailService.SendEmail(user.Email, "BingoApp - Successfully Registered", "Congratulations,\n You have successfully activated your account!\n " +
"Welcome to the dark side.");
}
return null;
}
Your _urlHelper.Action(..) looks a bit suspicious to me.
I'm not sure you should pass the full controller name, that is, including the actual word controller.
Try _urlHelper.Action("ConfirmEmail", "Identity", instead.
As a tip: I try to avoid magic strings like these by using nameof(IdentityController) because it will return the controller name without the controller postfix.
I use asp net core Identity. This is what I am trying to do and I have no Idea how to do this so need some expert help on this. When a new user registers in my application , a password reset link is sent to the person's email, Now when the link token in the link has expired and the user clicks on the link I need to show a message as token expired. How do I find out If the token has expired or not when the user clicks on the link which is in the email. Can any one suggest ways to achieve this scenario?
The ASP.NET Core has a built-in Claims system to store information about the user .
To do that ,
add a helper method to store the expiration datetime in Register.cshtml.cs:
private async Task AddTokenExpirationInfo(IdentityUser user, int span=1*24*60)
{
var expiresAt = DateTime.Now.Add(TimeSpan.FromMinutes(span));
var tokenExpiredAtClaim = new Claim("ActivtationTokenExpiredAt", expiresAt.ToUniversalTime().Ticks.ToString());
await _userManager.AddClaimAsync(user, tokenExpiredAtClaim);
}
add a helper method to check whether the token has expired in the ConfirmEmail.cshtml.cs :
private async Task<bool> TokenExpiredValidate(IdentityUser user) {
var claims = (await _userManager.GetClaimsAsync(user))
.Where(c => c.Type == "ActivtationTokenExpiredAt");
var expiredAt = claims.FirstOrDefault()?.Value;
bool expired = true; // default value
if (expiredAt != null)
{
var expires = Convert.ToInt64(expiredAt);
var now = DateTime.Now.Ticks;
expired= now <= expires? false : true;
}
else {
expired = false;
}
// clear claims
await _userManager.RemoveClaimsAsync(user, claims);
return expired;
}
invoke AddTokenExpirationInfo when there's a registeration request :
public async Task<IActionResult> OnPostAsync(string returnUrl = null)
{
returnUrl = returnUrl ?? Url.Content("~/");
if (ModelState.IsValid)
{
var user = new IdentityUser { UserName = Input.Email, Email = Input.Email };
var result = await _userManager.CreateAsync(user, Input.Password);
if (result.Succeeded)
{
_logger.LogInformation("User created a new account with password.");
var code = await _userManager.GenerateEmailConfirmationTokenAsync(user);
var callbackUrl = Url.Page(
"/Account/ConfirmEmail",
pageHandler: null,
values: new { userId = user.Id, code = code },
protocol: Request.Scheme);
await _emailSender.SendEmailAsync(Input.Email, "Confirm your email",
$"Please confirm your account by <a href='{HtmlEncoder.Default.Encode(callbackUrl)}'>clicking here</a>.");
///////////////// invoke here ////////////////////
AddTokenExpirationInfo(user);
await _signInManager.SignInAsync(user, isPersistent: false);
return LocalRedirect(returnUrl);
}
foreach (var error in result.Errors)
{
ModelState.AddModelError(string.Empty, error.Description);
}
}
// If we got this far, something failed, redisplay form
return Page();
}
invoke TokenExpiredValidate in your ConfirmEmail.cshtml.cs:
public async Task<IActionResult> OnGetAsync(string userId, string code)
{
if (userId == null || code == null)
{
return RedirectToPage("/Index");
}
var user = await _userManager.FindByIdAsync(userId);
if (user == null)
{
return NotFound($"Unable to load user with ID '{userId}'.");
}
var result = await _userManager.ConfirmEmailAsync(user, code);
if (!result.Succeeded)
{
throw new InvalidOperationException($"Error confirming email for user with ID '{userId}':");
}
if (await TokenExpiredValidate(user))
throw new InvalidOperationException($"Token has alread expired '{userId}':");
return Page();
}
When a user registers , there' will be a record in the AspNetUserClaims table :
When a user confirmed successfully , the record will be removed . Just as a reminder , a more robust method is to use a background service to clear the expired record .
This is my ExternalLoginConfirmation task, that is the default code added when the project is created. I have added these lines to get the user claims:
user.Firstname = info.Principal.Claims.FirstOrDefault(c => c.Type == ClaimTypes.GivenName)?.ToString();
user.Lastname = info.Principal.Claims.FirstOrDefault(c => c.Type == ClaimTypes.Surname)?.ToString();
user.Gender = info.Principal.Claims.FirstOrDefault(c => c.Type == ClaimTypes.Gender)?.ToString();
However I'm not able to get the user profile image as it does not exist in ClaimTypes.
public async Task<IActionResult> ExternalLoginConfirmation(ExternalLoginConfirmationViewModel model, string returnUrl = null)
{
if (ModelState.IsValid)
{
// Get the information about the user from the external login provider
var info = await _signInManager.GetExternalLoginInfoAsync();
if (info == null)
{
return View("ExternalLoginFailure");
}
var user = new ApplicationUser { UserName = model.Email, Email = model.Email };
var result = await _userManager.CreateAsync(user);
if (result.Succeeded)
{
result = await _userManager.AddLoginAsync(user, info);
if (result.Succeeded)
{
user.Firstname = info.Principal.Claims.FirstOrDefault(c => c.Type == ClaimTypes.GivenName)?.ToString();
user.Lastname = info.Principal.Claims.FirstOrDefault(c => c.Type == ClaimTypes.Surname)?.ToString();
user.Gender = info.Principal.Claims.FirstOrDefault(c => c.Type == ClaimTypes.Gender)?.ToString();
await _signInManager.SignInAsync(user, isPersistent: false);
_logger.LogInformation(6, "User created an account using {Name} provider.", info.LoginProvider);
return RedirectToLocal(returnUrl);
}
}
AddErrors(result);
}
ViewData["ReturnUrl"] = returnUrl;
return View(model);
}
Note that info is missing ExternalPrincipal and the following functionality is not available either:
info.ExternalPrincipal.FindFirstValue("pictureUrl");
This is the soultion I'm using now:
var result = await _userManager.CreateAsync(user);
if (result.Succeeded)
{
result = await _userManager.AddLoginAsync(user, info);
if (result.Succeeded)
{
if (info.LoginProvider.ToLower().IndexOf("google") != -1)
{ await _userManager.AddClaimAsync(user, new Claim("GooglePlusId", info.ProviderKey));
try {
HttpClient client = new HttpClient();
HttpResponseMessage x = await client.GetAsync($"https://www.googleapis.com/plus/v1/people/{info.ProviderKey}?fields=image&key=YOUR_GOOGLE_PLUS_API_KEY");
dynamic img = Newtonsoft.Json.JsonConvert.DeserializeObject(await x.Content.ReadAsStringAsync());
user.PhotoLink = img.image.url;
db.SaveChanges();
}
catch { }
}
if (info.LoginProvider.ToLower().IndexOf("facebook") != -1)
{
user.PhotoLink = $"http://graph.facebook.com/{info.ProviderKey}/picture?type=square&width=50";
}
await _signInManager.SignInAsync(user, isPersistent: false);
_logger.LogInformation(6, "User created an account using {Name} provider.", info.LoginProvider);
return RedirectToLocal(returnUrl);
}
}
P.S.: this code should be added to ExternalLoginConfirmation action in AccountControllers controller