Skip to content

Commit

Permalink
introduced authentication context for graph client, a few fixes for o…
Browse files Browse the repository at this point in the history
…nedrive tests
  • Loading branch information
vgrem committed Jan 12, 2025
1 parent f5230e8 commit 53eddff
Show file tree
Hide file tree
Showing 10 changed files with 281 additions and 121 deletions.
2 changes: 2 additions & 0 deletions examples/auth/interactive.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,5 @@
client = GraphClient.with_token_interactive(test_tenant, test_client_id)
me = client.me.get().execute_query()
print("Welcome, {0}!".format(me.given_name))
site = client.sites.root.get().execute_query()
print("Site Url: {0}!".format(site.web_url))
23 changes: 4 additions & 19 deletions examples/auth/with_user_creds.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,26 +4,11 @@
https://github.com/AzureAD/microsoft-authentication-library-for-python/wiki/Username-Password-Authentication
"""

import msal

from office365.graph_client import GraphClient
from tests import test_client_id, test_tenant, test_user_credentials


def acquire_token():
authority_url = "https://login.microsoftonline.com/{0}".format(test_tenant)
app = msal.PublicClientApplication(
authority=authority_url, client_id=test_client_id
)

result = app.acquire_token_by_username_password(
username=test_user_credentials.userName,
password=test_user_credentials.password,
scopes=["https://graph.microsoft.com/.default"],
)
return result

from tests import test_client_id, test_password, test_tenant, test_username

client = GraphClient(acquire_token)
client = GraphClient.with_username_and_password(
test_tenant, test_client_id, test_username, test_password
)
me = client.me.get().execute_query()
print(me.user_principal_name)
29 changes: 29 additions & 0 deletions examples/auth/with_user_creds_custom.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
"""
Username Password Authentication flow
https://github.com/AzureAD/microsoft-authentication-library-for-python/wiki/Username-Password-Authentication
"""

import msal

from office365.graph_client import GraphClient
from tests import test_client_id, test_tenant, test_user_credentials


def acquire_token():
authority_url = "https://login.microsoftonline.com/{0}".format(test_tenant)
app = msal.PublicClientApplication(
authority=authority_url, client_id=test_client_id
)

result = app.acquire_token_by_username_password(
username=test_user_credentials.userName,
password=test_user_credentials.password,
scopes=["https://graph.microsoft.com/.default"],
)
return result


client = GraphClient(acquire_token)
me = client.me.get().execute_query()
print(me.user_principal_name)
129 changes: 29 additions & 100 deletions office365/graph_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@
from office365.outlook.calendar.rooms.list import RoomList
from office365.planner.planner import Planner
from office365.reports.root import ReportRoot
from office365.runtime.auth.token_response import TokenResponse
from office365.runtime.auth.entra.authentication_context import AuthenticationContext
from office365.runtime.client_runtime_context import ClientRuntimeContext
from office365.runtime.http.http_method import HttpMethod
from office365.runtime.http.request_options import RequestOptions
Expand All @@ -77,11 +77,16 @@
class GraphClient(ClientRuntimeContext):
"""Graph Service client"""

def __init__(self, acquire_token_callback):
# type: (Callable[[], dict]) -> None
def __init__(self, acquire_token_callback=None, auth_context=None):
# type: (Callable[[], dict], AuthenticationContext) -> None
super(GraphClient, self).__init__()
self._pending_request = None
self._acquire_token_callback = acquire_token_callback
if acquire_token_callback is not None:
self._auth_context = AuthenticationContext().with_access_token(
acquire_token_callback
)
else:
self._auth_context = auth_context

@staticmethod
def with_certificate(
Expand All @@ -98,27 +103,10 @@ def with_certificate(
:param Any token_cache: Default cache is in memory only,
Refer https://msal-python.readthedocs.io/en/latest/#msal.SerializableTokenCache
"""
if scopes is None:
scopes = ["https://graph.microsoft.com/.default"]
authority_url = "https://login.microsoftonline.com/{0}".format(tenant)
import msal

app = msal.ConfidentialClientApplication(
client_id,
authority=authority_url,
client_credential={
"thumbprint": thumbprint,
"private_key": private_key,
},
token_cache=token_cache, # Default cache is in memory only.
# You can learn how to use SerializableTokenCache from
# https://msal-python.readthedocs.io/en/latest/#msal.SerializableTokenCache
)

def _acquire_token():
return app.acquire_token_for_client(scopes=scopes)

return GraphClient(_acquire_token)
auth_ctx = AuthenticationContext(
tenant=tenant, scopes=scopes, token_cache=token_cache
).with_certificate(client_id, thumbprint, private_key)
return GraphClient(auth_context=auth_ctx)

