Repository: robocorp/rpaframework

Issue:
`RPA.Email.Exchange` OAuth2 `exchangelib` token refresh failing on single-tenant apps
Initializing the flow and and retrieving the token works.
Refreshing the token with our `RPA.MFA` works as well.
But getting the token auto-refreshed by `exchangelib` fails beacause the library uses a **common** URL for the token refresh.

```
InvalidClientIdError: (invalid_request) AADSTS50194: Application 'b546eb47-6740-488d-942a-8a3a1a11f251'(Robocorp - Web app) is not configured as a multi-tenant application. Usage of the /common endpoint is not supported for such applications created after '10/15/2018'. Use a tenant-specific endpoint or configure the application to be multi-tenant
```

### Solution

- Patch the 3rd-party library in such a way it will be able to use tenant-customized token refresh URLs for refreshing the token with the right URL, thus avoiding this error.
- ~Optionally notify the author about this feature request in the origin library.~ (credentials behaviour override is possible)


Discussion:

Resolving patch:
diff --git a/packages/main/src/RPA/Email/Exchange.py b/packages/main/src/RPA/Email/Exchange.py
--- a/packages/main/src/RPA/Email/Exchange.py
+++ b/packages/main/src/RPA/Email/Exchange.py
@@ -30,7 +30,12 @@
 from exchangelib.folders import Inbox
 from oauthlib.oauth2 import OAuth2Token
 
-from RPA.Email.common import OAuthMixin, OAuthProvider, counter_duplicate_path
+from RPA.Email.common import (
+    OAuthConfig,
+    OAuthMixin,
+    OAuthProvider,
+    counter_duplicate_path,
+)
 from RPA.Robocorp.Vault import Vault
 from RPA.Robocorp.utils import protect_keywords
 
@@ -77,16 +82,31 @@ class OAuth2Creds(OAuth2AuthorizationCodeCredentials):
 
     """OAuth2 auth code flow credentials wrapper supporting token state on refresh."""
 
-    def __init__(self, *args, on_token_refresh: Callable, **kwargs):
+    def __init__(
+        self, *args, on_token_refresh: Callable, oauth_provider: OAuthConfig, **kwargs
+    ):
         super().__init__(*args, **kwargs)
         # Additional behaviour to trigger during token refresh (when it expires), like
         #  saving the newly generated structure in the Vault.
         self._on_token_refresh = on_token_refresh
+        self._token_url = oauth_provider.token_url
+        self._scope = oauth_provider.scope
 
     def on_token_auto_refreshed(self, access_token: OAuth2Token):
+        """Saves the newly obtained token internally and in the Vault."""
         super().on_token_auto_refreshed(access_token)
         self._on_token_refresh(access_token)
 
+    @property
+    def token_url(self) -> str:
+        """Custom token URL coming from the OAuth2 provider settings."""
+        return self._token_url
+
+    @property
+    def scope(self) -> List[str]:
+        """Custom permissions list coming from the OAuth2 provider settings."""
+        return self._scope.split()
+
 
 class Exchange(OAuthMixin):
     """`Exchange` is a library for sending, reading, and deleting emails.
@@ -264,7 +284,8 @@ def __init__(
         tenant: Optional[str] = None,
     ) -> None:
         # Init the OAuth2 support. (mandatory usage)
-        super().__init__(OAuthProvider.MICROSOFT, tenant=tenant or "common")
+        self._tenant = tenant or "common"
+        super().__init__(OAuthProvider.MICROSOFT, tenant=self._tenant)
 
         protect_keywords("RPA.Email.Exchange", self.TO_PROTECT)
         self.logger = logging.getLogger(__name__)
@@ -341,6 +362,8 @@ def authorize(
         if is_oauth:
             self.credentials = OAuth2Creds(
                 on_token_refresh=self.on_token_refresh,
+                oauth_provider=self._oauth_provider,
+                tenant_id=self._tenant,
                 identity=Identity(upn=username),
                 client_id=client_id,
                 client_secret=client_secret,


Test patch: