Search code examples
c#oauth-2.0smtpoffice365mailkit

MailKit OAuth2 SMTP Office365 Send Mail


Since Microsoft announced it is removing basic auth smtp support, I am trying to find a different way of sending emails from my application. My applications are backend applications so therefore it has to be a non-interactive flow.

When I use the password flow, everything works perfectly, however, password grant flow is legacy since it also exposes the user password:

static async Task<string> GetAccessToken(FormUrlEncodedContent content, string tokenEndpoint)
{
    var client = new HttpClient();
    var response = await client.PostAsync(tokenEndpoint, content).ConfigureAwait(continueOnCapturedContext: false);
    var jsonString = await response.Content.ReadAsStringAsync();
    client.Dispose();

    var doc = JsonDocument.Parse(jsonString);
    JsonElement root = doc.RootElement;
    if (root.TryGetProperty("access_token", out JsonElement tokenElement))
        return tokenElement.GetString()!;

    throw new Exception("Failed to get access token");
}

static void SendO365(SaslMechanism accessToken, string host, int port, string from, string to)
{
    using (var client = new SmtpClient())
    {
        client.ServerCertificateValidationCallback = (s, c, h, e) => true;

        try
        {
            client.Connect(host, port, SecureSocketOptions.Auto);
            client.Authenticate(accessToken);
            var msg = new MimeMessage();
            msg.From.Add(MailboxAddress.Parse(from));
            msg.To.Add(MailboxAddress.Parse(to));
            msg.Subject = "Testing SMTP";
            msg.Body = new TextPart("plain") { Text = "This is a test message." };
            client.Send(msg);
        }
        catch (Exception ex)
        {
            Console.WriteLine(ex.Message);
        }
    }
}

var content = new FormUrlEncodedContent(new List<KeyValuePair<string, string>>
{
    new KeyValuePair<string, string>("client_id", "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxx"),
    new KeyValuePair<string, string>("client_secret", "yyy"),
    new KeyValuePair<string, string>("grant_type", "password"),
    new KeyValuePair<string, string>("resource", "https://outlook.office365.com"),
    new KeyValuePair<string, string>("scope", ".default"),
    new KeyValuePair<string, string>("username", "[email protected]"),
    new KeyValuePair<string, string>("password", "zzz"),
});

string tenantId = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxx"
string tokenEndpoint = $"https://login.microsoftonline.com/{tenantId}/oauth2/token";
var accessToken = GetAccessToken(content, tokenEndpoint).Result;

var userEmail = "[email protected]";
var smtpServer = "smtp.office365.com";
var smtpPort = 587;
var toEmail = "[email protected]";

SendO365(new SaslMechanismOAuth2(userEmail, accessToken), smtpServer, smtpPort, userEmail, toEmail);

In Entra I have set the Mail.Send scope.

I tried switching to the client_credentials flow since it is more secure but I always get an error Authentication unsuccessful on client.Authenticate(accessToken).

Anyone has an idea?

Edit

GitHub Repository: https://github.com/tsafadi/MailKit-OAuth-SMTP/tree/master/MailKit.SMTP-OAuth


Solution

  • As you've discovered, you need to use the client-credential workflow.

    I have documented what you need to do here: https://github.com/jstedfast/MailKit/blob/master/ExchangeOAuth2.md (specifically, see the Web Services docs).

    Re-posting here to make StackOverflow happy.

    Registering Your Application with Microsoft

    Whether you are writing a Desktop, Mobile or Web Service application, the first thing you'll need to do is register your application with Microsoft's Identity Platform. To do this, go to Microsoft's Quickstart guide and follow the instructions.

    Configuring the Correct API Permissions for Your Application

    There are several different API permissions that you may want to configure depending on which protocols your application intends to use.

    Follow the instructions for adding the POP, IMAP, and/or SMTP permissions to your Entra AD application.

    Web Services

    Registering Service Principals for Your Web Service

    Once your web service has been registered, the tenant admin will need to register your service principal.

    To use the New-ServicePrincipal cmdlet, open an Azure Powershell terminal and install ExchangeOnlineManagement and connect to your tenant as shown below:

    Install-Module -Name ExchangeOnlineManagement -allowprerelease
    Import-module ExchangeOnlineManagement 
    Connect-ExchangeOnline -Organization <tenantId>
    

    Next, register the Service Principal for your web service:

    New-ServicePrincipal -AppId <APPLICATION_ID> -ObjectId <OBJECT_ID> [-Organization <ORGANIZATION_ID>]
    

    Granting Permissions for Your Web Service

    In order to grant permissions for your web service to access an Office365 and/or Exchange account, you'll need to first get the Service Principal ID registered in the previous step using the following command:

    Get-ServicePrincipal | fl
    

    Once you have the Service Principal ID for your web service, use the following command to add full mailbox permissions for the email account that your web service will be accessing:

    Add-MailboxPermission -Identity "[email protected]" -User 
    <SERVICE_PRINCIPAL_ID> -AccessRights FullAccess
    

    Authenticating a Web Service with OAuth2

    Now that you have the Client ID and Tenant ID strings, you'll need to plug those values into your application.

    The following sample code uses the Microsoft.Identity.Client nuget package for obtaining the access token which will be needed by MailKit to pass on to the Exchange server.

    var confidentialClientApplication = ConfidentialClientApplicationBuilder.Create (clientId)
        .WithAuthority ($"https://login.microsoftonline.com/{tenantId}/v2.0")
        .WithCertificate (certificate) // or .WithClientSecret (clientSecret)
        .Build ();
     
    var scopes = new string[] {
        // For IMAP and POP3, use the following scope
        //"https://ps.outlook.com/.default"
    
        // For SMTP, use the following scope
        "https://outlook.office365.com/.default"
    };
    
    var authToken = await confidentialClientApplication.AcquireTokenForClient (scopes).ExecuteAsync ();
    var oauth2 = new SaslMechanismOAuth2 (accountEmailAddress, authToken.AccessToken);
    
    using (var client = new SmtpClient ()) {
        client.Connect ("smtp.office365.com", 587, SecureSocketOptions.SslOnConnect);
        client.Authenticate (oauth2);
        client.Send (message);
        client.Disconnect (true);
    }
    

    Additional Resources

    For more information, check out the Microsoft.Identity.Client documentation.