🔍 Code Extractor

class SamlTokenProvider

Maturity: 37

Provides SAML-based claims authentication for Office 365 SharePoint Online, handling security token acquisition and cookie-based authentication.

File:
/tf/active/vicechatdev/SPFCsync/venv/lib64/python3.11/site-packages/office365/runtime/auth/providers/saml_token_provider.py
Lines:
42 - 327
Complexity:
complex

Purpose

This class implements a SAML (Security Assertion Markup Language) token provider for authenticating with Office 365 SharePoint Online using claims-based authentication. It supports both federated (ADFS) and non-federated authentication scenarios, manages the complete authentication flow including user realm discovery, security token acquisition from STS endpoints, and conversion of tokens to authentication cookies. The provider can operate in browser mode to mimic browser-based authentication patterns.

Source Code

class SamlTokenProvider(AuthenticationProvider, office365.logger.LoggerContext):
    def __init__(self, url, username, password, browser_mode):
        """
        SAML Security Token Service provider (claims-based authentication)

        :param str url: Site or Web absolute url
        :param str username: Typically a UPN in the form of an email address
        :param str password: The password
        :param bool browser_mode:
        """
        # Security Token Service info
        self._sts_profile = STSProfile(resolve_base_url(url))
        # Obtain authentication cookies, using the browser mode
        self._browser_mode = browser_mode
        # Last occurred error
        self.error = ""
        self._username = username
        self._password = password
        self._cached_auth_cookies = None
        self.__ns_prefixes = {
            "S": "{http://www.w3.org/2003/05/soap-envelope}",
            "s": "{http://www.w3.org/2003/05/soap-envelope}",
            "psf": "{http://schemas.microsoft.com/Passport/SoapServices/SOAPFault}",
            "wst": "{http://schemas.xmlsoap.org/ws/2005/02/trust}",
            "wsse": "{http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd}",
            "saml": "{urn:oasis:names:tc:SAML:1.0:assertion}",
            "u": "{http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd}",
            "wsa": "{http://www.w3.org/2005/08/addressing}",
            "wsp": "{http://schemas.xmlsoap.org/ws/2004/09/policy}",
            "ps": "{http://schemas.microsoft.com/LiveID/SoapServices/v1}",
            "ds": "{http://www.w3.org/2000/09/xmldsig#}",
        }
        for key in self.__ns_prefixes.keys():
            ElementTree.register_namespace(key, self.__ns_prefixes[key][1:-1])

    def authenticate_request(self, request):
        """
        Authenticate request handler
        """
        logger = self.logger(self.authenticate_request.__name__)
        self.ensure_authentication_cookie()
        logger.debug_secrets(self._cached_auth_cookies)
        cookie_header_value = "; ".join(
            [
                "=".join([key, str(val)])
                for key, val in self._cached_auth_cookies.items()
            ]
        )
        request.set_header("Cookie", cookie_header_value)

    def ensure_authentication_cookie(self):
        if self._cached_auth_cookies is None:
            self._cached_auth_cookies = self.get_authentication_cookie()
        return True

    def get_authentication_cookie(self):
        """Acquire authentication cookie"""
        logger = self.logger(self.ensure_authentication_cookie.__name__)
        logger.debug("get_authentication_cookie called")

        try:
            logger.debug("Acquiring Access Token..")
            user_realm = self._get_user_realm()
            if user_realm.IsFederated:
                token = self._acquire_service_token_from_adfs(user_realm.STSAuthUrl)
            else:
                token = self._acquire_service_token()
            return self._get_authentication_cookie(token, user_realm.IsFederated)
        except requests.exceptions.RequestException as e:
            logger.error(e.response.text)
            self.error = "Error: {}".format(e)
            raise ValueError(e.response.text)

    def _get_user_realm(self):
        """Get User Realm"""
        resp = requests.post(
            self._sts_profile.user_realm_service_url,
            data="login={0}&xml=1".format(self._username),
            headers={"Content-Type": "application/x-www-form-urlencoded"},
        )
        xml = ElementTree.fromstring(resp.content)
        node = xml.find("NameSpaceType")
        if node is not None:
            if node.text == "Federated":
                info = UserRealmInfo(xml.find("STSAuthURL").text, True)
            else:
                info = UserRealmInfo(None, False)
            return info
        return None

    def get_last_error(self):
        return self.error

    def _acquire_service_token_from_adfs(self, adfs_url):
        logger = self.logger(self._acquire_service_token_from_adfs.__name__)

        payload = self._prepare_request_from_template(
            "FederatedSAML.xml",
            {
                "auth_url": adfs_url,
                "message_id": str(uuid.uuid4()),
                "username": xml_escape(self._username),
                "password": xml_escape(self._password),
                "created": self._sts_profile.created,
                "expires": self._sts_profile.expires,
                "issuer": self._sts_profile.tokenIssuer,
            },
        )

        response = requests.post(
            adfs_url,
            data=payload,
            headers={"Content-Type": "application/soap+xml; charset=utf-8"},
        )
        dom = minidom.parseString(response.content.decode())
        assertion_node = dom.getElementsByTagNameNS(
            "urn:oasis:names:tc:SAML:1.0:assertion", "Assertion"
        )[0].toxml()

        try:
            payload = self._prepare_request_from_template(
                "RST2.xml",
                {
                    "auth_url": self._sts_profile.tenant,
                    "serviceTokenUrl": self._sts_profile.security_token_service_url,
                    "assertion_node": assertion_node,
                },
            )

            # 3. get security token
            response = requests.post(
                self._sts_profile.security_token_service_url,
                data=payload,
                headers={"Content-Type": "application/soap+xml"},
            )
            token = self._process_service_token_response(response)
            logger.debug_secrets("security token: %s", token)
            return token
        except ElementTree.ParseError as e:
            self.error = (
                "An error occurred while parsing the server response: {}".format(e)
            )
            logger.error(self.error)
            return None

    def _acquire_service_token(self):
        """Retrieve service token"""
        logger = self.logger(self._acquire_service_token.__name__)

        payload = self._prepare_request_from_template(
            "SAML.xml",
            {
                "auth_url": self._sts_profile.authorityUrl,
                "username": xml_escape(self._username),
                "password": xml_escape(self._password),
                "message_id": str(uuid.uuid4()),
                "created": self._sts_profile.created,
                "expires": self._sts_profile.expires,
                "issuer": self._sts_profile.tokenIssuer,
            },
        )
        logger.debug_secrets("options: %s", payload)
        response = requests.post(
            self._sts_profile.security_token_service_url,
            data=payload,
            headers={"Content-Type": "application/x-www-form-urlencoded"},
        )
        token = self._process_service_token_response(response)
        logger.debug_secrets("security token: %s", token)
        return token

    def _process_service_token_response(self, response):
        logger = self.logger(self._process_service_token_response.__name__)
        logger.debug_secrets(
            "response: %s\nresponse.content: %s", response, response.content
        )

        try:
            xml = ElementTree.fromstring(response.content)
        except ElementTree.ParseError as e:
            self.error = (
                "An error occurred while parsing the server response: {}".format(e)
            )
            logger.error(self.error)
            return None

        # check for errors
        if xml.find("{0}Body/{0}Fault".format(self.__ns_prefixes["s"])) is not None:
            error = xml.find(
                "{0}Body/{0}Fault/{0}Detail/{1}error/{1}internalerror/{1}text".format(
                    self.__ns_prefixes["s"], self.__ns_prefixes["psf"]
                )
            )
            if error is None:
                self.error = (
                    "An error occurred while retrieving token from XML response."
                )
            else:
                self.error = "An error occurred while retrieving token from XML response: {0}".format(
                    error.text
                )
            logger.error(self.error)
            raise ValueError(self.error)

        # extract token
        token = xml.find(
            "{0}Body/{1}RequestSecurityTokenResponse/{1}RequestedSecurityToken/{2}BinarySecurityToken".format(
                self.__ns_prefixes["s"],
                self.__ns_prefixes["wst"],
                self.__ns_prefixes["wsse"],
            )
        )
        if token is None:
            self.error = "Cannot get binary security token for from {0}".format(
                self._sts_profile.security_token_service_url
            )
            logger.error(self.error)
            raise ValueError(self.error)
        logger.debug_secrets("token: %s", token)
        return token.text

    def _get_authentication_cookie(self, security_token, federated=False):
        """Retrieve auth cookie from STS

        :type federated: bool
        :type security_token: str
        """
        logger = self.logger(self._get_authentication_cookie.__name__)

        session = requests.session()
        logger.debug_secrets(
            "session: %s\nsession.post(%s, data=%s)",
            session,
            self._sts_profile.signin_page_url,
            security_token,
        )
        if not federated or self._browser_mode:
            headers = {"Content-Type": "application/x-www-form-urlencoded"}
            if self._browser_mode:
                headers[
                    "User-Agent"
                ] = "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; WOW64; Trident/5.0)"
            session.post(
                self._sts_profile.signin_page_url, data=security_token, headers=headers
            )
        else:
            idcrl_endpoint = "https://{}/_vti_bin/idcrl.svc/".format(
                self._sts_profile.tenant
            )
            session.get(
                idcrl_endpoint,
                headers={
                    "User-Agent": "Office365 Python Client",
                    "X-IDCRL_ACCEPTED": "t",
                    "Authorization": "BPOSIDCRL {0}".format(security_token),
                },
            )
        logger.debug_secrets("session.cookies: %s", session.cookies)
        cookies = requests.utils.dict_from_cookiejar(session.cookies)
        logger.debug_secrets("cookies: %s", cookies)
        if not is_valid_auth_cookies(cookies):
            self.error = (
                "An error occurred while retrieving auth cookies from {0}".format(
                    self._sts_profile.signin_page_url
                )
            )
            logger.error(self.error)
            raise ValueError(self.error)
        return cookies

    @staticmethod
    def _prepare_request_from_template(template_name, params):
        """Construct the request body to acquire security token from STS endpoint"""
        logger = SamlTokenProvider.logger()
        logger.debug_secrets("params: %s", params)
        f = open(
            os.path.join(os.path.dirname(__file__), "templates", template_name),
            encoding="utf8",
        )
        try:
            data = f.read()
            for key in params:
                data = data.replace("{" + key + "}", str(params[key]))
            return data
        finally:
            f.close()

