3
votes

I am writing an application to communicate with Exchange Online using the EWS Managed API and authenticate my application via OAuth 2.0 utilizing ADAL library.

The access token expires after 60 minutes. After which I need to refresh the access token. Currently, I am doing it in StreamSubscriptionConnection OnNotificationEvent handler, as well as my OnDisconnect event handler to refresh the OAuth access token using the following code.

private void OnNotificationEventHandler(object sender, NotificationEventArgs args)
{
    exchangeService.Credentials = new OAuthCredentials(GetOAuthAccessToken().Result);

    // Do my work
}

I also added the same refresh access token code in my OnDisconnect event handler since StreamSubscriptionConnection is only kept open for 30 minutes at most.

private void OnDisconnectEventHandler(object sender, SubscriptionErrorEventArgs args)
{
    exchangeService.Credentials = new OAuthCredentials(GetOAuthAccessToken().Result);
    streamingSubscriptionConnection.Open();
}

Here is the code I have for access token.

private async Task<string> GetOAuthAccessToken(PromptBehavior promptBehavior = PromptBehavior.Auto)
{
    var authenticationContext = new AuthenticationContext(myAadTenant);

    var authenticationResult = await authenticationContext.AcquireTokenAsync(exchangeOnlineServerName, myClientId, redirectUri, new PlatformParameters(promptBehavior));

    return authenticationResult.AccessToken;
}

Even thought the above approach "works", I feel like this isn't the best way of handling the situation because I pretty much need to make sure I refresh my access token whenever I communicate with EWS. If I add another event handler and I forget to add token refresh logic inside the event handler, I could potentially get a 401 while handling that event if my access token expires and yet I need to call EWS in the event handler.

The code above is simplified, I could put try catch whenever I communicate with EWS and if I get 401, I refresh my access token and try again but that doesn't solve the inconvenience I mentioned above.

I think there should be an easier way to handle the problem but I haven't found the right documentations. Here is the material I referred to while doing my development. https://blogs.msdn.microsoft.com/webdav_101/2015/05/11/best-practices-ews-authentication-and-access-issues/

2
Why not just use a timer thread to refresh the token every n minutes, and have it share some kind of lock with all authenticated web requests so you avoid the race condition? You could even reset the timer with each successful web request if EWS extends the timeout with each request. - hoodaticus
I don't think having a timer thread to refresh the token every n minutes would solve the issue. The token expires every 60 minutes and my application needs to respond to new mail event. So if the next timer refresh the token on 65 minutes mark but an event was received on 62 minute mark, then I would still have the problem. In order to handle that, I still need to refresh the token before I respond to the event and call EWS. I want to know if I could set up my EWS instance to automatically refresh token if needed. - Jackson H
then don't set your timer to refresh every 65 minutes lol. If the refresh period is 60 minutes I'd do it every 50. Goodness. - hoodaticus
That wouldn't work because you do not get a new OAuth access token until the the one you already have has expired. So calling AcquireTokenAsync() on 55 minute mark will not refresh the token. Unless, of course, there is a way to force renewal that I am not aware of. - Jackson H
Approach #2 is to simply re-acquire the token in response to a request erroring out. - hoodaticus

2 Answers

1
votes

Another way is when you communicate with Exchange online via EWS Managed API, you need to provide the exchangeService object. And you need to catch the 401 exception for every request and after got this exception, you need to re-set the Credentials property for the exchangeService object or re-create this object.

0
votes

I agree that there should be a nicer way to handle token refresh. It would be nice if the EWS API itself could manage token refresh, but as a workaround what I did was..

Put the reference to the EWS service into a public/internal property that can a) instantiate the service if it has not yet been instantiated, and b) ensures the authentication token is still valid (and if not, then perform token refresh). Then we need to make sure that this property is the single access point to the EWS service.

Broadly, this looks like

public class Mailbox 
{
    private ExchangeService exchangeService;

    public ExchangeService ExchangeService
    {
        get
        {
            if (this.exchangeService == null)
            {
                // Initialise the service
                CreateExchangeService();
            }
            else
            {
                // Ensure token is still valid
                ValidateAuthentication();
            }

            return this.exchangeService;
        }
    }
}

I'm not going to detail the CreateExchangeService, but the ValidateAuthentication is a basic call to EWS that will throw an exception if unauthenticated

private void ValidateAuthentication()
{
    try
    {
        Folder inbox = Folder.Bind(this.exchangeService, WellKnownFolderName.Inbox);
    }
    catch //(WebException webEx) when (((HttpWebResponse)webEx.Response).StatusCode == HttpStatusCode.Unauthorized)
    {
        RefreshOAuthCredentials();
    }
}

RefreshOAuthCredentials will simply refresh the token. Similarly I also have a OnDisconnect event handler on the StreamingSubscription that will attempt to reopen the connection (and reauth if the reopen fails). Neither of these are shown here for brevity.

The main point though is that we now have a single reference to the EWS service that performs an authentication check and reauth (if necessary) prior to every call. For example binding to a mail message looks like..

var mailMessage = EmailMessage.Bind(this.mailbox.ExchangeService, itemId);

No doubt this adds traffic/latency which could be avoided if there was a better way. And if anyone has a better way - I'm all ears!