Skip to content

Commit

Permalink
Certificate auth flow: fix for passing valid scope #541, Authenticati…
Browse files Browse the repository at this point in the history
…onContext enhancements
  • Loading branch information
vgrem committed Jul 27, 2022
1 parent 8daf2df commit 45fa370
Show file tree
Hide file tree
Showing 5 changed files with 47 additions and 47 deletions.
8 changes: 4 additions & 4 deletions examples/sharepoint/connect_with_client_certificate.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import os
from office365.sharepoint.client_context import ClientContext
from tests import test_site_url, test_tenant
from tests import test_tenant, test_team_site_url

cert_settings = {
'client_id': '51d03106-4726-442c-86db-70b32fa7547f',
'thumbprint': "61C754D8D9629BE91972B6A0C1999DC678FB0145",
'thumbprint': "78CA7402E8A2508A9772CB1B2E085945147D8050",
'cert_path': '{0}/selfsigncert.pem'.format(os.path.dirname(__file__)),
'scopes': ['{0}.default'.format(test_site_url)]
#'scopes': ['{0}.default'.format(test_site_url)]
}

ctx = ClientContext(test_site_url).with_client_certificate(test_tenant, **cert_settings)
ctx = ClientContext(test_team_site_url).with_client_certificate(test_tenant, **cert_settings)
current_web = ctx.web.get().execute_query()
print("{0}".format(current_web.url))
37 changes: 19 additions & 18 deletions office365/runtime/auth/authentication_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,18 @@
from office365.runtime.auth.providers.saml_token_provider import SamlTokenProvider
from office365.runtime.auth.token_response import TokenResponse
from office365.runtime.auth.user_credential import UserCredential
from office365.runtime.compat import get_absolute_url


class AuthenticationContext(object):

def __init__(self, authority_url):
def __init__(self, url):
"""
Authentication context for SharePoint Online/OneDrive For Business
:param str authority_url: authority url
:param str url: Absolute Web or Site Url
"""
self.authority_url = authority_url
self.url = url.rstrip("/")
self._provider = None

