oauth-2.0 通过EWS托管API与Exchange Online通话时刷新访问令牌

kknvjkwl  于 2022-10-31  发布在  其他
关注(0)|答案(3)|浏览(206)

我正在编写一个应用程序,以便使用EWS Managed API与Exchange Online进行通信,并通过OAuth 2.0利用ADAL库对我的应用程序进行身份验证。
访问令牌将在60分钟后过期。在此之后,我需要刷新访问令牌。目前,我正在StreamSubscriptionConnection OnNotificationEvent处理程序以及OnDisconnect事件处理程序中执行此操作,以使用以下代码刷新OAuth访问令牌。

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

    // Do my work
}

我还在OnDisconnect事件处理程序中添加了相同的刷新访问令牌代码,因为StreamSubscriptionConnection最多只能保持打开30分钟。

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

这是我的密码访问令牌。

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;
}

即使上面的方法“有效,”我还是觉得这不是处理这种情况的最佳方法,因为无论何时与EWS通信,我都需要确保刷新访问令牌。如果我添加了另一个事件处理程序,而忘记在事件处理程序中添加令牌刷新逻辑,如果我的访问令牌过期,而我需要在事件处理程序中调用EWS,那么在处理该事件时,我可能会收到401。
上面的代码是简化的,我可以在与EWS通信时输入try catch,如果我得到401,我会刷新我的访问令牌并重试,但这并不能解决我上面提到的不便。
我认为应该有一个更简单的方法来处理这个问题,但是我还没有找到合适的文档。下面是我在做开发时参考的材料。https://blogs.msdn.microsoft.com/webdav_101/2015/05/11/best-practices-ews-authentication-and-access-issues/

kx7yvsdv

kx7yvsdv1#

另一种方法是通过EWS托管API与Exchange联机通信时,需要提供exchangeService对象。并且需要捕获每个请求的401异常,在捕获此异常后,需要重新设置exchangeService对象的Credentials属性或重新创建此对象。

wooyq4lh

wooyq4lh2#

我同意应该有一个更好的方法来处理令牌刷新。如果EWS API本身可以管理令牌刷新,那就太好了,但作为一个变通办法,我所做的是...。
将对EWS服务的引用放入公共/内部属性中,该属性可a)示例化尚未示例化的服务,b)确保身份验证令牌仍然有效(如果无效,则执行令牌刷新)。然后,我们需要确保此属性是EWS服务的单一访问点。
大体上看起来

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;
        }
    }
}

我不打算详细介绍CreateExchangeService,但ValidateAuthentication是对EWS的基本调用,如果未经身份验证,它将引发异常

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

RefreshOAuthCredentials将简单地刷新令牌。类似地,我在StreamingSubscription上也有一个OnDisconnect事件处理程序,它将尝试重新打开连接(如果重新打开失败,则重新进行身份验证)。为了简洁起见,这里没有显示这两个事件处理程序。
不过,主要的一点是,我们现在有了对EWS服务的单个引用,该服务在每次调用之前执行身份验证检查和重新身份验证(如果需要)。

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

毫无疑问,这会增加流量/延迟,如果有更好的方法,这是可以避免的。如果有人有更好的方法-我洗耳恭听!

holgip5t

holgip5t3#

我不得不在netcore应用程序中处理同样的问题(使用官方EWS托管API客户端的fork,它不以任何方式触及这方面)。在完整请求之前主动做.Bind增加了流量/延迟,如PaulG的回答中所提到的。并且容易受到定时故障的影响-令牌可能在Bind本身运行时超时,下一个请求无论如何都将失败。
希望我的单点“401重试”解决方案能对大家有所帮助。

private async Task Example() {
   var view = new FolderView(100);
   view.Traversal = FolderTraversal.Shallow;
   view.Offset = 0;
   await EWS(service => service.FindFolders((FolderId)WellKnownFolderName.MsgFolderRoot, view));
}

private async Task<T> EWS<T>(Func<ExchangeService, Task<T>> action)
{
    // just creates instance/returns one from cache and does some other unimportant stuff
    var service = await _getExchangeServiceAndBuildFolderIdCache();
    try
    {
        return await action(service);
    }
    // catch (ServiceRequestException e) when ((e.InnerException as System.Net.WebException)?.Response.StatusCode == StatusCode401Unauthorized)
    // ^^ is the more correct version. Would be. Except e.InnerException.Response is already disposed here.
    // Next best option is to just do substring match on message (and hope localization doesn't mess it up).
    catch (ServiceRequestException e) when (e.InnerException?.Message.Contains("(401)") ?? false)
    {
        // this tries AcquireTokenSilent first (which has internal cache for access token, and transparently falls back to refresh token if one is available). If silent fails, it also tries full AcquireTokenXxx call
        service.Credentials = await GetEWSCredentials();
        if (service.Credentials == null)
        {
            throw;
        }
        return await action(service);
    }
}

相关问题