Core3/React confirmation email not sent - .net-core

This question applies to a core3/react project with an external identity provider, created as follows.
dotnet new react --auth Individual --use-local-db --output conf
and modified to support an external identity provider. The package is added
dotnet add package Microsoft.AspNetCore.Authentication.MicrosoftAccount
and startup is modified
services.AddAuthentication()
.AddMicrosoftAccount(options =>
{
options.ClientId = Configuration["Authentication:Microsoft:ClientId"];
options.ClientSecret = Configuration["Authentication:Microsoft:ClientSecret"];
options.CallbackPath = "/signin-microsoft";
})
After following the instructions provided by Microsoft I tested my work by registering as a user. No errors were thrown but the promised confirmation email never arrived.
Following the troubleshooting advice at the end of the instructions I set a breakpoint at the start of the SendEmailAsync method of my implementation of IEmailSender and repeated the exercise. The breakpoint is not hit.
If I manually confirm the account by updating the database,
I am able to log in.
The Forgot Password link takes me to a password recovery page and using this hits my breakpoint and successfully sends a password reset email with a link that works.
Clearly my implementation of IEmailSender works and is correctly registered. It's not exactly the same as the sample code because I have my own Exchange server and didn't use SendGrid, but it sent an email successfully for password reset and I can repeat this any number of times without a hitch.
Against the slim possibility that it is somehow the cause of the problem, here's my implementation
public class SmtpEmailSender : IEmailSender
{
public SmtpEmailSender(IOptions<SmtpOptions> options)
{
this.smtpOptions = options.Value;
}
private SmtpOptions smtpOptions { get; }
public Task SendEmailAsync(string email, string subject, string htmlMessage)
{
var smtp = new SmtpClient();
if (!smtpOptions.ValidateCertificate)
{
smtp.ServerCertificateValidationCallback = (s, c, h, e) => true;
}
smtp.Connect(smtpOptions.Host, smtpOptions.Port, SecureSocketOptions.Auto);
if (smtpOptions.Authenticate)
{
smtp.Authenticate(smtpOptions.Username, smtpOptions.Password);
}
var message = new MimeMessage()
{
Subject = subject,
Body = new BodyBuilder() { HtmlBody = htmlMessage }.ToMessageBody()
};
message.From.Add(new MailboxAddress(smtpOptions.Sender));
message.To.Add(new MailboxAddress(email));
return smtp.SendAsync(FormatOptions.Default, message).ContinueWith(antecedent =>
{
smtp.Disconnect(true);
smtp.Dispose();
});
}
}
Registration in startup.cs looks like this.
services.AddTransient<IEmailSender, SmtpEmailSender>();
services.Configure<SmtpOptions>(Configuration.GetSection("SmtpOptions"));
SmptOptions is just settings hauled out of appsettings.json and injected into the ctor. Obviously that aspect works or password reset emails wouldn't work.
There can't be anything wrong with the registration because the app stops producing a message about needing to read and follow the account confirmation instructions I linked.
To see whether the problem was caused by some inadvertent side-effect of my code I created an instrumented stub of IEmailSender
public class DummyEmailSender : IEmailSender
{
private readonly ILogger logger;
public DummyEmailSender(ILogger<DummyEmailSender> logger)
{
this.logger = logger;
}
public Task SendEmailAsync(string email, string subject, string htmlMessage)
{
logger.LogInformation($"SEND EMAIL\r\nemail={email} \r\nsubject={subject}\r\nhtmlMessage={htmlMessage}\r\n{new StackTrace().ToString().Substring(0,500)}");
return Task.CompletedTask;
}
}
I also updated service registration to match.
This is the simplest possible instrumented stub, and the observed behaviour is the same, it's invoked when the Forgot Password form is submitted and is not invoked when the Confirm Registration form is submitted.
Has anyone ever got the horrible thing to work? How?
Immediately before the failure, this URL https://wone.pdconsec.net/Identity/Account/ExternalLogin?returnUrl=%2Fauthentication%2Flogin&handler=Callback looks like this
Inspecting the page we find the Register button posts a form to /Identity/Account/ExternalLogin?returnUrl=%2Fauthentication%2Flogin&handler=Confirmation
The code for this is available from the dotnet repository.
After cloning the repo https://github.com/dotnet/aspnetcore.git I read the build instructions and succeeded in building dotnet 5 preview. Then I ran clean before switching to the tagged branch release/3.1 to build debugging packages for core3.1 but this fails because the tagged branch brings into play a version of msbuild that's just slightly too old and the remedy suggested by the error message doesn't seem to work. Since my grip on PowerShell is weak (the build script is PowerShell) I am reduced to code inspection. The pertinent code looks like this.
public override async Task<IActionResult> OnPostConfirmationAsync(string returnUrl = null)
{
returnUrl = returnUrl ?? Url.Content("~/");
// Get the information about the user from the external login provider
var info = await _signInManager.GetExternalLoginInfoAsync();
if (info == null)
{
ErrorMessage = "Error loading external login information during confirmation.";
return RedirectToPage("./Login", new { ReturnUrl = returnUrl });
}
if (ModelState.IsValid)
{
var user = CreateUser();
await _userStore.SetUserNameAsync(user, Input.Email, CancellationToken.None);
await _emailStore.SetEmailAsync(user, Input.Email, CancellationToken.None);
var result = await _userManager.CreateAsync(user);
if (result.Succeeded)
{
result = await _userManager.AddLoginAsync(user, info);
if (result.Succeeded)
{
_logger.LogInformation("User created an account using {Name} provider.", info.LoginProvider);
var userId = await _userManager.GetUserIdAsync(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(Input.Email, "Confirm your email",
$"Please confirm your account by <a href='{HtmlEncoder.Default.Encode(callbackUrl)}'>clicking here</a>.");
// If account confirmation is required, we need to show the link if we don't have a real email sender
if (_userManager.Options.SignIn.RequireConfirmedAccount)
{
return RedirectToPage("./RegisterConfirmation", new { Email = Input.Email });
}
await _signInManager.SignInAsync(user, isPersistent: false);
return LocalRedirect(returnUrl);
}
}
foreach (var error in result.Errors)
{
ModelState.AddModelError(string.Empty, error.Description);
}
}
ProviderDisplayName = info.ProviderDisplayName;
ReturnUrl = returnUrl;
return Page();
}
It looks like it ought to work. What do we know?
No unhandled errors are thrown, it makes it through to RegisterConfirmation which puts up a message about the email that never comes.
CreateUser is invoked and succeeds. We know this because the user is created in the database. So it definitely gets past there, which implies that ModelState isn't null and .IsValid is true.
IEmailSender.SendEmailAsync is not actually invoked, despite the code above.
If result.Succeeded is true there should be a log message saying something like "User created an account using Microsoft Account provider"
It redirects to https://localhost:5001/Identity/Account/RegisterConfirmation?Email=accountname#outlook.com
I'm seeing log messages for most things. Trying to register a second time after the first pass creates the user but fails to send the email, a warning about a DuplicateUserName appears on the console and in the event log. Setting the confirmation directly in the database we are able to log in and then interactively delete the account, and logs appear for these activities.
But no logs appear for confirmation. What's really doing my head in is the fact that it then redirects to https://localhost:5001/Identity/Account/RegisterConfirmation?Email=accountname#outlook.com
That's crazy. In order to get to there, userManager.AddLoginAsync() must return true and the very next line in that case is a write to the logger about creating the user account.
This makes no sense.

You should send confirmation email yourself, it doesn't send automatically.
After registering your user:
string token = await userManager.GenerateEmailConfirmationTokenAsync(user);
string urltoken = Base64UrlEncoder.Encode(token);
string link = string.Format(emailOptions.ConfirmationUrl, user.Id, urltoken);
string body = $"<a href='{link}'>confirm</a>";
await emailSender.SendEmailAsync(user.Email, "confirmation", body);

I created a whole new project and worked the exercise. It works perfectly.
What's the difference? The failing version was added to an existing project that has been jerked back and forth between 3.0 and 3.1 several times in the course of troubleshooting CICD issues. Clearly it's damaged in some unobvious way and this is a non-issue.
The only reason I haven't deleted the whole question is others may fall down this hole.

Related

Identity Server: Access tokens/items set in AuthorizationProeperties in ExternalLoginCallback on the client

Question
I have an identity server implementation that is being used by a number of applications in test and production. I am currently working on a new feature, where the client application using the identity server can perform Azure service management REST api calls. For this, it needs a token. I can generate this token, store it and even access it in the AccountController in the identity server.
My issue is figuring out how to send this to the client. I don't think this token belongs in the claims for the user. So I tried to add it as part of AuthenticationProperties as a token, but I cannot seem to access it in the client. Should I store it in a session like this SO user did link? There is one answer to this question, but that does not seem right (I even tried it out of desperation!)
Relevant sections of code
Generate the token
var resource = "https://management.azure.com/";
app.UseOpenIdConnectAuthentication(new OpenIdConnectOptions
{
Events = new OpenIdConnectEvents
{
OnAuthorizationCodeReceived = async context =>
{
// Acquire the token for the resource and save it
}
}
}
Restore it in AccountController
public async Task<IActionResult> ExternalLoginCallback(string returnUrl)
{
string resource = "https://management.azure.com/";
// snip
result = await authContext.AcquireTokenSilentAsync(resource, credential, new UserIdentifier(userObjectID, UserIdentifierType.UniqueId));
// snip
AuthenticationProperties props = null;
var tokens = new List<AuthenticationToken>();
var id_token = info.Properties.GetTokenValue("id_token");
if (id_token != null)
{
tokens.Add(new AuthenticationToken { Name = "id_token", Value = id_token });
}
if (result != null)
{
tokens.Add(new AuthenticationToken { Name = "management_token", Value = result.AccessToken });
}
if (tokens.Any())
{
props = new AuthenticationProperties();
props.StoreTokens(tokens);
}
// snip
// Can I access these "props" on the client? I even tried adding it to `Items`, no luck.
await HttpContext.Authentication.SignInAsync(user.UserId, user.DisplayName, provider, props, additionalClaims.ToArray());
}
So, my question, is this the right way go about it? If so, how do I access the authentication properties set? Or should I try saving this in the Session? If so, how do I store it in the client's session?
Any pointers would help. Thank you!
Just wanted to post an answer so that people wanting the same can benefit.
A token cache can be implemented to achieve this. This repository explains how.
Pay special attention to the AdalDistributedTokenCache linked here

How to Release a change that renames an User Role name

We're working on changes to an ASP.NET MVC app.
We're using Owin and OAuth2 to manage User permissions, but are managing the User DB object ourselves.
We have these on App Startup:
app.UseKentorOwinCookieSaver();
app.UseCookieAuthentication(GetCookieAuthenticationOptions(AuthenticationType))
.UseOpenIdConnectAuthentication(GetOpenIdConnectOptions(AuthenticationType));
And we manually assign Claims to users when they log in Role is an enum:
claimsIdentity.AddClaim(new Claim(ClaimTypes.Role, user.Role.ToString()));
If more detail is needed, the auth code is included at the end.
All of this is has been working fine, but we need to rename a role.
The code rename is trivial, and it all works just fine when I log in after the role is renamed. But if I'm already logged in, when the code changes, then my old role Claim string is still in my Auth Cookie, and is no longer recognised by the Auth code.
Becuase I'm already logged in, it doesn't take me to the LogIn page - it just shows me the "Forbidden" error page (As though I'd entered a link to a page I shouldn't have visited)
And because our Auth works by checking whether you have "Role 'x' or any Role greater than 'x'", thus we get Forbidden on every page (because now the user doesn't have any Role and thus fails every Auth test, because their Role isn't recognised as passing any test.
As a result the user has no way to log out.
As a developer, I can wipe my browser cookies and log in from scratch (at which point it works just fine) but a normal user (probably?) won't be able to do that.
My first thought was do somehting like this: http://www.britishdeveloper.co.uk/2010/09/force-client-refresh-browser-cache.html, to all users to log out and get them to log in again, once after the release.
Unfortunately, since EVERY page will fail, I've got nowhere to put that code that will run for the relevant users :(
I could hack around with the Authentication Code so that it knows about the old Roles and grants that Claim permission, but that seem hideous.
Another option would be to modify the Authorisation code so that it logged users out if they don't have any recognised Roles, but that doesn't really feel right either, for some reason I can't put my finger on.
Any suggestions or opinions about the right way to release such a change?
=-=-=-=-=-=-=-=-=-=
Auth code:
private const string AuthenticationType = "FrontEnd" + CookieAuthenticationDefaults.AuthenticationType;
private const string IdTokenClaimName = "id_token";
public void Configuration(IAppBuilder app)
{
app.UseKentorOwinCookieSaver();
app.UseCookieAuthentication(GetCookieAuthenticationOptions(AuthenticationType))
.UseOpenIdConnectAuthentication(GetOpenIdConnectOptions(AuthenticationType));
}
private static CookieAuthenticationOptions GetCookieAuthenticationOptions(string authenticationType)
{
return new CookieAuthenticationOptions
{
AuthenticationType = authenticationType,
};
}
private OpenIdConnectAuthenticationOptions GetOpenIdConnectOptions(string authenticationType)
{
return new OpenIdConnectAuthenticationOptions
{
Authority = AuthenticationConstants.AuthenticationAuthority,
ClientId = AuthenticationConstants.ClientId,
RedirectUri = AuthenticationConstants.ClientRedirectUrl,
ResponseType = "id_token",
Scope = "openid profile email",
SignInAsAuthenticationType = authenticationType,
UseTokenLifetime = false,
Notifications = new OpenIdConnectAuthenticationNotifications
{
SecurityTokenValidated = n => Task.Run(() => AuthorizeIfUserExists(n)),
RedirectToIdentityProvider = n => Task.Run(() => SendIdTokenToLogout(n))
}
};
}
private static void SendIdTokenToLogout(RedirectToIdentityProviderNotification<OpenIdConnectMessage, OpenIdConnectAuthenticationOptions> n)
{
if (n.ProtocolMessage.RequestType == OpenIdConnectRequestType.LogoutRequest)
{
var idTokenHint = n.OwinContext.Authentication.User.FindFirst(IdTokenClaimName).Value;
n.ProtocolMessage.IdTokenHint = idTokenHint;
}
}
private void AuthorizeIfUserExists(SecurityTokenValidatedNotification<OpenIdConnectMessage, OpenIdConnectAuthenticationOptions> authContext)
{
var identity = authContext.AuthenticationTicket.Identity;
var userIdClaim = GetUserIdClaim(identity);
var emailClaim = GetEmailClaim(identity);
var claimsIdentity = new ClaimsIdentity(
identity.AuthenticationType,
ClaimTypes.Name,
ClaimTypes.Role);
claimsIdentity.AddClaim(new Claim(IdTokenClaimName, authContext.ProtocolMessage.IdToken));
claimsIdentity.AddClaim(userIdClaim);
claimsIdentity.AddClaim(emailClaim);
using (var context = new DbDataContext())
{
var user = GetAndInitializeUserIfNecessary(context, userIdClaim.Value, emailClaim.Value);
// We add role and name claims to all successful logins that are also registered in our database.
if (user != null && !user.IsSoftDeleted)
{
claimsIdentity.AddClaim(new Claim(ClaimTypes.Role, user.Role.ToString()));
claimsIdentity.AddClaim(new Claim(ClaimTypes.Name, String.Format("{0} {1}", user.FirstName, user.Surname)));
}
}
authContext.AuthenticationTicket = new AuthenticationTicket(
claimsIdentity,
authContext.AuthenticationTicket.Properties);
}
I could hack around with the Authentication Code so that it knows about the old Roles and grants that Claim permission, but that seem hideous.
That seems best to me.
You have made a change which breaks backwards compatibility for users with active sessions. The usual approach for zero-downtime in that general case is to release code which supports both old and new clients, until you are sure that there are no old clients remaining, then delete the legacy code.

When calling AcquireTokenByRefreshToken on the AuthenticationContext instance with Microsoft.IdentityModel.Clients.ActiveDirectory?

I am developing a multi-tenant application registered on my Azure AD that consumes Office 365 apis, Graph API etc.
I followed this Microsoft sample to build my work which uses ADAL .NET library and OpenIdConnect: Microsoft.IdentityModel.Clients.ActiveDirectory, Version=2.19.0.0
In ADAL.NET, we use an AuthenticationContext instance with a custom inherited class for the TokenCache (see code the sample code here).
For each request to the authorized resources, depending on the API, we invoke one of these methods (see code below) to get the auth_token that will be put in the request Bearer parameter. Is it the correct way to do it?
We never make use of the method AcquireTokenByRefreshTokenAsync, does it mean that our application never uses the refresh_token? Does it mean that our user will have to relog after one hour? Should we implement a kind of refreshing procedure with AcquireTokenByRefreshTokenAsync in the catch statement? Can it be made without prompting anything to the end-user?
REMARK: I posted a question regarding OpenIdConnect authentication ticket lifetime. To me these two questions are unrelated but they may be.
string signInUserId = ClaimsPrincipal.Current.FindFirst(ClaimTypes.NameIdentifier).Value;
string userObjectId = ClaimsPrincipal.Current.FindFirst("http://schemas.microsoft.com/identity/claims/objectidentifier").Value;
string tenantId = ClaimsPrincipal.Current.FindFirst("http://schemas.microsoft.com/identity/claims/tenantid").Value;
public async Task<string> AcquireOutlook365TokenAsync()
{
AuthenticationContext authContext = new AuthenticationContext(string.Format("{0}/{1}", SettingsHelper.AuthorizationUri, tenantId), new ADALTokenCache(signInUserId));
try
{
var result = await authContext.AcquireTokenSilentAsync(#"https://outlook.office365.com/",
new ClientCredential(SettingsHelper.ClientId, SettingsHelper.AppKey),
new UserIdentifier(userObjectId, UserIdentifierType.UniqueId));
return result.AccessToken;
}
catch (AdalException exception)
{
//handle token acquisition failure
if (exception.ErrorCode == AdalError.FailedToAcquireTokenSilently)
{
authContext.TokenCache.Clear();
}
throw new HttpResponseException(new HttpResponseMessage(HttpStatusCode.Unauthorized));
}
}
public async Task<string> AcquireAzureGraphTokenAsync()
{
AuthenticationContext authContext = new AuthenticationContext(string.Format("{0}/{1}", SettingsHelper.AuthorizationUri, tenantId), new ADALTokenCache(signInUserId));
try
{
var result = await authContext.AcquireTokenSilentAsync(#"https://graph.windows.net/",
new ClientCredential(SettingsHelper.ClientId, SettingsHelper.AppKey),
new UserIdentifier(userObjectId, UserIdentifierType.UniqueId));
return result.AccessToken;
}
catch (AdalException exception)
{
//Same as other method
}
}
ADAL uses the stored refresh tokens automatically and transparently, you aren't required to perform any explicit action. AcquireTOkenByRefreshToken is in the ADAL surface for legacy reasons, and has been removed from version 3.x. More background at http://www.cloudidentity.com/blog/2015/08/13/adal-3-didnt-return-refresh-tokens-for-5-months-and-nobody-noticed/

Office 365 Rest Api Having issues getting access token

So far i have this.
public static async Task<OutlookServicesClient> CreateOutlookClientAsync(string capability)
{
try
{
string authority = CommonAuthority;
// Create an AuthenticationContext using this authority.
_authenticationContext = new AuthenticationContext(authority);
//See the Discovery Service Sample (https://github.com/OfficeDev/Office365-Discovery-Service-Sample)
//for an approach that improves performance by storing the discovery service information in a cache.
DiscoveryClient discoveryClient = new DiscoveryClient(
async () => await GetTokenHelperAsync(_authenticationContext, DiscoveryResourceId));
// Get the specified capability ("Contacts").
CapabilityDiscoveryResult result =
await discoveryClient.DiscoverCapabilityAsync(capability);
var client = new OutlookServicesClient(
result.ServiceEndpointUri,
async () =>
await GetTokenHelperAsync(_authenticationContext, result.ServiceResourceId));
return client;
}
catch (Exception e)
{
Console.WriteLine(e.Message);
if (_authenticationContext != null && _authenticationContext.TokenCache != null)
_authenticationContext.TokenCache.Clear();
return null;
}
}
}
private static async Task<string> GetTokenHelperAsync(AuthenticationContext context, string resourceId)
{
string accessToken = null;
AuthenticationResult result = null;
string myId = WebConfigurationManager.AppSettings["ida:ClientID"];
string myKey = WebConfigurationManager.AppSettings["ida:Password"];
ClientCredential client = new ClientCredential(myId,myKey);
result = await context.AcquireTokenAsync(resourceId, client);
//result =context.AcquireToken(resourceId, ClientID,_returnUri);
accessToken = result.AccessToken;
return accessToken;
}
When i get to result one of two things happen if i user AcquireTokenAsync i get an error stating Application with identifier XXXX was not found in directory api.office.com otherwise if i run AcquireToken i get the login modal to pop but an error occurs indicating the request must contain client_secret .
I have no idea how to resolve this issue i suspect it may have something to do with the actual app configuration i have tried both creating my own app in Azure AD and using VS Connected Service, Has Anyone Else ran into a similar issues?
Based on the errors you're seeing, there seems to be an issue with how your app is registered. The first error usually happens when the app is not marked as multi-tenant, and you login to the app with a tenant other than the one where the app is registered.
The second error is odd. Client secret is what you're reading out of the ida:Password element and passing in the ClientCredential object.
I just put a .NET tutorial up yesterday that walks through setting this stuff up. Take a look and see if that helps get you unblocked.

LiveAuthClient broken?

It seems very much that the current version of LiveAuthClient is either broken or something in my setup/configuration is. I obtained LiveSDK version 5.4.3499.620 via Package Manager Console.
I'm developing an ASP.NET application and the problem is that the LiveAuthClient-class seems to not have the necessary members/events for authentication so it's basically unusable.
Notice that InitializeAsync is misspelled aswell.
What's wrong?
UPDATE:
I obtained another version of LiveSDK which is for ASP.NET applications but now I get the exception "Could not find key with id 1" everytime I try either InitializeSessionAsync or ExchangeAuthCodeAsync.
https://github.com/liveservices/LiveSDK-for-Windows/issues/3
I don't think this is a proper way to fix the issue but I don't have other options at the moment.
I'm a little late to the party, but since I stumbled across this trying to solve what I assume is the same problem (authenticating users with Live), I'll describe how I got it working.
First, the correct NuGet package for an ASP.NET project is LiveSDKServer.
Next, getting user info is a multi-step process:
Send the user to Live so they can authorize your app to access their data (the extent of which is determined by the "scopes" you specify)
Live redirects back to you with an access code
You then request user information using the access code
This is described fairly well in the Live SDK documentation, but I'll include my very simple working example below to put it all together. Managing tokens, user data, and exceptions is up to you.
public class HomeController : Controller
{
private const string ClientId = "your client id";
private const string ClientSecret = "your client secret";
private const string RedirectUrl = "http://yourdomain.com/home/livecallback";
[HttpGet]
public ActionResult Index()
{
// This is just a page with a link to home/signin
return View();
}
[HttpGet]
public RedirectResult SignIn()
{
// Send the user over to Live so they can authorize your application.
// Specify whatever scopes you need.
var authClient = new LiveAuthClient(ClientId, ClientSecret, RedirectUrl);
var scopes = new [] { "wl.signin", "wl.basic" };
var loginUrl = authClient.GetLoginUrl(scopes);
return Redirect(loginUrl);
}
[HttpGet]
public async Task<ActionResult> LiveCallback(string code)
{
// Get an access token using the authorization code
var authClient = new LiveAuthClient(ClientId, ClientSecret, RedirectUrl);
var exchangeResult = await authClient.ExchangeAuthCodeAsync(HttpContext);
if (exchangeResult.Status == LiveConnectSessionStatus.Connected)
{
var connectClient = new LiveConnectClient(authClient.Session);
var connectResult = await connectClient.GetAsync("me");
if (connectResult != null)
{
dynamic me = connectResult.Result;
ViewBag.Username = me.name; // <-- Access user info
}
}
return View("Index");
}
}

Resources