def with_client_certificate(self, tenant, client_id, thumbprint, cert_path, **kwargs):
Expand All @@ -30,7 +31,8 @@ def with_client_certificate(self, tenant, client_id, thumbprint, cert_path, **kw
def _acquire_token_for_client_certificate():
authority_url = 'https://login.microsoftonline.com/{0}'.format(tenant)
credentials = {"thumbprint": thumbprint, "private_key": open(cert_path).read()}
scopes = kwargs.get('scopes', ["{url}/.default".format(url=self.authority_url)])
resource = get_absolute_url(self.url)
scopes = kwargs.get('scopes', ["{url}/.default".format(url=resource)])
import msal
app = msal.ConfidentialClientApplication(
client_id,
Expand All @@ -40,25 +42,23 @@ def _acquire_token_for_client_certificate():
result = app.acquire_token_for_client(scopes)
return TokenResponse.from_json(result)

self.register_provider(_acquire_token_for_client_certificate)
self.with_access_token(_acquire_token_for_client_certificate)
return self

def register_provider(self, credentials_or_token_func, **kwargs):
if callable(credentials_or_token_func):
self._provider = OAuthTokenProvider(credentials_or_token_func)
elif isinstance(credentials_or_token_func, ClientCredential):
self._provider = ACSTokenProvider(self.authority_url, credentials_or_token_func.clientId,
credentials_or_token_func.clientSecret)
elif isinstance(credentials_or_token_func, UserCredential):
def with_access_token(self, token_func):
self._provider = OAuthTokenProvider(token_func)

def with_credentials(self, credentials, **kwargs):
if isinstance(credentials, ClientCredential):
self._provider = ACSTokenProvider(self.url, credentials.clientId, credentials.clientSecret)
elif isinstance(credentials, UserCredential):
allow_ntlm = kwargs.get('allow_ntlm', False)
if allow_ntlm:
from office365.runtime.auth.providers.ntlm_provider import NtlmProvider
self._provider = NtlmProvider(credentials_or_token_func.userName,
credentials_or_token_func.password)
self._provider = NtlmProvider(credentials.userName, credentials.password)
else:
browser_mode = kwargs.get('browser_mode', False)
self._provider = SamlTokenProvider(self.authority_url, credentials_or_token_func.userName,
credentials_or_token_func.password, browser_mode)
self._provider = SamlTokenProvider(self.url, credentials.userName, credentials.password, browser_mode)
else:
raise ValueError("Unknown credential type")

Expand All @@ -70,20 +70,21 @@ def acquire_token_for_user(self, username, password, browser_mode=False):
:type username: str
:type browser_mode: str
"""
self._provider = SamlTokenProvider(url=self.authority_url, username=username, password=password,
self._provider = SamlTokenProvider(url=self.url, username=username, password=password,
browser_mode=browser_mode)
return self._provider.ensure_authentication_cookie()

def acquire_token_for_app(self, client_id, client_secret):
"""Acquire token via client credentials (SharePoint App Principal)
Status: deprecated!
"""
self._provider = ACSTokenProvider(url=self.authority_url, client_id=client_id, client_secret=client_secret)
self._provider = ACSTokenProvider(url=self.url, client_id=client_id, client_secret=client_secret)
return self._provider.ensure_app_only_access_token()

def authenticate_request(self, request):
"""
Authenticate request
:type request: office365.runtime.http.request_options.RequestOptions
"""
self._provider.authenticate_request(request)
7 changes: 5 additions & 2 deletions office365/runtime/compat.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,12 @@
from urlparse import urljoin
import pytz as timezone
from email import message_from_string as message_from_bytes_or_string
from __builtin__ import xrange as range_or_xrange
elif is_py3:
from urllib.parse import urlparse
from urllib.parse import quote
from urllib.parse import urljoin
from datetime import timezone
from email import message_from_bytes as message_from_bytes_or_string
from builtins import range as range_or_xrange


def message_as_bytes_or_string(message):
Expand All @@ -47,6 +45,11 @@ def is_absolute_url(url):
return bool(urlparse(url).netloc)


def get_absolute_url(url):
path = urlparse(url).path
return url.replace(path, "")


def parse_query_string(url, key):
if is_py2:
import urlparse
Expand Down
37 changes: 16 additions & 21 deletions office365/sharepoint/client_context.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import copy

from office365.runtime.auth.authentication_context import AuthenticationContext
from office365.runtime.auth.providers.saml_token_provider import resolve_base_url
from office365.runtime.auth.user_credential import UserCredential
from office365.runtime.client_result import ClientResult
from office365.runtime.client_runtime_context import ClientRuntimeContext
Expand All @@ -20,7 +19,7 @@
from office365.sharepoint.tenant.administration.hub_site_collection import HubSiteCollection
from office365.sharepoint.webs.context_web_information import ContextWebInformation
from office365.sharepoint.webs.web import Web
from office365.runtime.compat import urlparse, is_absolute_url
from office365.runtime.compat import urlparse, is_absolute_url, get_absolute_url


class ClientContext(ClientRuntimeContext):
Expand All @@ -33,16 +32,12 @@ def __init__(self, base_url, auth_context=None):
:param str base_url: Absolute Web or Site Url
:param AuthenticationContext or None auth_context: Authentication context
"""
if base_url.endswith("/"):
base_url = base_url[:len(base_url) - 1]
if auth_context is None:
self._auth_context = AuthenticationContext(authority_url=base_url)
else:
self._auth_context = auth_context
super(ClientContext, self).__init__()
if auth_context is None:
auth_context = AuthenticationContext(url=base_url)
self._auth_context = auth_context
self._web = None
self._site = None
self._base_url = base_url
self._ctx_web_info = None
self._pending_request = None

Expand All @@ -62,23 +57,23 @@ def create_safe_url(self, orig_url, relative=True):
return url if relative else "".join([root_site_url, url])

@staticmethod
def from_url(abs_url):
def from_url(full_url):
"""
Constructs ClientContext from absolute Url
:param str abs_url: Absolute Url to resource
:param str full_url: Full Url to a resource
:return: ClientContext
"""
base_url = resolve_base_url(abs_url)
ctx = ClientContext(base_url)
result = Web.get_web_url_from_page_url(ctx, abs_url)
root_site_url = get_absolute_url(full_url)
ctx = ClientContext(root_site_url)
result = Web.get_web_url_from_page_url(ctx, full_url)

def _init_context_for_web(resp):
"""
:type resp: requests.Response
"""
resp.raise_for_status()
ctx._base_url = result.value
ctx._auth_context.url = result.value

ctx.after_execute(_init_context_for_web)
return ctx
Expand All @@ -100,7 +95,7 @@ def with_access_token(self, token_func):
"""
:type token_func: () -> TokenResponse
"""
self.authentication_context.register_provider(token_func)
self.authentication_context.with_access_token(token_func)
return self

def with_user_credentials(self, username, password, allow_ntlm=False, browser_mode=False):
Expand All @@ -112,7 +107,7 @@ def with_user_credentials(self, username, password, allow_ntlm=False, browser_mo
:type allow_ntlm: bool
:type browser_mode: bool
"""
self.authentication_context.register_provider(
self.authentication_context.with_credentials(
UserCredential(username, password),
allow_ntlm=allow_ntlm,
browser_mode=browser_mode)
Expand All @@ -124,7 +119,7 @@ def with_credentials(self, credentials):
:type credentials: UserCredential or ClientCredential
"""
self.authentication_context.register_provider(credentials)
self.authentication_context.with_credentials(credentials)
return self

def execute_batch(self, items_per_batch=100):
Expand Down Expand Up @@ -232,14 +227,14 @@ def clone(self, url, clear_queries=True):
:return ClientContext
"""
ctx = copy.deepcopy(self)
ctx._base_url = url
ctx._auth_context.url = url
ctx._ctx_web_info = None
if clear_queries:
ctx.clear()
return ctx

def authenticate_request(self, request):
self._auth_context.authenticate_request(request)
self.authentication_context.authenticate_request(request)

def _build_modification_query(self, request):
"""
Expand Down Expand Up @@ -372,7 +367,7 @@ def search(self):
@property
def base_url(self):
"""Represents absolute Web or Site Url"""
return self._base_url
return self.authentication_context.url

@property
def authentication_context(self):
Expand Down
5 changes: 3 additions & 2 deletions tests/sharepoint/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ def test1_connect_with_app_principal(self):
self.assertIsInstance(ctx.authentication_context._provider, ACSTokenProvider)

def test2_connect_with_app_principal_alt(self):
context_auth = AuthenticationContext(authority_url=test_site_url)
context_auth = AuthenticationContext(url=test_site_url)
context_auth.acquire_token_for_app(client_id=settings.get('client_credentials', 'client_id'),
client_secret=settings.get('client_credentials', 'client_secret'))
ctx = ClientContext(test_site_url, context_auth)
Expand All @@ -34,7 +34,8 @@ def test4_connect_with_user_credentials(self):
self.assertIsInstance(ctx.authentication_context._provider, SamlTokenProvider)

def test5_init_from_url(self):
ctx = ClientContext.from_url(test_site_url).with_credentials(test_user_credentials)
page_url = "{site_url}/SitePages/Home.aspx".format(site_url=test_team_site_url)
ctx = ClientContext.from_url(page_url).with_credentials(test_user_credentials)
web = ctx.web.get().execute_query()
self.assertIsNotNone(web.url)

Expand Down

0 comments on commit 45fa370

Please sign in to comment.