How to use OAuth2.0 authentication for Office 365 with Rebex Secure Mail and EWS, IMAP, POP3 or SMTP

  |   Lukas Pokorny

OAuth 2.0 authentication is quickly becoming a must-have feature for applications accessing cloud-based email services such as Microsoft 365 (Office 365, Outlook Online). Soon, it will be required. And although setting it up is relatively simple, there are many steps where things can easily go wrong.

In this article, we will show how to implement a .NET desktop WPF application that uses Rebex Secure Mail components to access an Office 365 mailbox via EWS (Exchange Web Services), IMAP, POP3, or SMTP protocols. Apart from Rebex Secure Mail and a JSON parser, the application does not rely on third-party libraries.

If your application is a service/daemon with no user present, see Office 365 with OAuth 2.0 in unattended (app-only) mode instead.

You can get our sample source code from Rebex Extras repository on GitHub straight away, and use the rest of the article as a guide to how it works.

Sample EWS app with OAuth 2.0

Quick overview

The two sample applications (one for EWS, one for IMAP) implement the whole OAuth 2.0 authorization code flow and don't rely on an external library for this. Another option is to use Microsoft.Identity.Client package - in that case, you might prefer another set of sample apps that use that API instead.

But even if you end up using Microsoft's API, this article is still useful, because the prerequisites and the final steps are the same, and it's useful to understand what's actually going on under the hood. The underlying logic is the same, Microsoft.Identity.Client just adds more abstraction layers and performs actions described in steps 5, 6 and 9 under the hood.

Before you actually run the application, make sure you have all the prerequisites:

  1. Register your application in Azure and grant it appropriate permissions.
  2. Choose a suitable "tenant".
  3. Determine scope of permissions you need.
  4. Consider additional options.

Then, in the actual application, you would do this:

  1. Direct the user to an authentication URL (via embedded browser).
  2. Retrieve the authentication code and redeem it for an access token.
  3. Access the mailbox using EWS.
  4. Or using IMAP, POP3, or SMTP with one extra simple step.
  5. Refresh the access token (if needed).

This applies to desktop Windows applications using delegated authentication. That simply means applications with a single signed-in user present. Most mobile applications would use the same approach as well.

Note: For applications with no signed-in user present (such as services or daemons), the process is a bit different, and the application needs to be configured for unattended (app-only) authentication. This is outside the scope of this article - see Office 365 with OAuth 2.0 in app-only mode instead.

Now, let's get started:

1. Register your application with Microsoft

Before you can use OAuth 2.0 authentication for accessing Office 365 mailbox via EWS, IMAP, POP3 or SMTP protocols, you have to register your application with Azure Active Directory and configure it properly.

During the process, you will obtain your application's Client ID, which is a GUID that uniquely identifies your application. You will also specify a tenant and scope of permissions (more on these below).

2. Specify a suitable "tenant"

You also have to specify what kinds of Microsoft users are going to use your application. This means choosing a suitable Tenant ID value that corresponds to your application registration ("Supported account types" in the app registration form). It will usually be common (users with personal Microsoft accounts or work/school accounts), organizations (only users with work/school accounts), or consumers (only users with personal accounts).

Alternatively, if you only wish to allow users from your organization, it could be a domain name or a GUID (see the endpoints documentation) for details.

In our sample applications, we used organizations, but you can modify that easily.

3. Determine scope of permissions

Before you can direct a user to Microsoft's server to authorize your app, you also have to determine which kinds of scopes (permissions) to actually ask for.

Microsoft identity platform's scopes are not exactly trivial, but for the purpose of mailbox access, only few of them are needed:

  • "profile" - This one is needed to retrieve the user name, which is required for Office 365's IMAP, POP3 and SMTP authentication. It's not needed for EWS.
  • "email" - This is not required, but at least for SMTP, you'll probably want to retrieve it.
  • "openid" - If you request profile or email, you'll have to include this as well to retrieve the JSON Web Token that contains that information.
  • "offline_access" - This permission makes it possible for the application to keep accessing the mailbox even when the original access token expires after an hour. With offline_access, the application receives a refresh token which can be used to obtain a new access token.
  • "https://outlook.office365.com/EWS.AccessAsUser.All" - Required for accessing Office 365 via EWS.
  • "https://outlook.office365.com/IMAP.AccessAsUser.All" - Required for accessing Office 365 via IMAP.
  • "https://outlook.office365.com/POP.AccessAsUser.All" - Required for accessing Office 365 via POP3.
  • "https://outlook.office365.com/SMTP.Send" - Required for accessing Office 365 via SMTP.

