23

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.

user3288287
  • 233
  • 1
  • 2
  • 6

4 Answers4

31

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);
}
jstedfast
  • 35,744
  • 5
  • 97
  • 110
  • 3
    Sir, this library saved my day and worked like a charm! Thanks for sharing. – user3288287 Jun 13 '14 at 19:26
  • @jstedfast: Perfect. Can you tell me how to add attachments with the email using mailkit? – Jitendra Pancholi Sep 18 '14 at 12:07
  • I've got some documentation here that explains how: https://github.com/jstedfast/MimeKit/blob/master/component/GettingStarted.md#creating-a-message-with-attachments – jstedfast Sep 23 '14 at 21:58
  • @jstedfast: I've tried this code and receive an error on the line: client.Authenticate (credentials, cancel.Token); Error is: Authentication Failed I'm using OAuth 2.0 and already have my token. I assume the "usersLoginName" variable should be the email address that was used when creating the token. I also assume NetworkCredential is a System.Net.NetworkCredential. The intellisense for the 2nd parameter of a NetworkCredential is "password". FYI - When I input the gmail password in that variable, it works - but thats not OAuth obviously. – JeffT Oct 03 '14 at 00:00
  • The password string should be the OAuth 2.0 token if you want to authenticate via XOAUTH2. The usersLoginName should be the account name, yes. For GMail it would be "username@gmail.com". – jstedfast Oct 03 '14 at 15:49
  • 2
    Thanks @jstedfast. The code is correct. In case someone else is trying this and receive an Authentication Failed error, my issue was with the "scope" in the OAuth. I only wanted to send mail, so I used "https://www.googleapis.com/auth/gmail.compose", but authenticating with MailKit will require you to change that to "https://mail.google.com/" (full gmail privelages). – JeffT Oct 03 '14 at 20:16
  • @jstedfast Can you give more detailed answer to the http://stackoverflow.com/questions/34851484/how-to-send-an-email-in-net-according-to-new-google-security-policies ? –  Jan 19 '16 at 12:33
  • 3
    How does one obtain a X509 certificate that Google trusts? – Andy Furniss Jun 28 '18 at 10:49
  • Follow their directions? – jstedfast Jun 28 '18 at 11:19
  • Thanks, this worked a treat. May I ask how you came up with this code @jstedfast as I haven't seen anything like it whilst researching this topic today. Without this specific example I don't know how long it would have taken me! – Murphybro2 Oct 22 '19 at 16:38
  • I can’t take credit for coming up with the code snippet - Google used to have this exact code snippet in their documentation for how to use their OAuth library (other than maybe the scope string). – jstedfast Oct 22 '19 at 16:43
  • 1
    I have just come across it yes, thanks! I apologise if this is overstepping boundaries, but do you perhaps know how to do the same but with the json file you can download instead? Following the example shown here - https://stackoverflow.com/a/38403006/4662037 - I can attempt to read the json key that I get from the same place as the cert I use in your example, but Mailkit throws an AuthenticationException with a message "334: eyJzdGF0..." – Murphybro2 Oct 23 '19 at 09:26
  • What did you use as the password? The entire JSON string? I don't see anything in the JSON that looks like the `auth_token` that you would need to use. It looks like this JSON is for something else... – jstedfast Oct 23 '19 at 13:46
  • 1
    I got the `private_key` from the JSON and then used the `FromPrivateKey` method instead of `FromCertificate`. That provides me with a `ServiceAccountCredential` which I can use to request the access token and then authenticate (same as above). However, an `AuthenticationException` is thrown by Mailkit when calling the `Authenticate` method with my token. Basically the same as this answer - https://stackoverflow.com/a/38403006/4662037. – Murphybro2 Oct 23 '19 at 14:43
  • I wouldn't know, sorry. It's been 5 years since I even looked at this stuff. – jstedfast Oct 23 '19 at 14:44
  • To save anyone who might be new and lost in all the documentation around OAuth. In this case we're talking about Server to Server authentication which doesn't involve direct user engagement. You will need to configure not only client credentials in the Google API console, but also a service account which identifies your app with Google. https://developers.google.com/identity/protocols/oauth2/service-account#java – DoctorBambi Aug 30 '22 at 19:29
8

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

Ogglas
  • 62,132
  • 37
  • 328
  • 418
  • sadly this answer is for "public" applications – CervEd Sep 27 '22 at 12:00
  • @CervEd True but you could store the refresh token if you need to run it in a service. Far from optimal though. https://learn.microsoft.com/en-us/azure/active-directory/develop/msal-acquire-cache-tokens#advanced-accessing-the-users-cached-tokens-in-background-apps-and-services – Ogglas Oct 13 '22 at 10:20
  • 2
    You can but it still requires a public credential flow, even if only intermittent. A private solution is doable but it's a major pain in the rear-end. The whole situation is a mess and I do not have happy thoughts or high regard of whoever designed this abomination – CervEd Oct 14 '22 at 15:29
  • This answer worked like a charm for me! For those who are trying it, just do not forget to enable SMTP AUTH in O365. – Baskman Apr 27 '23 at 06:41
2

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.

Zhaph - Ben Duguid
  • 26,785
  • 5
  • 80
  • 117
0

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();
Steven Pfeifer
  • 345
  • 4
  • 11