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();
Related
I am using gmail api to read mail from mail account. But to access mail i have to authorize by selecting or login the mail. If it's only one mail and it's already logged in, it can auto authorize without have select the mail. But if multiple account logged in i have to select the mail to authorize. My goal is to authorize multiple gmail account without selecting the mail. Because this mail reading function will run in crone job. I use following code for authorization.
Thanks in advance.
UserCredential credential;
using (FileStream stream = new FileStream(userfilepath, FileMode.Open, FileAccess.Read))
{
String FolderPath = Convert.ToString(ConfigurationManager.AppSettings["CredentialsInfo"]);
String FilePath = Path.Combine(FolderPath, "APITokenCredentials");
var flow = new GoogleAuthorizationCodeFlow(new GoogleAuthorizationCodeFlow.Initializer
{
ClientSecrets = new ClientSecrets
{
ClientId = cid,
ClientSecret = csecret
},
Scopes = new[] { GmailService.Scope.MailGoogleCom },
DataStore = new FileDataStore(FilePath, true)
});
var token = new Google.Apis.Auth.OAuth2.Responses.TokenResponse()
{
AccessToken = accesstoken,
ExpiresInSeconds = 3600,
Issued = DateTime.Now
};
credential = new UserCredential(flow, usermail, token);
// Create Gmail API service.
var refreshResult = credential.RefreshTokenAsync(CancellationToken.None).Result;
GmailService service = new GmailService(new BaseClientService.Initializer()
{
HttpClientInitializer = credential,
ApplicationName = ApplicationName,
});
return service;
}
In your case the best option is using a Service Account, these types of accounts are intended for non-human access.
Examples:
Running workloads on virtual machines (VMs).
Running workloads on on-premises workstations or data centers that call Google APIs.
Running workloads which are not tied to the lifecycle of a human user.
Documentation
C# Google API Github
.NET Gmail API Documentation
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.
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.
I have been trying to get the simplest examples of IdentityServer4 to work for requesting access in pure code behind. I can get an access token when using a client request but not when doing a user login..
var discos = new DiscoveryClient(authority);
var disco = await discos.GetAsync();
if (disco.IsError)
{
Console.WriteLine(disco.Error);
return null;
}
var tokenClient = new TokenClient(disco.TokenEndpoint, "ro.client", "secret");
var tokenResponse = await tokenClient.RequestResourceOwnerPasswordAsync("username", "password", "api1");
This is the client making the request using user details.
I get a perpetual unsupported_grant_type..
The server has it setup as:
new Client
{
ClientId = "ro.client",
AllowedGrantTypes = GrantTypes.ResourceOwnerPassword,
ClientSecrets =
{
new Secret("secret".Sha256())
},
AllowedScopes = { "api1" }
}
Can anyone please identify what Im mising. User can login using the front end quick start UI that the software offers and this is built in functionality.. Why wont it work if the company is valid.
Your ClientSecrets is configured for the Sha256 of "secret" not the literal string "secret".
Update your tokenClient to pass the Sha256 of "secret" instead of the literal string "secret"
var tokenClient = new TokenClient(disco.TokenEndpoint, "ro.client", "secret".Sha256());
Now resolved.
I have managed to get this working using a newer version of the nuget package so may have been some conflict with what I was reading and old requirements.
For ref I have uploaded the working demo with Server, API and console client to github:
https://github.com/PheonixProject/IdentityServer4Demo
I am try to send emails using GMAIL OAuth protocol but the GoogleWebAuthorizationBroker.AuthorizeAsync method hangs.
I try to run this from server (it is a web.api project that receive the send email information from the client through a htttp POST, then my service should connect to Google Gmail and send the email) Then the service act as a proxy between the Gmail and the client.
I got the client secret stuff from google console for Web Applications.
I use Visual Studio 2013 C# NET4.5. The service has access to the directory where I store the DataStore.
The code that i use is the following and hang over GoogleWebAuthorizationBroker.AuthorizeAsync
DbGoogleDataStore is the same implementation of DataStore as Google (I planning to create my data store in the future in the database, for that I separate it)
public void SendMailUsingOauthProtocol(SmtpConfigurationSettings settings, SmtpSendMessageInputModel filter)
{
var msg = new AE.Net.Mail.MailMessage() // MailMessage
{
Subject = filter.Subject,
Body = filter.Body64,
From = new MailAddress(settings.Username),
};
msg.To.Add(new MailAddress(filter.To));
msg.ReplyTo.Add(new MailAddress(filter.To));
var msgStr = new StringWriter();
msg.Save(msgStr);
// Get from settings the Client secret
MemoryStream stream = new MemoryStream();
StreamWriter writer = new StreamWriter(stream);
writer.Write(settings.AutClientSecret);
writer.Flush();
stream.Position = 0;
//ClientSecrets secret = GoogleClientSecrets.Load(stream).Secrets;
var storeFilePath = Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().GetName().CodeBase);
storeFilePath = storeFilePath + #"\GmailOauth\";
Debug.WriteLine(storeFilePath);
IDataStore fileStore = DbGoogleDataStore.Factory(storeFilePath, true);
// Create the credential.. this step go authenticate with the user if it is necessary
UserCredential credential = GoogleWebAuthorizationBroker.AuthorizeAsync(
stream,
Scopes,
"user",
CancellationToken.None,
fileStore
).Result;
//Create GMAIL API Service
var service = new GmailService(new BaseClientService.Initializer()
{
HttpClientInitializer = credential,
ApplicationName = ApplicationName
});
var result = service.Users.Messages.Send(new Message { Raw = Base64UrlEncode(msgStr.ToString()) }, "me").Execute();
}
Please, Someone has a idea about how to do this?