Mailkit connect to Office365 programmatically - c#

I need to get emails from my Office365 account programmatically (C#).
I decided to use Mailkit and to create an application password on Azure portal.
I registered a new app, set its Redirect Uri and gave it some permissions:
I then created a client secret to access the account without user interaction.
Now, here is my code:
var opt = new ConfidentialClientApplicationOptions()
{
ClientId = "xxx_clientid",
TenantId = "xx_tenant_id",
ClientSecret = "xxx_client_secret_value",
RedirectUri = "http://localhost",
};
var scopes = new string[] {
"email",
"offline_access",
"https://outlook.office.com/IMAP.AccessAsUser.All", // Only needed for IMAP
//"https://outlook.office.com/POP.AccessAsUser.All", // Only needed for POP
//"https://outlook.office.com/SMTP.Send", // Only needed for SMTP
};
var app = ConfidentialClientApplicationBuilder.CreateWithApplicationOptions(opt).Build();
var authToken = await app.AcquireTokenForClient(scopes).ExecuteAsync(); // <--- Exception
var oauth2 = new SaslMechanismOAuth2(authToken.Account.Username, authToken.AccessToken);
using (var client = new ImapClient(new ProtocolLogger("imapLog.txt")))
{
client.Connect("outlook.office365.com", 993, SecureSocketOptions.SslOnConnect);
//client.AuthenticationMechanisms.Remove("XOAUTH2");
client.Authenticate(oauth2);
var inbox = client.Inbox;
inbox.Open(MailKit.FolderAccess.ReadOnly);
Console.WriteLine("Total messages: {0}", inbox.Count);
Console.WriteLine("Recent messages: {0}", inbox.Recent);
client.Disconnect(true);
}
Running the code I get this exception:
Microsoft.Identity.Client.MsalServiceException: 'AADSTS70011: The provided request must include a 'scope' input parameter. The provided value for the input parameter 'scope' is not valid. The scope email offline_access https://outlook.office.com/IMAP.AccessAsUser.All is not valid.
I tried following the guide found on GitHub:
Using OAuth2 With Exchange
The problem is that I need to use an app password instead.

Related

How to read email from outlook account with oauth2 and no user interaction

I working on a console app that must logon to a outlook365 email account and then get the attachments in emails.
I currently have the code I found on ExchangeOAuth2.
When I get to the part the token has to be retrieved, it opens a website and I have to select an account to be able to continue.
How can I prevent this. There is no user interaction.
My code:
string? host = Configuration.GetSection("email").GetValue<string>("host");
int? port = Configuration.GetSection("email").GetValue<int>("port");
string? clientId = Configuration.GetSection("email").GetValue<string>("clientId");
string? tenantId = Configuration.GetSection("email").GetValue<string>("tenantId");
if (string.IsNullOrEmpty(host) || port == null || string.IsNullOrEmpty(clientId) ||
string.IsNullOrEmpty(tenantId))
{
return;
}
var options = new PublicClientApplicationOptions
{
ClientId = clientId,
TenantId = tenantId,
RedirectUri = "http://localhost"
};
var publicClientApplication = PublicClientApplicationBuilder
.CreateWithApplicationOptions(options)
.Build();
var scopes = new string[] {
"email",
"offline_access",
"https://outlook.office.com/IMAP.AccessAsUser.All" // Only needed for IMAP
};
var authToken = await
publicClientApplication.AcquireTokenInteractive(scopes).ExecuteAsync();
var oauth2 = new SaslMechanismOAuth2(authToken.Account.Username,
authToken.AccessToken);
[update]
I have changed my code to use microsoft graph.
I used this tutorial
But this is still asking me to go to a website en fill in the code.
That is exactly what I'm not looking for.
There is no user interaction, the console must run on it's own.
Why is this so hard?
Does anyone have an example on how to do this?
I have figured it out.
In the azure envronment for the email account, some API permissions had to be set.

Code: BadRequest Message: /me request is only valid with delegated authentication flow

I am trying to upload file on onedrive by using microsoft graph onedrive api.
I am using the method for authentication
Client credentials provider
https://learn.microsoft.com/en-us/graph/sdks/choose-authentication-providers?tabs=CS#client-credentials-provider
Like:
// /.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 = "my-tenantid";
// Values from app registration
var clientId = "YOUR_CLIENT_ID";
var clientSecret = "YOUR_CLIENT_SECRET";
// using Azure.Identity;
var options = new TokenCredentialOptions
{
AuthorityHost = AzureAuthorityHosts.AzurePublicCloud
};
// https://learn.microsoft.com/dotnet/api/azure.identity.clientsecretcredential
var clientSecretCredential = new ClientSecretCredential(
tenantId, clientId, clientSecret, options);
var graphClient = new GraphServiceClient(clientSecretCredential, scopes);
HttpPostedFileBase file = Request.Files;[0];
int fileSize = file.ContentLength;
string fileName = file.FileName;
string mimeType = file.ContentType;
Stream fileContent = file.InputStream;
var res = await graphClient.Me.Drive.Root.ItemWithPath(fileName).Content
.Request()
.PutAsync<DriveItem>(fileContent);
After executing this code then it gives an error in response.
Message: /me request is only valid with delegated authentication flow.
Inner error:
AdditionalData:
date: 2021-12-29T05:30:08
request-id: b51e50ea-4a62-4dc7-b8d2-b26d75268cdc
client-request-id: b51e50ea-4a62-4dc7-b8d2-b26d75268cdc
ClientRequestId: b51e50ea-4a62-4dc7-b8d2-b26d75268cdc
Client credential flow will generate the token on behalf the app itself, so in this scenario, users don't need to sign in first to generate the token stand for the user and then call the api. And because of the design,when you used Me in the graph SDK, your code/app don't know who is Me so it can't work. You should know the user_id first and use /users/{id | userPrincipalName} instead of /Me, in the SDK, that is graphClient.Users["your_user_id"] instead of graphClient.Me
In your scenario, there're 2 solutions, one way is using delegated authentication flow like what you said in your title, another way is get the user id before calling the graph api so that you can use Users["id"] but not Me
===================== Update=========================
I haven't finished the code yet but I found the correct solution now.
Firstly, we can upload file to one drive by this api, you may check the screenshot if this is one drive or sharepoint:
https://graph.microsoft.com/v1.0/users/user_id/drive/items/root:/testupload2.txt:/content
If it is, then the next is easy, using the code below to get an access token and send http request to calling the api:
var scopes = new[] { "https://graph.microsoft.com/.default" };
var tenantId = "tenant_name.onmicrosoft.com";
var clientId = "your_azuread_clientid";
var clientSecret = "corresponding_client_secret";
var clientSecretCredential = new ClientSecretCredential(
tenantId, clientId, clientSecret);
var tokenRequestContext = new TokenRequestContext(scopes);
var token = clientSecretCredential.GetTokenAsync(tokenRequestContext).Result.Token;
I know it's complex because the api is not the same as this one which has SDK sample, but I think it also deserves to try if they are similar.

Gmail SMTP XOAUTH2 error: JWT has expired

Yesterday I created a ClientID & Client Secret (using this guide) to authenticate a desktop application (C#/.NET) to send emails from a Gmail account.
My method, which authenticates and sends an email, looks as follows:
public static async System.Threading.Tasks.Task<int> SendEmailOAuth2Async(string sFromMailAddress, string sClientID, string sClientSecret)
{
var credential = await GoogleWebAuthorizationBroker.AuthorizeAsync(
new ClientSecrets
{
ClientId = sClientID,
ClientSecret = sClientSecret
},
new[] { "email", "profile", "https://mail.google.com/" },
"user",
CancellationToken.None
) ;
var jwtPayload = GoogleJsonWebSignature.ValidateAsync(credential.Token.IdToken).Result;
var username = jwtPayload.Email;
var mailMessage = new MimeMessage();
mailMessage.From.Add(new MailboxAddress("from name", sFromMailAddress));
mailMessage.To.Add(new MailboxAddress("to name", "someone#outlook.com"));
mailMessage.Subject = "Automated Mail with OAuth";
mailMessage.Body = new TextPart("plain")
{
Text = "Hello"
};
using (var client = new SmtpClient())
{
client.Connect("smtp.gmail.com", 587, SecureSocketOptions.StartTls);
// use the access token
var oauth2 = new SaslMechanismOAuth2(sFromMailAddress, credential.Token.AccessToken);
client.Authenticate(oauth2);
client.Send(mailMessage);
client.Disconnect(true);
}
return 0;
}
Yesterday, sending an email with this method worked. Today, I get a
System.AggregateException: 'One or more errors occured. (JWT has
expired.)'
I am new to OAuth and tokens. What can I do to get this to work again?
After generating the credentials object using GoogleWebAuthorizationBroker.AuthorizeAsync, I then attempted to run
GoogleJsonWebSignature.ValidateAsync(credential.Token.IdToken).Result
but was met with "Expired JWT." Using this method call-
await credential.RefreshTokenAsync(CancellationToken.None);
the credential.Token object was updated and I then was able to call GoogleJsonWebSignature.ValidateAsync method (using the same credential object) successfully.
Thank you Garth J Lancaster on different website. Not sure if I'm allowed to link from here.

Unable to authorize Azure LogAnalytics Workspace

I am trying to connect to my workspace in the Azure Portal. I am getting the error as
Operation returned an invalid status code 'Unauthorized'.
The creds object has fetched the Authentication Token and I have added resource permissions to my app as mentioned in this link
using System;
using Microsoft.Azure.OperationalInsights;
using Microsoft.Rest.Azure.Authentication;
namespace LogAnalytics
{
class Program
{
static void Main(string[] args)
{
var workspaceId = "**myworkspaceId**";
var clientId = "**myClientId**";
var clientSecret = "**myClientSecret**";
//<your AAD domain>
var domain = "**myDomain**";
var authEndpoint = "https://login.microsoftonline.com";
var tokenAudience = "https://api.loganalytics.io/";
var adSettings = new ActiveDirectoryServiceSettings
{
AuthenticationEndpoint = new Uri(authEndpoint),
TokenAudience = new Uri(tokenAudience),
ValidateAuthority = true
};
var creds = ApplicationTokenProvider.LoginSilentAsync(domain,clientId, clientSecret,
strong textadSettings).GetAwaiter().GetResult();
var client = new OperationalInsightsDataClient(creds);
client.WorkspaceId = workspaceId;
//Error happens below
var results = client.Query("union * | take 5");
Console.WriteLine(results);
Console.ReadLine();
}
}
}
Operation returned an invalid status code 'Unauthorized'.
According to the error message and the code you provided, you need to add permission in your registered application in Azure AD.
Note: If you want to add permission to application you need to be admin, and then you could use the ClientId and ClientSecret to get Authentication Token and read log analytics.
However, if you are not admin, you could delegate permission to user and access to Azure AD with username and password.
To get authentication token with user, you could can use the function UserTokenProvider.LoginSilentAsync(nativeClientAppClientid, domainName, userName, password).GetAwaiter().GetResult() to get our credentials.

SMTP and OAuth 2

Does .NET support SMTP authentication via OAuth protocol? Basically, I would like to be able to send emails on users' behalves using OAuth access tokens. However, I couldn't find a support for this in the .NET framework.
Google provides some samples for this in other environments but not .NET.
System.Net.Mail does not support OAuth or OAuth2. However, you can use MailKit's (note: only supports OAuth2) SmtpClient to send messages as long as you have the user's OAuth access token (MailKit does not have code that will fetch the OAuth token, but it can use it if you have it).
The first thing you need to do is follow Google's instructions for obtaining OAuth 2.0 credentials for your application.
Once you've done that, the easiest way to obtain an access token is to use Google's Google.Apis.Auth library:
var certificate = new X509Certificate2 (#"C:\path\to\certificate.p12", "password", X509KeyStorageFlags.Exportable);
var credential = new ServiceAccountCredential (new ServiceAccountCredential
.Initializer ("your-developer-id#developer.gserviceaccount.com") {
// Note: other scopes can be found here: https://developers.google.com/gmail/api/auth/scopes
Scopes = new[] { "https://mail.google.com/" },
User = "username#gmail.com"
}.FromCertificate (certificate));
bool result = await credential.RequestAccessTokenAsync (CancellationToken.None);
// Note: result will be true if the access token was received successfully
Now that you have an access token (credential.Token.AccessToken), you can use it with MailKit as if it were the password:
using (var client = new SmtpClient ()) {
client.Connect ("smtp.gmail.com", 587, SecureSocketOptions.StartTls);
// use the access token
var oauth2 = new SaslMechanismOAuth2 ("username#gmail.com", credential.Token.AccessToken);
client.Authenticate (oauth2);
client.Send (message);
client.Disconnect (true);
}
I got it working by using Microsoft.Identity.Client and MailKit.Net.Smtp.SmtpClient like this using Office 365 / Exchange Online. App registration requires API permissions SMTP.Send.
var options = new PublicClientApplicationOptions
{
ClientId = "00000000-0000-0000-0000-000000000000",
TenantId = " 00000000-0000-0000-0000-000000000000",
RedirectUri = "http://localhost"
};
var publicClientApplication = PublicClientApplicationBuilder
.CreateWithApplicationOptions(options)
.Build();
var scopes = new string[] {
"email",
"offline_access",
"https://outlook.office.com/SMTP.Send" // Only needed for SMTP
};
var authToken = await publicClientApplication.AcquireTokenInteractive(scopes).ExecuteAsync();
//Test refresh token
var newAuthToken = await publicClientApplication.AcquireTokenSilent(scopes, authToken.Account).ExecuteAsync(cancellationToken);
var oauth2 = new SaslMechanismOAuth2(authToken.Account.Username, authToken.AccessToken);
using (var client = new SmtpClient())
{
await client.ConnectAsync("smtp.office365.com", 587, SecureSocketOptions.StartTls);
await client.AuthenticateAsync(oauth2);
var message = new MimeMessage();
message.From.Add(MailboxAddress.Parse(authToken.Account.Username));
message.To.Add(MailboxAddress.Parse("toEmail"));
message.Subject = "Test";
message.Body = new TextPart("plain") { Text = #"Oscar Testar" };
await client.SendAsync(message, cancellationToken);
await client.DisconnectAsync(true);
}
Based on this example:
https://github.com/jstedfast/MailKit/blob/master/ExchangeOAuth2.md
Just adding to the above answer. I also spend lot of time to find out things for sending email using gmail oAuth2 with mailkit in .net. As I am using this to send email to my App users. Thanks to mailkit developers.
Now we need:
Authorization code
Client ID
Client Secret
Refresh Token
Access Token
You can directly get the Client Id and Client Secret from google console by creating your project.
Next you can enable gmail app from the Google Developers OAuth Playground by using your own OAuth credentials in left top setting button.
After that Select and Authorize the API https://mail.google.com/.
Now you can directly refresh token by this http POST request https://developers.google.com/oauthplayground/refreshAccessToken. you will find the parameter in there.
Now you can directly use this code in your C# code using MailKit:
using (var client = new SmtpClient())
{
client.Connect("smtp.gmail.com", 587, SecureSocketOptions.StartTls);
var oauth2 = new SaslMechanismOAuth2(GMailAccount, token.AccessToken);
client.Authenticate(oauth2);
await client.SendAsync(mailMessage);
client.Disconnect(true);
}
Now you will be able to send email through your gmail account from server side.
Using MailKit as referenced in the other answers, I was hitting an authentication issue requiring more scopes to be requested from Gmail. For anyone experiencing "Authentication Failed error" with either of the other answers, this answer uses the Gmail API instead in order to avoid requesting more scopes.
Using some pieces from this answer: https://stackoverflow.com/a/35795756/7242722
Here's a complete example which worked for me:
var fromAddress = new MailboxAddress(fromName, fromEmail);
var toAddress = new MailboxAddress(toName, toEmail);
List<MailboxAddress> ccMailAddresses = new List<MailboxAddress>();
if (ccEmails != null)
foreach (string ccEmail in ccEmails)
ccMailAddresses.Add(new MailboxAddress(string.Empty, ccEmail));
var message = new MimeMessage();
message.To.Add(toAddress);
message.From.Add(fromAddress);
message.Subject = subject;
var bodyBuilder = new BodyBuilder();
bodyBuilder.HtmlBody = body;
bodyBuilder.TextBody = HtmlUtilities.ConvertToPlainText(body);
message.Body = bodyBuilder.ToMessageBody();
foreach (MailboxAddress ccMailAddress in ccMailAddresses)
message.Cc.Add(ccMailAddress);
GoogleAuthorizationCodeFlow authorizationCodeFlow = new GoogleAuthorizationCodeFlow(new GoogleAuthorizationCodeFlow.Initializer
{
ClientSecrets = new ClientSecrets()
{
ClientId = <ClientId>,
ClientSecret = <ClientSecret>
},
});
TokenResponse tokenResponse = await authorizationCodeFlow.RefreshTokenAsync("id", <RefreshToken>, CancellationToken.None);
UserCredential credential = new UserCredential(authorizationCodeFlow, "id", tokenResponse);
var gmailService = new GmailService(new BaseClientService.Initializer()
{
ApplicationName = <AppName>,
HttpClientInitializer = credential,
});
Google.Apis.Gmail.v1.Data.Message gmailMessage = new Google.Apis.Gmail.v1.Data.Message();
gmailMessage.Raw = Utilities.Base64UrlEncode(message.ToString());
var result = gmailService.Users.Messages.Send(gmailMessage, "me").Execute();

Categories

Resources