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?
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