I am using the scaffolded Identity with .NET 5. I tried the "Forgot Password" link on the login page. This works in that the email is sent with the link to reset the password.
However, clicking on the link in the email leads to an error relating to the base 64 code being malformed. Malformed input: 321 is an invalid input.
The base 64 code is there but there's an issue with it. The base 64 code is generated with this:
var code = await _userManager.GeneratePasswordResetTokenAsync(user);
code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code));
var callbackUrl = Url.Page(
"/Account/ResetPassword",
pageHandler: null,
values: new { area = "Identity", code },
protocol: Request.Scheme);
await _emailSender.SendEmailAsync(
Input.Email,
"Reset Password",
$"Please reset your password by <a href='{HtmlEncoder.Default.Encode(callbackUrl)}'>clicking here</a>.");
It breaks on the ResetPassword.cshtml.cs GET.
if (code == null)
{
return BadRequest("A code must be supplied for password reset.");
}
else
{
Input = new InputModel
{
Code = Encoding.UTF8.GetString(WebEncoders.Base64UrlDecode(code))
};
return Page();
}
Can anyone point me in the right direction to fixing this? I am not sure it's relevant but I am using SendGrid's SMTP server.
UPDATE. I spotted that there is a trailing inverted comma at the end of the code. Removing this fixes the issue.
code = code.Replace("'", "");
Now I just need to find out how it's getting there.
Tried and ruled out the following.
Turned off SendGrid link tracking.
Turned off Outlook safelinks.
The above made no difference.
Then it dawned on me that the email link was not rendering as HTML. i.e. it was showing the link text and not just the "click here".
Then I read that smtp client is now obsolete and MS recommends using MailKit instead.
So I decided to replace my email sender code with the following using Mailkit and the issue went away.
public Task SendEmailAsync(string email, string subject, string htmlMessage)
{
var message = new MimeMessage();
message.From.Add(new MailboxAddress("(Admin)", "test#test.com"));
message.To.Add(new MailboxAddress("Recipient Name", email));
message.Subject = subject;
message.Body = new TextPart("html")
{
Text = htmlMessage
};
string Username = _config["MailSettings:Username"];
string Password = _config["MailSettings:Password"];
var port = Convert.ToInt32(_config["MailSettings:Port"]);
using var client = new SmtpClient();
client.Connect(_config["MailSettings:Host"], port, false);
client.Authenticate(Username, Password);
try
{
client.Send(message);
client.Disconnect(true);
}
catch (Exception e)
{
Console.WriteLine(e);
throw;
}
return Task.CompletedTask;
}
I think this is the part that saved the day:
message.Body = new TextPart("html")
{
Text = htmlMessage
};
Related
Blazor server application
I have a web appliction that is using AzureAd and OpenIdConnect to login to this application.
I am sending mail by using Microsoft graph and I am using the example in Microsoft doc with some changes like this:
#inject Microsoft.Graph.GraphServiceClient GraphServiceClient
#inject MicrosoftIdentityConsentAndConditionalAccessHandler ConsentHandler
var message = new Message
{
Subject = "Meet for lunch?",
Body = new ItemBody
{
ContentType = BodyType.Text,
Content = "The new cafeteria is open."
},
ToRecipients = new List<Recipient>()
{
new Recipient
{
EmailAddress = new EmailAddress
{
Address = "fannyd#contoso.onmicrosoft.com"
}
}
},
CcRecipients = new List<Recipient>()
{
new Recipient
{
EmailAddress = new EmailAddress
{
Address = "danas#contoso.onmicrosoft.com"
}
}
}
};
var saveToSentItems = false;
try
{
await GraphServiceClient.Me
.SendMail(message,saveToSentItems)
.Request()
.PostAsync();
}
catch(Exception ex)
{
ConsentHandler.HandleException(ex);
}
Scenario of the error
This works perfect but just for one time , if I try to send the same email again it doesn't work and I got no error but just redirect me to empty page.
If I logout and login again, then it works fine.
The error that I got in the second time:
Message = "IDW10502: An MsalUiRequiredException was thrown due to a challenge for the user. See https://aka.ms/ms-id-web/ca_incremental-consent. "
MsalUiRequiredException = {"No account or login hint was passed to the AcquireTokenSilent call. "}
My Question
How can fix the code up to send multi emails?. I think that I have a problem with token but I don't know where should I start?.
Thanks
Add the other catch block to retrieve the specific issue if we have any related to permissions or other ServiceExceptions.
try
{
SendMail(); // Sending mail code here.
}
catch (Microsoft.Graph.ServiceException e)
{
// get the error here if we have any.
}
After some search, I think that I have to move the the permission Delegated permissions to Application Permissions like this:
Why should I use application permission?
In my case the user logged in for the first time and clicked on the button then the email will be send, but in the second time the application has to communicate with API graph without interaction from the user, that means without user and this exactly what I need(application permission).
I adjust the code like the following:
Client credentials provider:
The client credential flow enables service applications to run without user interaction. Access is based on the identity of the application. this is from Microsoft doc
private GraphServiceClient CreateGraphServiceClient()
{
// The client credentials flow requires that you request the
// /.default scope, and preconfigure your permissions on the
// app registration in Azure. An administrator must grant consent
// to those permissions beforehand.
var scopes = new[] { "https://graph.microsoft.com/.default" };
// Multi-tenant apps can use "common",
// single-tenant apps must use the tenant ID from the Azure portal
var tenantId = "common";
// Values from app registration
var clientId = "YOUR_CLIENT_ID";
var clientSecret = "YOUR_CLIENT_SECRET";
// using Azure.Identity;
var options = new TokenCredentialOptions
{
AuthorityHost = AzureAuthorityHosts.AzurePublicCloud
};
var clientSecretCredential = new ClientSecretCredential(
tenantId, clientId, clientSecret, options);
return new GraphServiceClient(clientSecretCredential, scopes);
}
Send mail with UserId, you can see the code in Microsoft doc:
puplic SendMyEmail()
{
GraphServiceClient graphClient = CreateGraphServiceClient;
var message = new Message
{
Subject = "Meet for lunch?",
Body = new ItemBody
{
ContentType = BodyType.Text,
Content = "The new cafeteria is open."
},
ToRecipients = new List<Recipient>()
{
new Recipient
{
EmailAddress = new EmailAddress
{
Address = "fannyd#contoso.onmicrosoft.com"
}
}
},
CcRecipients = new List<Recipient>()
{
new Recipient
{
EmailAddress = new EmailAddress
{
Address = "danas#contoso.onmicrosoft.com"
}
}
}
};
var saveToSentItems = false;
//See GetUserId down
string userId = await GetUserId();
await graphClient.Users[UserId]
.SendMail(message,saveToSentItems)
.Request()
.PostAsync();
}
}
UserId:
To get user Id you need AuthenticationStateProvider, this has to inject in the service of your application and then add to the constructor of your class, then you can use it.
puplic class MyClass
{
private readonly MicrosoftIdentityConsentAndConditionalAccessHandler ConsentHandler;
private readonly AuthenticationStateProvider authenticationState;
puplic MyClass(
MicrosoftIdentityConsentAndConditionalAccessHandler ConsentHandler,
AuthenticationStateProvider authenticationState)
{
this.authenticationState = authenticationState;
this.ConsentHandler = ConsentHandler;
}
public async Task<string> GetUserId()
{
var authSate = await authenticationState.GetAuthenticationStateAsync();
return authSate.User.FindFirstValue("http://schemas.microsoft.com/identity/claims/objectidentifier");
}
//Here your
private GraphServiceClient CreateGraphServiceClient() { ...}
puplic SendMyEmail() {....}
}
I need to send an email to users from an ASP.NET Core 2 application, following some business rules. However, I need to ensure that the account the email is being sent to actually exists (for some reason, it may be that the account stopped being valid). The customer is using Azure Active Directory, so I need to query AAD somehow so it lets me know whether the account exists or not.
So far I have been looking for Microsoft Graph as a way to do this, however every example I have seen so far requires prior authentication and use a delegate authentication mechanism. I don't want my users having to authenticate nor to prompt the authentication screen.
Given this situation, what would you recommend using? If you can also point me to an example, that would be great. Thanks!
You don't really need to throw/catch exception for every invalid user as you're doing in current code. I have nothing against exception handling in general for other reasons but to see if the user exists or not you can try using Filter.
So your graph query could look like -
https://graph.microsoft.com/v1.0/users?$filter=startswith(userPrincipalName,'someuser#mytenant.onmicrosoft.com')
I have shown startswith here becuase eq didn't work for me in a quick trial. Although I would recommend two things:
Go through Microsoft documentation on Filters here and see what works best for your requirements - Use query parameters to customize responses with Microsoft Graph
Play a little bit with different queries in Microsoft Graph Explorer it's very simple and easy to use.
Here is a modified version for your code.
Note that I'm checking for the collection count to be > 0 and not checking for it to be null, as even in case user is not found the UsersCollectionPage was not null for my test run.
using Microsoft.Identity.Client;
using Microsoft.Graph.Auth;
using Microsoft.Graph;
...
private async Task<bool> ValidateAccounts(string accounts) {
var confidentialClientApplication = ConfidentialClientApplicationBuilder
.Create("clientId here")
.WithTenantId("tokenId here")
.WithClientSecret("secret here")
.Build();
var authProvider = new ClientCredentialProvider(confidentialClientApplication);
var graphClient = new GraphServiceClient(authProvider);
var valid = true;
try {
foreach (var account in accounts.Split(';')) {
var user = await graphClient.Users.Request().Filter("startswith(userPrincipalName, '" + account + "')").GetAsync();
if (user.Count <= 0) {
valid = false;
break;
}
}
} catch (ServiceException ex) {
valid = false;
}
return valid;
}
On a side note, I'm not not sure of your requirements but you could probably get creative by combining multiple user names in single query and then checking for result counts or other propertes. You could use or between multiple criteria or probably use any operator. I haven't really tried this out though.
Finally I came up with something workable. It's not nice, and it uses preview software. First, install Microsoft.Graph and Microsoft.Identity.Client packages. Then install Microsoft.Graph.Auth, which at the time of this writing, is in preview (v1.0.0-preview.1) so you'll need to tick "include prerelease" checkbox in nuget manager.
Then in your AAD, you need to get the ClientId, TenantId and SecretId. In my case, my app was already using AAD authentication so I already had ClientId and TenantId in my appsettings.json file. I only needed to create a new SecretId (in the Certificate & secrets section of my app registration). Then I needed to add permissions (in the API permissions section of my app registration) to include Microsoft.Graph with at least User.Read.All permission.
using Microsoft.Identity.Client;
using Microsoft.Graph.Auth;
using Microsoft.Graph;
...
private async Task<bool> ValidateAccounts(string accounts) {
var confidentialClientApplication = ConfidentialClientApplicationBuilder
.Create("clientId here")
.WithTenantId("tokenId here")
.WithClientSecret("secret here")
.Build();
var authProvider = new ClientCredentialProvider(confidentialClientApplication);
var graphClient = new GraphServiceClient(authProvider);
var valid = true;
try {
foreach (var account in accounts.Split(';')) {
var user = await graphClient.Users[account]
.Request()
.GetAsync();
if (user == null) {
valid = false;
break;
}
}
} catch (ServiceException ex) {
valid = false;
}
return valid;
}
Here, the function takes a semicolon-separated string for each account. The GetAsync method will throw a ServiceException if the user does not exist. I don't like that, but couldn't find another way. So that's about it. Hope this helps someone else, and hope someone could come up with a better solution eventually.
Import following namespaces (You needs to install relevant packages using nuget):
using Microsoft.Graph;
using Microsoft.IdentityModel.Clients.ActiveDirectory;
Set your Azure AD app values:
private string _tenant => "your_tenant_id";
private string _appId => "your_ad_app_client_id";
private string _appSecret => "your_app_client_secret";
Create Graph Service Client using this:
public static GraphServiceClient CreateGraphServiceClient()
{
var clientCredential = new ClientCredential(_appId, _appSecret);
var authenticationContext = new AuthenticationContext("https://login.microsoftonline.com/{_tenant}");
var authenticationResult = authenticationContext.AcquireTokenAsync("https://graph.microsoft.com", clientCredential).Result;
var delegateAuthProvider = new DelegateAuthenticationProvider((requestMessage) =>
{
requestMessage.Headers.Authorization = new AuthenticationHeaderValue("bearer", authenticationResult.AccessToken);
return Task.FromResult(0);
});
return new GraphServiceClient(delegateAuthProvider);
}
var graphServiceClient = GraphServiceClientHelper.CreateGraphServiceClient();
Then call graph api and filter users by Email Address as follows:
var user = await graphServiceClient.Users.Request().Filter("mail eq '" + UserEmailAddress + "'").GetAsync();
if (user.Count == 0) {
//user not exist
}
Below code worked for me.
using Microsoft.Graph;
using Microsoft.Graph.Auth;
using Microsoft.Identity.Client;
private static async Task<bool> ValidateAccounts(string accounts)
{
var confidentialClientApplication = ConfidentialClientApplicationBuilder
.Create("client id")
.WithTenantId("tenant id")
.WithClientSecret("client secret")
.Build();
var authProvider = new ClientCredentialProvider(confidentialClientApplication);
var graphClient = new GraphServiceClient(authProvider);
var valid = true;
try
{
foreach (var account in accounts.Split(';'))
{
var user = await
graphClient.Users.Request().Filter($"identities/any(c:c/issuerAssignedId eq
'{account}' and c/issuer eq 'xyz.onmicrosoft.com')").GetAsync();
if (user.Count <= 0)
{
valid = false;
break;
}
}
}
catch(Exception ex)
{
valid = false;
}
return valid;
}
Here's my code to send email via SendGrid.
public Task<Result> Execute(string apiKey, string subject, string message, string email)
{
var client = new SendGridClient(apiKey);
string senderEmail = this.Configuration["Email:Address"];
string senderName = this.Configuration["Email:Name"];
var msg = new SendGridMessage()
{
From = new EmailAddress(senderEmail, senderName),
Subject = subject,
PlainTextContent = message,
HtmlContent = message
};
msg.AddTo(new EmailAddress(email));
var response = client.SendEmailAsync(msg);
return response;
}
It is able to send email confirmation and password reset emails, when invoked by ASP.Net Core Identity code.
However, when I try to send email using my own controller action (for a Contact Us feature), nothing happens.
This is the controller action.
public async Task<JsonResult> SendContactEmail(int id, string message)
{
Page page = await this.Get(id);
var result = _emailSender.SendEmailAsync(page.Email, page.Subject, message);
return new JsonResult(result);
}
Is there a way to troubleshoot this? I don't get any exceptions and the response object doesn't seem to have any useful property like a message from the SendGrid server.
UPDATE:
SendGrid's API does return an informative result. I have updated my code to return this.
https://github.com/sendgrid/sendgrid-csharp/blob/master/src/SendGrid/Response.cs
I have an API which dose not any related View and it's for authentication's methods, my change password has been doing something wrong, I don't know what's the exact problem then I have to trace it
My change password is :
[Route("password")]
[BasicAuthenticationFilter]
[HttpPost]
public async Task<UpdateResult<bool>> ChangePassword([FromBody]ChangePasswordViewModel password)
{
try
{
var result = await _userManagerService.ChangePassword(Thread.CurrentPrincipal.Identity.Name, password.NewPassword, password.OldPassword);
return UpdateResult<bool>.Success(result);
var ChangePasswordViewModel = new ChangePasswordViewModel { OldPassword = "", NewPassword = "" };
}
catch (Exception ex)
{
return UpdateResult<bool>.Fail(ex.Message, 1);
}
}
Because it's httpPost I'm using PostMan to trace it and my url is like this :
http://localhost:2511/v1/auth/password?password=new ChangePasswordViewModel{ OldPassword = "13532548", NewPassword = "123456" }
But it send my password parameter null, What's my problem?
You specified that your parameters should be passed in your request body by using [FromBody] attribute like this [FromBody]ChangePasswordViewModel password then this URL
http://localhost:2511/v1/auth/password?password=new ChangePasswordViewModel{ OldPassword = "13532548", NewPassword = "123456" }
is incorrect for passing data to your action. With Postman, you should do this if you want to reach your action:
Second just because you're using basic authentication then make sure to give the authentication data to Postamn too like this:
NOTE: Using the Sign-in button is NOT an option
A year ago I was having a problem creating a moment. Back then I was using version 1.2 of the Google+ API .Net client. As I described in this post, I had it working although the code failed to insert a moment from time to time. I was hoping that the process is more stable and easier to implement now, and it seems like it as can be seen in the example that you can download here - the current version as of this writing is v1.8. So I created a simple project following the SimpleOAuth2 sample in the download, but implementing Google+. This is the code I came up:
public partial class _Default : System.Web.UI.Page
{
private PlusService service;
// Application logic should manage users authentication.
// This sample works with only one user. You can change
// it by retrieving data from the session.
private const string UserId = "user-id";
protected void Page_Load(object sender, EventArgs e)
{
GoogleAuthorizationCodeFlow flow;
var assembly = Assembly.GetExecutingAssembly();
using (var stream = assembly.GetManifestResourceStream(
"GPlusSample.client_secrets.json"))
{
flow = new GoogleAuthorizationCodeFlow(
new GoogleAuthorizationCodeFlow.Initializer
{
DataStore = new FileDataStore("GPlusSample.Store"),
ClientSecretsStream = stream,
//
// Tried only this scope but it did not work
//Scopes = new[] { PlusService.Scope.PlusMe }
//
// I tried the following: but did not work either
//Scopes = new[] { PlusService.Scope.PlusMe,
// "https://www.googleapis.com/auth/plus.moments.write" }
//
// I tried this as well and it failed
//Scopes = new[] { PlusService.Scope.PlusLogin }
//
// Maybe this... but still no joy
Scopes = new[] { PlusService.Scope.PlusLogin,
PlusService.Scope.PlusMe }
});
}
var uri = Request.Url.ToString();
var code = Request["code"];
if (code != null)
{
var token = flow.ExchangeCodeForTokenAsync(UserId, code,
uri.Substring(0, uri.IndexOf("?")), CancellationToken.None).Result;
// Extract the right state.
var oauthState = AuthWebUtility.ExtracRedirectFromState(
flow.DataStore, UserId, Request["state"]).Result;
Response.Redirect(oauthState);
}
else
{
var result = new AuthorizationCodeWebApp(flow, uri, uri)
.AuthorizeAsync(UserId, CancellationToken.None).Result;
if (result.RedirectUri != null)
{
// Redirect the user to the authorization server.
Response.Redirect(result.RedirectUri);
}
else
{
// The data store contains the user credential,
// so the user has been already authenticated.
service = new PlusService(new BaseClientService.Initializer
{
ApplicationName = "Plus API Sample",
HttpClientInitializer = result.Credential
});
}
}
}
/// <summary>Gets the TasksLists of the user.</summary>
public async System.Threading.Tasks.Task InsertMoment()
{
try
{
var me = service.People.Get("me").Execute();
var request = service.Moments.Insert(new Moment()
{
Target = new ItemScope {
Id=Guid.NewGuid().ToString(),
Image="http://www.google.com/s2/static/images/GoogleyEyes.png",
Type="",
Name = "test message",
Description="test",
Text="test message",
},
Type = "http://schemas.google.com/AddActivity",
}, me.Id, MomentsResource.InsertRequest.CollectionEnum.Vault);
var response =await request.ExecuteAsync();
output.Text = "<h1>" + response.Id + "</h1>";
}
catch (Exception ex)
{
var str = ex.ToString();
str = str.Replace(Environment.NewLine, Environment.NewLine + "<br/>");
str = str.Replace(" ", " ");
output.Text = string.Format("<font color=\"red\">{0}</font>", str);
}
}
protected async void createMomentButton_Click(object sender, EventArgs e)
{
await InsertMoment();
}
}
That code always give me a 401 Unauthorized error, even if I have the Google+ API turned on for my project. Here's the actual error I got:
The service plus has thrown an exception: Google.GoogleApiException:
Google.Apis.Requests.RequestError Unauthorized [401] Errors [
Message[Unauthorized] Location[ - ] Reason[unauthorized]
Domain[global] ]
It's interesting to see that the insert moment is failing even though the call to People.Get("me") works - get("me") works with all of the scope combinations I listed above. It's important to note that each time I try a new scope, I first log out of my Google account and delete the access token that is stored in GPlusSample.Store.
EDIT
I tried setting just the Url instead of individual items as suggested by Ian and I got the exact same error.
var request = service.Moments.Insert(new Moment()
{
Target = new ItemScope {
Url = "https://developers.google.com/+/web/snippet/examples/thing"
},
Type = "http://schemas.google.com/AddActivity",
}, me.Id, MomentsResource.InsertRequest.CollectionEnum.Vault);
var response =await request.ExecuteAsync();
https://www.googleapis.com/auth/plus.login is the right scope for writing moments, but you need to have requested the specific app activity types you want to write as well. The parameter for this is request_visible_actions, and it takes a space separated list of arguments of the types (Listed on https://developers.google.com/+/api/moment-types/ - e.g. http://schemas.google.com/AddActivity).
The client library may not have a method for adding request_visible_actions, so you may have to add it on to the auth URL you redirect the user to manually (remember to URLencode the app activity type URLs!)