Make sure that you actually granted the required permissions to your application in Azure Portal.

4. Consider additional options

You might also wish specify prompt type, which determines the type of required user interaction. The following values are available:

  • "" - This is the default mode. It authentication server will not request the credentials if the user has already signed in.
  • "login" - The authentication server will always request user's credentials.
  • "none" - The server will never ask the user for credentials, which means this will only work once the user has already signed in.
  • "consent" - This will explicitly ask the user to grant requested permissions to the app (even if he already did before).
  • "select_account" - This will present the user with account selection dialog, which can be useful for users with multiple Microsoft accounts.

For detailed information about prompt types, see the documentation.

Another option that varies from app to app is redirect URI. This indicates where the authentication server should redirect the user once successfully authenticated (or failed). However, for the purpose of desktop or mobile application with an embedded browser, we can simply use a special URL that exists specifically for this purpose.

In our sample app, this special URL is used by default.

5. Direct the user to an authentication URL

Once the application has all of the information above (put them here for EWS or here for IMAP), it's ready to ask the user to grant it the required permissions to access his or her Office 365 mailbox. It does so by opening a window with an embedded browser for the user, and points it to an URL at Microsoft's authentication server.

In our sample app, we implemented a OAuthAzureAuthorizationWindow class for this purpose, and initiating the process is very simple:

// create a Window that handles OAuth2 authorization
var authenticationWindow = new OAuthAzureAuthorizationWindow();
authenticationWindow.Owner = this;

// specify the kind of authorization we need
authenticationWindow.ClientId = ClientId; // application's client ID
authenticationWindow.TenantId = "organizations"; // kinds of users to allow
authenticationWindow.PromptType = ""; // prompt type to use
authenticationWindow.Scopes = Scopes; // scope of permissions to request

// perform the authorization and obtain the credentials
var credentials = await authenticationWindow.AuthorizeAsync(); 

The source code for OAuthAzureAuthorizationWindow class comes with the sample app as well, and can be easily customized. This is what the AuthorizeAsync method actually does:

// get redirect URI and determine expected authority
string redirectUri = RedirectUri ?? OAuthAzureCredentials.DefaultRedirectUri;

// create an instance of OAuthAzureCredentials helper class
_credentials = new OAuthAzureCredentials(ClientId, TenantId, PromptType, redirectUri, Scopes);

// create task completion source for the whole action
_taskCompletion = new TaskCompletionSource<OAuthAzureCredentials>();

// Direct the user to authorization endpoint.
// Once completed, the application will receive an authorization code.

// show the window and navigate to authentication URI
Show();
webBrowser.Navigate(_credentials.AuthorizationUri);

// return task that will finish on succcess or failure
return _taskCompletion.Task;

The OAuthAzureCredentials helper class (source included) encapsulates the OAuth 2.0 authentication logic, while OAuthAzureAuthorizationWindow class adds WPF-specific bits - mostly the embedded browser window provided by .NET's WPF WebBrowser class.

6. Retrieve the authentication code and redeem it for an access token

Once the user has authenticated to Microsoft's server and granted us permissions, the browser window will be directed to an URL that contains an authentication code. This is essential, as this is what the application needs in order to obtain an OAuth 2.0 access token for accessing Office 365 mailboxes.

Our WPF authentication window class grabs the code from the redirected URL's query string and calls OAuthAzureCredentials's RequestAccessTokenAsync method to redeem it:

// get query part of the browser URL
var query = System.Web.HttpUtility.ParseQueryString(e.Uri.Query);

...

// get authorization code from the supplied URL
string authorizationCode = query["code"];
if (authorizationCode == null)
{
    // we are not on the final authentication page yet...
    return;
}

// The user has granted us permissions, and we received an authorization code.
// Next, we will exchange the code for an access token using the '/token' endpoint.

await _credentials.RequestAccessTokenAsync(authorizationCode);

// close the window and finish the successful asynchronous authentication request
Close();
_taskCompletion.SetResult(_credentials);

And this is what actually goes on inside the RequestAccessTokenAsync method:

// construct the request body
string requestBody = string.Format(
    CultureInfo.InvariantCulture,
    "grant_type=authorization_code&code={0}&scope={1}&redirect_uri={2}&client_id={3}",
    authorizationCode,
    _scopes,
    Uri.EscapeDataString(RedirectUri),
    _clientId);

// send it via POST request and receive response
string responseJson = await HttpPostAsync(_tokenEndPoint, requestBody);

// deserialize JSON response
var response = DeserializeJson(responseJson);