Parameters

Name Type Default Kind
bases AuthenticationProvider, office365.logger.LoggerContext -

Parameter Details

url: The absolute URL of the SharePoint site or web to authenticate against. Used to resolve the base URL and determine the STS profile configuration.

username: The user's login name, typically a UPN (User Principal Name) in email address format (e.g., user@domain.com).

password: The user's password for authentication. This is used in SAML token requests and is XML-escaped before transmission.

browser_mode: Boolean flag indicating whether to use browser-mode authentication. When True, mimics browser behavior by setting appropriate User-Agent headers and using specific authentication endpoints.

Return Value

Instantiation returns a SamlTokenProvider object that can be used to authenticate HTTP requests. The authenticate_request method modifies requests in-place by adding Cookie headers. The get_authentication_cookie method returns a dictionary of authentication cookies (typically FedAuth and rtFa cookies). Methods like _acquire_service_token return security token strings, while _get_user_realm returns a UserRealmInfo object.

Class Interface

Methods

__init__(self, url: str, username: str, password: str, browser_mode: bool)

Purpose: Initializes the SAML token provider with authentication parameters and configures XML namespaces

Parameters:

  • url: Site or Web absolute URL
  • username: User Principal Name (UPN) typically in email format
  • password: User password
  • browser_mode: Flag to enable browser-mode authentication