@staticmethod
def with_client_secret(
Expand All @@ -135,22 +123,11 @@ def with_client_secret(
:param Any token_cache: Default cache is in memory only,
Refer https://msal-python.readthedocs.io/en/latest/#msal.SerializableTokenCache
"""
if scopes is None:
scopes = ["https://graph.microsoft.com/.default"]
authority_url = "https://login.microsoftonline.com/{0}".format(tenant)
import msal

app = msal.ConfidentialClientApplication(
client_id,
authority=authority_url,
client_credential=client_secret,
token_cache=token_cache,
)

def _acquire_token():
return app.acquire_token_for_client(scopes=scopes)

return GraphClient(_acquire_token)
auth_ctx = AuthenticationContext(
tenant=tenant, scopes=scopes, token_cache=token_cache
).with_client_secret(client_id, client_secret)
return GraphClient(auth_context=auth_ctx)

@staticmethod
def with_token_interactive(tenant, client_id, username=None, scopes=None):
Expand All @@ -164,32 +141,10 @@ def with_token_interactive(tenant, client_id, username=None, scopes=None):
:param str username: Typically a UPN in the form of an email address.
:param list[str] or None scopes: Scopes requested to access an API
"""
if scopes is None:
scopes = ["https://graph.microsoft.com/.default"]
authority_url = "https://login.microsoftonline.com/{0}".format(tenant)
import msal

app = msal.PublicClientApplication(client_id, authority=authority_url)

def _acquire_token():
# The pattern to acquire a token looks like this.
result = None

# Firstly, check the cache to see if this end user has signed in before
accounts = app.get_accounts(username=username)
if accounts:
chosen = accounts[0] # Assuming the end user chose this one to proceed
# Now let's try to find a token in cache for this account
result = app.acquire_token_silent(scopes, account=chosen)

if not result:
result = app.acquire_token_interactive(
scopes,
login_hint=username,
)
return result

return GraphClient(_acquire_token)
auth_ctx = AuthenticationContext(
tenant=tenant, scopes=scopes
).with_token_interactive(client_id, username)
return GraphClient(auth_context=auth_ctx)

@staticmethod
def with_username_and_password(tenant, client_id, username, password, scopes=None):
Expand All @@ -203,31 +158,10 @@ def with_username_and_password(tenant, client_id, username, password, scopes=Non
:param str password: The password.
:param list[str] or None scopes: Scopes requested to access an API
"""
if scopes is None:
scopes = ["https://graph.microsoft.com/.default"]
authority_url = "https://login.microsoftonline.com/{0}".format(tenant)
import msal

app = msal.PublicClientApplication(
authority=authority_url,
client_id=client_id,
)

def _acquire_token():
result = None
accounts = app.get_accounts(username=username)
if accounts:
result = app.acquire_token_silent(scopes, account=accounts[0])

if not result:
result = app.acquire_token_by_username_password(
username=username,
password=password,
scopes=scopes,
)
return result

return GraphClient(_acquire_token)
auth_ctx = AuthenticationContext(
tenant=tenant, scopes=scopes
).with_username_and_password(client_id, username, password)
return GraphClient(auth_context=auth_ctx)

def execute_batch(self, items_per_batch=20, success_callback=None):
"""Constructs and submit a batch request
Expand All @@ -238,7 +172,7 @@ def execute_batch(self, items_per_batch=20, success_callback=None):
:param (List[ClientObject|ClientResult])-> None success_callback: A success callback
"""
batch_request = ODataV4BatchRequest(V4JsonFormat())
batch_request.beforeExecute += self._authenticate_request
batch_request.beforeExecute += self._auth_context.authenticate_request
while self.has_pending_request:
qry = self._get_next_query(items_per_batch)
batch_request.execute_query(qry)
Expand All @@ -250,7 +184,9 @@ def pending_request(self):
# type: () -> GraphRequest
if self._pending_request is None:
self._pending_request = GraphRequest()
self._pending_request.beforeExecute += self._authenticate_request
self._pending_request.beforeExecute += (
self._auth_context.authenticate_request
)
self._pending_request.beforeExecute += self._build_specific_query
return self._pending_request

Expand All @@ -266,13 +202,6 @@ def _build_specific_query(self, request):
elif isinstance(self.current_query, DeleteEntityQuery):
request.method = HttpMethod.Delete

def _authenticate_request(self, request):
# type: (RequestOptions) -> None
"""Authenticate request."""
token_json = self._acquire_token_callback()
token = TokenResponse.from_json(token_json)
request.ensure_header("Authorization", "Bearer {0}".format(token.accessToken))

@property
def admin(self):
"""A container for administrator functionality for SharePoint and OneDrive."""
Expand Down
Empty file.
Loading

0 comments on commit 53eddff

Please sign in to comment.