aboutsummaryrefslogtreecommitdiffstats
path: root/cloud_mdir_sync/office365.py
diff options
context:
space:
mode:
Diffstat (limited to 'cloud_mdir_sync/office365.py')
-rw-r--r--cloud_mdir_sync/office365.py136
1 files changed, 79 insertions, 57 deletions
diff --git a/cloud_mdir_sync/office365.py b/cloud_mdir_sync/office365.py
index 7d6fd86..d76c891 100644
--- a/cloud_mdir_sync/office365.py
+++ b/cloud_mdir_sync/office365.py
@@ -2,6 +2,7 @@
import asyncio
import datetime
import functools
+import json
import logging
import os
import pickle
@@ -10,7 +11,7 @@ import webbrowser
from typing import Any, Dict, Optional, Union
import aiohttp
-import requests
+import oauthlib
from . import config, mailbox, messages, oauth, util
from .util import asyncio_complete
@@ -27,7 +28,8 @@ def _retry_protect(func):
@functools.wraps(func)
async def async_wrapper(self, *args, **kwargs):
while True:
- while (self.graph_token is None or self.owa_token is None):
+ while ("Authorization" not in self.headers
+ or "Authorization" not in self.owa_headers):
await self.authenticate()
try:
@@ -38,8 +40,6 @@ def _retry_protect(func):
)
if (e.code == 401 or # Unauthorized
e.code == 403): # Forbidden
- self.graph_token = None
- self.owa_token = None
await self.authenticate()
continue
if e.code == 429: # Too Many Requests
@@ -80,9 +80,11 @@ def _retry_protect(func):
class GraphAPI(oauth.Account):
"""An OAUTH2 authenticated session to the Microsoft Graph API"""
+ client_id = "122f4826-adf9-465d-8e84-e9d00bc9f234"
graph_scopes = [
"https://graph.microsoft.com/User.Read",
- "https://graph.microsoft.com/Mail.ReadWrite"
+ "https://graph.microsoft.com/Mail.ReadWrite",
+ "offline_access",
]
graph_token: Optional[Dict[str,str]] = None
owa_scopes = ["https://outlook.office.com/mail.read"]
@@ -106,11 +108,12 @@ class GraphAPI(oauth.Account):
self.owa_headers: Dict[str, str] = {}
async def go(self):
- import msal
- self.msl_cache = msal.SerializableTokenCache()
auth = self.cfg.msgdb.get_authenticator(self.domain_id)
- if auth is not None:
- self.msl_cache.deserialize(auth)
+ if isinstance(auth, dict):
+ self.graph_token = auth
+ # the msal version used a string here
+ else:
+ self.graph_token = None
connector = aiohttp.connector.TCPConnector(
limit=MAX_CONCURRENT_OPERATIONS,
@@ -118,78 +121,93 @@ class GraphAPI(oauth.Account):
self.session = aiohttp.ClientSession(connector=connector,
raise_for_status=False)
- self.msal = msal.PublicClientApplication(
- client_id="122f4826-adf9-465d-8e84-e9d00bc9f234",
- authority=f"https://login.microsoftonline.com/{self.tenant}",
- token_cache=self.msl_cache)
-
if self.oauth_smtp:
self.owa_scopes = self.owa_scopes + [
"https://outlook.office.com/SMTP.Send"
]
- def _cached_authenticate(self):
- accounts = self.msal.get_accounts(self.user)
- if len(accounts) != 1:
+ self.redirect_url = self.cfg.web_app.url + "oauth2/msal"
+ self.oauth = oauth.OAuth2Session(
+ client_id=self.client_id,
+ client=oauth.NativePublicApplicationClient(self.client_id),
+ redirect_uri=self.redirect_url,
+ token=self.graph_token,
+ strict_scopes=False)
+
+ await self._do_authenticate()
+
+ def _set_token(self, graph_token, owa_token):
+ # Only store the refresh token, access tokens are more dangerous to
+ # keep as they are valid across a password change for their lifetime
+ self.cfg.msgdb.set_authenticator(
+ self.domain_id,
+ {"refresh_token": graph_token["refresh_token"]})
+ self.headers["Authorization"] = graph_token[
+ "token_type"] + " " + graph_token["access_token"]
+ self.owa_headers["Authorization"] = owa_token[
+ "token_type"] + " " + owa_token["access_token"]
+ self.graph_token = graph_token
+ self.owa_token = owa_token
+ return True
+
+ async def _refresh_authenticate(self):
+ if self.graph_token is None:
return False
try:
- if self.graph_token is None:
- self.graph_token = self.msal.acquire_token_silent(
- scopes=self.graph_scopes, account=accounts[0])
- if self.graph_token is None or "access_token" not in self.graph_token:
- self.graph_token = None
- return False
-
- if self.owa_token is None:
- self.owa_token = self.msal.acquire_token_silent(
- scopes=self.owa_scopes, account=accounts[0])
- if self.owa_token is None or "access_token" not in self.owa_token:
- self.owa_token = None
- return False
- except requests.RequestException as e:
- self.cfg.logger.error(f"msal failed on request {e}")
+ graph_token, owa_token = await asyncio.gather(
+ self.oauth.refresh_token(
+ self.session,
+ f'https://login.microsoftonline.com/{self.tenant}/oauth2/v2.0/token',
+ client_id=self.client_id,
+ scopes=self.graph_scopes,
+ refresh_token=self.graph_token["refresh_token"]),
+ self.oauth.refresh_token(
+ self.session,
+ f'https://login.microsoftonline.com/{self.tenant}/oauth2/v2.0/token',
+ client_id=self.client_id,
+ scopes=self.owa_scopes,
+ refresh_token=self.graph_token["refresh_token"]))
+ except oauthlib.oauth2.OAuth2Error:
self.graph_token = None
self.owa_token = None
return False
-
- self.headers["Authorization"] = self.graph_token[
- "token_type"] + " " + self.graph_token["access_token"]
- self.owa_headers["Authorization"] = self.owa_token[
- "token_type"] + " " + self.owa_token["access_token"]
- self.cfg.msgdb.set_authenticator(self.domain_id,
- self.msl_cache.serialize())
- return True
+ return self._set_token(graph_token, owa_token)
@util.log_progress(lambda self: f"Azure AD Authentication for {self.name}")
async def _do_authenticate(self):
- while not self._cached_authenticate():
+ while not await self._refresh_authenticate():
self.graph_token = None
self.owa_token = None
- redirect_url = self.cfg.web_app.url + "oauth2/msal"
state = hex(id(self)) + secrets.token_urlsafe(8)
- url = self.msal.get_authorization_request_url(
- scopes=self.graph_scopes + self.owa_scopes,
+ url = self.oauth.authorization_url(
+ f'https://login.microsoftonline.com/{self.tenant}/oauth2/v2.0/authorize',
state=state,
- login_hint=self.user,
- redirect_uri=redirect_url)
+ scopes=self.graph_scopes + self.owa_scopes,
+ login_hint=self.user)
print(
f"Goto {self.cfg.web_app.url} in a web browser to authenticate"
)
webbrowser.open(url)
- q = await self.cfg.web_app.auth_redir(url, state, redirect_url)
- code = q["code"]
-
- try:
- self.graph_token = self.msal.acquire_token_by_authorization_code(
- code=code,
- scopes=self.graph_scopes,
- redirect_uri=redirect_url)
- except requests.RequestException as e:
- self.cfg.logger.error(f"msal failed on request {e}")
- await asyncio.sleep(10)
+ q = await self.cfg.web_app.auth_redir(url, state,
+ self.redirect_url)
+
+ graph_token = await self.oauth.fetch_token(
+ self.session,
+ f'https://login.microsoftonline.com/{self.tenant}/oauth2/v2.0/token',
+ include_client_id=True,
+ scopes=self.graph_scopes,
+ code=q["code"])
+ owa_token = await self.oauth.refresh_token(
+ self.session,
+ f'https://login.microsoftonline.com/{self.tenant}/oauth2/v2.0/token',
+ client_id=self.client_id,
+ scopes=self.owa_scopes,
+ refresh_token=graph_token["refresh_token"])
+ if self._set_token(graph_token, owa_token):
+ return
async def authenticate(self):
"""Obtain OAUTH bearer tokens for MS services. For users this has to be done
@@ -198,6 +216,10 @@ class GraphAPI(oauth.Account):
tokens within some limited time period."""
# Ensure we only ever have one authentication open at once. Other
# threads will all block here on the single authenticator.
+ if "Authorization" in self.headers:
+ del self.headers["Authorization"]
+ if "Authorization" in self.owa_headers:
+ del self.owa_headers["Authorization"]
if self.authenticator is None:
self.authenticator = asyncio.create_task(self._do_authenticate())
auth = self.authenticator