A while ago I wrote in C# a simple utility method that was called by an application to send emails automatically.
The application used to authenticate in EWS with Basic authentication (username + password) and everything worked fine.
Starting from September 2022 Microsoft started disabling this now deprecated authentication method, so I decided to update this utility method with an OAuth system autentication.
The method is the following:
public static void SendMail(string to, string cc, string bcc, string replyTo, string from, string subject, bool isHtmlFormat, string body, string[] attachments)
{
var cca = ConfidentialClientApplicationBuilder.Create("my-app-id")//app id
.WithClientSecret("my-client-secret") //Client secret
.WithTenantId("my-tenant-id") //Id tenant
.Build();
var authResult = cca.AcquireTokenForClient(new string[] { "https://outlook.office365.com/.default" }).ExecuteAsync().Result;
string[] recipients = to.Replace(" ", "").Split(';');
string[] repliesTo = string.IsNullOrWhiteSpace(replyTo) ? Array.Empty<string>() : replyTo.Replace(" ", "").Split(';');
string[] ccs = string.IsNullOrWhiteSpace(cc) ? Array.Empty<string>() : cc.Replace(" ", "").Split(';');
string[] bccs = string.IsNullOrWhiteSpace(bcc) ? Array.Empty<string>() : bcc.Replace(" ", "").Split(';');
ExchangeService service = new ExchangeService
{
Url = new Uri("https://outlook.office365.com/EWS/Exchange.asmx"),
Credentials = new OAuthCredentials(authResult.AccessToken),
ImpersonatedUserId = new ImpersonatedUserId(ConnectingIdType.SmtpAddress, from)
};
service.HttpHeaders.Add("X-AnchorMailbox", from); //Include x-anchormailbox header
EmailMessage emailMessage = new EmailMessage(service)
{
From = new EmailAddress(from),
Subject = subject,
Body = new MessageBody(isHtmlFormat ? BodyType.HTML : BodyType.Text, body)
};
emailMessage.ToRecipients.AddRange(recipients);
emailMessage.ReplyTo.AddRange(repliesTo);
emailMessage.CcRecipients.AddRange(ccs);
emailMessage.BccRecipients.AddRange(bccs);
foreach (string attachment in attachments ?? Array.Empty<string>())
{
emailMessage.Attachments.AddFileAttachment(attachment);
}
emailMessage.Send();
}
The function is called in a very straightforward way:
MailHelper.SendMail("MyEmail#myCompany.com", null, null, null, "NoReply#myCompany.com", "Test subject", false, "This is a test body", null);
The problem is that as soon as the Method emailMessage.Send(); is called, a Microsoft.Exchange.WebServices.Data.ServiceRequestException reporting a 403 forbidden error is thrown.
I already registered the app in the Azure Active Directory interface, set a secret and set the following permissions:
The accounts in my tenant are set to allow Exchange services:
I already double checked IDs and account names to be sure it was not a trivial mistake, but I'm not an expert when it comes to EWS, so I'm surely missing something, unfortunately I don't know where.
Thanks id advance.
The error 403 Forbidden usually occurs if you don't have required permissions or missed granting admin consent to the added API permissions.
Please note that, the code you mentioned is using "Client credentials flow" that works with only Application permissions but you added all Delegated permissions.
In that case, you will get 403 Forbidden error even you granted consent to Delegated permissions.
I tried to reproduce the same in my environment via Postman and got the below results:
I created one Azure AD application and added API permissions same as you like below:
Now, I generated an access token using "Client credentials flow" via Postman like below:
POST https://login.microsoftonline.com/tenantID/oauth2/v2.0/token
client_id:appID
grant_type:client_credentials
client_secret:secret
scope:https://graph.microsoft.com/.default
Response:
When I used the above token to send mail with below query, I got 403 Forbidden error like this:
POST https://graph.microsoft.com/v1.0/users/userID/sendmail
Response:
To resolve the error, you need to add Application permissions and grant admin consent to them like below:
Now I generated the access token again and used it in running below query to send sample mail:
In your case, add Application permissions by granting admin consent to them and run the code again.
Related
I am writing a web app that I would like to have access to Sharepoint Document Libraries from a particular site using the currently logged on user credentials. I have looked at a number of articles that suggest using the PnP Framework and using a certificate instead of the client/secret ids.
I have tried both, the code of which is below:
string siteCollectionURL = "https://mycompanyname.sharepoint.com/sites/staffportal";
var authManager = new AuthenticationManager(ApplicationId, "C:\\pathtopfxfile\certifcatefx.pfx", "certificatepassword", "https://mycompany.sharepoint.com/sites/staffportal");
using (var clientContext = authManager.GetACSAppOnlyContext(siteCollectionURL,ApplicationId,ClientSecretId))
{
clientContext.Load(clientContext.Web, p => p.Title);
clientContext.ExecuteQuery();
return Ok(clientContext.Web.Title);
}
Unfortunately on the ExecuteQuery line I am consistently getting the 401 error, indicating that I am not authorized.
I registered the app in Azure -> Enterprise applications:
I have checked the following articles:
How to use the PnP Framework in a c# application
Secure Authentication of SharePoint with PnP Framework with C#(Code)
And tried the code snippets, but I cannot seem to find anything that suggests using the currently logged in user to the Web app.(see screen shot) - the user is a global administrator
Below is the error:
Any suggestions would be much appreciated.
Regards,
Darren
UPDATE TO ARTICLE
jimas13's link is what pointed me in the right direction. The tweaks I mentioned in the comment I have posted below for anyone wanting to write an MVC C# web app. This does require that the app needs to be registered and needs a self-signed certificate setup.
The two Async lines need to be written as follows:
public static async Task<AuthenticationResult> GetToken(IConfidentialClientApplication app, string[] scopes)
{
return await app.AcquireTokenForClient(scopes).ExecuteAsync();
}
public static async Task<Web> GetClientContext(string Uri,AuthenticationResult authResult)
{
using (var clientContext = ContextHelper.GetClientContext(Uri, authResult.AccessToken))
{
Web web = clientContext.Web;
clientContext.Load(web);
await clientContext.ExecuteQueryAsync();
return web;
}
}
The rest of the code is here:
public IActionResult Index()
{
AuthenticationConfiguration.AuthenticationConfiguration config = AuthenticationConfiguration.AuthenticationConfiguration.ReadFromJsonFile("appsettings.json");
string siteURL = config.SiteUrl;
string[] scopes = new string[] { config.Scope };
CertificateDescription certificate = config.Certificate;
ICertificateLoader certificateLoader = new DefaultCertificateLoader();
certificateLoader.LoadIfNeeded(certificate);
IConfidentialClientApplication app = ConfidentialClientApplicationBuilder.Create(config.ClientId)
.WithCertificate(certificate.Certificate)
.WithTenantId(config.Tenant)
.WithAuthority(config.Authority)
.Build();
AuthenticationResult result = GetToken(app, scopes).Result;
Web WebSite = GetClientContext(siteURL, result).Result;
return Ok(WebSite.Title);
}
The .SiteUrl and .Scope were added to the AuthenticationConfiguration.cs file as a property and then also added to the appsettings.json file.
I'm trying to connect to SharePoint online in a console App and print the title of the site.
Its giving me the error : "The sign-in name or password does not match one in the Microsoft account system."
I have checked and made sure the username and password are 100% right.
I dont know what else to check
Heres my code:
private static void SPCredentialsConnect()
{
const string SiteUrl = "https://tenant.sharepoint.com/sites/mysite";
const string pwd = "appPassword";
const string username = "username#tenant.onmicrosoft.com";
SecureString securestring = new SecureString();
pwd.ToCharArray().ToList().ForEach(s => securestring.AppendChar(s));
ClientContext context = new ClientContext(SiteUrl);
context.Credentials = new SharePointOnlineCredentials(username, securestring);
try
{
var web = context.Web;
context.Load(web);
context.ExecuteQuery();
Console.WriteLine($"web title: {web.Title}");
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
}
Have your issue fixed? “The sign-in name or password does not match one in the Microsoft account system” Error will occur sometimes and fixed after a while with nothing changed.
AppOnly Authentication for sharepointonline can't be registed in Azure Active Directory.
It should be register in
https://contoso.sharepoint.com/_layouts/15/appregnew.aspx
And grant permission in
https://contoso-admin.sharepoint.com/_layouts/15/appinv.aspx
You can refer to following document
https://learn.microsoft.com/en-us/sharepoint/dev/solution-guidance/security-apponly-azureacs
Consider using the PnP.Framework (a NuGet package), and use the AuthenticationManager object for SPO sites. This method bypasses MFA (which is mandatory in our organization, FWIW). You can find a lot more information and examples here, including steps on getting the client id and client secret for a site. Here is what we use to log into SPO sites:
using (ClientContext context =
new AuthenticationManager().GetACSAppOnlyContext(SiteUrl, clientID, clientSecret))
{
...
}
Also, once you connect, you should adjust the Context.Load to grab the title if you want to use that value right away. Here's what I used in my code:
context.Load(web, p => p.Id, p => p.Title);
context.ExecuteQuery();
Console.WriteLine($"Logged into source {web.Title} ({web.Id})");
Good luck!
Steve in Spain
I'm trying to use the Gmail API reading the emails, but I'm running into the problem that I want to do a server-side authentification but with all the examples from google Doc., he always shows me window asking me to add my credentials (Gmail & password).
public static async void CreateService()
{
GoogleCredential credential;
using (var stream = new FileStream(#"key.json", FileMode.Open,
FileAccess.Read))
{
credential = GoogleCredential.FromStream(stream)
.CreateScoped(GmailService.Scope.GmailLabels,
GmailService.Scope.GmailModify, GmailService.Scope.GmailMetadata,
GmailService.Scope.GmailReadonly);
}
var service = new GmailService(new BaseClientService.Initializer()
{
HttpClientInitializer = credential,
ApplicationName = "Gmail",
});
Console.WriteLine(ListMessages(service, "me", ""));
}
Then I got this code from the documentation of the google api of how to read the messages from a user.
public static List<Message> ListMessages(GmailService service, String userId, String query)
{
List<Message> result = new List<Message>();
UsersResource.MessagesResource.ListRequest request = service.Users.Messages.List(userId);
request.Q = query;
do
{
try
{
ListMessagesResponse response = request.Execute();
result.AddRange(response.Messages);
request.PageToken = response.NextPageToken;
}
catch (Exception e)
{
Console.WriteLine("An error occurred: " + e.Message);
}
} while (!String.IsNullOrEmpty(request.PageToken));
return result;
}
But when I run it I get this error: An error occurred:
Google.Apis.Requests.RequestError
Bad Request [400]
Errors [
Message[Bad Request] Location[ - ] Reason[failedPrecondition] Domain[global]
]
Answer:
If you want to use a user as the authentication account, then no. This is not possible and you will always get a login window pop-up.
Other Methods:
You can however create and use a service account to impersonate your user and bypass the need for authenticating on run. They require a little extra set up but you can create them in the Google Developer Admin Console.
Code Example:
After creating your service account and giving it the roles and permissions it needs (see links below), you only need to make small edits to your code to use it instead of your regular account. This is an example in Python, but you can find other examples on the managing keys page:
import os
from google.oauth2 import service_account
import googleapiclient.discovery
def create_key(service_account_email):
"""Creates a key for a service account."""
credentials = service_account.Credentials.from_service_account_file(
filename=os.environ['GOOGLE_APPLICATION_CREDENTIALS'],
scopes=['YOUR SCOPES HERE'])
# Rememer here that your credentials will need to be downloaded
# for the service account, YOU CAN NOT USE YOUR ACCOUNT'S CREDENTIALS!
service = googleapiclient.discovery.build(
'service', 'version', credentials=credentials)
key = service.projects().serviceAccounts().keys().create(
name='projects/-/serviceAccounts/' + service_account_email, body={}
).execute()
print('Created key: ' + key['name'])
References:
Google Developer Admin Console
Google Cloud - Service Accounts
Understanding Service Accounts
Creating and Managing Service Accounts
Creating and Managing Service Account Keys
Granting Roles to Service Accounts
Every time I try to call the server, I get an error code : ErrorConnectionFailed with Connection failed. Try later. message.
I suspect that it comes from the fact that the credentials of service are empty. Although I have no idea why. If I create the credentials manually using my windows account login and password, it works fine : new WebCredentials(login, password, domain);
I have a console program that works fine (see below), but it does not on a web site.
static void Main(string[] args)
{
var service = GetContextualService(email);
EmailMessage email = EmailMessage.Bind(service, new ItemId(validEmailId));
Console.ReadKey();
}
private static ExchangeService GetContextualService(string email)
{
ExchangeService service = new ExchangeService();
// I don't even need credentials on a Console program to make it work
//service.Credentials = new WebCredentials(CredentialCache.DefaultCredentials);
//service.UseDefaultCredentials = true;
service.AutodiscoverUrl(email, RedirectionUrlValidationCallback);
return service;
}
private static bool RedirectionUrlValidationCallback(string redirectionUrl)
{
// The default for the validation callback is to reject the URL.
bool result = false;
Uri redirectionUri = new Uri(redirectionUrl);
// Validate the contents of the redirection URL. In this simple validation
// callback, the redirection URL is considered valid if it is using HTTPS
// to encrypt the authentication credentials.
if (redirectionUri.Scheme == "https")
{
result = true;
}
return result;
}
While using on a website even with new WebCredentials(CredentialCache.DefaultCredentials);, it returns an exception. (see below)
private ExchangeService GetContextualService(string email)
{
ExchangeService service = new ExchangeService();
service.Credentials = new WebCredentials(CredentialCache.DefaultCredentials);
//service.UseDefaultCredentials = true;
service.AutodiscoverUrl(email, RedirectionUrlValidationCallback);
return service;
}
[HttpPost]
public List<InternetMessageHeader> GetMailHeader(JObject data)
{
ExchangeService service = GetContextualService(data.GetValue("email").Value<string>());
ItemId id = new ItemId(data.GetValue("mailId").Value<string>());
// EXCEPTION BELOW
EmailMessage email = EmailMessage.Bind(service, id);
return email.InternetMessageHeaders.ToList();
}
Why does any call to EWS returns me an exception ?
Why is it working fine on a console program and not on a web server ?
Any thought is welcome !
Strictly based on the code you posted, all I can say is that when you call AutoDiscoverUrl() it may not be talking to the same server that you need to talk to with EWS. Altho typically AD and EWS are on the CAS, it's possible (I think) to get an EWS URL that points to some other server. I've not been in the code in the EWS Editor in some time, but if it does not call the Managed API, it might do AD slightly differently. I'd suggest calling out the EWS URL before you try the Bind() and seeing if you can paste it into a browser. (It'll redirect you OWA, but the connection will be proven.) I'd also call out the AD URL in the redirection callback so you know who you're talking to. Sorry I can't be of more help.
I'm using MailSystem.NET library for sendting and recieving e-mails. Everything works fine except Pop3Client authentication using SSL when there is a backslash in the username.
Let's say that I have the following code:
var client = new Pop3Client();
var username = #"xxx\admin";
var password = #"passW0rd";
var host = #"abc.def.gh";
var port = 995;
var result = client.ConnectSsl(host, port, true);
Console.WriteLine(result);
result = client.Authenticate(username, password, SaslMechanism.Login);
Console.WriteLine(result);
And the output is:
+OK The Microsoft Exchange POP3 service is ready.
Command "auth login" failed : -ERR Protocol error. 14
So, what the heck? When I try to connect and authenticate e.g. to a google with username such a johnnyboy#gmail.com, it works. But if there is a backslash in it and I go against MS Exchange, it doesn't work.
The credentials are ok, I doublechecked them using PegasusMail.
Can someone explain what could be wrong?
Ok, answer is simple.
Since 2003, Exchange does not support obsolete SASL mechanism AUTH LOGIN. There must be used at least AUTH PLAIN. But to do it, the whole authentication must be reworked.
After AUTH PLAIN there should be username and password in one command with \000 char as a leading and as a separator. So, the resulting command should be base64 encoded string like:
\000username\000password
see Connecting to POP/SMTP Server via Telnet
So, what I did was simple. I extended Pop3Client class and created a new method Authenticate(string username, string password) without SaslMechanism.
public class Pop3ClientExt : Pop3Client
{
public string Authenticate(string username, string password)
{
var nullchar = '\u0000';
var auth = nullchar + username + nullchar + password;
this.Command("auth plain");
string response = this.Command(System.Convert.ToBase64String(System.Text.Encoding.GetEncoding("iso-8859-1").GetBytes(auth)));
return response;
}
}
And now, in case of Microsoft Exchange server, I'll call this Authenticate method instead of the old one.