Returns: None

authenticate_request(self, request) -> None

Purpose: Authenticates an HTTP request by adding authentication cookies to the Cookie header

Parameters:

  • request: HTTP request object to be authenticated (must have set_header method)

Returns: None - modifies the request object in-place by setting Cookie header

ensure_authentication_cookie(self) -> bool

Purpose: Ensures authentication cookies are available, acquiring them if not already cached

Returns: Always returns True after ensuring cookies are available

get_authentication_cookie(self) -> dict

Purpose: Acquires authentication cookies by performing the complete SAML authentication flow

Returns: Dictionary of authentication cookies (e.g., {'FedAuth': '...', 'rtFa': '...'})

get_last_error(self) -> str

Purpose: Returns the last error message that occurred during authentication

Returns: String containing the last error message, or empty string if no error

_get_user_realm(self) -> UserRealmInfo

Purpose: Determines the user's authentication realm (federated or managed) by querying the user realm service

Returns: UserRealmInfo object containing realm type and ADFS URL if federated

_acquire_service_token_from_adfs(self, adfs_url: str) -> str

Purpose: Acquires a security token from an ADFS endpoint for federated authentication scenarios

Parameters:

  • adfs_url: The ADFS STS authentication URL obtained from user realm discovery

Returns: Security token string, or None if acquisition fails

_acquire_service_token(self) -> str

Purpose: Acquires a security token from the Office 365 STS endpoint for non-federated authentication

Returns: Security token string

_process_service_token_response(self, response) -> str

Purpose: Parses the STS response XML to extract the binary security token and handle errors

Parameters:

  • response: HTTP response object from the STS endpoint containing XML

Returns: Binary security token string extracted from the XML response

_get_authentication_cookie(self, security_token: str, federated: bool = False) -> dict

Purpose: Exchanges a security token for authentication cookies by posting to the sign-in page

Parameters:

  • security_token: The binary security token obtained from STS
  • federated: Boolean indicating if this is federated authentication

Returns: Dictionary of authentication cookies

_prepare_request_from_template(template_name: str, params: dict) -> str static

Purpose: Loads an XML template file and substitutes parameters to create a SOAP request body