// get OAuth 2.0 access token
AccessToken = GetStringValue(response, "access_token");
...

Here, we construct an HTTP POST request and send it to an endpoint at Microsoft's server (the actual URL is https://login.microsoftonline.com/{tenant}/oauth2/v2.0/token, where {tenant} is the tenant ID, as explained above).

The server will reply with a JSON response, which contains not only the OAuth 2.0 access token, but also additional information, such as user name, e-mail, or refresh token (based on the scope of permissions requested).

To parse the JSON response, we used Microsoft's new JSON API, but you can easily use the popular Newtonsoft.Json package as well - just add a reference to it, uncomment the relevant line of code in DeserializeJson method (and remove MS API code).

7. Access the mailbox using EWS

Now that the application finally has the access token, it can access the user's mailbox at outlook.office365.com via the protocol we need using one of Rebex Secure Mail components.

With EWS (Exchange Web Services), this is very straightforward:

// connect to the server
var client = new Rebex.Net.Ews();
await client.ConnectAsync("outlook.office365.com", SslMode.Implicit);

// authenticate using the OAuth 2.0 access token
await client.LoginAsync(_credentials.AccessToken, EwsAuthentication.OAuth20);

// do something
EwsFolderInfo folderInfo = await client.GetFolderInfoAsync(EwsFolderId.Inbox);

// or something else
EwsMessageCollection list = await client.GetMessageListAsync(EwsFolderId.Inbox, ...);

...

8. Or access the mailbox using IMAP, POP3, or SMTP

For IMAP, POP3, or SMTP, one extra step is needed before the application can access the mailbox. We have to create a token suitable for these protocols that encodes the OAuth 2.0 access token and the user name:

// prepare authentication token suitable for IMAP, POP3, or SMTP
string userName = _credentials.UserName;
string accessToken = _credentials.AccessToken;
string pattern = string.Format("user={0}{1}auth=Bearer {2}{1}{1}", userName, '\x1', accessToken);
string token = Convert.ToBase64String(Encoding.ASCII.GetBytes(pattern));

We are not quite sure why this is the case, but Office 365 apparently won't allow access if the user name is not specified, which is why you have to ask for openid and profile scopes as well if you need to use one of these three protocols.

// connect to the server
var client = new Imap();
await client.ConnectAsync("outlook.office365.com", SslMode.Implicit);

// authenticate using the constructed token
await client.LoginAsync(token, ImapAuthentication.OAuth20);

// do something
await client.SelectFolderAsync("Inbox", readOnly: true);

In case of SMTP, it's useful to ask for email scope as well (in addition to openid and profile). Otherwise, the SMTP application would not know which 'From' address to use for outgoing messages.

9. Refreshing the access token (if needed)

The access token expires in one hour. If the application needs to keep accessing the mailbox for longer periods of time without asking the user to authenticate again, it also has to take care of refreshing the access token. To be able to do this, it must have been granted the offline_access scope.

To refresh the access token, application calls the RefreshAccessTokenAsync method, which again constructs a relevant HTTP POST request and posts it to an endpoint at Microsoft's server. The server responds with a new access and refresh tokens:

// construct the request body
string requestBody = "grant_type=refresh_token&client_id=" + _clientId + "&refresh_token=" + RefreshToken;

// send it via POST request and receive response
string responseJson = await HttpPostAsync(_tokenEndPoint, requestBody);

// deserialize JSON response
var response = DeserializeJson(responseJson);

// update OAuth 2.0 access token and refresh token
AccessToken = GetStringValue(response, "access_token");
RefreshToken = GetStringValue(response, "refresh_token");

What next?

We tried to make the sample code simple to read and reuse, and although we utilized .NET 4.5's await/async extensively, the code can be easily converted to .NET Framework 3.5 or 4.0. Just make sure the embedded browser on the target platform supports TLS 1.2, otherwise the user would not be able to authenticate. Actually, we have already converted the code for you.

You might also want to consider more options such as domain_hint, state or code_challenge. These are not required for desktop apps (yet), but it might be a good idea to already take advantage of them for the extra security they offer.

Another option to consider is to discard our simple OAuth 2.0 helper classes and use Microsoft.Identity.Client API instead. This is available for .NET 4.5 or later, and makes it possible to perform the whole process without the need to delve into the internals. It's a powerful API, but also somewhat complex, and seemingly simple actions might be hard to figure out. But once you get it working, you get all the latest features with no extra effort. If you prefer this option, we already have a ready-to-use sample apps for EWS and IMAP as well.