vb.netexchangewebservicesoffice365api

401 Error when using EWS after period of time


I'm having an issue with 401 errors appearing after a certain amount of time (I believe due to the Token Expiry) in my application. The code I have for the Exchange connection is:

Connect is called every time a user does anything related to EWS i.e selecting an email in the CRM Program.

Public Shared Function Connect() As ExchangeService
    app = PublicClientApplicationBuilder.Create(ClientID).WithAuthority(Authority).WithRedirectUri(RedirectUri).Build()

    ' Authenticate the user and get the ExchangeService object
    Dim ewsService As ExchangeService = InitializeEwsService()

    ' Return the initialized ExchangeService object
    Return ewsService
End Function

InitialzeEwsSErvice:

        Private Shared Function InitializeEwsService() As ExchangeService
            Authenticate() ' Ensure authentication before initializing EWS

            Dim ewsService As New ExchangeService(ExchangeVersion.Exchange2013)
            ewsService.Url = New Uri(EwsUrl)
            ewsService.Credentials = New OAuthCredentials(authenticationResult.AccessToken)
            Return ewsService
        End Function
Private Shared Sub Authenticate()
    Try
        ' Check if we already have a valid token
        If authenticationResult Is Nothing OrElse authenticationResult.ExpiresOn.UtcDateTime <= DateTime.UtcNow Then
            ' Attempt silent authentication
            Try
                authenticationResult = app.AcquireTokenSilent(Scopes, authenticationResult.Account).ExecuteAsync().Result
            Catch ex As MsalUiRequiredException
                ' Silent authentication failed, fallback to interactive login
                Try
                    authenticationResult = app.AcquireTokenInteractive(Scopes).ExecuteAsync().Result
                Catch interactiveEx As Exception
                    ' Handle authentication error
                    Throw New Exception("Interactive authentication failed: " & interactiveEx.Message)
                End Try
            Catch ex As Exception
                ' Silent authentication failed, fallback to interactive login
                authenticationResult = app.AcquireTokenInteractive(Scopes).ExecuteAsync().Result


            End Try
        End If
    Catch ex As AggregateException
        ' Handle authentication error
        Throw New Exception("Authentication failed: " & ex.InnerException.Message)
    End Try
End Sub

I've even tried a crude reset of everything when the emailRead code is triggered it catches the 401 error and runs a Handle401 function which clears everything, but even after a sign in it just gets stuck in a loop of a 401Handle error.

I also notice the sign in is different, when the app first loads the users has to enter their password and then do MFA, after an hour or so the user does get the sign in window appearing, but they click their email and are never prompted for a password which leads straight to the 401 error, it's almost as though the expired token is never being refreshed but I can't figure out what I am missing?


Solution

  • If you want a token to be renewed then you need to use MSAL token cache see https://learn.microsoft.com/en-us/entra/msal/dotnet/acquiring-tokens/acquire-token-silently so this

    app = PublicClientApplicationBuilder.Create(ClientID).WithAuthority(Authority).WithRedirectUri(RedirectUri).Build()
    

    Should only be created once else you won't have a cache to use doing InitializeEwsService for every EWS request would work but in that case I would make

    If authenticationResult Is Nothing OrElse authenticationResult.ExpiresOn.UtcDateTime <= DateTime.UtcNow Then
    

    This isn't necessary as MSAL will handle token expiration and renewal when you call app.AcquireTokenSilent it will either get a new token if one doesn't exists, use the cached token if it does or renew the cached token from the refresh token if its expired.

    One problem with the EWS Managed API is that it doesn't offer an authentication call back, recreating the ExchangeService each time should work but if you do that i would suggest you make Authenticate return the access token rather having called AcquireToken the scope of authenticationResult should only be function level.

    You can modify the source of the EWS Managed API to make the ExchangeService call back to check the MSAL token cache each time which makes you code a lot more efficient and readable see https://github.com/gscales/EWS-BasicToOAuth-Info/blob/main/EWA%20Managed%20API%20MASL%20Token%20Refresh.md