Parameters:

  • template_name: Name of the template file (e.g., 'SAML.xml', 'FederatedSAML.xml')
  • params: Dictionary of parameter names to values for template substitution

Returns: String containing the prepared XML request body with substituted parameters

Attributes

Name Type Description Scope
_sts_profile STSProfile Contains Security Token Service configuration including URLs and endpoints for the target SharePoint site instance
_browser_mode bool Flag indicating whether to use browser-mode authentication with browser-like headers instance
error str Stores the last error message that occurred during authentication operations instance
_username str The user's login name (UPN) used for authentication instance
_password str The user's password used for authentication instance
_cached_auth_cookies dict or None Cached authentication cookies to avoid repeated authentication; None until first successful authentication instance
__ns_prefixes dict Dictionary mapping XML namespace prefixes to their full URIs for SOAP/SAML XML processing instance

Dependencies

  • requests
  • office365.logger
  • office365.runtime.auth.authentication_provider
  • office365.runtime.auth.sts_profile
  • office365.runtime.auth.user_realm_info

Required Imports

import os
import uuid
from xml.dom import minidom
from xml.etree import ElementTree
import requests
import requests.utils
import office365.logger
from office365.runtime.auth.authentication_provider import AuthenticationProvider
from office365.runtime.auth.sts_profile import STSProfile
from office365.runtime.auth.user_realm_info import UserRealmInfo

Usage Example

from office365.runtime.auth.saml_token_provider import SamlTokenProvider
import requests

# Initialize the SAML token provider
url = 'https://contoso.sharepoint.com/sites/mysite'
username = 'user@contoso.com'
password = 'mypassword'
browser_mode = False

provider = SamlTokenProvider(url, username, password, browser_mode)

# Get authentication cookies
try:
    cookies = provider.get_authentication_cookie()
    print(f"Authentication successful: {cookies}")
except ValueError as e:
    print(f"Authentication failed: {e}")
    print(f"Error details: {provider.get_last_error()}")

# Use with HTTP requests
request = requests.Request('GET', url)
provider.authenticate_request(request)
prepared = request.prepare()
response = requests.Session().send(prepared)

Best Practices

  • Always handle ValueError exceptions when calling get_authentication_cookie() as authentication can fail for various reasons (invalid credentials, network issues, STS errors)
  • Check get_last_error() after authentication failures to get detailed error messages
  • The provider caches authentication cookies after first successful authentication; subsequent calls to authenticate_request use cached cookies
  • Credentials (username and password) are stored in instance variables and logged with debug_secrets, ensure proper logging configuration for security
  • The provider automatically handles both federated (ADFS) and non-federated authentication by detecting the user realm type
  • Browser mode should be enabled when authenticating against environments that require browser-like behavior
  • The class maintains state through _cached_auth_cookies; create a new instance if you need to re-authenticate with different credentials
  • XML namespace prefixes are registered globally during initialization, which may affect other XML processing in the application
  • Template files must be accessible at runtime; ensure they are packaged with the application
  • The provider inherits from LoggerContext, use the logger() method for consistent logging throughout the authentication flow

Similar Components

AI-powered semantic similarity - components with related functionality:

  • class NtlmProvider 64.2% similar

    NtlmProvider is an authentication provider class that implements NTLM (NT LAN Manager) authentication for SharePoint On-Premises environments.

    From: /tf/active/vicechatdev/SPFCsync/venv/lib64/python3.11/site-packages/office365/runtime/auth/providers/ntlm_provider.py
  • class STSProfile 60.8% similar

    STSProfile is a configuration class that manages Security Token Service (STS) profile settings for Microsoft Online authentication, including URLs, timestamps, and service endpoints.

    From: /tf/active/vicechatdev/SPFCsync/venv/lib64/python3.11/site-packages/office365/runtime/auth/sts_profile.py
  • class AuthenticationProvider 60.0% similar

    Abstract base class that defines the interface for authentication providers in the Office365 runtime library.

    From: /tf/active/vicechatdev/SPFCsync/venv/lib64/python3.11/site-packages/office365/runtime/auth/authentication_provider.py
  • class SamlOrWsFedProvider 59.8% similar

    An abstract class that provides configuration details for setting up SAML or WS-Fed external domain-based identity provider (IdP) integrations.

    From: /tf/active/vicechatdev/SPFCsync/venv/lib64/python3.11/site-packages/office365/directory/identities/providers/saml_or_wsfed.py
  • function test_sharepoint_token 59.6% similar

    Tests SharePoint OAuth2 authentication by acquiring an access token using client credentials flow and validates it with a SharePoint API call.

    From: /tf/active/vicechatdev/SPFCsync/diagnose_sharepoint.py
← Back to Browse