OAuth 2.0 device flow with Office365/Exchange IMAP/POP3/SMTP

This article shows how to implement OAuth 2.0 device flow to access Office365 via IMAP, POP3 or SMTP using Mail.dll .net email client.

Device flow allows operator/administrator to authenticate your application on a different machine than your application is installed.

Make sure IMAP/POP3/SMTP is enabled for your organization and mailbox:
Enable IMAP/POP3/SMTP in Office 365

Register your application in Azure Portal, here’s a detailed guide how to do that:
https://docs.microsoft.com/en-us/azure/active-directory/develop/quickstart-register-app

Then you need to apply correct API permissions and grant the admin consent for your domain.

In the API permissions / Add a permission wizard, select Microsoft Graph and then Delegated permissions to find the following permission scopes listed:

  • offline_access
  • email
  • IMAP.AccessAsUser.All
  • POP.AccessAsUser.All
  • SMTP.Send

Remember to Grant admin consent.

Use Microsoft Authentication Library for .NET (MSAL.NET) nuget package to obtain an access token:
https://www.nuget.org/packages/Microsoft.Identity.Client/

string clientId = "Application (client) ID";
string tenantId = "Directory (tenant) ID";

IPublicClientApplication app = PublicClientApplicationBuilder
    .Create(clientId)
    .WithTenantId(tenantId)
    .Build();
// This allows saving access/refresh tokens to some storage
TokenCacheHelper.EnableSerialization(app.UserTokenCache);

var scopes = new string[] 
{
    "offline_access",
    "email",
    "https://outlook.office.com/IMAP.AccessAsUser.All",
    "https://outlook.office.com/POP.AccessAsUser.All",
    "https://outlook.office.com/SMTP.Send",
};

Now acquire an access token and a user name:

string userName;
string accessToken;

var account = (await app.GetAccountsAsync()).FirstOrDefault();
try
{
    AuthenticationResult refresh = await app
        .AcquireTokenSilent(scopes, account)
        .ExecuteAsync();

    userName = refresh.Account.Username;
    accessToken = refresh.AccessToken;
}
catch (MsalUiRequiredException e)
{
    var acquire = await app.AcquireTokenWithDeviceCode(
        scopes, 
        callback=>
    {
        // Write url and code to logs so the operator can react:
        Console.WriteLine(callback.VerificationUrl);
        Console.WriteLine(callback.UserCode);

        // This happens on the first run, manually,
        //  on the operator machine.
        // The code below code is only to illustrate 
        // the operator opening browser on his machine,
        // opening the url and using the code 
        // (extracted from the application logs)
        // to authenticate the app.
        System.Diagnostics.Process.Start(
            new ProcessStartInfo(result.VerificationUrl) 
                        { UseShellExecute = true }
            );

        return Task.CompletedTask;
    }).ExecuteAsync();

    userName = acquire.Account.Username;
    accessToken = acquire.AccessToken;
}

AcquireTokenWithDeviceCode call waits until operator/administrator gives consent by going to VerificationUrl, entering UserCode and authenticating – this usually happens on a different machine than the application is installed.

Finally your app will exit AcquireTokenWithDeviceCode method and connect using IMAP/POP3/SMTP, authenticate and download emails:

using (Imap client = new Imap())
{
    client.ConnectSSL("outlook.office365.com");
    client.LoginOAUTH2(user, accessToken);
 
    client.SelectInbox();

    // ...

    client.Close();
} 

You can find more details on this flow here:

https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-device-code

Token serialization

Below is a simple implementation that saves MSAL token cache to file. Please note that most likely you should store this cache in an encrypted form:

static class TokenCacheHelper
{
    public static void EnableSerialization(ITokenCache tokenCache)
    {
        tokenCache.SetBeforeAccess(BeforeAccessNotification);
        tokenCache.SetAfterAccess(AfterAccessNotification);
    }

    private static readonly string _fileName = "msalcache.bin3";

    private static readonly object _fileLock = new object();


    private static void BeforeAccessNotification(TokenCacheNotificationArgs args)
    {
        lock (_fileLock)
        {
            byte[] data = null;
            if (File.Exists(_fileName))
                data = File.ReadAllBytes(_fileName);
            args.TokenCache.DeserializeMsalV3(data);
        }
    }

    private static void AfterAccessNotification(TokenCacheNotificationArgs args)
    {
        if (args.HasStateChanged)
        {
            lock (_fileLock)
            {
                byte[] data = args.TokenCache.SerializeMsalV3();
                File.WriteAllBytes(_fileName, data);
            }
        }
    }
};

More details on MSAL token serialization are available here:

https://docs.microsoft.com/en-us/azure/active-directory/develop/msal-net-token-cache-serialization

Extending Sign-in frequency with policies

You can extend how often operator needs to re-authenticate the application up to 1 year:

Side note: Have in mind that similarly a client credential flow requires a client secret which is valid for 2 years maximum.

Tags:     

Questions?

Consider using our Q&A forum for asking any questions.