I'm trying to implement SendGrid in Angular project template with ASP.NET Core.
I'm using this example: Account confirmation and password recovery in ASP.NET Core
Unfortunately I always get this error (status: 500):
System.InvalidOperationException: Unable to resolve service for type 'MyWebApp.Extensions.EmailSender' while attempting to activate 'MyWebApp.Controllers.AuthenticationController'
Here is the Controller:
[HttpPost]
public async Task<IActionResult> Register([FromBody]RegistrationViewModel model)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
var userIdentity = _mapper.Map<AppUser>(model);
var result = await _userManager.CreateAsync(userIdentity, model.Password);
if (!result.Succeeded)
return new BadRequestObjectResult(Errors.AddErrorsToModelState(result, ModelState));
//Enable account confirmation
var code = await _userManager.GenerateEmailConfirmationTokenAsync(userIdentity);
var callbackUrl = Url.EmailConfirmationLink(userIdentity.Id, code, Request.Scheme);
await _emailSender.SendEmailConfirmationAsync(model.Email, callbackUrl);
//await _appDbContext.SaveChangesAsync();
return new OkObjectResult("Account created");
}
And the EmailSender:
public class EmailSender : IEmailSender
{
public EmailSender(IOptions<AuthMessageSenderOptions> optionsAccessor)
{
Options = optionsAccessor.Value;
}
public AuthMessageSenderOptions Options { get; } //set only via Secret Manager
public Task SendEmailAsync(string email, string subject, string message)
{
return Execute(Options.SendGridKey, subject, message, email);
}
public Task Execute(string apiKey, string subject, string message, string email)
{
var client = new SendGridClient(apiKey);
var msg = new SendGridMessage()
{
From = new EmailAddress(<email-address>, <name>),
Subject = subject,
PlainTextContent = message,
HtmlContent = message
};
msg.AddTo(new EmailAddress(email));
return client.SendEmailAsync(msg);
}
}
I tried to write the apiKey directly in the code, but same result.
I've already successfully added a user in the database, but without SendGrid.
Make sure that the service and its abstraction is registered with the service collection so that the service provider is aware of how to resolve them
Startup.ConfigureServices
services.AddSingleton<IEmailSender, EmailSender>();
services.Configure<AuthMessageSenderOptions>(Configuration);
Related
I'm trying to learn how to use authentication and SignInAsync via an API. For some reason, my code works only if I call the API from Postman or from Swagger, but not when I call it from code in the front-end.
To test this, I created two small projects: a web razor pages front end and a simple API. The API has the 3 possible POST requests (TestLogin, TestLogout and TestVerify) as show below:
[Route("api/[controller]")]
[ApiController]
[Authorize()]
public class LoginTestController : ControllerBase
{
[AllowAnonymous]
[HttpPost("TestLogin")]
public async Task<ActionResult> TestLoginAction([FromBody] LoginData loginData)
{
var claims = new List<Claim>
{
new Claim(ClaimTypes.Name, loginData.UserName),
new Claim(ClaimTypes.Role, loginData.UserRole)
};
var identity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);
var principal = new ClaimsPrincipal(identity);
if (principal is not null && principal.Identity is not null)
{
await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, principal);
return Ok("Successfully logged in.");
}
else
{
return BadRequest("Login failed.");
}
}
[HttpPost("TestVerify")]
public ActionResult TestVerify()
{
var user = User.Identity;
if (user is not null && user.IsAuthenticated)
return Ok($"Logged in user: " + user.Name);
else
return Ok("No user logged in. Please login.");
}
[HttpPost("TestLogout")]
public async Task<ActionResult> TestLogout()
{
await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
return Ok();
}
}
In my Program.cs, I add the cookie authentication
builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie();
and
app.UseAuthentication();
app.UseAuthorization();
In my front-end razor page, I called the APIs this way using this code:
public string Message { get; set; }
public async Task OnPostLogin()
{
var client = new HttpClient();
var userLoginInfo = new LoginData
{
UserName = "TestUser",
UserRole = "TestRole"
};
string api = "https://localhost:7049/api/LoginTest/TestLogin";
var response = await client.PostAsJsonAsync(api, userLoginInfo);
Message = await response.Content.ReadAsStringAsync();
}
public async Task OnPostLogout()
{
var client = new HttpClient();
var userLoginInfo = new LoginData();
string api = "https://localhost:7049/api/LoginTest/TestLogout";
var response = await client.PostAsJsonAsync(api, userLoginInfo);
Message = await response.Content.ReadAsStringAsync();
}
public async Task OnPostValidate()
{
var client = new HttpClient();
var userLoginInfo = new LoginData();
string api = "https://localhost:7049/api/LoginTest/TestVerify";
var response = await client.PostAsJsonAsync(api, userLoginInfo);
int statusCode = ((int)response.StatusCode);
if (response.IsSuccessStatusCode)
Message = await response.Content.ReadAsStringAsync();
else
Message = $"Failed with status code: ({response.StatusCode}/{statusCode})";
}
If I try via the front-end, I click on the Login, and it says it is successfully, but when I click on the Validate, it returns a 404 presumably because it doesn't think it is authorized even though I did a log in (or if I authorize anonymous for the validate, it says no one is logged in). If I do the same calls via Swagger, the login works the same, but the validate remembers the login, which is the desired behavior. Why is the login not being remembered when I call the API via the front-end, but it is when I do it via Swagger? And how can I fix this?
Thanks.
I am trying to find out why the account verification email is not being sent from my app when creating a new user account.
I was able to send two emails on one of my first attempts. The emails ended up in my spam filter, but they did get through. I do not know what may have changed since then.
Checking the SendGrid control panel, I can confirm that two emails was sent the first day I tried it, but none of my later attempts have generated any emails.
This article suggests to set a breakpoint on EmailSender.Execute. I have done that, and found that it is indeed not being hit. How do I debug further?
The SendGrid account information is specified in secrets.json:
{
"SendGridUser": "{account name}",
"SendGridKey": "{api-key}"
}
A service is configured in Startup.cs:
services.AddTransient<IEmailSender, EmailSender>();
services.Configure<AuthMessageSenderOptions>(Configuration);
AuthMessageSenderOptions:
public class AuthMessageSenderOptions
{
public string SendGridUser { get; set; }
public string SendGridKey { get; set; }
}
The EmailSender service:
public class EmailSender : IEmailSender
{
public EmailSender(IOptions<AuthMessageSenderOptions> optionsAccessor)
{
Options = optionsAccessor.Value;
}
public AuthMessageSenderOptions Options { get; } //set only via Secret Manager
public Task SendEmailAsync(string email, string subject, string message)
{
return Execute(Options.SendGridKey, subject, message, email);
}
public Task Execute(string apiKey, string subject, string message, string email)
{
var client = new SendGridClient(apiKey);
var msg = new SendGridMessage()
{
From = new EmailAddress("my#email.com", Options.SendGridUser),
Subject = subject,
PlainTextContent = message,
HtmlContent = message
};
msg.AddTo(new EmailAddress(email));
// Disable click tracking.
// See https://sendgrid.com/docs/User_Guide/Settings/tracking.html
msg.SetClickTracking(false, false);
return client.SendEmailAsync(msg);
}
}
A user is created like this:
// user is an ApplicationUser-object
IdentityResult result = await userManager.CreateAsync(auto.Map<ApplicationUser>(user), "P&55w0rd");
if (result.Succeeded)
{
ApplicationUser u = await userManager.FindByNameAsync(user.UserName);
if (u != null)
{
// newUser.RN is the role name to be assigned to the new user
await userManager.AddToRoleAsync(u, newUser.RN);
}
return RedirectToAction("Details", new { id = user.Id });
}
else
{
foreach (var error in result.Errors)
{
ModelState.AddModelError("", error.Description);
}
}
A new user is created, added to the role and we are redirected to Users/Details/{id}, but the account verification email is not sent.
Sendgrid does not send the emails if the sender's email is not authenticated properly.
Hope this will be helpful for someone.
Doc: https://app.sendgrid.com/settings/sender_auth
I'd be surprised if Identity does it by default, only by setting up the EmailSender. You do not seem to provide any logic for the confirmation and nowhere to call the EmailSender.
You need to inject the IEmailSender as a service in your controller where you are creating the user, and add the logic to generate a confirmation token and actually send the email.
I'd expect something in the lines of:
var token = await userManager.GenerateEmailConfirmationTokenAsync(user);
var confirmationLink = Url.Action(nameof(ConfirmEmail), "Account",
new { token , email = user.Email },
Request.Scheme);
await _emailSender.SendEmailAsync(user.Email, "Confirmation email link", confirmationLink);
Of course you could further look how to make your email prettier, but that's the core of it.
Also, just to make sure that you have the whole picture, Identity does not also provide an email implementation by default, you also have to set it up: https://learn.microsoft.com/en-us/aspnet/core/security/authentication/accconfirm?view=aspnetcore-3.1&tabs=visual-studio#install-sendgrid
In startup.cs I have:
services.AddTransient<IEmailSender, EmailSender>();
services.Configure<AuthMessageSenderOptions>(Configuration);
In EmailSender class I have:
public Task SendEmailAsync(string email, string subject, string message)
{
return Execute( subject, message, email);
}
public Task Execute( string subject, string message, string email)
{
var apiKey = Environment.GetEnvironmentVariable("KEY");
//var client = new SendGridClient(apiKey);
var client = new SendGridClient(apiKey);
var msg = new SendGridMessage()
{
From = new EmailAddress("Joe#contoso.com", Options.SendGridUser),
Subject = subject,
PlainTextContent = message,
HtmlContent = message
};
msg.AddTo(new EmailAddress(email));
// Disable click tracking.
// See https://sendgrid.com/docs/User_Guide/Settings/tracking.html
msg.SetClickTracking(false, false);
return client.SendEmailAsync(msg);
}
I am getting this page instead of sending mail.As refereed to Microsoft documentation I do not have /Identity/Account/Manage/PersonalData page also.
Haven't used SendGrid before, but based on the URL I'd say you need to complete your user registration for your account poudyalrupak2#gmail.com before being able to send e-mails: https://sendgrid.com/docs/ui/account-and-settings/verifying-your-account/
Also, as a small improvement on your code, you should make any method which calls async methods async
public async Task Execute( string subject, string message, string email)
, and then await on any async calls:
return await client.SendEmailAsync(msg);
MVC Assembly: Microsoft.AspNet.Identity.UserManager
CORE Assembly: Microsoft.Extensions.Identity.Core
I am migrating from MVC to .NET CORE 2.2. I have created a MyManager class that inherits from UserManager. The UserManager class in MVC has the following properties:
public IIdentityMessageService EmailService { get; set; }
public IIdentityMessageService SmsService { get; set; }
In .NET CORE 2.2., the UserManager does not have these properties. I am guessing that the MVC UserManager uses those properties to automatically send messages. It looks like it uses the EmailService to automatically send the message that verifies the email address. However, this behavior does not appear to happen in .NET CORE. So, it looks like the UserManager no longer handles this.
The question is this: How do you verify a user's email address?
Is this being handled in a different Microsoft assembly or has Microsoft gotten out of that business and farmed it off to a 3rd-party assembly? Or, is there something else?
this is how i send email verification using Sendgrid in asp.net core.
step 1 : Create a class called EmailSender that implements IEmailSender Interface(need to include using Microsoft.AspNetCore.Identity.UI.Services; for IEmailSender) :
public class EmailSender : IEmailSender
{
public async Task SendEmailAsync(string email, string subject, string htmlMessage)
{
var apiKey = "send grid api key";
//it looks like this :
// var apiKey = "SG.2MpCzyTvIQ.WhHMg6-VBjuqbn9k-8P9m6X9cHM73fk2fzAT5sA4zKc";
var client = new SendGridClient(apiKey);
var from = new EmailAddress($"noreply#domaimName.com", "Email title");
var to = new EmailAddress(email, email);
var plainTextContent = htmlMessage;
var htmlContent = htmlMessage;
var msg = MailHelper.CreateSingleEmail(from, to, subject, plainTextContent, htmlContent);
try
{
var response = await client.SendEmailAsync(msg);
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
//return Task.CompletedTask;
}
}
Step 2 : You need to add it as a service using Dependency Injection inside the Startup.cs file
services.AddTransient<IEmailSender, EmailSender>();
Step 3 : Inject it into the constructor of the class you want to use it in.
[AllowAnonymous]
public class RegisterModel : PageModel
{
private readonly IEmailSender _emailSender;
public RegisterModel(IEmailSender emailSender)
{
_emailSender = emailSender;
}
public async Task<IActionResult> OnPostAsync(string returnUrl = null)
{
returnUrl = returnUrl ?? Url.Content("~/");
if (ModelState.IsValid)
{
var user = new ApplicationUser
{
UserName = Input.Email,
Email = Input.Email,
DateCreated = DateTime.Now
};
var result = await _userManager.CreateAsync(user, Input.Password);
if (result.Succeeded)
{
_logger.LogInformation("User created a new account with password.");
returnUrl = Url.Action("ConfirmEmail", "Home");
var code = await _userManager.GenerateEmailConfirmationTokenAsync(user);
await _userManager.AddToRoleAsync(user,"ROleName");
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>.");
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();
}
and this should send your email. it works for me and it should for you too.
I don't know how to invoke the SendEmailAsync function
Register Post page
public async Task<IActionResult> Register(RegisterViewModel model, string returnUrl = null)
{
ViewData["ReturnUrl"] = returnUrl;
if (ModelState.IsValid)
{
var user = new ApplicationUser { UserName = model.Email, Email = model.Email };
var result = await _userManager.CreateAsync(user, model.Password);
if (result.Succeeded)
{
// For more information on how to enable account confirmation and password reset please visit http://go.microsoft.com/fwlink/?LinkID=532713
// Send an email with this link
var code = await _userManager.GenerateEmailConfirmationTokenAsync(user);
var callbackUrl = Url.Action("ConfirmEmail", "Account", new { userId = user.Id, code = code }, protocol: HttpContext.Request.Scheme);
await _emailSender.SendEmailAsync(model.Email, "Confirm your account",
$"Please confirm your account by clicking this link: <a href='{callbackUrl}'>link</a>");
await _signInManager.SignInAsync(user, isPersistent: false);
_logger.LogInformation(3, "User created a new account with password.");
return RedirectToLocal(returnUrl);
}
AddErrors(result);
}
// If we got this far, something failed, redisplay form
return View(model);
}
Email configuration Page
// this code was already there
public interface IEmailSender
{
Task SendEmailAsync(string email, string subject, string message);
}
//I got this code from some other site. I don't know how to use this together.
public class sendMail : IEmailSender // this line is written by me
{
public async Task SendEmailAsync(string email, string subject, string message)
{
var emailMessage = new MimeMessage();
emailMessage.From.Add(new MailboxAddress("Joe Bloggs", "jbloggs#example.com"));
emailMessage.To.Add(new MailboxAddress("", email));
emailMessage.Subject = subject;
emailMessage.Body = new TextPart("plain") { Text = message };
using (var client = new SmtpClient())
{
client.LocalDomain = "some.domain.com";
await client.ConnectAsync("smtp.relay.uri", 25, SecureSocketOptions.None).ConfigureAwait(false);
await client.SendAsync(emailMessage).ConfigureAwait(false);
await client.DisconnectAsync(true).ConfigureAwait(false);
}
}
}
how the write the class definition to send email.
Break point does not reach here, I think there is some problem with my implementation.
in Startup make sure you change this:
services.AddTransient<IEmailSender, AuthMessageSender>();
to
services.AddTransient<IEmailSender, sendMail>();
so that your implementaiton gets injected