diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
new file mode 100644
index 00000000..c06a4d20
--- /dev/null
+++ b/.pre-commit-config.yaml
@@ -0,0 +1,51 @@
+exclude: 'docs|migrations'
+
+repos:
+ - repo: https://github.com/pre-commit/pre-commit-hooks
+ rev: v4.1.0
+ hooks:
+ - id: trailing-whitespace
+ - id: end-of-file-fixer
+ - id: check-yaml
+ - id: check-toml
+ - id: check-case-conflict
+ - id: check-merge-conflict
+ - id: debug-statements
+
+ - repo: https://github.com/asottile/pyupgrade
+ rev: v2.31.0
+ hooks:
+ - id: pyupgrade
+ args: [--py36-plus]
+
+ - repo: https://github.com/myint/autoflake
+ rev: 'v1.4'
+ hooks:
+ - id: autoflake
+ args: ['--in-place', '--remove-all-unused-imports', '--ignore-init-module-imports']
+
+ - repo: https://github.com/pycqa/isort
+ rev: 5.10.1
+ hooks:
+ - id: isort
+ name: isort (python)
+ args: ['--settings-path=pyproject.toml']
+
+ - repo: https://github.com/psf/black
+ rev: 21.12b0
+ hooks:
+ - id: black
+
+ - repo: https://github.com/adamchainz/django-upgrade
+ rev: 1.3.2
+ hooks:
+ - id: django-upgrade
+ args: [--target-version, "2.2"]
+
+ - repo: https://github.com/pycqa/flake8
+ rev: 4.0.1
+ hooks:
+ - id: flake8
+ args: ['--config=setup.cfg']
+ additional_dependencies: [flake8-bugbear, flake8-isort]
+ verbose: true
diff --git a/djangosaml2/__init__.py b/djangosaml2/__init__.py
index 7744fbcc..6a4a7459 100644
--- a/djangosaml2/__init__.py
+++ b/djangosaml2/__init__.py
@@ -1 +1 @@
-default_app_config = 'djangosaml2.apps.DjangoSaml2Config'
+default_app_config = "djangosaml2.apps.DjangoSaml2Config"
diff --git a/djangosaml2/apps.py b/djangosaml2/apps.py
index e03f7c8d..8fc266d8 100644
--- a/djangosaml2/apps.py
+++ b/djangosaml2/apps.py
@@ -2,7 +2,7 @@
class DjangoSaml2Config(AppConfig):
- name = 'djangosaml2'
+ name = "djangosaml2"
verbose_name = "DjangoSAML2"
def ready(self):
diff --git a/djangosaml2/backends.py b/djangosaml2/backends.py
index 08ae5f64..7528a35a 100644
--- a/djangosaml2/backends.py
+++ b/djangosaml2/backends.py
@@ -14,23 +14,22 @@
# limitations under the License.
import logging
-from typing import Any, Optional, Tuple
import warnings
+from typing import Any, Optional, Tuple
from django.apps import apps
from django.conf import settings
+from django.core.exceptions import ImproperlyConfigured, MultipleObjectsReturned
+
from django.contrib import auth
from django.contrib.auth.backends import ModelBackend
-from django.core.exceptions import (ImproperlyConfigured,
- MultipleObjectsReturned)
-
-logger = logging.getLogger('djangosaml2')
+logger = logging.getLogger("djangosaml2")
def set_attribute(obj: Any, attr: str, new_value: Any) -> bool:
- """ Set an attribute of an object to a specific value, if it wasn't that already.
- Return True if the attribute was changed and False otherwise.
+ """Set an attribute of an object to a specific value, if it wasn't that already.
+ Return True if the attribute was changed and False otherwise.
"""
if not hasattr(obj, attr):
setattr(obj, attr, new_value)
@@ -49,52 +48,60 @@ class Saml2Backend(ModelBackend):
@property
def _user_model(self):
- """ Returns the user model specified in the settings, or the default one from this Django installation """
- if hasattr(settings, 'SAML_USER_MODEL'):
+ """Returns the user model specified in the settings, or the default one from this Django installation"""
+ if hasattr(settings, "SAML_USER_MODEL"):
try:
return apps.get_model(settings.SAML_USER_MODEL)
except LookupError:
raise ImproperlyConfigured(
- f"Model '{settings.SAML_USER_MODEL}' could not be loaded")
+ f"Model '{settings.SAML_USER_MODEL}' could not be loaded"
+ )
except ValueError:
raise ImproperlyConfigured(
- f"Model was specified as '{settings.SAML_USER_MODEL}', but it must be of the form 'app_label.model_name'")
+ f"Model was specified as '{settings.SAML_USER_MODEL}', but it must be of the form 'app_label.model_name'"
+ )
return auth.get_user_model()
@property
def _user_lookup_attribute(self) -> str:
- """ Returns the attribute on which to match the identifier with when performing a user lookup """
- if hasattr(settings, 'SAML_DJANGO_USER_MAIN_ATTRIBUTE'):
+ """Returns the attribute on which to match the identifier with when performing a user lookup"""
+ if hasattr(settings, "SAML_DJANGO_USER_MAIN_ATTRIBUTE"):
return settings.SAML_DJANGO_USER_MAIN_ATTRIBUTE
- return getattr(self._user_model, 'USERNAME_FIELD', 'username')
+ return getattr(self._user_model, "USERNAME_FIELD", "username")
- def _extract_user_identifier_params(self, session_info: dict, attributes: dict, attribute_mapping: dict) -> Tuple[str, Optional[Any]]:
- """ Returns the attribute to perform a user lookup on, and the value to use for it.
- The value could be the name_id, or any other saml attribute from the request.
+ def _extract_user_identifier_params(
+ self, session_info: dict, attributes: dict, attribute_mapping: dict
+ ) -> Tuple[str, Optional[Any]]:
+ """Returns the attribute to perform a user lookup on, and the value to use for it.
+ The value could be the name_id, or any other saml attribute from the request.
"""
# Lookup key
user_lookup_key = self._user_lookup_attribute
# Lookup value
- if getattr(settings, 'SAML_USE_NAME_ID_AS_USERNAME', False):
- if session_info.get('name_id'):
+ if getattr(settings, "SAML_USE_NAME_ID_AS_USERNAME", False):
+ if session_info.get("name_id"):
logger.debug(f"name_id: {session_info['name_id']}")
- user_lookup_value = session_info['name_id'].text
+ user_lookup_value = session_info["name_id"].text
else:
logger.error(
- 'The nameid is not available. Cannot find user without a nameid.')
+ "The nameid is not available. Cannot find user without a nameid."
+ )
user_lookup_value = None
else:
# Obtain the value of the custom attribute to use
user_lookup_value = self._get_attribute_value(
- user_lookup_key, attributes, attribute_mapping)
+ user_lookup_key, attributes, attribute_mapping
+ )
return user_lookup_key, self.clean_user_main_attribute(user_lookup_value)
- def _get_attribute_value(self, django_field: str, attributes: dict, attribute_mapping: dict):
+ def _get_attribute_value(
+ self, django_field: str, attributes: dict, attribute_mapping: dict
+ ):
saml_attribute = None
- logger.debug('attribute_mapping: %s', attribute_mapping)
+ logger.debug("attribute_mapping: %s", attribute_mapping)
for saml_attr, django_fields in attribute_mapping.items():
if django_field in django_fields and saml_attr in attributes:
saml_attribute = attributes.get(saml_attr, [None])
@@ -102,54 +109,75 @@ def _get_attribute_value(self, django_field: str, attributes: dict, attribute_ma
if saml_attribute:
return saml_attribute[0]
else:
- logger.error('attributes[saml_attr] attribute '
- 'value is missing. Probably the user '
- 'session is expired.')
-
- def authenticate(self, request, session_info=None, attribute_mapping=None, create_unknown_user=True, assertion_info=None, **kwargs):
+ logger.error(
+ "attributes[saml_attr] attribute "
+ "value is missing. Probably the user "
+ "session is expired."
+ )
+
+ def authenticate(
+ self,
+ request,
+ session_info=None,
+ attribute_mapping=None,
+ create_unknown_user=True,
+ assertion_info=None,
+ **kwargs,
+ ):
if session_info is None or attribute_mapping is None:
- logger.info('Session info or attribute mapping are None')
+ logger.info("Session info or attribute mapping are None")
return None
- if 'ava' not in session_info:
+ if "ava" not in session_info:
logger.error('"ava" key not found in session_info')
return None
- idp_entityid = session_info['issuer']
+ idp_entityid = session_info["issuer"]
- attributes = self.clean_attributes(session_info['ava'], idp_entityid)
+ attributes = self.clean_attributes(session_info["ava"], idp_entityid)
- logger.debug(f'attributes: {attributes}')
+ logger.debug(f"attributes: {attributes}")
- if not self.is_authorized(attributes, attribute_mapping, idp_entityid, assertion_info):
- logger.error('Request not authorized')
+ if not self.is_authorized(
+ attributes, attribute_mapping, idp_entityid, assertion_info
+ ):
+ logger.error("Request not authorized")
return None
user_lookup_key, user_lookup_value = self._extract_user_identifier_params(
- session_info, attributes, attribute_mapping)
+ session_info, attributes, attribute_mapping
+ )
if not user_lookup_value:
- logger.error('Could not determine user identifier')
+ logger.error("Could not determine user identifier")
return None
user, created = self.get_or_create_user(
- user_lookup_key, user_lookup_value, create_unknown_user,
- idp_entityid=idp_entityid, attributes=attributes, attribute_mapping=attribute_mapping, request=request
+ user_lookup_key,
+ user_lookup_value,
+ create_unknown_user,
+ idp_entityid=idp_entityid,
+ attributes=attributes,
+ attribute_mapping=attribute_mapping,
+ request=request,
)
# Update user with new attributes from incoming request
if user is not None:
user = self._update_user(
- user, attributes, attribute_mapping, force_save=created)
+ user, attributes, attribute_mapping, force_save=created
+ )
if self.user_can_authenticate(user):
return user
- def _update_user(self, user, attributes: dict, attribute_mapping: dict, force_save: bool = False):
- """ Update a user with a set of attributes and returns the updated user.
+ def _update_user(
+ self, user, attributes: dict, attribute_mapping: dict, force_save: bool = False
+ ):
+ """Update a user with a set of attributes and returns the updated user.
- By default it uses a mapping defined in the settings constant
- SAML_ATTRIBUTE_MAPPING. For each attribute, if the user object has
- that field defined it will be set.
+ By default it uses a mapping defined in the settings constant
+ SAML_ATTRIBUTE_MAPPING. For each attribute, if the user object has
+ that field defined it will be set.
"""
# No attributes to set on the user instance, nothing to update
@@ -166,7 +194,8 @@ def _update_user(self, user, attributes: dict, attribute_mapping: dict, force_sa
attr_value_list = attributes.get(saml_attr)
if not attr_value_list:
logger.debug(
- f'Could not find value for "{saml_attr}", not updating fields "{django_attrs}"')
+ f'Could not find value for "{saml_attr}", not updating fields "{django_attrs}"'
+ )
continue
for attr in django_attrs:
@@ -180,13 +209,11 @@ def _update_user(self, user, attributes: dict, attribute_mapping: dict, force_sa
if callable(user_attr):
modified = user_attr(attr_value_list)
else:
- modified = set_attribute(
- user, attr, attr_value_list[0])
+ modified = set_attribute(user, attr, attr_value_list[0])
has_updated_fields = has_updated_fields or modified
else:
- logger.debug(
- f'Could not find attribute "{attr}" on user "{user}"')
+ logger.debug(f'Could not find attribute "{attr}" on user "{user}"')
if has_updated_fields or force_save:
user = self.save_user(user)
@@ -198,11 +225,18 @@ def _update_user(self, user, attributes: dict, attribute_mapping: dict, force_sa
# ############################################
def clean_attributes(self, attributes: dict, idp_entityid: str, **kwargs) -> dict:
- """ Hook to clean or filter attributes from the SAML response. No-op by default. """
+ """Hook to clean or filter attributes from the SAML response. No-op by default."""
return attributes
- def is_authorized(self, attributes: dict, attribute_mapping: dict, idp_entityid: str, assertion_info: dict, **kwargs) -> bool:
- """ Hook to allow custom authorization policies based on SAML attributes. True by default. """
+ def is_authorized(
+ self,
+ attributes: dict,
+ attribute_mapping: dict,
+ idp_entityid: str,
+ assertion_info: dict,
+ **kwargs,
+ ) -> bool:
+ """Hook to allow custom authorization policies based on SAML attributes. True by default."""
return True
def user_can_authenticate(self, user) -> bool:
@@ -210,26 +244,35 @@ def user_can_authenticate(self, user) -> bool:
Reject users with is_active=False. Custom user models that don't have
that attribute are allowed.
"""
- is_active = getattr(user, 'is_active', None)
+ is_active = getattr(user, "is_active", None)
return is_active or is_active is None
def clean_user_main_attribute(self, main_attribute: Any) -> Any:
- """ Hook to clean the extracted user-identifying value. No-op by default. """
+ """Hook to clean the extracted user-identifying value. No-op by default."""
return main_attribute
- def get_or_create_user(self,
- user_lookup_key: str, user_lookup_value: Any, create_unknown_user: bool,
- idp_entityid: str, attributes: dict, attribute_mapping: dict, request
- ) -> Tuple[Optional[settings.AUTH_USER_MODEL], bool]:
- """ Look up the user to authenticate. If he doesn't exist, this method creates him (if so desired).
- The default implementation looks only at the user_identifier. Override this method in order to do more complex behaviour,
- e.g. customize this per IdP.
+ def get_or_create_user(
+ self,
+ user_lookup_key: str,
+ user_lookup_value: Any,
+ create_unknown_user: bool,
+ idp_entityid: str,
+ attributes: dict,
+ attribute_mapping: dict,
+ request,
+ ) -> Tuple[Optional[settings.AUTH_USER_MODEL], bool]:
+ """Look up the user to authenticate. If he doesn't exist, this method creates him (if so desired).
+ The default implementation looks only at the user_identifier. Override this method in order to do more complex behaviour,
+ e.g. customize this per IdP.
"""
UserModel = self._user_model
# Construct query parameters to query the userModel with. An additional lookup modifier could be specified in the settings.
user_query_args = {
- user_lookup_key + getattr(settings, 'SAML_DJANGO_USER_MAIN_ATTRIBUTE_LOOKUP', ''): user_lookup_value
+ user_lookup_key
+ + getattr(
+ settings, "SAML_DJANGO_USER_MAIN_ATTRIBUTE_LOOKUP", ""
+ ): user_lookup_value
}
# Lookup existing user
@@ -238,30 +281,35 @@ def get_or_create_user(self,
try:
user = UserModel.objects.get(**user_query_args)
except MultipleObjectsReturned:
- logger.error("Multiple users match, model: %s, lookup: %s",
- UserModel._meta, user_query_args)
+ logger.error(
+ "Multiple users match, model: %s, lookup: %s",
+ UserModel._meta,
+ user_query_args,
+ )
except UserModel.DoesNotExist:
# Create new one if desired by settings
if create_unknown_user:
user = UserModel(**{user_lookup_key: user_lookup_value})
created = True
- logger.debug(f'New user created: {user}')
+ logger.debug(f"New user created: {user}")
else:
logger.error(
- f'The user does not exist, model: {UserModel._meta}, lookup: {user_query_args}')
+ f"The user does not exist, model: {UserModel._meta}, lookup: {user_query_args}"
+ )
return user, created
- def save_user(self, user: settings.AUTH_USER_MODEL, *args, **kwargs) -> settings.AUTH_USER_MODEL:
- """ Hook to add custom logic around saving a user. Return the saved user instance.
- """
+ def save_user(
+ self, user: settings.AUTH_USER_MODEL, *args, **kwargs
+ ) -> settings.AUTH_USER_MODEL:
+ """Hook to add custom logic around saving a user. Return the saved user instance."""
is_new_instance = user.pk is None
user.save()
if is_new_instance:
- logger.debug('New user created')
+ logger.debug("New user created")
else:
- logger.debug(f'User {user} updated with incoming attributes')
+ logger.debug(f"User {user} updated with incoming attributes")
return user
@@ -271,41 +319,60 @@ def save_user(self, user: settings.AUTH_USER_MODEL, *args, **kwargs) -> settings
def get_attribute_value(self, django_field, attributes, attribute_mapping):
warnings.warn(
- "get_attribute_value() is deprecated, look at the Saml2Backend on how to subclass it", DeprecationWarning)
+ "get_attribute_value() is deprecated, look at the Saml2Backend on how to subclass it",
+ DeprecationWarning,
+ )
return self._get_attribute_value(django_field, attributes, attribute_mapping)
def get_django_user_main_attribute(self):
warnings.warn(
- "get_django_user_main_attribute() is deprecated, look at the Saml2Backend on how to subclass it", DeprecationWarning)
+ "get_django_user_main_attribute() is deprecated, look at the Saml2Backend on how to subclass it",
+ DeprecationWarning,
+ )
return self._user_lookup_attribute
def get_django_user_main_attribute_lookup(self):
warnings.warn(
- "get_django_user_main_attribute_lookup() is deprecated, look at the Saml2Backend on how to subclass it", DeprecationWarning)
- return getattr(settings, 'SAML_DJANGO_USER_MAIN_ATTRIBUTE_LOOKUP', '')
+ "get_django_user_main_attribute_lookup() is deprecated, look at the Saml2Backend on how to subclass it",
+ DeprecationWarning,
+ )
+ return getattr(settings, "SAML_DJANGO_USER_MAIN_ATTRIBUTE_LOOKUP", "")
def get_user_query_args(self, main_attribute):
warnings.warn(
- "get_user_query_args() is deprecated, look at the Saml2Backend on how to subclass it", DeprecationWarning)
- return {self.get_django_user_main_attribute() + self.get_django_user_main_attribute_lookup()}
+ "get_user_query_args() is deprecated, look at the Saml2Backend on how to subclass it",
+ DeprecationWarning,
+ )
+ return {
+ self.get_django_user_main_attribute()
+ + self.get_django_user_main_attribute_lookup()
+ }
def configure_user(self, user, attributes, attribute_mapping):
warnings.warn(
- "configure_user() is deprecated, look at the Saml2Backend on how to subclass it", DeprecationWarning)
+ "configure_user() is deprecated, look at the Saml2Backend on how to subclass it",
+ DeprecationWarning,
+ )
return self._update_user(user, attributes, attribute_mapping)
def update_user(self, user, attributes, attribute_mapping, force_save=False):
warnings.warn(
- "update_user() is deprecated, look at the Saml2Backend on how to subclass it", DeprecationWarning)
+ "update_user() is deprecated, look at the Saml2Backend on how to subclass it",
+ DeprecationWarning,
+ )
return self._update_user(user, attributes, attribute_mapping)
def _set_attribute(self, obj, attr, value):
warnings.warn(
- "_set_attribute() is deprecated, look at the Saml2Backend on how to subclass it", DeprecationWarning)
+ "_set_attribute() is deprecated, look at the Saml2Backend on how to subclass it",
+ DeprecationWarning,
+ )
return set_attribute(obj, attr, value)
def get_saml_user_model():
warnings.warn(
- "_set_attribute() is deprecated, look at the Saml2Backend on how to subclass it", DeprecationWarning)
+ "_set_attribute() is deprecated, look at the Saml2Backend on how to subclass it",
+ DeprecationWarning,
+ )
return Saml2Backend()._user_model
diff --git a/djangosaml2/cache.py b/djangosaml2/cache.py
index c6d2f16b..b3d31fc4 100644
--- a/djangosaml2/cache.py
+++ b/djangosaml2/cache.py
@@ -19,7 +19,7 @@
class DjangoSessionCacheAdapter(dict):
"""A cache of things that are stored in the Django Session"""
- key_prefix = '_saml2'
+ key_prefix = "_saml2"
def __init__(self, django_session, key_suffix):
self.session = django_session
@@ -43,14 +43,13 @@ def sync(self):
self.session.modified = True
-class OutstandingQueriesCache(object):
+class OutstandingQueriesCache:
"""Handles the queries that have been sent to the IdP and have not
been replied yet.
"""
def __init__(self, django_session):
- self._db = DjangoSessionCacheAdapter(
- django_session, '_outstanding_queries')
+ self._db = DjangoSessionCacheAdapter(django_session, "_outstanding_queries")
def outstanding_queries(self):
return self._db._get_objects()
@@ -79,7 +78,7 @@ class IdentityCache(Cache):
"""
def __init__(self, django_session):
- self._db = DjangoSessionCacheAdapter(django_session, '_identities')
+ self._db = DjangoSessionCacheAdapter(django_session, "_identities")
self._sync = True
@@ -89,4 +88,4 @@ class StateCache(DjangoSessionCacheAdapter):
"""
def __init__(self, django_session):
- super().__init__(django_session, '_state')
+ super().__init__(django_session, "_state")
diff --git a/djangosaml2/conf.py b/djangosaml2/conf.py
index 811791fb..e4cf140a 100644
--- a/djangosaml2/conf.py
+++ b/djangosaml2/conf.py
@@ -20,47 +20,48 @@
from django.core.exceptions import ImproperlyConfigured
from django.http import HttpRequest
from django.utils.module_loading import import_string
+
from saml2.config import SPConfig
from .utils import get_custom_setting
def get_config_loader(path: str) -> Callable:
- """ Import the function at a given path and return it
- """
+ """Import the function at a given path and return it"""
try:
config_loader = import_string(path)
except ImportError as e:
- raise ImproperlyConfigured(
- f'Error importing SAML config loader {path}: "{e}"')
+ raise ImproperlyConfigured(f'Error importing SAML config loader {path}: "{e}"')
if not callable(config_loader):
- raise ImproperlyConfigured(
- "SAML config loader must be a callable object.")
+ raise ImproperlyConfigured("SAML config loader must be a callable object.")
return config_loader
def config_settings_loader(request: Optional[HttpRequest] = None) -> SPConfig:
- """ Utility function to load the pysaml2 configuration.
- The configuration can be modified based on the request being passed.
- This is the default config loader, which just loads the config from the settings.
+ """Utility function to load the pysaml2 configuration.
+ The configuration can be modified based on the request being passed.
+ This is the default config loader, which just loads the config from the settings.
"""
conf = SPConfig()
conf.load(copy.deepcopy(settings.SAML_CONFIG))
return conf
-def get_config(config_loader_path: Optional[Union[Callable, str]] = None,
- request: Optional[HttpRequest] = None) -> SPConfig:
- """ Load a config_loader function if necessary, and call that
- function with the request as argument.
- If the config_loader_path is a callable instead of a string,
- no importing is necessary and it will be used directly.
- Return the resulting SPConfig.
+def get_config(
+ config_loader_path: Optional[Union[Callable, str]] = None,
+ request: Optional[HttpRequest] = None,
+) -> SPConfig:
+ """Load a config_loader function if necessary, and call that
+ function with the request as argument.
+ If the config_loader_path is a callable instead of a string,
+ no importing is necessary and it will be used directly.
+ Return the resulting SPConfig.
"""
config_loader_path = config_loader_path or get_custom_setting(
- 'SAML_CONFIG_LOADER', 'djangosaml2.conf.config_settings_loader')
+ "SAML_CONFIG_LOADER", "djangosaml2.conf.config_settings_loader"
+ )
if callable(config_loader_path):
config_loader = config_loader_path
diff --git a/djangosaml2/middleware.py b/djangosaml2/middleware.py
index abebaae9..6418c473 100644
--- a/djangosaml2/middleware.py
+++ b/djangosaml2/middleware.py
@@ -2,19 +2,19 @@
from django import VERSION
from django.conf import settings
-from django.contrib.sessions.backends.base import UpdateError
-from django.contrib.sessions.middleware import SessionMiddleware
from django.core.exceptions import SuspiciousOperation
from django.utils.cache import patch_vary_headers
from django.utils.http import http_date
+from django.contrib.sessions.backends.base import UpdateError
+from django.contrib.sessions.middleware import SessionMiddleware
-django_version = float('{}.{}'.format(*VERSION[:2]))
-SAMESITE_NONE = None if django_version < 3.1 else 'None'
+django_version = float("{}.{}".format(*VERSION[:2]))
+SAMESITE_NONE = None if django_version < 3.1 else "None"
class SamlSessionMiddleware(SessionMiddleware):
- cookie_name = getattr(settings, 'SAML_SESSION_COOKIE_NAME', 'saml_session')
+ cookie_name = getattr(settings, "SAML_SESSION_COOKIE_NAME", "saml_session")
def process_request(self, request):
session_key = request.COOKIES.get(self.cookie_name, None)
@@ -41,10 +41,10 @@ def process_response(self, request, response):
domain=settings.SESSION_COOKIE_DOMAIN,
samesite=SAMESITE_NONE,
)
- patch_vary_headers(response, ('Cookie',))
+ patch_vary_headers(response, ("Cookie",))
else:
if accessed:
- patch_vary_headers(response, ('Cookie',))
+ patch_vary_headers(response, ("Cookie",))
# relies and the global one
if (modified or settings.SESSION_SAVE_EVERY_REQUEST) and not empty:
if request.saml_session.get_expire_at_browser_close():
@@ -69,10 +69,11 @@ def process_response(self, request, response):
self.cookie_name,
request.saml_session.session_key,
max_age=max_age,
- expires=expires, domain=settings.SESSION_COOKIE_DOMAIN,
+ expires=expires,
+ domain=settings.SESSION_COOKIE_DOMAIN,
path=settings.SESSION_COOKIE_PATH,
secure=settings.SESSION_COOKIE_SECURE or None,
httponly=settings.SESSION_COOKIE_HTTPONLY or None,
- samesite=SAMESITE_NONE
+ samesite=SAMESITE_NONE,
)
return response
diff --git a/djangosaml2/overrides.py b/djangosaml2/overrides.py
index 4c158c54..f2b8ce34 100644
--- a/djangosaml2/overrides.py
+++ b/djangosaml2/overrides.py
@@ -1,9 +1,10 @@
import logging
-import saml2.client
from django.conf import settings
-logger = logging.getLogger('djangosaml2')
+import saml2.client
+
+logger = logging.getLogger("djangosaml2")
class Saml2Client(saml2.client.Saml2Client):
@@ -16,10 +17,14 @@ class Saml2Client(saml2.client.Saml2Client):
"""
def do_logout(self, *args, **kwargs):
- if not kwargs.get('expected_binding'):
+ if not kwargs.get("expected_binding"):
try:
- kwargs['expected_binding'] = settings.SAML_LOGOUT_REQUEST_PREFERRED_BINDING
+ kwargs[
+ "expected_binding"
+ ] = settings.SAML_LOGOUT_REQUEST_PREFERRED_BINDING
except AttributeError:
- logger.warning('SAML_LOGOUT_REQUEST_PREFERRED_BINDING setting is'
- ' not defined. Default binding will be used.')
+ logger.warning(
+ "SAML_LOGOUT_REQUEST_PREFERRED_BINDING setting is"
+ " not defined. Default binding will be used."
+ )
return super().do_logout(*args, **kwargs)
diff --git a/djangosaml2/templatetags/idplist.py b/djangosaml2/templatetags/idplist.py
index cf4eccad..47959704 100644
--- a/djangosaml2/templatetags/idplist.py
+++ b/djangosaml2/templatetags/idplist.py
@@ -21,14 +21,13 @@
class IdPListNode(template.Node):
-
def __init__(self, variable_name):
self.variable_name = variable_name
def render(self, context):
conf = config_settings_loader()
context[self.variable_name] = available_idps(conf)
- return ''
+ return ""
@register.tag
@@ -37,9 +36,11 @@ def idplist(parser, token):
tag_name, as_part, variable = token.split_contents()
except ValueError:
raise template.TemplateSyntaxError(
- '%r tag requires two arguments' % token.contents.split()[0])
- if not as_part == 'as':
+ "%r tag requires two arguments" % token.contents.split()[0]
+ )
+ if not as_part == "as":
raise template.TemplateSyntaxError(
- '%r tag first argument must be the literal "as"' % tag_name)
+ '%r tag first argument must be the literal "as"' % tag_name
+ )
return IdPListNode(variable)
diff --git a/djangosaml2/tests/__init__.py b/djangosaml2/tests/__init__.py
index 867c05c7..03e0e93a 100644
--- a/djangosaml2/tests/__init__.py
+++ b/djangosaml2/tests/__init__.py
@@ -23,27 +23,34 @@
from django import http
from django.conf import settings
-from django.contrib.auth import SESSION_KEY, get_user_model
-from django.contrib.auth.models import AnonymousUser
from django.core.exceptions import ImproperlyConfigured
from django.test import Client, TestCase, override_settings
from django.test.client import RequestFactory
from django.urls import reverse, reverse_lazy
+
+from django.contrib.auth import SESSION_KEY, get_user_model
+from django.contrib.auth.models import AnonymousUser
+
+from saml2.config import SPConfig
+from saml2.s_utils import (
+ UnknownSystemEntity,
+ decode_base64_and_inflate,
+ deflate_and_base64_encode,
+)
+
from djangosaml2 import views
from djangosaml2.cache import OutstandingQueriesCache
from djangosaml2.conf import get_config
from djangosaml2.middleware import SamlSessionMiddleware
from djangosaml2.tests import conf
-from djangosaml2.utils import (get_fallback_login_redirect_url,
- get_idp_sso_supported_bindings,
- get_session_id_from_saml2,
- get_subject_id_from_saml2,
- saml2_from_httpredirect_request)
-from djangosaml2.views import (EchoAttributesView, finish_logout)
-from saml2.config import SPConfig
-from saml2.s_utils import (decode_base64_and_inflate,
- deflate_and_base64_encode,
- UnknownSystemEntity)
+from djangosaml2.utils import (
+ get_fallback_login_redirect_url,
+ get_idp_sso_supported_bindings,
+ get_session_id_from_saml2,
+ get_subject_id_from_saml2,
+ saml2_from_httpredirect_request,
+)
+from djangosaml2.views import EchoAttributesView, finish_logout
from .auth_response import auth_response
from .utils import SAMLPostFormParser
@@ -54,7 +61,7 @@
def dummy_loader(request):
- return 'dummy_loader'
+ return "dummy_loader"
def dummy_get_response(request: http.HttpRequest):
@@ -66,92 +73,110 @@ def dummy_get_response(request: http.HttpRequest):
return http.HttpResponse("Session test")
-non_callable = 'just a string'
+non_callable = "just a string"
class UtilsTests(TestCase):
def test_get_config_valid_path(self):
- self.assertEqual(get_config(
- 'djangosaml2.tests.dummy_loader'), 'dummy_loader')
+ self.assertEqual(get_config("djangosaml2.tests.dummy_loader"), "dummy_loader")
def test_get_config_wrongly_formatted_path(self):
- with self.assertRaisesMessage(ImproperlyConfigured, 'SAML config loader must be a callable object.'):
- get_config('djangosaml2.tests.non_callable')
+ with self.assertRaisesMessage(
+ ImproperlyConfigured, "SAML config loader must be a callable object."
+ ):
+ get_config("djangosaml2.tests.non_callable")
def test_get_config_nonsense_path(self):
- with self.assertRaisesMessage(ImproperlyConfigured, 'Error importing SAML config loader lalala.nonexisting.blabla: "No module named \'lalala\'"'):
- get_config('lalala.nonexisting.blabla')
+ with self.assertRaisesMessage(
+ ImproperlyConfigured,
+ "Error importing SAML config loader lalala.nonexisting.blabla: \"No module named 'lalala'\"",
+ ):
+ get_config("lalala.nonexisting.blabla")
def test_get_config_missing_function(self):
- with self.assertRaisesMessage(ImproperlyConfigured, 'Module "djangosaml2.tests" does not define a "nonexisting_function" attribute/class'):
- get_config('djangosaml2.tests.nonexisting_function')
+ with self.assertRaisesMessage(
+ ImproperlyConfigured,
+ 'Module "djangosaml2.tests" does not define a "nonexisting_function" attribute/class',
+ ):
+ get_config("djangosaml2.tests.nonexisting_function")
- @override_settings(LOGIN_REDIRECT_URL='/accounts/profile/')
+ @override_settings(LOGIN_REDIRECT_URL="/accounts/profile/")
def test_get_fallback_login_redirect_url(self):
- self.assertEqual(get_fallback_login_redirect_url(), '/accounts/profile/')
+ self.assertEqual(get_fallback_login_redirect_url(), "/accounts/profile/")
with override_settings():
del settings.LOGIN_REDIRECT_URL
# Neither LOGIN_REDIRECT_URL nor ACS_DEFAULT_REDIRECT_URL is configured
- self.assertEqual(get_fallback_login_redirect_url(), '/')
+ self.assertEqual(get_fallback_login_redirect_url(), "/")
- with override_settings(ACS_DEFAULT_REDIRECT_URL='testprofiles:dashboard'):
+ with override_settings(ACS_DEFAULT_REDIRECT_URL="testprofiles:dashboard"):
# ACS_DEFAULT_REDIRECT_URL is configured, so it is used (and resolved)
- self.assertEqual(get_fallback_login_redirect_url(), '/dashboard/')
+ self.assertEqual(get_fallback_login_redirect_url(), "/dashboard/")
- with override_settings(ACS_DEFAULT_REDIRECT_URL=reverse_lazy('testprofiles:dashboard')):
+ with override_settings(
+ ACS_DEFAULT_REDIRECT_URL=reverse_lazy("testprofiles:dashboard")
+ ):
# Lazy urls are resolved
- self.assertEqual(get_fallback_login_redirect_url(), '/dashboard/')
+ self.assertEqual(get_fallback_login_redirect_url(), "/dashboard/")
class SAML2Tests(TestCase):
- urls = 'djangosaml2.tests.urls'
+ urls = "djangosaml2.tests.urls"
def init_cookies(self):
- self.client.cookies[settings.SESSION_COOKIE_NAME] = 'testing'
+ self.client.cookies[settings.SESSION_COOKIE_NAME] = "testing"
def add_outstanding_query(self, session_id, came_from):
- settings.SESSION_ENGINE = 'django.contrib.sessions.backends.db'
+ settings.SESSION_ENGINE = "django.contrib.sessions.backends.db"
engine = import_module(settings.SESSION_ENGINE)
self.saml_session = engine.SessionStore()
self.saml_session.save()
self.oq_cache = OutstandingQueriesCache(self.saml_session)
- self.oq_cache.set(session_id
- if isinstance(session_id, str) else session_id.decode(),
- came_from)
+ self.oq_cache.set(
+ session_id if isinstance(session_id, str) else session_id.decode(),
+ came_from,
+ )
self.saml_session.save()
- self.client.cookies[settings.SESSION_COOKIE_NAME] = self.saml_session.session_key
+ self.client.cookies[
+ settings.SESSION_COOKIE_NAME
+ ] = self.saml_session.session_key
- def b64_for_post(self, xml_text, encoding='utf-8'):
- return base64.b64encode(xml_text.encode(encoding)).decode('ascii')
+ def b64_for_post(self, xml_text, encoding="utf-8"):
+ return base64.b64encode(xml_text.encode(encoding)).decode("ascii")
def test_get_idp_sso_supported_bindings_noargs(self):
settings.SAML_CONFIG = conf.create_conf(
- sp_host='sp.example.com',
- idp_hosts=['idp.example.com'],
- metadata_file='remote_metadata_one_idp.xml',
+ sp_host="sp.example.com",
+ idp_hosts=["idp.example.com"],
+ metadata_file="remote_metadata_one_idp.xml",
+ )
+ idp_id = "https://idp.example.com/simplesaml/saml2/idp/metadata.php"
+ self.assertEqual(
+ get_idp_sso_supported_bindings()[0],
+ list(
+ settings.SAML_CONFIG["service"]["sp"]["idp"][idp_id][
+ "single_sign_on_service"
+ ].keys()
+ )[0],
)
- idp_id = 'https://idp.example.com/simplesaml/saml2/idp/metadata.php'
- self.assertEqual(get_idp_sso_supported_bindings()[0], list(
- settings.SAML_CONFIG['service']['sp']['idp'][idp_id]['single_sign_on_service'].keys())[0])
def test_get_idp_sso_supported_bindings_unknown_idp(self):
settings.SAML_CONFIG = conf.create_conf(
- sp_host='sp.example.com',
- idp_hosts=['idp.example.com'],
- metadata_file='remote_metadata_one_idp.xml',
+ sp_host="sp.example.com",
+ idp_hosts=["idp.example.com"],
+ metadata_file="remote_metadata_one_idp.xml",
)
with self.assertRaises(UnknownSystemEntity):
- get_idp_sso_supported_bindings(idp_entity_id='random')
+ get_idp_sso_supported_bindings(idp_entity_id="random")
def test_get_idp_sso_supported_bindings_no_idps(self):
settings.SAML_CONFIG = conf.create_conf(
- sp_host='sp.example.com',
+ sp_host="sp.example.com",
idp_hosts=[],
- metadata_file='remote_metadata_no_idp.xml',
+ metadata_file="remote_metadata_no_idp.xml",
)
with self.assertRaisesMessage(ImproperlyConfigured, "No IdP configured!"):
get_idp_sso_supported_bindings()
@@ -164,23 +189,24 @@ def test_unsigned_post_authn_request(self):
https://github.com/knaperek/djangosaml2/issues/168
"""
settings.SAML_CONFIG = conf.create_conf(
- sp_host='sp.example.com',
- idp_hosts=['idp.example.com'],
- metadata_file='remote_metadata_post_binding.xml',
- authn_requests_signed=False
+ sp_host="sp.example.com",
+ idp_hosts=["idp.example.com"],
+ metadata_file="remote_metadata_post_binding.xml",
+ authn_requests_signed=False,
)
- response = self.client.get(reverse('saml2_login'))
+ response = self.client.get(reverse("saml2_login"))
self.assertEqual(response.status_code, 200)
# Using POST-binding returns a page with form containing the SAMLRequest
response_parser = SAMLPostFormParser()
- response_parser.feed(response.content.decode('utf-8'))
+ response_parser.feed(response.content.decode("utf-8"))
saml_request = response_parser.saml_request_value
self.assertIsNotNone(saml_request)
- self.assertIn('AuthnRequest xmlns', base64.b64decode(
- saml_request).decode('utf-8'))
+ self.assertIn(
+ "AuthnRequest xmlns", base64.b64decode(saml_request).decode("utf-8")
+ )
def test_login_evil_redirect(self):
"""
@@ -190,29 +216,31 @@ def test_login_evil_redirect(self):
# monkey patch SAML configuration
settings.SAML_CONFIG = conf.create_conf(
- sp_host='sp.example.com',
- idp_hosts=['idp.example.com'],
- metadata_file='remote_metadata_one_idp.xml',
+ sp_host="sp.example.com",
+ idp_hosts=["idp.example.com"],
+ metadata_file="remote_metadata_one_idp.xml",
)
- for redirect_url in ['/dashboard/', 'testprofiles:dashboard']:
+ for redirect_url in ["/dashboard/", "testprofiles:dashboard"]:
with self.subTest(LOGIN_REDIRECT_URL=redirect_url):
with override_settings(LOGIN_REDIRECT_URL=redirect_url):
response = self.client.get(
- reverse('saml2_login') + '?next=http://evil.com')
- url = urlparse(response['Location'])
+ reverse("saml2_login") + "?next=http://evil.com"
+ )
+ url = urlparse(response["Location"])
params = parse_qs(url.query)
- self.assertEqual(params['RelayState'], ['/dashboard/'])
+ self.assertEqual(params["RelayState"], ["/dashboard/"])
with self.subTest(ACS_DEFAULT_REDIRECT_URL=redirect_url):
with override_settings(ACS_DEFAULT_REDIRECT_URL=redirect_url):
response = self.client.get(
- reverse('saml2_login') + '?next=http://evil.com')
- url = urlparse(response['Location'])
+ reverse("saml2_login") + "?next=http://evil.com"
+ )
+ url = urlparse(response["Location"])
params = parse_qs(url.query)
- self.assertEqual(params['RelayState'], ['/dashboard/'])
+ self.assertEqual(params["RelayState"], ["/dashboard/"])
def test_no_redirect(self):
"""
@@ -222,66 +250,70 @@ def test_no_redirect(self):
# monkey patch SAML configuration
settings.SAML_CONFIG = conf.create_conf(
- sp_host='sp.example.com',
- idp_hosts=['idp.example.com'],
- metadata_file='remote_metadata_one_idp.xml',
+ sp_host="sp.example.com",
+ idp_hosts=["idp.example.com"],
+ metadata_file="remote_metadata_one_idp.xml",
)
- for redirect_url in ['/dashboard/', 'testprofiles:dashboard']:
+ for redirect_url in ["/dashboard/", "testprofiles:dashboard"]:
with self.subTest(LOGIN_REDIRECT_URL=redirect_url):
with override_settings(LOGIN_REDIRECT_URL=redirect_url):
- response = self.client.get(reverse('saml2_login') + '?next=')
- url = urlparse(response['Location'])
+ response = self.client.get(reverse("saml2_login") + "?next=")
+ url = urlparse(response["Location"])
params = parse_qs(url.query)
- self.assertEqual(params['RelayState'], ['/dashboard/'])
+ self.assertEqual(params["RelayState"], ["/dashboard/"])
with self.subTest(ACS_DEFAULT_REDIRECT_URL=redirect_url):
with override_settings(ACS_DEFAULT_REDIRECT_URL=redirect_url):
- response = self.client.get(reverse('saml2_login') + '?next=')
- url = urlparse(response['Location'])
+ response = self.client.get(reverse("saml2_login") + "?next=")
+ url = urlparse(response["Location"])
params = parse_qs(url.query)
- self.assertEqual(params['RelayState'], ['/dashboard/'])
+ self.assertEqual(params["RelayState"], ["/dashboard/"])
@override_settings(SAML_IGNORE_AUTHENTICATED_USERS_ON_LOGIN=True)
def test_login_already_logged(self):
- self.client.force_login(User.objects.create(username='user', password='pass'))
+ self.client.force_login(User.objects.create(username="user", password="pass"))
- for redirect_url in ['/dashboard/', 'testprofiles:dashboard']:
+ for redirect_url in ["/dashboard/", "testprofiles:dashboard"]:
with self.subTest(LOGIN_REDIRECT_URL=redirect_url):
with override_settings(LOGIN_REDIRECT_URL=redirect_url):
- with self.subTest('no next url'):
- response = self.client.get(reverse('saml2_login'))
- self.assertRedirects(response, '/dashboard/')
+ with self.subTest("no next url"):
+ response = self.client.get(reverse("saml2_login"))
+ self.assertRedirects(response, "/dashboard/")
- with self.subTest('evil next url'):
- response = self.client.get(reverse('saml2_login') + '?next=http://evil.com')
- self.assertRedirects(response, '/dashboard/')
+ with self.subTest("evil next url"):
+ response = self.client.get(
+ reverse("saml2_login") + "?next=http://evil.com"
+ )
+ self.assertRedirects(response, "/dashboard/")
with self.subTest(ACS_DEFAULT_REDIRECT_URL=redirect_url):
with override_settings(ACS_DEFAULT_REDIRECT_URL=redirect_url):
- with self.subTest('no next url'):
- response = self.client.get(reverse('saml2_login'))
- self.assertRedirects(response, '/dashboard/')
+ with self.subTest("no next url"):
+ response = self.client.get(reverse("saml2_login"))
+ self.assertRedirects(response, "/dashboard/")
- with self.subTest('evil next url'):
- response = self.client.get(reverse('saml2_login') + '?next=http://evil.com')
- self.assertRedirects(response, '/dashboard/')
+ with self.subTest("evil next url"):
+ response = self.client.get(
+ reverse("saml2_login") + "?next=http://evil.com"
+ )
+ self.assertRedirects(response, "/dashboard/")
def test_unknown_idp(self):
# monkey patch SAML configuration
settings.SAML_CONFIG = conf.create_conf(
- sp_host='sp.example.com',
- metadata_file='remote_metadata_three_idps.xml',
+ sp_host="sp.example.com",
+ metadata_file="remote_metadata_three_idps.xml",
)
- response = self.client.get(reverse('saml2_login')+'?idp=https://unknown.org')
+ response = self.client.get(reverse("saml2_login") + "?idp=https://unknown.org")
self.assertEqual(response.status_code, 403)
-
def test_login_authn_context(self):
- sp_kwargs = {"requested_authn_context": {
+ sp_kwargs = {
+ "requested_authn_context": {
"authn_context_class_ref": [
"urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport",
"urn:oasis:names:tc:SAML:2.0:ac:classes:TLSClient",
@@ -292,252 +324,270 @@ def test_login_authn_context(self):
# monkey patch SAML configuration
settings.SAML_CONFIG = conf.create_conf(
- sp_host='sp.example.com',
- idp_hosts=['idp.example.com'],
- metadata_file='remote_metadata_one_idp.xml',
- sp_kwargs=sp_kwargs
+ sp_host="sp.example.com",
+ idp_hosts=["idp.example.com"],
+ metadata_file="remote_metadata_one_idp.xml",
+ sp_kwargs=sp_kwargs,
)
- response = self.client.get(reverse('saml2_login'))
+ response = self.client.get(reverse("saml2_login"))
self.assertEqual(response.status_code, 302)
- location = response['Location']
+ location = response["Location"]
url = urlparse(location)
- self.assertEqual(url.hostname, 'idp.example.com')
- self.assertEqual(url.path, '/simplesaml/saml2/idp/SSOService.php')
+ self.assertEqual(url.hostname, "idp.example.com")
+ self.assertEqual(url.path, "/simplesaml/saml2/idp/SSOService.php")
params = parse_qs(url.query)
- self.assertIn('SAMLRequest', params)
-
- saml_request = params['SAMLRequest'][0]
- self.assertIn('urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport', decode_base64_and_inflate(
- saml_request).decode('utf-8'))
+ self.assertIn("SAMLRequest", params)
+ saml_request = params["SAMLRequest"][0]
+ self.assertIn(
+ "urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport",
+ decode_base64_and_inflate(saml_request).decode("utf-8"),
+ )
def test_login_one_idp(self):
# monkey patch SAML configuration
settings.SAML_CONFIG = conf.create_conf(
- sp_host='sp.example.com',
- idp_hosts=['idp.example.com'],
- metadata_file='remote_metadata_one_idp.xml',
+ sp_host="sp.example.com",
+ idp_hosts=["idp.example.com"],
+ metadata_file="remote_metadata_one_idp.xml",
)
- response = self.client.get(reverse('saml2_login'))
+ response = self.client.get(reverse("saml2_login"))
self.assertEqual(response.status_code, 302)
- location = response['Location']
+ location = response["Location"]
url = urlparse(location)
- self.assertEqual(url.hostname, 'idp.example.com')
- self.assertEqual(url.path, '/simplesaml/saml2/idp/SSOService.php')
+ self.assertEqual(url.hostname, "idp.example.com")
+ self.assertEqual(url.path, "/simplesaml/saml2/idp/SSOService.php")
params = parse_qs(url.query)
- self.assertIn('SAMLRequest', params)
- self.assertIn('RelayState', params)
-
- saml_request = params['SAMLRequest'][0]
- self.assertIn('AuthnRequest xmlns', decode_base64_and_inflate(
- saml_request).decode('utf-8'))
+ self.assertIn("SAMLRequest", params)
+ self.assertIn("RelayState", params)
+ saml_request = params["SAMLRequest"][0]
+ self.assertIn(
+ "AuthnRequest xmlns",
+ decode_base64_and_inflate(saml_request).decode("utf-8"),
+ )
# if we set a next arg in the login view, it is preserverd
# in the RelayState argument
- nexturl = '/another-view/'
- response = self.client.get(reverse('saml2_login'), {'next': nexturl})
+ nexturl = "/another-view/"
+ response = self.client.get(reverse("saml2_login"), {"next": nexturl})
self.assertEqual(response.status_code, 302)
- location = response['Location']
+ location = response["Location"]
url = urlparse(location)
- self.assertEqual(url.hostname, 'idp.example.com')
- self.assertEqual(url.path, '/simplesaml/saml2/idp/SSOService.php')
+ self.assertEqual(url.hostname, "idp.example.com")
+ self.assertEqual(url.path, "/simplesaml/saml2/idp/SSOService.php")
params = parse_qs(url.query)
- self.assertIn('SAMLRequest', params)
- self.assertIn('RelayState', params)
- self.assertEqual(params['RelayState'][0], nexturl)
+ self.assertIn("SAMLRequest", params)
+ self.assertIn("RelayState", params)
+ self.assertEqual(params["RelayState"][0], nexturl)
def test_login_several_idps(self):
settings.SAML_CONFIG = conf.create_conf(
- sp_host='sp.example.com',
- idp_hosts=['idp1.example.com',
- 'idp2.example.com',
- 'idp3.example.com'],
- metadata_file='remote_metadata_three_idps.xml',
+ sp_host="sp.example.com",
+ idp_hosts=["idp1.example.com", "idp2.example.com", "idp3.example.com"],
+ metadata_file="remote_metadata_three_idps.xml",
)
- response = self.client.get(reverse('saml2_login'))
+ response = self.client.get(reverse("saml2_login"))
# a WAYF page should be displayed
- self.assertContains(response, 'Where are you from?', status_code=200)
+ self.assertContains(response, "Where are you from?", status_code=200)
for i in range(1, 4):
- link = '/login/?idp=https://idp%d.example.com/simplesaml/saml2/idp/metadata.php&next=/'
+ link = "/login/?idp=https://idp%d.example.com/simplesaml/saml2/idp/metadata.php&next=/"
self.assertContains(response, link % i)
# click on the second idp
- response = self.client.get(reverse('saml2_login'), {
- 'idp': 'https://idp2.example.com/simplesaml/saml2/idp/metadata.php',
- 'next': '/',
- })
+ response = self.client.get(
+ reverse("saml2_login"),
+ {
+ "idp": "https://idp2.example.com/simplesaml/saml2/idp/metadata.php",
+ "next": "/",
+ },
+ )
self.assertEqual(response.status_code, 302)
- location = response['Location']
+ location = response["Location"]
url = urlparse(location)
- self.assertEqual(url.hostname, 'idp2.example.com')
- self.assertEqual(url.path, '/simplesaml/saml2/idp/SSOService.php')
+ self.assertEqual(url.hostname, "idp2.example.com")
+ self.assertEqual(url.path, "/simplesaml/saml2/idp/SSOService.php")
params = parse_qs(url.query)
- self.assertIn('SAMLRequest', params)
- self.assertIn('RelayState', params)
+ self.assertIn("SAMLRequest", params)
+ self.assertIn("RelayState", params)
- saml_request = params['SAMLRequest'][0]
- self.assertIn('AuthnRequest xmlns', decode_base64_and_inflate(
- saml_request).decode('utf-8'))
+ saml_request = params["SAMLRequest"][0]
+ self.assertIn(
+ "AuthnRequest xmlns",
+ decode_base64_and_inflate(saml_request).decode("utf-8"),
+ )
- @override_settings(ACS_DEFAULT_REDIRECT_URL='testprofiles:dashboard')
+ @override_settings(ACS_DEFAULT_REDIRECT_URL="testprofiles:dashboard")
def test_assertion_consumer_service(self):
# Get initial number of users
initial_user_count = User.objects.count()
settings.SAML_CONFIG = conf.create_conf(
- sp_host='sp.example.com',
- idp_hosts=['idp.example.com'],
- metadata_file='remote_metadata_one_idp.xml',
+ sp_host="sp.example.com",
+ idp_hosts=["idp.example.com"],
+ metadata_file="remote_metadata_one_idp.xml",
)
- response = self.client.get(reverse('saml2_login'))
+ response = self.client.get(reverse("saml2_login"))
saml2_req = saml2_from_httpredirect_request(response.url)
session_id = get_session_id_from_saml2(saml2_req)
# session_id should start with a letter since it is a NCName
- came_from = '/another-view/'
+ came_from = "/another-view/"
self.add_outstanding_query(session_id, came_from)
# this will create a user
- saml_response = auth_response(session_id, 'student')
- _url = reverse('saml2_acs')
- response = self.client.post(_url, {
- 'SAMLResponse': self.b64_for_post(saml_response),
- 'RelayState': came_from,
- })
+ saml_response = auth_response(session_id, "student")
+ _url = reverse("saml2_acs")
+ response = self.client.post(
+ _url,
+ {
+ "SAMLResponse": self.b64_for_post(saml_response),
+ "RelayState": came_from,
+ },
+ )
self.assertEqual(response.status_code, 302)
- location = response['Location']
+ location = response["Location"]
url = urlparse(location)
self.assertEqual(url.path, came_from)
self.assertEqual(User.objects.count(), initial_user_count + 1)
user_id = self.client.session[SESSION_KEY]
user = User.objects.get(id=user_id)
- self.assertEqual(user.username, 'student')
+ self.assertEqual(user.username, "student")
# let's create another user and log in with that one
- new_user = User.objects.create(username='teacher', password='not-used')
+ new_user = User.objects.create(username="teacher", password="not-used")
# session_id = "a1111111111111111111111111111111"
client = Client()
- response = client.get(reverse('saml2_login'))
+ response = client.get(reverse("saml2_login"))
saml2_req = saml2_from_httpredirect_request(response.url)
session_id = get_session_id_from_saml2(saml2_req)
- came_from = '' # bad, let's see if we can deal with this
- saml_response = auth_response(session_id, 'teacher')
- self.add_outstanding_query(session_id, '/')
- response = client.post(reverse('saml2_acs'), {
- 'SAMLResponse': self.b64_for_post(saml_response),
- 'RelayState': came_from,
- })
+ came_from = "" # bad, let's see if we can deal with this
+ saml_response = auth_response(session_id, "teacher")
+ self.add_outstanding_query(session_id, "/")
+ response = client.post(
+ reverse("saml2_acs"),
+ {
+ "SAMLResponse": self.b64_for_post(saml_response),
+ "RelayState": came_from,
+ },
+ )
# as the RelayState is empty we have redirect to ACS_DEFAULT_REDIRECT_URL
- self.assertRedirects(response, '/dashboard/')
+ self.assertRedirects(response, "/dashboard/")
self.assertEqual(str(new_user.id), client.session[SESSION_KEY])
- @override_settings(ACS_DEFAULT_REDIRECT_URL='testprofiles:dashboard')
+ @override_settings(ACS_DEFAULT_REDIRECT_URL="testprofiles:dashboard")
def test_assertion_consumer_service_default_relay_state(self):
settings.SAML_CONFIG = conf.create_conf(
- sp_host='sp.example.com',
- idp_hosts=['idp.example.com'],
- metadata_file='remote_metadata_one_idp.xml',
+ sp_host="sp.example.com",
+ idp_hosts=["idp.example.com"],
+ metadata_file="remote_metadata_one_idp.xml",
)
- new_user = User.objects.create(username='teacher', password='not-used')
+ new_user = User.objects.create(username="teacher", password="not-used")
- response = self.client.get(reverse('saml2_login'))
+ response = self.client.get(reverse("saml2_login"))
saml2_req = saml2_from_httpredirect_request(response.url)
session_id = get_session_id_from_saml2(saml2_req)
- saml_response = auth_response(session_id, 'teacher')
- self.add_outstanding_query(session_id, '/')
- response = self.client.post(reverse('saml2_acs'), {
- 'SAMLResponse': self.b64_for_post(saml_response),
- })
+ saml_response = auth_response(session_id, "teacher")
+ self.add_outstanding_query(session_id, "/")
+ response = self.client.post(
+ reverse("saml2_acs"),
+ {
+ "SAMLResponse": self.b64_for_post(saml_response),
+ },
+ )
self.assertEqual(response.status_code, 302)
# The RelayState is missing, redirect to ACS_DEFAULT_REDIRECT_URL
- self.assertRedirects(response, '/dashboard/')
+ self.assertRedirects(response, "/dashboard/")
self.assertEqual(str(new_user.id), self.client.session[SESSION_KEY])
def test_assertion_consumer_service_already_logged_in_allowed(self):
- self.client.force_login(User.objects.create(
- username='user', password='pass'))
+ self.client.force_login(User.objects.create(username="user", password="pass"))
settings.SAML_IGNORE_AUTHENTICATED_USERS_ON_LOGIN = True
- came_from = '/dummy-url/'
- response = self.client.get(
- reverse('saml2_login') + f'?next={came_from}')
+ came_from = "/dummy-url/"
+ response = self.client.get(reverse("saml2_login") + f"?next={came_from}")
self.assertEqual(response.status_code, 302)
- url = urlparse(response['Location'])
+ url = urlparse(response["Location"])
self.assertEqual(url.path, came_from)
def test_assertion_consumer_service_already_logged_in_error(self):
- self.client.force_login(User.objects.create(
- username='user', password='pass'))
+ self.client.force_login(User.objects.create(username="user", password="pass"))
settings.SAML_IGNORE_AUTHENTICATED_USERS_ON_LOGIN = False
- came_from = '/dummy-url/'
- response = self.client.get(
- reverse('saml2_login') + f'?next={came_from}')
+ came_from = "/dummy-url/"
+ response = self.client.get(reverse("saml2_login") + f"?next={came_from}")
self.assertEqual(response.status_code, 200)
self.assertInHTML(
- "
You are already logged in and you are trying to go to the login page again.
", response.content.decode())
+ "You are already logged in and you are trying to go to the login page again.
",
+ response.content.decode(),
+ )
def test_assertion_consumer_service_no_session(self):
settings.SAML_CONFIG = conf.create_conf(
- sp_host='sp.example.com',
- idp_hosts=['idp.example.com'],
- metadata_file='remote_metadata_one_idp.xml',
+ sp_host="sp.example.com",
+ idp_hosts=["idp.example.com"],
+ metadata_file="remote_metadata_one_idp.xml",
)
- response = self.client.get(reverse('saml2_login'))
+ response = self.client.get(reverse("saml2_login"))
saml2_req = saml2_from_httpredirect_request(response.url)
session_id = get_session_id_from_saml2(saml2_req)
# session_id should start with a letter since it is a NCName
- came_from = '/another-view/'
+ came_from = "/another-view/"
self.add_outstanding_query(session_id, came_from)
# Authentication is confirmed.
- saml_response = auth_response(session_id, 'student')
- response = self.client.post(reverse('saml2_acs'), {
- 'SAMLResponse': self.b64_for_post(saml_response),
- 'RelayState': came_from,
- })
+ saml_response = auth_response(session_id, "student")
+ response = self.client.post(
+ reverse("saml2_acs"),
+ {
+ "SAMLResponse": self.b64_for_post(saml_response),
+ "RelayState": came_from,
+ },
+ )
self.assertEqual(response.status_code, 302)
- location = response['Location']
+ location = response["Location"]
url = urlparse(location)
self.assertEqual(url.path, came_from)
# Session should no longer be in outstanding queries.
- saml_response = auth_response(session_id, 'student')
- response = self.client.post(reverse('saml2_acs'), {
- 'SAMLResponse': self.b64_for_post(saml_response),
- 'RelayState': came_from,
- })
+ saml_response = auth_response(session_id, "student")
+ response = self.client.post(
+ reverse("saml2_acs"),
+ {
+ "SAMLResponse": self.b64_for_post(saml_response),
+ "RelayState": came_from,
+ },
+ )
self.assertEqual(response.status_code, 403)
def test_missing_param_to_assertion_consumer_service_request(self):
# Send request without SAML2Response parameter
- response = self.client.post(reverse('saml2_acs'))
+ response = self.client.post(reverse("saml2_acs"))
# Assert that view responded with "Bad Request" error
self.assertEqual(response.status_code, 400)
def test_bad_request_method_to_assertion_consumer_service(self):
# Send request with non-POST method.
- response = self.client.get(reverse('saml2_acs'))
+ response = self.client.get(reverse("saml2_acs"))
# Assert that view responded with method not allowed status
self.assertEqual(response.status_code, 405)
@@ -545,33 +595,36 @@ def do_login(self):
"""Auxiliary method used in several tests (mainly logout tests)"""
self.init_cookies()
- response = self.client.get(reverse('saml2_login'))
+ response = self.client.get(reverse("saml2_login"))
saml2_req = saml2_from_httpredirect_request(response.url)
session_id = get_session_id_from_saml2(saml2_req)
# session_id should start with a letter since it is a NCName
- came_from = '/another-view/'
+ came_from = "/another-view/"
self.add_outstanding_query(session_id, came_from)
- saml_response = auth_response(session_id, 'student')
+ saml_response = auth_response(session_id, "student")
# this will create a user
- response = self.client.post(reverse('saml2_acs'), {
- 'SAMLResponse': self.b64_for_post(saml_response),
- 'RelayState': came_from,
- })
+ response = self.client.post(
+ reverse("saml2_acs"),
+ {
+ "SAMLResponse": self.b64_for_post(saml_response),
+ "RelayState": came_from,
+ },
+ )
subject_id = get_subject_id_from_saml2(saml_response)
self.assertEqual(response.status_code, 302)
return subject_id
def test_echo_view_no_saml_session(self):
settings.SAML_CONFIG = conf.create_conf(
- sp_host='sp.example.com',
- idp_hosts=['idp.example.com'],
- metadata_file='remote_metadata_one_idp.xml',
+ sp_host="sp.example.com",
+ idp_hosts=["idp.example.com"],
+ metadata_file="remote_metadata_one_idp.xml",
)
self.do_login()
- request = RequestFactory().get('/bar/foo')
+ request = RequestFactory().get("/bar/foo")
request.COOKIES = self.client.cookies
request.user = User.objects.last()
@@ -580,258 +633,289 @@ def test_echo_view_no_saml_session(self):
response = EchoAttributesView.as_view()(request)
self.assertEqual(response.status_code, 200)
- self.assertEqual(response.content.decode(
- ), 'No active SAML identity found. Are you sure you have logged in via SAML?')
+ self.assertEqual(
+ response.content.decode(),
+ "No active SAML identity found. Are you sure you have logged in via SAML?",
+ )
def test_echo_view_success(self):
settings.SAML_CONFIG = conf.create_conf(
- sp_host='sp.example.com',
- idp_hosts=['idp.example.com'],
- metadata_file='remote_metadata_one_idp.xml',
+ sp_host="sp.example.com",
+ idp_hosts=["idp.example.com"],
+ metadata_file="remote_metadata_one_idp.xml",
)
self.do_login()
- request = RequestFactory().get('/')
+ request = RequestFactory().get("/")
request.user = User.objects.last()
middleware = SamlSessionMiddleware(dummy_get_response)
middleware.process_request(request)
saml_session_name = getattr(
- settings, 'SAML_SESSION_COOKIE_NAME', 'saml_session')
+ settings, "SAML_SESSION_COOKIE_NAME", "saml_session"
+ )
getattr(request, saml_session_name)[
- '_saml2_subject_id'] = '1f87035b4c1325b296a53d92097e6b3fa36d7e30ee82e3fcb0680d60243c1f03'
+ "_saml2_subject_id"
+ ] = "1f87035b4c1325b296a53d92097e6b3fa36d7e30ee82e3fcb0680d60243c1f03"
getattr(request, saml_session_name).save()
response = EchoAttributesView.as_view()(request)
self.assertEqual(response.status_code, 200)
- self.assertIn('SAML attributes
',
- response.content.decode(), 'Echo page not rendered')
+ self.assertIn(
+ "SAML attributes
",
+ response.content.decode(),
+ "Echo page not rendered",
+ )
def test_logout(self):
settings.SAML_CONFIG = conf.create_conf(
- sp_host='sp.example.com',
- idp_hosts=['idp.example.com'],
- metadata_file='remote_metadata_one_idp.xml',
+ sp_host="sp.example.com",
+ idp_hosts=["idp.example.com"],
+ metadata_file="remote_metadata_one_idp.xml",
)
self.do_login()
- response = self.client.get(reverse('saml2_logout'))
+ response = self.client.get(reverse("saml2_logout"))
self.assertEqual(response.status_code, 302)
- location = response['Location']
+ location = response["Location"]
url = urlparse(location)
- self.assertEqual(url.hostname, 'idp.example.com')
- self.assertEqual(url.path,
- '/simplesaml/saml2/idp/SingleLogoutService.php')
+ self.assertEqual(url.hostname, "idp.example.com")
+ self.assertEqual(url.path, "/simplesaml/saml2/idp/SingleLogoutService.php")
params = parse_qs(url.query)
- self.assertIn('SAMLRequest', params)
+ self.assertIn("SAMLRequest", params)
- saml_request = params['SAMLRequest'][0]
+ saml_request = params["SAMLRequest"][0]
- self.assertIn('LogoutRequest xmlns', decode_base64_and_inflate(
- saml_request).decode('utf-8'), 'Not a valid LogoutRequest')
+ self.assertIn(
+ "LogoutRequest xmlns",
+ decode_base64_and_inflate(saml_request).decode("utf-8"),
+ "Not a valid LogoutRequest",
+ )
def test_logout_service_local(self):
settings.SAML_CONFIG = conf.create_conf(
- sp_host='sp.example.com',
- idp_hosts=['idp.example.com'],
- metadata_file='remote_metadata_one_idp.xml',
+ sp_host="sp.example.com",
+ idp_hosts=["idp.example.com"],
+ metadata_file="remote_metadata_one_idp.xml",
)
self.do_login()
- response = self.client.get(reverse('saml2_logout'))
+ response = self.client.get(reverse("saml2_logout"))
self.assertEqual(response.status_code, 302)
- location = response['Location']
+ location = response["Location"]
url = urlparse(location)
- self.assertEqual(url.hostname, 'idp.example.com')
- self.assertEqual(url.path,
- '/simplesaml/saml2/idp/SingleLogoutService.php')
+ self.assertEqual(url.hostname, "idp.example.com")
+ self.assertEqual(url.path, "/simplesaml/saml2/idp/SingleLogoutService.php")
params = parse_qs(url.query)
- self.assertIn('SAMLRequest', params)
+ self.assertIn("SAMLRequest", params)
- saml_request = params['SAMLRequest'][0]
+ saml_request = params["SAMLRequest"][0]
- self.assertIn('LogoutRequest xmlns', decode_base64_and_inflate(
- saml_request).decode('utf-8'), 'Not a valid LogoutRequest')
+ self.assertIn(
+ "LogoutRequest xmlns",
+ decode_base64_and_inflate(saml_request).decode("utf-8"),
+ "Not a valid LogoutRequest",
+ )
# now simulate a logout response sent by the idp
expected_request = """http://sp.example.com/saml2/metadata/1f87035b4c1325b296a53d92097e6b3fa36d7e30ee82e3fcb0680d60243c1f03a0123456789abcdef0123456789abcdef"""
request_id = re.findall(r' ID="(.*?)" ', expected_request)[0]
- instant = datetime.datetime.now().strftime('%Y-%m-%dT%H:%M:%SZ')
+ instant = datetime.datetime.now().strftime("%Y-%m-%dT%H:%M:%SZ")
saml_response = """
-https://idp.example.com/simplesaml/saml2/idp/metadata.php""" % (
- request_id, instant)
+https://idp.example.com/simplesaml/saml2/idp/metadata.php""".format(
+ request_id, instant
+ )
- response = self.client.get(reverse('saml2_ls'), {
- 'SAMLResponse': deflate_and_base64_encode(saml_response),
- })
+ response = self.client.get(
+ reverse("saml2_ls"),
+ {
+ "SAMLResponse": deflate_and_base64_encode(saml_response),
+ },
+ )
self.assertContains(response, "Logged out", status_code=200)
self.assertListEqual(list(self.client.session.keys()), [])
def test_logout_service_global(self):
settings.SAML_CONFIG = conf.create_conf(
- sp_host='sp.example.com',
- idp_hosts=['idp.example.com'],
- metadata_file='remote_metadata_one_idp.xml',
+ sp_host="sp.example.com",
+ idp_hosts=["idp.example.com"],
+ metadata_file="remote_metadata_one_idp.xml",
)
subject_id = self.do_login()
# now simulate a global logout process initiated by another SP
subject_id = views._get_subject_id(self.saml_session)
- instant = datetime.datetime.now().strftime('%Y-%m-%dT%H:%M:%SZ')
+ instant = datetime.datetime.now().strftime("%Y-%m-%dT%H:%M:%SZ")
saml_request = """
-https://idp.example.com/simplesaml/saml2/idp/metadata.php%s_1837687b7bc9faad85839dbeb319627889f3021757""" % (instant, subject_id)
+https://idp.example.com/simplesaml/saml2/idp/metadata.php{}_1837687b7bc9faad85839dbeb319627889f3021757""".format(
+ instant, subject_id
+ )
- response = self.client.get(reverse('saml2_ls'), {
- 'SAMLRequest': deflate_and_base64_encode(saml_request),
- })
+ response = self.client.get(
+ reverse("saml2_ls"),
+ {
+ "SAMLRequest": deflate_and_base64_encode(saml_request),
+ },
+ )
self.assertEqual(response.status_code, 302)
- location = response['Location']
+ location = response["Location"]
url = urlparse(location)
- self.assertEqual(url.hostname, 'idp.example.com')
- self.assertEqual(url.path,
- '/simplesaml/saml2/idp/SingleLogoutService.php')
+ self.assertEqual(url.hostname, "idp.example.com")
+ self.assertEqual(url.path, "/simplesaml/saml2/idp/SingleLogoutService.php")
params = parse_qs(url.query)
- self.assertIn('SAMLResponse', params)
- saml_response = params['SAMLResponse'][0]
+ self.assertIn("SAMLResponse", params)
+ saml_response = params["SAMLResponse"][0]
- self.assertIn('Response xmlns', decode_base64_and_inflate(
- saml_response).decode('utf-8'), 'Not a valid Response')
+ self.assertIn(
+ "Response xmlns",
+ decode_base64_and_inflate(saml_response).decode("utf-8"),
+ "Not a valid Response",
+ )
def test_incomplete_logout(self):
- settings.SAML_CONFIG = conf.create_conf(sp_host='sp.example.com',
- idp_hosts=['idp.example.com'])
+ settings.SAML_CONFIG = conf.create_conf(
+ sp_host="sp.example.com", idp_hosts=["idp.example.com"]
+ )
# don't do a login
# now simulate a global logout process initiated by another SP
- instant = datetime.datetime.now().strftime('%Y-%m-%dT%H:%M:%SZ')
- saml_request = 'https://idp.example.com/simplesaml/saml2/idp/metadata.php%s_1837687b7bc9faad85839dbeb319627889f3021757' % (
- instant, 'invalid-subject-id')
+ instant = datetime.datetime.now().strftime("%Y-%m-%dT%H:%M:%SZ")
+ saml_request = 'https://idp.example.com/simplesaml/saml2/idp/metadata.php{}_1837687b7bc9faad85839dbeb319627889f3021757'.format(
+ instant, "invalid-subject-id"
+ )
- response = self.client.get(reverse('saml2_ls'), {
- 'SAMLRequest': deflate_and_base64_encode(saml_request),
- })
- self.assertContains(response, 'Logout error', status_code=403)
+ response = self.client.get(
+ reverse("saml2_ls"),
+ {
+ "SAMLRequest": deflate_and_base64_encode(saml_request),
+ },
+ )
+ self.assertContains(response, "Logout error", status_code=403)
def test_finish_logout_renders_error_template(self):
- request = RequestFactory().get('/bar/foo')
+ request = RequestFactory().get("/bar/foo")
response = finish_logout(request, None)
self.assertContains(response, "Logout error
", status_code=200)
def test_sigalg_not_passed_when_not_signing_request(self):
# monkey patch SAML configuration
settings.SAML_CONFIG = conf.create_conf(
- sp_host='sp.example.com',
- idp_hosts=['idp.example.com'],
- metadata_file='remote_metadata_one_idp.xml',
+ sp_host="sp.example.com",
+ idp_hosts=["idp.example.com"],
+ metadata_file="remote_metadata_one_idp.xml",
)
with mock.patch(
- 'djangosaml2.views.Saml2Client.prepare_for_authenticate',
- return_value=('session_id', {'url': 'fake'}),
-
+ "djangosaml2.views.Saml2Client.prepare_for_authenticate",
+ return_value=("session_id", {"url": "fake"}),
) as prepare_for_auth_mock:
- self.client.get(reverse('saml2_login'))
+ self.client.get(reverse("saml2_login"))
prepare_for_auth_mock.assert_called_once()
_args, kwargs = prepare_for_auth_mock.call_args
- self.assertNotIn('sigalg', kwargs)
+ self.assertNotIn("sigalg", kwargs)
def test_sigalg_passed_when_signing_request(self):
# monkey patch SAML configuration
settings.SAML_CONFIG = conf.create_conf(
- sp_host='sp.example.com',
- idp_hosts=['idp.example.com'],
- metadata_file='remote_metadata_one_idp.xml',
+ sp_host="sp.example.com",
+ idp_hosts=["idp.example.com"],
+ metadata_file="remote_metadata_one_idp.xml",
)
- settings.SAML_CONFIG['service']['sp']['authn_requests_signed'] = True
+ settings.SAML_CONFIG["service"]["sp"]["authn_requests_signed"] = True
with mock.patch(
- 'djangosaml2.views.Saml2Client.prepare_for_authenticate',
- return_value=('session_id', {'url': 'fake'}),
-
+ "djangosaml2.views.Saml2Client.prepare_for_authenticate",
+ return_value=("session_id", {"url": "fake"}),
) as prepare_for_auth_mock:
- self.client.get(reverse('saml2_login'))
+ self.client.get(reverse("saml2_login"))
prepare_for_auth_mock.assert_called_once()
_args, kwargs = prepare_for_auth_mock.call_args
- self.assertIn('sigalg', kwargs)
+ self.assertIn("sigalg", kwargs)
@override_settings(SAML2_DISCO_URL="https://that-ds.org/ds")
def test_discovery_service(self):
settings.SAML_CONFIG = conf.create_conf(
- sp_host='sp.example.com',
- idp_hosts=['idp.example.com'],
- metadata_file='remote_metadata_three_idps.xml',
+ sp_host="sp.example.com",
+ idp_hosts=["idp.example.com"],
+ metadata_file="remote_metadata_three_idps.xml",
)
- response = self.client.get(reverse('saml2_login'))
+ response = self.client.get(reverse("saml2_login"))
self.assertEqual(response.status_code, 302)
self.assertIn("https://that-ds.org/ds", response.url)
+
def test_config_loader(request):
config = SPConfig()
- config.load({'entityid': 'testentity'})
+ config.load({"entityid": "testentity"})
return config
def test_config_loader_callable(request):
config = SPConfig()
- config.load({'entityid': 'testentity_callable'})
+ config.load({"entityid": "testentity_callable"})
return config
def test_config_loader_with_real_conf(request):
config = SPConfig()
- config.load(conf.create_conf(sp_host='sp.example.com',
- idp_hosts=['idp.example.com'],
- metadata_file='remote_metadata_one_idp.xml'))
+ config.load(
+ conf.create_conf(
+ sp_host="sp.example.com",
+ idp_hosts=["idp.example.com"],
+ metadata_file="remote_metadata_one_idp.xml",
+ )
+ )
return config
class ConfTests(TestCase):
-
def test_custom_conf_loader(self):
- config_loader_path = 'djangosaml2.tests.test_config_loader'
- request = RequestFactory().get('/bar/foo')
+ config_loader_path = "djangosaml2.tests.test_config_loader"
+ request = RequestFactory().get("/bar/foo")
conf = get_config(config_loader_path, request)
- self.assertEqual(conf.entityid, 'testentity')
+ self.assertEqual(conf.entityid, "testentity")
def test_custom_conf_loader_callable(self):
config_loader_path = test_config_loader_callable
- request = RequestFactory().get('/bar/foo')
+ request = RequestFactory().get("/bar/foo")
conf = get_config(config_loader_path, request)
- self.assertEqual(conf.entityid, 'testentity_callable')
+ self.assertEqual(conf.entityid, "testentity_callable")
def test_custom_conf_loader_from_view(self):
- config_loader_path = 'djangosaml2.tests.test_config_loader_with_real_conf'
- request = RequestFactory().get('/login/')
+ config_loader_path = "djangosaml2.tests.test_config_loader_with_real_conf"
+ request = RequestFactory().get("/login/")
request.user = AnonymousUser()
middleware = SamlSessionMiddleware(dummy_get_response)
middleware.process_request(request)
saml_session_name = getattr(
- settings, 'SAML_SESSION_COOKIE_NAME', 'saml_session')
+ settings, "SAML_SESSION_COOKIE_NAME", "saml_session"
+ )
getattr(request, saml_session_name).save()
- response = views.LoginView.as_view(
- config_loader_path=config_loader_path)(request)
+ response = views.LoginView.as_view(config_loader_path=config_loader_path)(
+ request
+ )
self.assertEqual(response.status_code, 302)
- location = response['Location']
+ location = response["Location"]
url = urlparse(location)
- self.assertEqual(url.hostname, 'idp.example.com')
- self.assertEqual(url.path, '/simplesaml/saml2/idp/SSOService.php')
+ self.assertEqual(url.hostname, "idp.example.com")
+ self.assertEqual(url.path, "/simplesaml/saml2/idp/SSOService.php")
class SessionEnabledTestCase(TestCase):
@@ -845,11 +929,12 @@ def set_session_cookies(self, session):
session_cookie = settings.SESSION_COOKIE_NAME
self.client.cookies[session_cookie] = session.session_key
cookie_data = {
- 'max-age': None,
- 'path': '/',
- 'domain': settings.SESSION_COOKIE_DOMAIN,
- 'secure': settings.SESSION_COOKIE_SECURE or None,
- 'expires': None}
+ "max-age": None,
+ "path": "/",
+ "domain": settings.SESSION_COOKIE_DOMAIN,
+ "secure": settings.SESSION_COOKIE_SECURE or None,
+ "expires": None,
+ }
self.client.cookies[session_cookie].update(cookie_data)
@@ -860,26 +945,28 @@ def test_middleware_cookie_expireatbrowserclose(self):
session.save()
self.set_session_cookies(session)
- config_loader_path = 'djangosaml2.tests.test_config_loader_with_real_conf'
- request = RequestFactory().get('/login/')
+ config_loader_path = "djangosaml2.tests.test_config_loader_with_real_conf"
+ request = RequestFactory().get("/login/")
request.user = AnonymousUser()
request.session = session
middleware = SamlSessionMiddleware(dummy_get_response)
middleware.process_request(request)
saml_session_name = getattr(
- settings, 'SAML_SESSION_COOKIE_NAME', 'saml_session')
+ settings, "SAML_SESSION_COOKIE_NAME", "saml_session"
+ )
getattr(request, saml_session_name).save()
- response = views.LoginView.as_view(
- config_loader_path=config_loader_path)(request)
+ response = views.LoginView.as_view(config_loader_path=config_loader_path)(
+ request
+ )
response = middleware.process_response(request, response)
cookie = response.cookies[saml_session_name]
- self.assertEqual(cookie['expires'], '')
- self.assertEqual(cookie['max-age'], '')
+ self.assertEqual(cookie["expires"], "")
+ self.assertEqual(cookie["max-age"], "")
def test_middleware_cookie_with_expiry(self):
with override_settings(SESSION_EXPIRE_AT_BROWSER_CLOSE=False):
@@ -887,24 +974,26 @@ def test_middleware_cookie_with_expiry(self):
session.save()
self.set_session_cookies(session)
- config_loader_path = 'djangosaml2.tests.test_config_loader_with_real_conf'
- request = RequestFactory().get('/login/')
+ config_loader_path = "djangosaml2.tests.test_config_loader_with_real_conf"
+ request = RequestFactory().get("/login/")
request.user = AnonymousUser()
request.session = session
middleware = SamlSessionMiddleware(dummy_get_response)
middleware.process_request(request)
saml_session_name = getattr(
- settings, 'SAML_SESSION_COOKIE_NAME', 'saml_session')
+ settings, "SAML_SESSION_COOKIE_NAME", "saml_session"
+ )
getattr(request, saml_session_name).save()
- response = views.LoginView.as_view(
- config_loader_path=config_loader_path)(request)
+ response = views.LoginView.as_view(config_loader_path=config_loader_path)(
+ request
+ )
response = middleware.process_response(request, response)
cookie = response.cookies[saml_session_name]
- self.assertIsNotNone(cookie['expires'])
- self.assertNotEqual(cookie['expires'], '')
- self.assertNotEqual(cookie['max-age'], '')
+ self.assertIsNotNone(cookie["expires"])
+ self.assertNotEqual(cookie["expires"], "")
+ self.assertNotEqual(cookie["max-age"], "")
diff --git a/djangosaml2/tests/attribute-maps/django_saml_uri.py b/djangosaml2/tests/attribute-maps/django_saml_uri.py
index 65323c5c..3d6dc33a 100644
--- a/djangosaml2/tests/attribute-maps/django_saml_uri.py
+++ b/djangosaml2/tests/attribute-maps/django_saml_uri.py
@@ -1,19 +1,19 @@
-X500ATTR_OID = 'urn:oid:2.5.4.'
-PKCS_9 = 'urn:oid:1.2.840.113549.1.9.1.'
-UCL_DIR_PILOT = 'urn:oid:0.9.2342.19200300.100.1.'
+X500ATTR_OID = "urn:oid:2.5.4."
+PKCS_9 = "urn:oid:1.2.840.113549.1.9.1."
+UCL_DIR_PILOT = "urn:oid:0.9.2342.19200300.100.1."
MAP = {
- 'identifier': 'urn:oasis:names:tc:SAML:2.0:attrname-format:uri',
- 'fro': {
- X500ATTR_OID+'3': 'first_name', # cn
- X500ATTR_OID+'4': 'last_name', # sn
- PKCS_9+'1': 'email',
- UCL_DIR_PILOT+'1': 'uid',
+ "identifier": "urn:oasis:names:tc:SAML:2.0:attrname-format:uri",
+ "fro": {
+ X500ATTR_OID + "3": "first_name", # cn
+ X500ATTR_OID + "4": "last_name", # sn
+ PKCS_9 + "1": "email",
+ UCL_DIR_PILOT + "1": "uid",
+ },
+ "to": {
+ "first_name": X500ATTR_OID + "3",
+ "last_name": X500ATTR_OID + "4",
+ "email": PKCS_9 + "1",
+ "uid": UCL_DIR_PILOT + "1",
},
- 'to': {
- 'first_name': X500ATTR_OID+'3',
- 'last_name': X500ATTR_OID+'4',
- 'email': PKCS_9+'1',
- 'uid': UCL_DIR_PILOT+'1',
- }
}
diff --git a/djangosaml2/tests/attribute-maps/saml_uri.py b/djangosaml2/tests/attribute-maps/saml_uri.py
index f92f49ec..d2e948a9 100644
--- a/djangosaml2/tests/attribute-maps/saml_uri.py
+++ b/djangosaml2/tests/attribute-maps/saml_uri.py
@@ -1,10 +1,10 @@
-__author__ = 'rolandh'
+__author__ = "rolandh"
EDUPERSON_OID = "urn:oid:1.3.6.1.4.1.5923.1.1.1."
X500ATTR_OID = "urn:oid:2.5.4."
NOREDUPERSON_OID = "urn:oid:1.3.6.1.4.1.2428.90.1."
NETSCAPE_LDAP = "urn:oid:2.16.840.1.113730.3.1."
-UCL_DIR_PILOT = 'urn:oid:0.9.2342.19200300.100.1.'
+UCL_DIR_PILOT = "urn:oid:0.9.2342.19200300.100.1."
PKCS_9 = "urn:oid:1.2.840.113549.1.9.1."
UMICH = "urn:oid:1.3.6.1.4.1.250.1.57."
SCHAC = "urn:oid:1.3.6.1.4.1.25178.1.2."
@@ -12,232 +12,232 @@
MAP = {
"identifier": "urn:oasis:names:tc:SAML:2.0:attrname-format:uri",
"fro": {
- EDUPERSON_OID+'2': 'eduPersonNickname',
- EDUPERSON_OID+'9': 'eduPersonScopedAffiliation',
- EDUPERSON_OID+'11': 'eduPersonAssurance',
- EDUPERSON_OID+'10': 'eduPersonTargetedID',
- EDUPERSON_OID+'4': 'eduPersonOrgUnitDN',
- NOREDUPERSON_OID+'6': 'norEduOrgAcronym',
- NOREDUPERSON_OID+'7': 'norEduOrgUniqueIdentifier',
- NOREDUPERSON_OID+'4': 'norEduPersonLIN',
- EDUPERSON_OID+'1': 'eduPersonAffiliation',
- NOREDUPERSON_OID+'2': 'norEduOrgUnitUniqueNumber',
- NETSCAPE_LDAP+'40': 'userSMIMECertificate',
- NOREDUPERSON_OID+'1': 'norEduOrgUniqueNumber',
- NETSCAPE_LDAP+'241': 'displayName',
- UCL_DIR_PILOT+'37': 'associatedDomain',
- EDUPERSON_OID+'6': 'eduPersonPrincipalName',
- NOREDUPERSON_OID+'8': 'norEduOrgUnitUniqueIdentifier',
- NOREDUPERSON_OID+'9': 'federationFeideSchemaVersion',
- X500ATTR_OID+'53': 'deltaRevocationList',
- X500ATTR_OID+'52': 'supportedAlgorithms',
- X500ATTR_OID+'51': 'houseIdentifier',
- X500ATTR_OID+'50': 'uniqueMember',
- X500ATTR_OID+'19': 'physicalDeliveryOfficeName',
- X500ATTR_OID+'18': 'postOfficeBox',
- X500ATTR_OID+'17': 'postalCode',
- X500ATTR_OID+'16': 'postalAddress',
- X500ATTR_OID+'15': 'businessCategory',
- X500ATTR_OID+'14': 'searchGuide',
- EDUPERSON_OID+'5': 'eduPersonPrimaryAffiliation',
- X500ATTR_OID+'12': 'title',
- X500ATTR_OID+'11': 'ou',
- X500ATTR_OID+'10': 'o',
- X500ATTR_OID+'37': 'cACertificate',
- X500ATTR_OID+'36': 'userCertificate',
- X500ATTR_OID+'31': 'member',
- X500ATTR_OID+'30': 'supportedApplicationContext',
- X500ATTR_OID+'33': 'roleOccupant',
- X500ATTR_OID+'32': 'owner',
- NETSCAPE_LDAP+'1': 'carLicense',
- PKCS_9+'1': 'email',
- NETSCAPE_LDAP+'3': 'employeeNumber',
- NETSCAPE_LDAP+'2': 'departmentNumber',
- X500ATTR_OID+'39': 'certificateRevocationList',
- X500ATTR_OID+'38': 'authorityRevocationList',
- NETSCAPE_LDAP+'216': 'userPKCS12',
- EDUPERSON_OID+'8': 'eduPersonPrimaryOrgUnitDN',
- X500ATTR_OID+'9': 'street',
- X500ATTR_OID+'8': 'st',
- NETSCAPE_LDAP+'39': 'preferredLanguage',
- EDUPERSON_OID+'7': 'eduPersonEntitlement',
- X500ATTR_OID+'2': 'knowledgeInformation',
- X500ATTR_OID+'7': 'l',
- X500ATTR_OID+'6': 'c',
- X500ATTR_OID+'5': 'serialNumber',
- X500ATTR_OID+'4': 'sn',
- X500ATTR_OID+'3': 'cn',
- UCL_DIR_PILOT+'60': 'jpegPhoto',
- X500ATTR_OID+'65': 'pseudonym',
- NOREDUPERSON_OID+'5': 'norEduPersonNIN',
- UCL_DIR_PILOT+'3': 'mail',
- UCL_DIR_PILOT+'25': 'dc',
- X500ATTR_OID+'40': 'crossCertificatePair',
- X500ATTR_OID+'42': 'givenName',
- X500ATTR_OID+'43': 'initials',
- X500ATTR_OID+'44': 'generationQualifier',
- X500ATTR_OID+'45': 'x500UniqueIdentifier',
- X500ATTR_OID+'46': 'dnQualifier',
- X500ATTR_OID+'47': 'enhancedSearchGuide',
- X500ATTR_OID+'48': 'protocolInformation',
- X500ATTR_OID+'54': 'dmdName',
- NETSCAPE_LDAP+'4': 'employeeType',
- X500ATTR_OID+'22': 'teletexTerminalIdentifier',
- X500ATTR_OID+'23': 'facsimileTelephoneNumber',
- X500ATTR_OID+'20': 'telephoneNumber',
- X500ATTR_OID+'21': 'telexNumber',
- X500ATTR_OID+'26': 'registeredAddress',
- X500ATTR_OID+'27': 'destinationIndicator',
- X500ATTR_OID+'24': 'x121Address',
- X500ATTR_OID+'25': 'internationaliSDNNumber',
- X500ATTR_OID+'28': 'preferredDeliveryMethod',
- X500ATTR_OID+'29': 'presentationAddress',
- EDUPERSON_OID+'3': 'eduPersonOrgDN',
- NOREDUPERSON_OID+'3': 'norEduPersonBirthDate',
- UMICH+'57': 'labeledURI',
- UCL_DIR_PILOT+'1': 'uid',
- SCHAC+'1': 'schacMotherTongue',
- SCHAC+'2': 'schacGender',
- SCHAC+'3': 'schacDateOfBirth',
- SCHAC+'4': 'schacPlaceOfBirth',
- SCHAC+'5': 'schacCountryOfCitizenship',
- SCHAC+'6': 'schacSn1',
- SCHAC+'7': 'schacSn2',
- SCHAC+'8': 'schacPersonalTitle',
- SCHAC+'9': 'schacHomeOrganization',
- SCHAC+'10': 'schacHomeOrganizationType',
- SCHAC+'11': 'schacCountryOfResidence',
- SCHAC+'12': 'schacUserPresenceID',
- SCHAC+'13': 'schacPersonalPosition',
- SCHAC+'14': 'schacPersonalUniqueCode',
- SCHAC+'15': 'schacPersonalUniqueID',
- SCHAC+'17': 'schacExpiryDate',
- SCHAC+'18': 'schacUserPrivateAttribute',
- SCHAC+'19': 'schacUserStatus',
- SCHAC+'20': 'schacProjectMembership',
- SCHAC+'21': 'schacProjectSpecificRole',
+ EDUPERSON_OID + "2": "eduPersonNickname",
+ EDUPERSON_OID + "9": "eduPersonScopedAffiliation",
+ EDUPERSON_OID + "11": "eduPersonAssurance",
+ EDUPERSON_OID + "10": "eduPersonTargetedID",
+ EDUPERSON_OID + "4": "eduPersonOrgUnitDN",
+ NOREDUPERSON_OID + "6": "norEduOrgAcronym",
+ NOREDUPERSON_OID + "7": "norEduOrgUniqueIdentifier",
+ NOREDUPERSON_OID + "4": "norEduPersonLIN",
+ EDUPERSON_OID + "1": "eduPersonAffiliation",
+ NOREDUPERSON_OID + "2": "norEduOrgUnitUniqueNumber",
+ NETSCAPE_LDAP + "40": "userSMIMECertificate",
+ NOREDUPERSON_OID + "1": "norEduOrgUniqueNumber",
+ NETSCAPE_LDAP + "241": "displayName",
+ UCL_DIR_PILOT + "37": "associatedDomain",
+ EDUPERSON_OID + "6": "eduPersonPrincipalName",
+ NOREDUPERSON_OID + "8": "norEduOrgUnitUniqueIdentifier",
+ NOREDUPERSON_OID + "9": "federationFeideSchemaVersion",
+ X500ATTR_OID + "53": "deltaRevocationList",
+ X500ATTR_OID + "52": "supportedAlgorithms",
+ X500ATTR_OID + "51": "houseIdentifier",
+ X500ATTR_OID + "50": "uniqueMember",
+ X500ATTR_OID + "19": "physicalDeliveryOfficeName",
+ X500ATTR_OID + "18": "postOfficeBox",
+ X500ATTR_OID + "17": "postalCode",
+ X500ATTR_OID + "16": "postalAddress",
+ X500ATTR_OID + "15": "businessCategory",
+ X500ATTR_OID + "14": "searchGuide",
+ EDUPERSON_OID + "5": "eduPersonPrimaryAffiliation",
+ X500ATTR_OID + "12": "title",
+ X500ATTR_OID + "11": "ou",
+ X500ATTR_OID + "10": "o",
+ X500ATTR_OID + "37": "cACertificate",
+ X500ATTR_OID + "36": "userCertificate",
+ X500ATTR_OID + "31": "member",
+ X500ATTR_OID + "30": "supportedApplicationContext",
+ X500ATTR_OID + "33": "roleOccupant",
+ X500ATTR_OID + "32": "owner",
+ NETSCAPE_LDAP + "1": "carLicense",
+ PKCS_9 + "1": "email",
+ NETSCAPE_LDAP + "3": "employeeNumber",
+ NETSCAPE_LDAP + "2": "departmentNumber",
+ X500ATTR_OID + "39": "certificateRevocationList",
+ X500ATTR_OID + "38": "authorityRevocationList",
+ NETSCAPE_LDAP + "216": "userPKCS12",
+ EDUPERSON_OID + "8": "eduPersonPrimaryOrgUnitDN",
+ X500ATTR_OID + "9": "street",
+ X500ATTR_OID + "8": "st",
+ NETSCAPE_LDAP + "39": "preferredLanguage",
+ EDUPERSON_OID + "7": "eduPersonEntitlement",
+ X500ATTR_OID + "2": "knowledgeInformation",
+ X500ATTR_OID + "7": "l",
+ X500ATTR_OID + "6": "c",
+ X500ATTR_OID + "5": "serialNumber",
+ X500ATTR_OID + "4": "sn",
+ X500ATTR_OID + "3": "cn",
+ UCL_DIR_PILOT + "60": "jpegPhoto",
+ X500ATTR_OID + "65": "pseudonym",
+ NOREDUPERSON_OID + "5": "norEduPersonNIN",
+ UCL_DIR_PILOT + "3": "mail",
+ UCL_DIR_PILOT + "25": "dc",
+ X500ATTR_OID + "40": "crossCertificatePair",
+ X500ATTR_OID + "42": "givenName",
+ X500ATTR_OID + "43": "initials",
+ X500ATTR_OID + "44": "generationQualifier",
+ X500ATTR_OID + "45": "x500UniqueIdentifier",
+ X500ATTR_OID + "46": "dnQualifier",
+ X500ATTR_OID + "47": "enhancedSearchGuide",
+ X500ATTR_OID + "48": "protocolInformation",
+ X500ATTR_OID + "54": "dmdName",
+ NETSCAPE_LDAP + "4": "employeeType",
+ X500ATTR_OID + "22": "teletexTerminalIdentifier",
+ X500ATTR_OID + "23": "facsimileTelephoneNumber",
+ X500ATTR_OID + "20": "telephoneNumber",
+ X500ATTR_OID + "21": "telexNumber",
+ X500ATTR_OID + "26": "registeredAddress",
+ X500ATTR_OID + "27": "destinationIndicator",
+ X500ATTR_OID + "24": "x121Address",
+ X500ATTR_OID + "25": "internationaliSDNNumber",
+ X500ATTR_OID + "28": "preferredDeliveryMethod",
+ X500ATTR_OID + "29": "presentationAddress",
+ EDUPERSON_OID + "3": "eduPersonOrgDN",
+ NOREDUPERSON_OID + "3": "norEduPersonBirthDate",
+ UMICH + "57": "labeledURI",
+ UCL_DIR_PILOT + "1": "uid",
+ SCHAC + "1": "schacMotherTongue",
+ SCHAC + "2": "schacGender",
+ SCHAC + "3": "schacDateOfBirth",
+ SCHAC + "4": "schacPlaceOfBirth",
+ SCHAC + "5": "schacCountryOfCitizenship",
+ SCHAC + "6": "schacSn1",
+ SCHAC + "7": "schacSn2",
+ SCHAC + "8": "schacPersonalTitle",
+ SCHAC + "9": "schacHomeOrganization",
+ SCHAC + "10": "schacHomeOrganizationType",
+ SCHAC + "11": "schacCountryOfResidence",
+ SCHAC + "12": "schacUserPresenceID",
+ SCHAC + "13": "schacPersonalPosition",
+ SCHAC + "14": "schacPersonalUniqueCode",
+ SCHAC + "15": "schacPersonalUniqueID",
+ SCHAC + "17": "schacExpiryDate",
+ SCHAC + "18": "schacUserPrivateAttribute",
+ SCHAC + "19": "schacUserStatus",
+ SCHAC + "20": "schacProjectMembership",
+ SCHAC + "21": "schacProjectSpecificRole",
},
"to": {
- 'roleOccupant': X500ATTR_OID+'33',
- 'gn': X500ATTR_OID+'42',
- 'norEduPersonNIN': NOREDUPERSON_OID+'5',
- 'title': X500ATTR_OID+'12',
- 'facsimileTelephoneNumber': X500ATTR_OID+'23',
- 'mail': UCL_DIR_PILOT+'3',
- 'postOfficeBox': X500ATTR_OID+'18',
- 'fax': X500ATTR_OID+'23',
- 'telephoneNumber': X500ATTR_OID+'20',
- 'norEduPersonBirthDate': NOREDUPERSON_OID+'3',
- 'rfc822Mailbox': UCL_DIR_PILOT+'3',
- 'dc': UCL_DIR_PILOT+'25',
- 'countryName': X500ATTR_OID+'6',
- 'emailAddress': PKCS_9+'1',
- 'employeeNumber': NETSCAPE_LDAP+'3',
- 'organizationName': X500ATTR_OID+'10',
- 'eduPersonAssurance': EDUPERSON_OID+'11',
- 'norEduOrgAcronym': NOREDUPERSON_OID+'6',
- 'registeredAddress': X500ATTR_OID+'26',
- 'physicalDeliveryOfficeName': X500ATTR_OID+'19',
- 'associatedDomain': UCL_DIR_PILOT+'37',
- 'l': X500ATTR_OID+'7',
- 'stateOrProvinceName': X500ATTR_OID+'8',
- 'federationFeideSchemaVersion': NOREDUPERSON_OID+'9',
- 'pkcs9email': PKCS_9+'1',
- 'givenName': X500ATTR_OID+'42',
- 'givenname': X500ATTR_OID+'42',
- 'x500UniqueIdentifier': X500ATTR_OID+'45',
- 'eduPersonNickname': EDUPERSON_OID+'2',
- 'houseIdentifier': X500ATTR_OID+'51',
- 'street': X500ATTR_OID+'9',
- 'supportedAlgorithms': X500ATTR_OID+'52',
- 'preferredLanguage': NETSCAPE_LDAP+'39',
- 'postalAddress': X500ATTR_OID+'16',
- 'email': PKCS_9+'1',
- 'norEduOrgUnitUniqueIdentifier': NOREDUPERSON_OID+'8',
- 'eduPersonPrimaryOrgUnitDN': EDUPERSON_OID+'8',
- 'c': X500ATTR_OID+'6',
- 'teletexTerminalIdentifier': X500ATTR_OID+'22',
- 'o': X500ATTR_OID+'10',
- 'cACertificate': X500ATTR_OID+'37',
- 'telexNumber': X500ATTR_OID+'21',
- 'ou': X500ATTR_OID+'11',
- 'initials': X500ATTR_OID+'43',
- 'eduPersonOrgUnitDN': EDUPERSON_OID+'4',
- 'deltaRevocationList': X500ATTR_OID+'53',
- 'norEduPersonLIN': NOREDUPERSON_OID+'4',
- 'supportedApplicationContext': X500ATTR_OID+'30',
- 'eduPersonEntitlement': EDUPERSON_OID+'7',
- 'generationQualifier': X500ATTR_OID+'44',
- 'eduPersonAffiliation': EDUPERSON_OID+'1',
- 'edupersonaffiliation': EDUPERSON_OID+'1',
- 'eduPersonPrincipalName': EDUPERSON_OID+'6',
- 'edupersonprincipalname': EDUPERSON_OID+'6',
- 'localityName': X500ATTR_OID+'7',
- 'owner': X500ATTR_OID+'32',
- 'norEduOrgUnitUniqueNumber': NOREDUPERSON_OID+'2',
- 'searchGuide': X500ATTR_OID+'14',
- 'certificateRevocationList': X500ATTR_OID+'39',
- 'organizationalUnitName': X500ATTR_OID+'11',
- 'userCertificate': X500ATTR_OID+'36',
- 'preferredDeliveryMethod': X500ATTR_OID+'28',
- 'internationaliSDNNumber': X500ATTR_OID+'25',
- 'uniqueMember': X500ATTR_OID+'50',
- 'departmentNumber': NETSCAPE_LDAP+'2',
- 'enhancedSearchGuide': X500ATTR_OID+'47',
- 'userPKCS12': NETSCAPE_LDAP+'216',
- 'eduPersonTargetedID': EDUPERSON_OID+'10',
- 'norEduOrgUniqueNumber': NOREDUPERSON_OID+'1',
- 'x121Address': X500ATTR_OID+'24',
- 'destinationIndicator': X500ATTR_OID+'27',
- 'eduPersonPrimaryAffiliation': EDUPERSON_OID+'5',
- 'surname': X500ATTR_OID+'4',
- 'jpegPhoto': UCL_DIR_PILOT+'60',
- 'eduPersonScopedAffiliation': EDUPERSON_OID+'9',
- 'edupersonscopedaffiliation': EDUPERSON_OID+'9',
- 'protocolInformation': X500ATTR_OID+'48',
- 'knowledgeInformation': X500ATTR_OID+'2',
- 'employeeType': NETSCAPE_LDAP+'4',
- 'userSMIMECertificate': NETSCAPE_LDAP+'40',
- 'member': X500ATTR_OID+'31',
- 'streetAddress': X500ATTR_OID+'9',
- 'dmdName': X500ATTR_OID+'54',
- 'postalCode': X500ATTR_OID+'17',
- 'pseudonym': X500ATTR_OID+'65',
- 'dnQualifier': X500ATTR_OID+'46',
- 'crossCertificatePair': X500ATTR_OID+'40',
- 'eduPersonOrgDN': EDUPERSON_OID+'3',
- 'authorityRevocationList': X500ATTR_OID+'38',
- 'displayName': NETSCAPE_LDAP+'241',
- 'businessCategory': X500ATTR_OID+'15',
- 'serialNumber': X500ATTR_OID+'5',
- 'norEduOrgUniqueIdentifier': NOREDUPERSON_OID+'7',
- 'st': X500ATTR_OID+'8',
- 'carLicense': NETSCAPE_LDAP+'1',
- 'presentationAddress': X500ATTR_OID+'29',
- 'sn': X500ATTR_OID+'4',
- 'cn': X500ATTR_OID+'3',
- 'domainComponent': UCL_DIR_PILOT+'25',
- 'labeledURI': UMICH+'57',
- 'uid': UCL_DIR_PILOT+'1',
- 'schacMotherTongue': SCHAC+'1',
- 'schacGender': SCHAC+'2',
- 'schacDateOfBirth': SCHAC+'3',
- 'schacPlaceOfBirth': SCHAC+'4',
- 'schacCountryOfCitizenship': SCHAC+'5',
- 'schacSn1': SCHAC+'6',
- 'schacSn2': SCHAC+'7',
- 'schacPersonalTitle': SCHAC+'8',
- 'schacHomeOrganization': SCHAC+'9',
- 'schacHomeOrganizationType': SCHAC+'10',
- 'schacCountryOfResidence': SCHAC+'11',
- 'schacUserPresenceID': SCHAC+'12',
- 'schacPersonalPosition': SCHAC+'13',
- 'schacPersonalUniqueCode': SCHAC+'14',
- 'schacPersonalUniqueID': SCHAC+'15',
- 'schacExpiryDate': SCHAC+'17',
- 'schacUserPrivateAttribute': SCHAC+'18',
- 'schacUserStatus': SCHAC+'19',
- 'schacProjectMembership': SCHAC+'20',
- 'schacProjectSpecificRole': SCHAC+'21',
- }
+ "roleOccupant": X500ATTR_OID + "33",
+ "gn": X500ATTR_OID + "42",
+ "norEduPersonNIN": NOREDUPERSON_OID + "5",
+ "title": X500ATTR_OID + "12",
+ "facsimileTelephoneNumber": X500ATTR_OID + "23",
+ "mail": UCL_DIR_PILOT + "3",
+ "postOfficeBox": X500ATTR_OID + "18",
+ "fax": X500ATTR_OID + "23",
+ "telephoneNumber": X500ATTR_OID + "20",
+ "norEduPersonBirthDate": NOREDUPERSON_OID + "3",
+ "rfc822Mailbox": UCL_DIR_PILOT + "3",
+ "dc": UCL_DIR_PILOT + "25",
+ "countryName": X500ATTR_OID + "6",
+ "emailAddress": PKCS_9 + "1",
+ "employeeNumber": NETSCAPE_LDAP + "3",
+ "organizationName": X500ATTR_OID + "10",
+ "eduPersonAssurance": EDUPERSON_OID + "11",
+ "norEduOrgAcronym": NOREDUPERSON_OID + "6",
+ "registeredAddress": X500ATTR_OID + "26",
+ "physicalDeliveryOfficeName": X500ATTR_OID + "19",
+ "associatedDomain": UCL_DIR_PILOT + "37",
+ "l": X500ATTR_OID + "7",
+ "stateOrProvinceName": X500ATTR_OID + "8",
+ "federationFeideSchemaVersion": NOREDUPERSON_OID + "9",
+ "pkcs9email": PKCS_9 + "1",
+ "givenName": X500ATTR_OID + "42",
+ "givenname": X500ATTR_OID + "42",
+ "x500UniqueIdentifier": X500ATTR_OID + "45",
+ "eduPersonNickname": EDUPERSON_OID + "2",
+ "houseIdentifier": X500ATTR_OID + "51",
+ "street": X500ATTR_OID + "9",
+ "supportedAlgorithms": X500ATTR_OID + "52",
+ "preferredLanguage": NETSCAPE_LDAP + "39",
+ "postalAddress": X500ATTR_OID + "16",
+ "email": PKCS_9 + "1",
+ "norEduOrgUnitUniqueIdentifier": NOREDUPERSON_OID + "8",
+ "eduPersonPrimaryOrgUnitDN": EDUPERSON_OID + "8",
+ "c": X500ATTR_OID + "6",
+ "teletexTerminalIdentifier": X500ATTR_OID + "22",
+ "o": X500ATTR_OID + "10",
+ "cACertificate": X500ATTR_OID + "37",
+ "telexNumber": X500ATTR_OID + "21",
+ "ou": X500ATTR_OID + "11",
+ "initials": X500ATTR_OID + "43",
+ "eduPersonOrgUnitDN": EDUPERSON_OID + "4",
+ "deltaRevocationList": X500ATTR_OID + "53",
+ "norEduPersonLIN": NOREDUPERSON_OID + "4",
+ "supportedApplicationContext": X500ATTR_OID + "30",
+ "eduPersonEntitlement": EDUPERSON_OID + "7",
+ "generationQualifier": X500ATTR_OID + "44",
+ "eduPersonAffiliation": EDUPERSON_OID + "1",
+ "edupersonaffiliation": EDUPERSON_OID + "1",
+ "eduPersonPrincipalName": EDUPERSON_OID + "6",
+ "edupersonprincipalname": EDUPERSON_OID + "6",
+ "localityName": X500ATTR_OID + "7",
+ "owner": X500ATTR_OID + "32",
+ "norEduOrgUnitUniqueNumber": NOREDUPERSON_OID + "2",
+ "searchGuide": X500ATTR_OID + "14",
+ "certificateRevocationList": X500ATTR_OID + "39",
+ "organizationalUnitName": X500ATTR_OID + "11",
+ "userCertificate": X500ATTR_OID + "36",
+ "preferredDeliveryMethod": X500ATTR_OID + "28",
+ "internationaliSDNNumber": X500ATTR_OID + "25",
+ "uniqueMember": X500ATTR_OID + "50",
+ "departmentNumber": NETSCAPE_LDAP + "2",
+ "enhancedSearchGuide": X500ATTR_OID + "47",
+ "userPKCS12": NETSCAPE_LDAP + "216",
+ "eduPersonTargetedID": EDUPERSON_OID + "10",
+ "norEduOrgUniqueNumber": NOREDUPERSON_OID + "1",
+ "x121Address": X500ATTR_OID + "24",
+ "destinationIndicator": X500ATTR_OID + "27",
+ "eduPersonPrimaryAffiliation": EDUPERSON_OID + "5",
+ "surname": X500ATTR_OID + "4",
+ "jpegPhoto": UCL_DIR_PILOT + "60",
+ "eduPersonScopedAffiliation": EDUPERSON_OID + "9",
+ "edupersonscopedaffiliation": EDUPERSON_OID + "9",
+ "protocolInformation": X500ATTR_OID + "48",
+ "knowledgeInformation": X500ATTR_OID + "2",
+ "employeeType": NETSCAPE_LDAP + "4",
+ "userSMIMECertificate": NETSCAPE_LDAP + "40",
+ "member": X500ATTR_OID + "31",
+ "streetAddress": X500ATTR_OID + "9",
+ "dmdName": X500ATTR_OID + "54",
+ "postalCode": X500ATTR_OID + "17",
+ "pseudonym": X500ATTR_OID + "65",
+ "dnQualifier": X500ATTR_OID + "46",
+ "crossCertificatePair": X500ATTR_OID + "40",
+ "eduPersonOrgDN": EDUPERSON_OID + "3",
+ "authorityRevocationList": X500ATTR_OID + "38",
+ "displayName": NETSCAPE_LDAP + "241",
+ "businessCategory": X500ATTR_OID + "15",
+ "serialNumber": X500ATTR_OID + "5",
+ "norEduOrgUniqueIdentifier": NOREDUPERSON_OID + "7",
+ "st": X500ATTR_OID + "8",
+ "carLicense": NETSCAPE_LDAP + "1",
+ "presentationAddress": X500ATTR_OID + "29",
+ "sn": X500ATTR_OID + "4",
+ "cn": X500ATTR_OID + "3",
+ "domainComponent": UCL_DIR_PILOT + "25",
+ "labeledURI": UMICH + "57",
+ "uid": UCL_DIR_PILOT + "1",
+ "schacMotherTongue": SCHAC + "1",
+ "schacGender": SCHAC + "2",
+ "schacDateOfBirth": SCHAC + "3",
+ "schacPlaceOfBirth": SCHAC + "4",
+ "schacCountryOfCitizenship": SCHAC + "5",
+ "schacSn1": SCHAC + "6",
+ "schacSn2": SCHAC + "7",
+ "schacPersonalTitle": SCHAC + "8",
+ "schacHomeOrganization": SCHAC + "9",
+ "schacHomeOrganizationType": SCHAC + "10",
+ "schacCountryOfResidence": SCHAC + "11",
+ "schacUserPresenceID": SCHAC + "12",
+ "schacPersonalPosition": SCHAC + "13",
+ "schacPersonalUniqueCode": SCHAC + "14",
+ "schacPersonalUniqueID": SCHAC + "15",
+ "schacExpiryDate": SCHAC + "17",
+ "schacUserPrivateAttribute": SCHAC + "18",
+ "schacUserStatus": SCHAC + "19",
+ "schacProjectMembership": SCHAC + "20",
+ "schacProjectSpecificRole": SCHAC + "21",
+ },
}
diff --git a/djangosaml2/tests/auth_response.py b/djangosaml2/tests/auth_response.py
index 3c84c8fd..b25a6ced 100644
--- a/djangosaml2/tests/auth_response.py
+++ b/djangosaml2/tests/auth_response.py
@@ -16,12 +16,14 @@
import datetime
-def auth_response(session_id,
- uid,
- audience='http://sp.example.com/saml2/metadata/',
- acs_url='http://sp.example.com/saml2/acs/',
- metadata_url='http://sp.example.com/saml2/metadata/',
- attribute_statements=None):
+def auth_response(
+ session_id,
+ uid,
+ audience="http://sp.example.com/saml2/metadata/",
+ acs_url="http://sp.example.com/saml2/acs/",
+ metadata_url="http://sp.example.com/saml2/metadata/",
+ attribute_statements=None,
+):
"""Generates a fresh signed authentication response
Params:
@@ -44,60 +46,61 @@ def auth_response(session_id,
if attribute_statements is None:
attribute_statements = (
- ''
+ ""
''
''
- '%(uid)s'
- ''
- ''
- ''
- ) % {'uid': uid}
+ "%(uid)s"
+ ""
+ ""
+ ""
+ ) % {"uid": uid}
saml_response_tpl = (
""
''
''
- 'https://idp.example.com/simplesaml/saml2/idp/metadata.php'
- ''
- ''
+ "https://idp.example.com/simplesaml/saml2/idp/metadata.php"
+ ""
+ ""
''
- ''
+ ""
''
''
- 'https://idp.example.com/simplesaml/saml2/idp/metadata.php'
- ''
- ''
+ "https://idp.example.com/simplesaml/saml2/idp/metadata.php"
+ ""
+ ""
''
- '1f87035b4c1325b296a53d92097e6b3fa36d7e30ee82e3fcb0680d60243c1f03'
- ''
+ "1f87035b4c1325b296a53d92097e6b3fa36d7e30ee82e3fcb0680d60243c1f03"
+ ""
''
''
- ''
- ''
+ ""
+ ""
''
- ''
- ''
- '%(audience)s'
- ''
- ''
- ''
+ ""
+ ""
+ "%(audience)s"
+ ""
+ ""
+ ""
''
- ''
- ''
- 'urn:oasis:names:tc:SAML:2.0:ac:classes:Password'
- ''
- ''
- ''
- '%(attribute_statements)s'
- ''
- '')
+ ""
+ ""
+ "urn:oasis:names:tc:SAML:2.0:ac:classes:Password"
+ ""
+ ""
+ ""
+ "%(attribute_statements)s"
+ ""
+ ""
+ )
return saml_response_tpl % {
- 'session_id': session_id,
- 'audience': audience,
- 'acs_url': acs_url,
- 'metadata_url': metadata_url,
- 'attribute_statements': attribute_statements,
- 'timestamp': timestamp.strftime('%Y-%m-%dT%H:%M:%SZ'),
- 'tomorrow': tomorrow.strftime('%Y-%m-%dT%H:%M:%SZ'),
- 'yesterday': yesterday.strftime('%Y-%m-%dT%H:%M:%SZ'),
+ "session_id": session_id,
+ "audience": audience,
+ "acs_url": acs_url,
+ "metadata_url": metadata_url,
+ "attribute_statements": attribute_statements,
+ "timestamp": timestamp.strftime("%Y-%m-%dT%H:%M:%SZ"),
+ "tomorrow": tomorrow.strftime("%Y-%m-%dT%H:%M:%SZ"),
+ "yesterday": yesterday.strftime("%Y-%m-%dT%H:%M:%SZ"),
}
diff --git a/djangosaml2/tests/conf.py b/djangosaml2/tests/conf.py
index 8bb6af4a..d02fe17f 100644
--- a/djangosaml2/tests/conf.py
+++ b/djangosaml2/tests/conf.py
@@ -18,9 +18,15 @@
import saml2
-def create_conf(sp_host='sp.example.com', idp_hosts=['idp.example.com'],
- metadata_file='remote_metadata.xml', authn_requests_signed=None,
- sp_kwargs:dict={}):
+def create_conf(
+ sp_host="sp.example.com",
+ idp_hosts=None,
+ metadata_file="remote_metadata.xml",
+ authn_requests_signed=None,
+ sp_kwargs: dict = None,
+):
+ if idp_hosts is None:
+ idp_hosts = ["idp.example.com"]
try:
from saml2.sigver import get_xmlsec_binary
@@ -30,79 +36,78 @@ def create_conf(sp_host='sp.example.com', idp_hosts=['idp.example.com'],
if get_xmlsec_binary:
xmlsec_path = get_xmlsec_binary(["/opt/local/bin"])
else:
- xmlsec_path = '/usr/bin/xmlsec1'
+ xmlsec_path = "/usr/bin/xmlsec1"
BASEDIR = os.path.dirname(os.path.abspath(__file__))
config = {
- 'xmlsec_binary': xmlsec_path,
- 'entityid': 'http://%s/saml2/metadata/' % sp_host,
- 'attribute_map_dir': os.path.join(BASEDIR, 'attribute-maps'),
-
- 'service': {
- 'sp': {
- 'name': 'Test SP',
- 'name_id_format': saml2.saml.NAMEID_FORMAT_PERSISTENT,
- 'endpoints': {
- 'assertion_consumer_service': [
- ('http://%s/saml2/acs/' % sp_host,
- saml2.BINDING_HTTP_POST),
+ "xmlsec_binary": xmlsec_path,
+ "entityid": "http://%s/saml2/metadata/" % sp_host,
+ "attribute_map_dir": os.path.join(BASEDIR, "attribute-maps"),
+ "service": {
+ "sp": {
+ "name": "Test SP",
+ "name_id_format": saml2.saml.NAMEID_FORMAT_PERSISTENT,
+ "endpoints": {
+ "assertion_consumer_service": [
+ ("http://%s/saml2/acs/" % sp_host, saml2.BINDING_HTTP_POST),
],
- 'single_logout_service': [
- ('http://%s/saml2/ls/' % sp_host,
- saml2.BINDING_HTTP_REDIRECT),
+ "single_logout_service": [
+ ("http://%s/saml2/ls/" % sp_host, saml2.BINDING_HTTP_REDIRECT),
],
},
- 'required_attributes': ['uid'],
- 'optional_attributes': ['eduPersonAffiliation'],
- 'idp': {}, # this is filled later
- 'want_response_signed': False,
+ "required_attributes": ["uid"],
+ "optional_attributes": ["eduPersonAffiliation"],
+ "idp": {}, # this is filled later
+ "want_response_signed": False,
},
},
-
- 'metadata': {
- 'local': [os.path.join(BASEDIR, metadata_file)],
+ "metadata": {
+ "local": [os.path.join(BASEDIR, metadata_file)],
},
-
- 'debug': 1,
-
+ "debug": 1,
# certificates
- 'key_file': os.path.join(BASEDIR, 'mycert.key'),
- 'cert_file': os.path.join(BASEDIR, 'mycert.pem'),
-
+ "key_file": os.path.join(BASEDIR, "mycert.key"),
+ "cert_file": os.path.join(BASEDIR, "mycert.pem"),
# These fields are only used when generating the metadata
- 'contact_person': [
- {'given_name': 'Technical givenname',
- 'sur_name': 'Technical surname',
- 'company': 'Example Inc.',
- 'email_address': 'technical@sp.example.com',
- 'contact_type': 'technical'},
- {'given_name': 'Administrative givenname',
- 'sur_name': 'Administrative surname',
- 'company': 'Example Inc.',
- 'email_address': 'administrative@sp.example.ccom',
- 'contact_type': 'administrative'},
+ "contact_person": [
+ {
+ "given_name": "Technical givenname",
+ "sur_name": "Technical surname",
+ "company": "Example Inc.",
+ "email_address": "technical@sp.example.com",
+ "contact_type": "technical",
+ },
+ {
+ "given_name": "Administrative givenname",
+ "sur_name": "Administrative surname",
+ "company": "Example Inc.",
+ "email_address": "administrative@sp.example.ccom",
+ "contact_type": "administrative",
+ },
],
- 'organization': {
- 'name': [('Ejemplo S.A.', 'es'), ('Example Inc.', 'en')],
- 'display_name': [('Ejemplo', 'es'), ('Example', 'en')],
- 'url': [('http://www.example.es', 'es'),
- ('http://www.example.com', 'en')],
+ "organization": {
+ "name": [("Ejemplo S.A.", "es"), ("Example Inc.", "en")],
+ "display_name": [("Ejemplo", "es"), ("Example", "en")],
+ "url": [("http://www.example.es", "es"), ("http://www.example.com", "en")],
},
- 'valid_for': 24,
+ "valid_for": 24,
}
- config['service']['sp'].update(**sp_kwargs)
+ if sp_kwargs is not None:
+ config["service"]["sp"].update(**sp_kwargs)
if authn_requests_signed is not None:
- config['service']['sp']['authn_requests_signed'] = authn_requests_signed
+ config["service"]["sp"]["authn_requests_signed"] = authn_requests_signed
for idp in idp_hosts:
- entity_id = 'https://%s/simplesaml/saml2/idp/metadata.php' % idp
- config['service']['sp']['idp'][entity_id] = {
- 'single_sign_on_service': {
- saml2.BINDING_HTTP_REDIRECT: 'https://%s/simplesaml/saml2/idp/SSOService.php' % idp,
+ entity_id = "https://%s/simplesaml/saml2/idp/metadata.php" % idp
+ config["service"]["sp"]["idp"][entity_id] = {
+ "single_sign_on_service": {
+ saml2.BINDING_HTTP_REDIRECT: "https://%s/simplesaml/saml2/idp/SSOService.php"
+ % idp,
},
- 'single_logout_service': {
- saml2.BINDING_HTTP_REDIRECT: 'https://%s/simplesaml/saml2/idp/SingleLogoutService.php' % idp,
+ "single_logout_service": {
+ saml2.BINDING_HTTP_REDIRECT: "https://%s/simplesaml/saml2/idp/SingleLogoutService.php"
+ % idp,
},
}
diff --git a/djangosaml2/urls.py b/djangosaml2/urls.py
index 48d767f5..32e896b3 100644
--- a/djangosaml2/urls.py
+++ b/djangosaml2/urls.py
@@ -18,10 +18,10 @@
from . import views
urlpatterns = [
- path('login/', views.LoginView.as_view(), name='saml2_login'),
- path('acs/', views.AssertionConsumerServiceView.as_view(), name='saml2_acs'),
- path('logout/', views.LogoutInitView.as_view(), name='saml2_logout'),
- path('ls/', views.LogoutView.as_view(), name='saml2_ls'),
- path('ls/post/', views.LogoutView.as_view(), name='saml2_ls_post'),
- path('metadata/', views.MetadataView.as_view(), name='saml2_metadata'),
+ path("login/", views.LoginView.as_view(), name="saml2_login"),
+ path("acs/", views.AssertionConsumerServiceView.as_view(), name="saml2_acs"),
+ path("logout/", views.LogoutInitView.as_view(), name="saml2_logout"),
+ path("ls/", views.LogoutView.as_view(), name="saml2_ls"),
+ path("ls/post/", views.LogoutView.as_view(), name="saml2_ls_post"),
+ path("metadata/", views.MetadataView.as_view(), name="saml2_metadata"),
]
diff --git a/djangosaml2/utils.py b/djangosaml2/utils.py
index ae392c0a..af2770b8 100644
--- a/djangosaml2/utils.py
+++ b/djangosaml2/utils.py
@@ -22,14 +22,15 @@
from django.core.exceptions import ImproperlyConfigured
from django.http import HttpResponse, HttpResponseRedirect
from django.shortcuts import resolve_url
+
try:
from django.utils.http import url_has_allowed_host_and_scheme
except ImportError: # django 2.2
from django.utils.http import is_safe_url as url_has_allowed_host_and_scheme
+
from saml2.config import SPConfig
from saml2.s_utils import UnknownSystemEntity
-
logger = logging.getLogger(__name__)
@@ -43,26 +44,26 @@ def available_idps(config: SPConfig, langpref=None) -> dict:
idps = set()
- for metadata_name, metadata in config.metadata.metadata.items():
- result = metadata.any('idpsso_descriptor', 'single_sign_on_service')
+ for metadata in config.metadata.metadata.values():
+ result = metadata.any("idpsso_descriptor", "single_sign_on_service")
if result:
idps.update(result.keys())
- return {
- idp: config.metadata.name(idp, langpref)
- for idp in idps
- }
+ return {idp: config.metadata.name(idp, langpref) for idp in idps}
-def get_idp_sso_supported_bindings(idp_entity_id: Optional[str] = None, config: Optional[SPConfig] = None) -> list:
+def get_idp_sso_supported_bindings(
+ idp_entity_id: Optional[str] = None, config: Optional[SPConfig] = None
+) -> list:
"""Returns the list of bindings supported by an IDP
This is not clear in the pysaml2 code, so wrapping it in a util"""
if config is None:
# avoid circular import
from .conf import get_config
+
config = get_config()
# load metadata store from config
- meta = getattr(config, 'metadata', {})
+ meta = getattr(config, "metadata", {})
# if idp is None, assume only one exists so just use that
if idp_entity_id is None:
try:
@@ -70,24 +71,31 @@ def get_idp_sso_supported_bindings(idp_entity_id: Optional[str] = None, config:
except IndexError:
raise ImproperlyConfigured("No IdP configured!")
try:
- return list(meta.service(idp_entity_id, 'idpsso_descriptor', 'single_sign_on_service').keys())
+ return list(
+ meta.service(
+ idp_entity_id, "idpsso_descriptor", "single_sign_on_service"
+ ).keys()
+ )
except UnknownSystemEntity:
raise UnknownSystemEntity
except Exception as e:
logger.error(f"get_idp_sso_supported_bindings failed with: {e}")
+
def get_location(http_info):
"""Extract the redirect URL from a pysaml2 http_info object"""
try:
- headers = dict(http_info['headers'])
- return headers['Location']
+ headers = dict(http_info["headers"])
+ return headers["Location"]
except KeyError:
- return http_info['url']
+ return http_info["url"]
def get_fallback_login_redirect_url():
- login_redirect_url = get_custom_setting('LOGIN_REDIRECT_URL', '/')
- return resolve_url(get_custom_setting('ACS_DEFAULT_REDIRECT_URL', login_redirect_url))
+ login_redirect_url = get_custom_setting("LOGIN_REDIRECT_URL", "/")
+ return resolve_url(
+ get_custom_setting("ACS_DEFAULT_REDIRECT_URL", login_redirect_url)
+ )
def validate_referral_url(request, url):
@@ -97,7 +105,8 @@ def validate_referral_url(request, url):
# If this setting is absent, the default is to use the hostname that was used for the current
# request.
saml_allowed_hosts = set(
- getattr(settings, 'SAML_ALLOWED_HOSTS', [request.get_host()]))
+ getattr(settings, "SAML_ALLOWED_HOSTS", [request.get_host()])
+ )
if not url_has_allowed_host_and_scheme(url=url, allowed_hosts=saml_allowed_hosts):
return get_fallback_login_redirect_url()
@@ -106,7 +115,7 @@ def validate_referral_url(request, url):
def saml2_from_httpredirect_request(url):
urlquery = urllib.parse.urlparse(url).query
- b64_inflated_saml2req = urllib.parse.parse_qs(urlquery)['SAMLRequest'][0]
+ b64_inflated_saml2req = urllib.parse.parse_qs(urlquery)["SAMLRequest"][0]
inflated_saml2req = base64.b64decode(b64_inflated_saml2req)
deflated_saml2req = zlib.decompress(inflated_saml2req, -15)
@@ -124,14 +133,14 @@ def get_subject_id_from_saml2(saml2_xml):
def add_param_in_url(url: str, param_key: str, param_value: str):
- params = list(url.split('?'))
- params.append(f'{param_key}={param_value}')
- new_url = params[0] + '?' + ''.join(params[1:])
+ params = list(url.split("?"))
+ params.append(f"{param_key}={param_value}")
+ new_url = params[0] + "?" + "".join(params[1:])
return new_url
def add_idp_hinting(request, http_response) -> bool:
- idphin_param = getattr(settings, 'SAML2_IDPHINT_PARAM', 'idphint')
+ idphin_param = getattr(settings, "SAML2_IDPHINT_PARAM", "idphint")
urllib.parse.urlencode(request.GET)
if idphin_param not in request.GET.keys():
@@ -139,36 +148,36 @@ def add_idp_hinting(request, http_response) -> bool:
idphint = request.GET[idphin_param]
# validation : TODO -> improve!
- if idphint[0:4] != 'http':
+ if idphint[0:4] != "http":
logger.warning(
f'Idp hinting: "{idphint}" doesn\'t contain a valid value.'
- 'idphint paramenter ignored.'
+ "idphint paramenter ignored."
)
return False
if http_response.status_code in (302, 303):
# redirect binding
# urlp = urllib.parse.urlparse(http_response.url)
- new_url = add_param_in_url(http_response.url,
- idphin_param, idphint)
+ new_url = add_param_in_url(http_response.url, idphin_param, idphint)
return HttpResponseRedirect(new_url)
elif http_response.status_code == 200:
# post binding
- res = re.search(r'action="(?P[a-z0-9\:\/\_\-\.]*)"',
- http_response.content.decode(), re.I)
+ res = re.search(
+ r'action="(?P[a-z0-9\:\/\_\-\.]*)"',
+ http_response.content.decode(),
+ re.I,
+ )
if not res:
return False
- orig_url = res.groupdict()['url']
+ orig_url = res.groupdict()["url"]
#
new_url = add_param_in_url(orig_url, idphin_param, idphint)
- content = http_response.content.decode()\
- .replace(orig_url, new_url)\
- .encode()
+ content = http_response.content.decode().replace(orig_url, new_url).encode()
return HttpResponse(content)
else:
logger.warning(
- f'Idp hinting: cannot detect request type [{http_response.status_code}]'
+ f"Idp hinting: cannot detect request type [{http_response.status_code}]"
)
return False
diff --git a/djangosaml2/views.py b/djangosaml2/views.py
index 0e382c94..a00c4268 100644
--- a/djangosaml2/views.py
+++ b/djangosaml2/views.py
@@ -15,36 +15,46 @@
import base64
import logging
-import saml2
from urllib.parse import quote
from django.conf import settings
-from django.contrib import auth
-from django.contrib.auth.mixins import LoginRequiredMixin
-from django.contrib.auth.views import LogoutView as AuthLogoutView
from django.core.exceptions import PermissionDenied, SuspiciousOperation
-from django.http import (HttpRequest, HttpResponse, HttpResponseBadRequest,
- HttpResponseRedirect, HttpResponseServerError)
+from django.http import (
+ HttpRequest,
+ HttpResponse,
+ HttpResponseBadRequest,
+ HttpResponseRedirect,
+ HttpResponseServerError,
+)
from django.shortcuts import render
from django.template import TemplateDoesNotExist
from django.urls import reverse
from django.utils.decorators import method_decorator
+from django.utils.module_loading import import_string
from django.views.decorators.csrf import csrf_exempt
from django.views.generic import View
-from django.utils.module_loading import import_string
+
+from django.contrib import auth
+from django.contrib.auth.mixins import LoginRequiredMixin
+from django.contrib.auth.views import LogoutView as AuthLogoutView
+
+import saml2
from saml2.client_base import LogoutError
from saml2.config import SPConfig
from saml2.ident import code, decode
from saml2.mdstore import SourceNotFound
from saml2.metadata import entity_descriptor
-from saml2.response import (RequestVersionTooLow,
- SignatureError, StatusAuthnFailed, StatusError,
- StatusNoAuthnContext, StatusRequestDenied,
- UnsolicitedResponse)
+from saml2.response import (
+ RequestVersionTooLow,
+ SignatureError,
+ StatusAuthnFailed,
+ StatusError,
+ StatusNoAuthnContext,
+ StatusRequestDenied,
+ UnsolicitedResponse,
+)
from saml2.s_utils import UnsupportedBinding
from saml2.saml import SCM_BEARER
-from saml2.saml import AuthnContextClassRef
-from saml2.samlp import RequestedAuthnContext
from saml2.samlp import AuthnRequest, IDPEntry, IDPList, Scoping
from saml2.sigver import MissingKey
from saml2.validate import ResponseLifetimeExceed, ToEarly
@@ -53,29 +63,32 @@
from .conf import get_config
from .exceptions import IdPConfigurationMissing
from .overrides import Saml2Client
-from .utils import (add_idp_hinting, available_idps, get_custom_setting,
- get_fallback_login_redirect_url,
- get_idp_sso_supported_bindings, get_location,
- validate_referral_url)
-
+from .utils import (
+ add_idp_hinting,
+ available_idps,
+ get_custom_setting,
+ get_fallback_login_redirect_url,
+ get_idp_sso_supported_bindings,
+ get_location,
+ validate_referral_url,
+)
-logger = logging.getLogger('djangosaml2')
+logger = logging.getLogger("djangosaml2")
def _set_subject_id(session, subject_id):
- session['_saml2_subject_id'] = code(subject_id)
+ session["_saml2_subject_id"] = code(subject_id)
def _get_subject_id(session):
try:
- return decode(session['_saml2_subject_id'])
+ return decode(session["_saml2_subject_id"])
except KeyError:
return None
class SPConfigMixin:
- """ Mixin for some of the SAML views with re-usable methods.
- """
+ """Mixin for some of the SAML views with re-usable methods."""
config_loader_path = None
@@ -89,82 +102,78 @@ def get_state_client(self, request: HttpRequest):
conf = self.get_sp_config(request)
state = StateCache(request.saml_session)
client = Saml2Client(
- conf, state_cache=state,
- identity_cache=IdentityCache(request.saml_session)
+ conf, state_cache=state, identity_cache=IdentityCache(request.saml_session)
)
return state, client
class LoginView(SPConfigMixin, View):
- """ SAML Authorization Request initiator.
-
- This view initiates the SAML2 Authorization handshake
- using the pysaml2 library to create the AuthnRequest.
-
- post_binding_form_template is a path to a template containing HTML form with
- hidden input elements, used to send the SAML message data when HTTP POST
- binding is being used. You can customize this template to include custom
- branding and/or text explaining the automatic redirection process. Please
- see the example template in templates/djangosaml2/example_post_binding_form.html
- If set to None or nonexistent template, default form from the saml2 library
- will be rendered.
+ """SAML Authorization Request initiator.
+
+ This view initiates the SAML2 Authorization handshake
+ using the pysaml2 library to create the AuthnRequest.
+
+ post_binding_form_template is a path to a template containing HTML form with
+ hidden input elements, used to send the SAML message data when HTTP POST
+ binding is being used. You can customize this template to include custom
+ branding and/or text explaining the automatic redirection process. Please
+ see the example template in templates/djangosaml2/example_post_binding_form.html
+ If set to None or nonexistent template, default form from the saml2 library
+ will be rendered.
"""
wayf_template = getattr(
- settings,
- 'SAML2_CUSTOM_WAYF_TEMPLATE','djangosaml2/wayf.html'
+ settings, "SAML2_CUSTOM_WAYF_TEMPLATE", "djangosaml2/wayf.html"
)
authorization_error_template = getattr(
settings,
- 'SAML2_CUSTOM_AUTHORIZATION_ERROR_TEMPLATE',
- 'djangosaml2/auth_error.html'
+ "SAML2_CUSTOM_AUTHORIZATION_ERROR_TEMPLATE",
+ "djangosaml2/auth_error.html",
)
post_binding_form_template = getattr(
settings,
- 'SAML2_CUSTOM_POST_BINDING_FORM_TEMPLATE',
- 'djangosaml2/post_binding_form.html'
+ "SAML2_CUSTOM_POST_BINDING_FORM_TEMPLATE",
+ "djangosaml2/post_binding_form.html",
)
def get_next_path(self, request: HttpRequest) -> str:
- ''' Returns the path to put in the RelayState to redirect the user to after having logged in.
- If the user is already logged in (and if allowed), he will redirect to there immediately.
- '''
+ """Returns the path to put in the RelayState to redirect the user to after having logged in.
+ If the user is already logged in (and if allowed), he will redirect to there immediately.
+ """
next_path = get_fallback_login_redirect_url()
- if 'next' in request.GET:
- next_path = request.GET['next']
- elif 'RelayState' in request.GET:
- next_path = request.GET['RelayState']
+ if "next" in request.GET:
+ next_path = request.GET["next"]
+ elif "RelayState" in request.GET:
+ next_path = request.GET["RelayState"]
next_path = validate_referral_url(request, next_path)
return next_path
def unknown_idp(self, request, idp):
- msg = (f'Error: IdP EntityID {idp} was not found in metadata')
+ msg = f"Error: IdP EntityID {idp} was not found in metadata"
logger.error(msg)
- return HttpResponse(
- msg.format('Please contact technical support.'), status=403
- )
+ return HttpResponse(msg.format("Please contact technical support."), status=403)
def load_sso_kwargs_scoping(self, sso_kwargs):
- """ Performs IdP Scoping if scoping param is present. """
- idp_scoping_param = self.request.GET.get('scoping', None)
+ """Performs IdP Scoping if scoping param is present."""
+ idp_scoping_param = self.request.GET.get("scoping", None)
if idp_scoping_param:
idp_scoping = Scoping()
idp_scoping.idp_list = IDPList()
idp_scoping.idp_list.idp_entry.append(
- IDPEntry(provider_id = idp_scoping_param)
+ IDPEntry(provider_id=idp_scoping_param)
)
- sso_kwargs['scoping'] = idp_scoping
+ sso_kwargs["scoping"] = idp_scoping
def load_sso_kwargs(self, sso_kwargs):
- """ Inherit me if you want to put your desidered things in sso_kwargs """
+ """Inherit me if you want to put your desidered things in sso_kwargs"""
def add_idp_hinting(self, http_response):
return add_idp_hinting(self.request, http_response) or http_response
def get(self, request, *args, **kwargs):
- logger.debug('Login process started')
+ logger.debug("Login process started")
next_path = self.get_next_path(request)
# if the user is already authenticated that maybe because of two reasons:
@@ -178,114 +187,125 @@ def get(self, request, *args, **kwargs):
# is True (default value) we will redirect him to the next_path path.
# Otherwise, we will show an (configurable) authorization error.
if request.user.is_authenticated:
- if get_custom_setting('SAML_IGNORE_AUTHENTICATED_USERS_ON_LOGIN', True):
+ if get_custom_setting("SAML_IGNORE_AUTHENTICATED_USERS_ON_LOGIN", True):
return HttpResponseRedirect(next_path)
- logger.debug('User is already logged in')
- return render(request, self.authorization_error_template, {
- 'came_from': next_path,
- })
+ logger.debug("User is already logged in")
+ return render(
+ request,
+ self.authorization_error_template,
+ {
+ "came_from": next_path,
+ },
+ )
try:
conf = self.get_sp_config(request)
- except SourceNotFound as excp: # pragma: no cover
+ except SourceNotFound: # pragma: no cover
# this is deprecated and it's here only for the doubts that something
# would happen the day after I'll remove it! :)
- return self.unknown_idp(request, idp='unknown')
+ return self.unknown_idp(request, idp="unknown")
# is a embedded wayf or DiscoveryService needed?
configured_idps = available_idps(conf)
- selected_idp = request.GET.get('idp', None)
+ selected_idp = request.GET.get("idp", None)
self.conf = conf
sso_kwargs = {}
# Do we have a Discovery Service?
if not selected_idp:
- discovery_service = getattr(settings, 'SAML2_DISCO_URL', None)
+ discovery_service = getattr(settings, "SAML2_DISCO_URL", None)
if discovery_service:
# We have to build the URL to redirect to with all the information
# for the Discovery Service to know how to send the flow back to us
- logger.debug(("A discovery process is needed trough a"
- "Discovery Service: {}").format(discovery_service))
- login_url = request.build_absolute_uri(reverse('saml2_login'))
- login_url = '{0}?next={1}'.format(login_url, quote(next_path, safe=''))
- ds_url = '{0}?entityID={1}&return={2}&returnIDParam=idp'
- ds_url = ds_url.format(discovery_service,
- quote(getattr(conf, 'entityid'), safe=''),
- quote(login_url, safe=''))
+ logger.debug(
+ (
+ "A discovery process is needed trough a" "Discovery Service: {}"
+ ).format(discovery_service)
+ )
+ login_url = "{}?next={}".format(
+ request.build_absolute_uri(reverse("saml2_login")),
+ quote(next_path, safe=""),
+ )
+ ds_url = "{}?entityID={}&return={}&returnIDParam=idp".format(
+ discovery_service,
+ quote(conf.entityid, safe=""),
+ quote(login_url, safe=""),
+ )
return HttpResponseRedirect(ds_url)
elif len(configured_idps) > 1:
- logger.debug('A discovery process trough WAYF page is needed')
- return render(request, self.wayf_template, {
- 'available_idps': configured_idps.items(),
- 'came_from': next_path,
- })
+ logger.debug("A discovery process trough WAYF page is needed")
+ return render(
+ request,
+ self.wayf_template,
+ {
+ "available_idps": configured_idps.items(),
+ "came_from": next_path,
+ },
+ )
# is the first one, otherwise next logger message will print None
- if not configured_idps: # pragma: no cover
- raise IdPConfigurationMissing(
- ('IdP is missing or its metadata is expired.'))
+ if not configured_idps: # pragma: no cover
+ raise IdPConfigurationMissing("IdP is missing or its metadata is expired.")
if selected_idp is None:
selected_idp = list(configured_idps.keys())[0]
# choose a binding to try first
- binding = getattr(settings, 'SAML_DEFAULT_BINDING',
- saml2.BINDING_HTTP_POST)
- logger.debug(f'Trying binding {binding} for IDP {selected_idp}')
+ binding = getattr(settings, "SAML_DEFAULT_BINDING", saml2.BINDING_HTTP_POST)
+ logger.debug(f"Trying binding {binding} for IDP {selected_idp}")
# ensure our selected binding is supported by the IDP
try:
supported_bindings = get_idp_sso_supported_bindings(
- selected_idp, config=conf)
+ selected_idp, config=conf
+ )
except saml2.s_utils.UnknownSystemEntity:
return self.unknown_idp(request, selected_idp)
if binding not in supported_bindings:
logger.debug(
- f'Binding {binding} not in IDP {selected_idp} '
- f'supported bindings: {supported_bindings}. Trying to switch ...',
+ f"Binding {binding} not in IDP {selected_idp} "
+ f"supported bindings: {supported_bindings}. Trying to switch ...",
)
if binding == saml2.BINDING_HTTP_POST:
logger.warning(
- f'IDP {selected_idp} does not support {binding} '
- f'trying {saml2.BINDING_HTTP_REDIRECT}',
+ f"IDP {selected_idp} does not support {binding} "
+ f"trying {saml2.BINDING_HTTP_REDIRECT}",
)
binding = saml2.BINDING_HTTP_REDIRECT
- else: # pragma: no cover
+ else: # pragma: no cover
logger.warning(
- f'IDP {selected_idp} does not support {binding} '
- f'trying {saml2.BINDING_HTTP_POST}',
+ f"IDP {selected_idp} does not support {binding} "
+ f"trying {saml2.BINDING_HTTP_POST}",
)
binding = saml2.BINDING_HTTP_POST
# if switched binding still not supported, give up
- if binding not in supported_bindings: # pragma: no cover
+ if binding not in supported_bindings: # pragma: no cover
raise UnsupportedBinding(
- f'IDP {selected_idp} does not support '
- f'{saml2.BINDING_HTTP_POST} or {saml2.BINDING_HTTP_REDIRECT}'
+ f"IDP {selected_idp} does not support "
+ f"{saml2.BINDING_HTTP_POST} or {saml2.BINDING_HTTP_REDIRECT}"
)
client = Saml2Client(conf)
# SSO options
- sign_requests = getattr(conf, '_sp_authn_requests_signed', False)
+ sign_requests = getattr(conf, "_sp_authn_requests_signed", False)
if sign_requests:
- sso_kwargs["sigalg"] = getattr(conf, '_sp_signing_algorithm',
- saml2.xmldsig.SIG_RSA_SHA256
+ sso_kwargs["sigalg"] = getattr(
+ conf, "_sp_signing_algorithm", saml2.xmldsig.SIG_RSA_SHA256
)
- sso_kwargs["digest_alg"] = getattr(conf,
- '_sp_digest_algorithm',
- saml2.xmldsig.DIGEST_SHA256
+ sso_kwargs["digest_alg"] = getattr(
+ conf, "_sp_digest_algorithm", saml2.xmldsig.DIGEST_SHA256
)
# pysaml needs a string otherwise: "cannot serialize True (type bool)"
- if getattr(conf, '_sp_force_authn', False):
- sso_kwargs['force_authn'] = "true"
- if getattr(conf, '_sp_allow_create', False):
- sso_kwargs['allow_create'] = "true"
+ if getattr(conf, "_sp_force_authn", False):
+ sso_kwargs["force_authn"] = "true"
+ if getattr(conf, "_sp_allow_create", False):
+ sso_kwargs["allow_create"] = "true"
# custom nsprefixes
- sso_kwargs['nsprefix'] = get_namespace_prefixes()
-
+ sso_kwargs["nsprefix"] = get_namespace_prefixes()
# Enrich sso_kwargs ...
# idp scoping
@@ -293,18 +313,21 @@ def get(self, request, *args, **kwargs):
# other customization to be inherited
self.load_sso_kwargs(sso_kwargs)
- logger.debug(f'Redirecting user to the IdP via {binding} binding.')
- _msg = 'Unable to know which IdP to use'
+ logger.debug(f"Redirecting user to the IdP via {binding} binding.")
+ _msg = "Unable to know which IdP to use"
http_response = None
if binding == saml2.BINDING_HTTP_REDIRECT:
try:
session_id, result = client.prepare_for_authenticate(
- entityid=selected_idp, relay_state=next_path,
- binding=binding, sign=sign_requests,
- **sso_kwargs)
+ entityid=selected_idp,
+ relay_state=next_path,
+ binding=binding,
+ sign=sign_requests,
+ **sso_kwargs,
+ )
except TypeError as e:
- logger.error(f'{_msg}: {e}')
+ logger.error(f"{_msg}: {e}")
return HttpResponse(_msg)
else:
http_response = HttpResponseRedirect(get_location(result))
@@ -315,54 +338,60 @@ def get(self, request, *args, **kwargs):
try:
location = client.sso_location(selected_idp, binding)
except TypeError as e:
- logger.error(f'{_msg}: {e}')
+ logger.error(f"{_msg}: {e}")
return HttpResponse(_msg)
session_id, request_xml = client.create_authn_request(
- location,
- binding=binding,
- **sso_kwargs
+ location, binding=binding, **sso_kwargs
)
try:
if isinstance(request_xml, AuthnRequest):
# request_xml will be an instance of AuthnRequest if the message is not signed
request_xml = str(request_xml)
- saml_request = base64.b64encode(
- bytes(request_xml, 'UTF-8')).decode('utf-8')
-
- http_response = render(request, self.post_binding_form_template, {
- 'target_url': location,
- 'params': {
- 'SAMLRequest': saml_request,
- 'RelayState': next_path,
+ saml_request = base64.b64encode(bytes(request_xml, "UTF-8")).decode(
+ "utf-8"
+ )
+
+ http_response = render(
+ request,
+ self.post_binding_form_template,
+ {
+ "target_url": location,
+ "params": {
+ "SAMLRequest": saml_request,
+ "RelayState": next_path,
+ },
},
- })
+ )
except TemplateDoesNotExist as e:
logger.debug(
- f'TemplateDoesNotExist: [{self.post_binding_form_template}] - {e}'
+ f"TemplateDoesNotExist: [{self.post_binding_form_template}] - {e}"
)
if not http_response:
# use the html provided by pysaml2 if no template was specified or it doesn't exist
try:
session_id, result = client.prepare_for_authenticate(
- entityid=selected_idp, relay_state=next_path,
- binding=binding, **sso_kwargs)
+ entityid=selected_idp,
+ relay_state=next_path,
+ binding=binding,
+ **sso_kwargs,
+ )
except TypeError as e:
_msg = f"Can't prepare the authentication for {selected_idp}"
- logger.error(f'{_msg}: {e}')
+ logger.error(f"{_msg}: {e}")
return HttpResponse(_msg)
else:
- http_response = HttpResponse(result['data'])
+ http_response = HttpResponse(result["data"])
else:
- raise UnsupportedBinding(f'Unsupported binding: {binding}')
+ raise UnsupportedBinding(f"Unsupported binding: {binding}")
# success, so save the session ID and return our response
oq_cache = OutstandingQueriesCache(request.saml_session)
oq_cache.set(session_id, next_path)
logger.debug(
f'Saving the session_id "{oq_cache.__dict__}" '
- 'in the OutstandingQueries cache',
+ "in the OutstandingQueries cache",
)
# idp hinting support, add idphint url parameter if present in this request
@@ -370,43 +399,54 @@ def get(self, request, *args, **kwargs):
return response
-@method_decorator(csrf_exempt, name='dispatch')
+@method_decorator(csrf_exempt, name="dispatch")
class AssertionConsumerServiceView(SPConfigMixin, View):
- """ The IdP will send its response to this view, which will process it using pysaml2 and
- log the user in using whatever SAML authentication backend has been enabled in
- settings.py. The `djangosaml2.backends.Saml2Backend` can be used for this purpose,
- though some implementations may instead register their own subclasses of Saml2Backend.
+ """The IdP will send its response to this view, which will process it using pysaml2 and
+ log the user in using whatever SAML authentication backend has been enabled in
+ settings.py. The `djangosaml2.backends.Saml2Backend` can be used for this purpose,
+ though some implementations may instead register their own subclasses of Saml2Backend.
"""
def custom_validation(self, response):
pass
def handle_acs_failure(self, request, exception=None, status=403, **kwargs):
- """ Error handler if the login attempt fails. Override this to customize the error response.
- """
+ """Error handler if the login attempt fails. Override this to customize the error response."""
# Backwards compatibility: if a custom setting was defined, use that one
custom_failure_function = get_custom_setting(
- 'SAML_ACS_FAILURE_RESPONSE_FUNCTION')
+ "SAML_ACS_FAILURE_RESPONSE_FUNCTION"
+ )
if custom_failure_function:
- failure_function = custom_failure_function if callable(
- custom_failure_function) else import_string(custom_failure_function)
+ failure_function = (
+ custom_failure_function
+ if callable(custom_failure_function)
+ else import_string(custom_failure_function)
+ )
return failure_function(request, exception, status, **kwargs)
- return render(request, 'djangosaml2/login_error.html', {'exception': exception}, status=status)
+ return render(
+ request,
+ "djangosaml2/login_error.html",
+ {"exception": exception},
+ status=status,
+ )
def post(self, request, attribute_mapping=None, create_unknown_user=None):
- """ SAML Authorization Response endpoint
- """
+ """SAML Authorization Response endpoint"""
- if 'SAMLResponse' not in request.POST:
+ if "SAMLResponse" not in request.POST:
logger.warning('Missing "SAMLResponse" parameter in POST data.')
- return HttpResponseBadRequest('Missing "SAMLResponse" parameter in POST data.')
+ return HttpResponseBadRequest(
+ 'Missing "SAMLResponse" parameter in POST data.'
+ )
attribute_mapping = attribute_mapping or get_custom_setting(
- 'SAML_ATTRIBUTE_MAPPING', {'uid': ('username', )})
+ "SAML_ATTRIBUTE_MAPPING", {"uid": ("username",)}
+ )
create_unknown_user = create_unknown_user or get_custom_setting(
- 'SAML_CREATE_UNKNOWN_USER', True)
+ "SAML_CREATE_UNKNOWN_USER", True
+ )
conf = self.get_sp_config(request)
identity_cache = IdentityCache(request.saml_session)
@@ -417,43 +457,46 @@ def post(self, request, attribute_mapping=None, create_unknown_user=None):
_exception = None
try:
- response = client.parse_authn_request_response(request.POST['SAMLResponse'],
- saml2.BINDING_HTTP_POST,
- outstanding_queries)
+ response = client.parse_authn_request_response(
+ request.POST["SAMLResponse"],
+ saml2.BINDING_HTTP_POST,
+ outstanding_queries,
+ )
except (StatusError, ToEarly) as e:
_exception = e
logger.exception("Error processing SAML Assertion.")
except ResponseLifetimeExceed as e:
_exception = e
logger.info(
- ("SAML Assertion is no longer valid. Possibly caused "
- "by network delay or replay attack."), exc_info=True)
+ (
+ "SAML Assertion is no longer valid. Possibly caused "
+ "by network delay or replay attack."
+ ),
+ exc_info=True,
+ )
except SignatureError as e:
_exception = e
logger.info("Invalid or malformed SAML Assertion.", exc_info=True)
except StatusAuthnFailed as e:
_exception = e
- logger.info("Authentication denied for user by IdP.",
- exc_info=True)
+ logger.info("Authentication denied for user by IdP.", exc_info=True)
except StatusRequestDenied as e:
_exception = e
logger.warning("Authentication interrupted at IdP.", exc_info=True)
except StatusNoAuthnContext as e:
_exception = e
- logger.warning(
- "Missing Authentication Context from IdP.", exc_info=True)
+ logger.warning("Missing Authentication Context from IdP.", exc_info=True)
except MissingKey as e:
_exception = e
logger.exception(
- "SAML Identity Provider is not configured correctly: certificate key is missing!")
+ "SAML Identity Provider is not configured correctly: certificate key is missing!"
+ )
except UnsolicitedResponse as e:
_exception = e
- logger.exception(
- "Received SAMLResponse when no request has been made.")
+ logger.exception("Received SAMLResponse when no request has been made.")
except RequestVersionTooLow as e:
_exception = e
- logger.exception(
- "Received SAMLResponse have a deprecated SAML2 VERSION.")
+ logger.exception("Received SAMLResponse have a deprecated SAML2 VERSION.")
except Exception as e:
_exception = e
logger.exception("SAMLResponse Error")
@@ -462,13 +505,21 @@ def post(self, request, attribute_mapping=None, create_unknown_user=None):
return self.handle_acs_failure(request, exception=_exception)
elif response is None:
logger.warning("Invalid SAML Assertion received (unknown error).")
- return self.handle_acs_failure(request, status=400, exception=SuspiciousOperation('Unknown SAML2 error'))
+ return self.handle_acs_failure(
+ request,
+ status=400,
+ exception=SuspiciousOperation("Unknown SAML2 error"),
+ )
try:
self.custom_validation(response)
except Exception as e:
logger.warning(f"SAML Response validation error: {e}")
- return self.handle_acs_failure(request, status=400, exception=SuspiciousOperation('SAML2 validation error'))
+ return self.handle_acs_failure(
+ request,
+ status=400,
+ exception=SuspiciousOperation("SAML2 validation error"),
+ )
session_id = response.session_id()
oq_cache.delete(session_id)
@@ -482,8 +533,10 @@ def post(self, request, attribute_mapping=None, create_unknown_user=None):
for sc in assertion.subject.subject_confirmation:
if sc.method == SCM_BEARER:
assertion_not_on_or_after = sc.subject_confirmation_data.not_on_or_after
- assertion_info = {'assertion_id': assertion.id,
- 'not_on_or_after': assertion_not_on_or_after}
+ assertion_info = {
+ "assertion_id": assertion.id,
+ "not_on_or_after": assertion_not_on_or_after,
+ }
break
if callable(attribute_mapping):
@@ -491,71 +544,73 @@ def post(self, request, attribute_mapping=None, create_unknown_user=None):
if callable(create_unknown_user):
create_unknown_user = create_unknown_user()
- logger.debug(
- 'Trying to authenticate the user. Session info: %s', session_info)
- user = auth.authenticate(request=request,
- session_info=session_info,
- attribute_mapping=attribute_mapping,
- create_unknown_user=create_unknown_user,
- assertion_info=assertion_info)
+ logger.debug("Trying to authenticate the user. Session info: %s", session_info)
+ user = auth.authenticate(
+ request=request,
+ session_info=session_info,
+ attribute_mapping=attribute_mapping,
+ create_unknown_user=create_unknown_user,
+ assertion_info=assertion_info,
+ )
if user is None:
logger.warning(
- "Could not authenticate user received in SAML Assertion. Session info: %s", session_info)
- return self.handle_acs_failure(request, exception=PermissionDenied('No user could be authenticated.'),
- session_info=session_info)
+ "Could not authenticate user received in SAML Assertion. Session info: %s",
+ session_info,
+ )
+ return self.handle_acs_failure(
+ request,
+ exception=PermissionDenied("No user could be authenticated."),
+ session_info=session_info,
+ )
auth.login(self.request, user)
- _set_subject_id(request.saml_session, session_info['name_id'])
+ _set_subject_id(request.saml_session, session_info["name_id"])
logger.debug("User %s authenticated via SSO.", user)
self.post_login_hook(request, user, session_info)
self.customize_session(user, session_info)
relay_state = self.build_relay_state()
- custom_redirect_url = self.custom_redirect(
- user, relay_state, session_info)
+ custom_redirect_url = self.custom_redirect(user, relay_state, session_info)
if custom_redirect_url:
return HttpResponseRedirect(custom_redirect_url)
relay_state = validate_referral_url(request, relay_state)
- logger.debug('Redirecting to the RelayState: %s', relay_state)
+ logger.debug("Redirecting to the RelayState: %s", relay_state)
return HttpResponseRedirect(relay_state)
- def post_login_hook(self, request: HttpRequest, user: settings.AUTH_USER_MODEL, session_info: dict) -> None:
- """ If desired, a hook to add logic after a user has succesfully logged in.
- """
+ def post_login_hook(
+ self, request: HttpRequest, user: settings.AUTH_USER_MODEL, session_info: dict
+ ) -> None:
+ """If desired, a hook to add logic after a user has succesfully logged in."""
def build_relay_state(self) -> str:
- """ The relay state is a URL used to redirect the user to the view where they came from.
- """
+ """The relay state is a URL used to redirect the user to the view where they came from."""
default_relay_state = get_fallback_login_redirect_url()
- relay_state = self.request.POST.get('RelayState', default_relay_state)
+ relay_state = self.request.POST.get("RelayState", default_relay_state)
relay_state = self.customize_relay_state(relay_state)
if not relay_state:
- logger.warning('The RelayState parameter exists but is empty')
+ logger.warning("The RelayState parameter exists but is empty")
relay_state = default_relay_state
return relay_state
def customize_session(self, user, session_info: dict):
- """ Subclasses can use this for customized functionality around user sessions.
- """
+ """Subclasses can use this for customized functionality around user sessions."""
def customize_relay_state(self, relay_state: str) -> str:
- """ Subclasses may override this method to implement custom logic for relay state.
- """
+ """Subclasses may override this method to implement custom logic for relay state."""
return relay_state
def custom_redirect(self, user, relay_state: str, session_info) -> str:
- """ Subclasses may override this method to implement custom logic for redirect.
+ """Subclasses may override this method to implement custom logic for redirect.
- For example, some sites may require user registration if the user has not
- yet been provisioned.
+ For example, some sites may require user registration if the user has not
+ yet been provisioned.
"""
return None
class EchoAttributesView(LoginRequiredMixin, SPConfigMixin, View):
- """Example view that echo the SAML attributes of an user
- """
+ """Example view that echo the SAML attributes of an user"""
def get(self, request, *args, **kwargs):
state, client = self.get_state_client(request)
@@ -563,18 +618,23 @@ def get(self, request, *args, **kwargs):
subject_id = _get_subject_id(request.saml_session)
try:
identity = client.users.get_identity(
- subject_id, check_not_on_or_after=False)
+ subject_id, check_not_on_or_after=False
+ )
except AttributeError:
- return HttpResponse("No active SAML identity found. Are you sure you have logged in via SAML?")
+ return HttpResponse(
+ "No active SAML identity found. Are you sure you have logged in via SAML?"
+ )
- return render(request, 'djangosaml2/echo_attributes.html', {'attributes': identity[0]})
+ return render(
+ request, "djangosaml2/echo_attributes.html", {"attributes": identity[0]}
+ )
class LogoutInitView(LoginRequiredMixin, SPConfigMixin, View):
- """ SAML Logout Request initiator
+ """SAML Logout Request initiator
- This view initiates the SAML2 Logout request
- using the pysaml2 library to create the LogoutRequest.
+ This view initiates the SAML2 Logout request
+ using the pysaml2 library to create the LogoutRequest.
"""
def get(self, request, *args, **kwargs):
@@ -583,18 +643,17 @@ def get(self, request, *args, **kwargs):
subject_id = _get_subject_id(request.saml_session)
if subject_id is None:
logger.warning(
- 'The session does not contain the subject id for user %s', request.user)
+ "The session does not contain the subject id for user %s", request.user
+ )
_error = None
try:
result = client.global_logout(subject_id)
except LogoutError as exp:
- logger.exception(
- 'Error Handled - SLO not supported by IDP: {}'.format(exp))
+ logger.exception(f"Error Handled - SLO not supported by IDP: {exp}")
_error = exp
except UnsupportedBinding as exp:
- logger.exception(
- 'Error Handled - SLO - unsupported binding by IDP: {}'.format(exp))
+ logger.exception(f"Error Handled - SLO - unsupported binding by IDP: {exp}")
_error = exp
auth.logout(request)
@@ -603,61 +662,65 @@ def get(self, request, *args, **kwargs):
if _error:
return self.handle_unsupported_slo_exception(request, _error)
-
if not result:
logger.error(
- "Looks like the user %s is not logged in any IdP/AA", subject_id)
+ "Looks like the user %s is not logged in any IdP/AA", subject_id
+ )
return HttpResponseBadRequest("You are not logged in any IdP/AA")
if len(result) > 1:
logger.error(
- 'Sorry, I do not know how to logout from several sources. I will logout just from the first one')
+ "Sorry, I do not know how to logout from several sources. I will logout just from the first one"
+ )
- for entityid, logout_info in result.items():
+ for logout_info in result.values():
if isinstance(logout_info, tuple):
binding, http_info = logout_info
if binding == saml2.BINDING_HTTP_POST:
logger.debug(
- 'Returning form to the IdP to continue the logout process')
- body = ''.join(http_info['data'])
+ "Returning form to the IdP to continue the logout process"
+ )
+ body = "".join(http_info["data"])
return HttpResponse(body)
elif binding == saml2.BINDING_HTTP_REDIRECT:
logger.debug(
- 'Redirecting to the IdP to continue the logout process')
+ "Redirecting to the IdP to continue the logout process"
+ )
return HttpResponseRedirect(get_location(http_info))
else:
- logger.error('Unknown binding: %s', binding)
- return HttpResponseServerError('Failed to log out')
+ logger.error("Unknown binding: %s", binding)
+ return HttpResponseServerError("Failed to log out")
# We must have had a soap logout
return finish_logout(request, logout_info)
logger.error(
- 'Could not logout because there only the HTTP_REDIRECT is supported')
- return HttpResponseServerError('Logout Binding not supported')
+ "Could not logout because there only the HTTP_REDIRECT is supported"
+ )
+ return HttpResponseServerError("Logout Binding not supported")
def handle_unsupported_slo_exception(self, request, exception, *args, **kwargs):
- """ Subclasses may override this method to implement custom logic for
- handling logout errors. Redirects to LOGOUT_REDIRECT_URL by default.
+ """Subclasses may override this method to implement custom logic for
+ handling logout errors. Redirects to LOGOUT_REDIRECT_URL by default.
- For example, a site may want to perform additional logic and redirect
- users somewhere other than the LOGOUT_REDIRECT_URL.
+ For example, a site may want to perform additional logic and redirect
+ users somewhere other than the LOGOUT_REDIRECT_URL.
"""
- return HttpResponseRedirect(getattr(settings, 'LOGOUT_REDIRECT_URL', '/'))
+ return HttpResponseRedirect(getattr(settings, "LOGOUT_REDIRECT_URL", "/"))
-@method_decorator(csrf_exempt, name='dispatch')
+@method_decorator(csrf_exempt, name="dispatch")
class LogoutView(SPConfigMixin, View):
- """ SAML Logout Response endpoint
-
- The IdP will send the logout response to this view,
- which will process it with pysaml2 help and log the user
- out.
- Note that the IdP can request a logout even when
- we didn't initiate the process as a single logout
- request started by another SP.
+ """SAML Logout Response endpoint
+
+ The IdP will send the logout response to this view,
+ which will process it with pysaml2 help and log the user
+ out.
+ Note that the IdP can request a logout even when
+ we didn't initiate the process as a single logout
+ request started by another SP.
"""
- logout_error_template = 'djangosaml2/logout_error.html'
+ logout_error_template = "djangosaml2/logout_error.html"
def get(self, request, *args, **kwargs):
return self.do_logout_service(
@@ -670,79 +733,87 @@ def post(self, request, *args, **kwargs):
)
def do_logout_service(self, request, data, binding):
- logger.debug('Logout service started')
+ logger.debug("Logout service started")
state, client = self.get_state_client(request)
- if 'SAMLResponse' in data: # we started the logout
- logger.debug('Receiving a logout response from the IdP')
+ if "SAMLResponse" in data: # we started the logout
+ logger.debug("Receiving a logout response from the IdP")
try:
response = client.parse_logout_request_response(
- data['SAMLResponse'], binding)
+ data["SAMLResponse"], binding
+ )
except StatusError as e:
response = None
- logger.warning(
- "Error logging out from remote provider: " + str(e))
+ logger.warning("Error logging out from remote provider: " + str(e))
state.sync()
return finish_logout(request, response)
- elif 'SAMLRequest' in data: # logout started by the IdP
- logger.debug('Receiving a logout request from the IdP')
+ elif "SAMLRequest" in data: # logout started by the IdP
+ logger.debug("Receiving a logout request from the IdP")
subject_id = _get_subject_id(request.saml_session)
if subject_id is None:
logger.warning(
- 'The session does not contain the subject id for user %s. Performing local logout',
- request.user)
+ "The session does not contain the subject id for user %s. Performing local logout",
+ request.user,
+ )
auth.logout(request)
return render(request, self.logout_error_template, status=403)
http_info = client.handle_logout_request(
- data['SAMLRequest'],
+ data["SAMLRequest"],
subject_id,
binding,
- relay_state=data.get('RelayState', ''))
+ relay_state=data.get("RelayState", ""),
+ )
state.sync()
auth.logout(request)
if (
- http_info.get('method', 'GET') == 'POST' and
- 'data' in http_info and
- ('Content-type', 'text/html') in http_info.get('headers', [])
+ http_info.get("method", "GET") == "POST"
+ and "data" in http_info
+ and ("Content-type", "text/html") in http_info.get("headers", [])
):
# need to send back to the IDP a signed POST response with user session
# return HTML form content to browser with auto form validation
# to finally send request to the IDP
- return HttpResponse(http_info['data'])
+ return HttpResponse(http_info["data"])
return HttpResponseRedirect(get_location(http_info))
- logger.error('No SAMLResponse or SAMLRequest parameter found')
- return HttpResponseBadRequest('No SAMLResponse or SAMLRequest parameter found')
+ logger.error("No SAMLResponse or SAMLRequest parameter found")
+ return HttpResponseBadRequest("No SAMLResponse or SAMLRequest parameter found")
def finish_logout(request, response, next_page=None):
- if (getattr(settings, 'SAML_IGNORE_LOGOUT_ERRORS', False) or (response and response.status_ok())):
+ if getattr(settings, "SAML_IGNORE_LOGOUT_ERRORS", False) or (
+ response and response.status_ok()
+ ):
if not next_page:
- next_page = getattr(settings, 'LOGOUT_REDIRECT_URL', '/')
- logger.debug(
- 'Performing django logout with a next_page of %s', next_page)
+ next_page = getattr(settings, "LOGOUT_REDIRECT_URL", "/")
+ logger.debug("Performing django logout with a next_page of %s", next_page)
return AuthLogoutView.as_view()(request, next_page=next_page)
- logger.error('Unknown error during the logout')
+ logger.error("Unknown error during the logout")
return render(request, "djangosaml2/logout_error.html", {})
class MetadataView(SPConfigMixin, View):
- """ Returns an XML with the SAML 2.0 metadata for this SP as configured in the settings.py file.
- """
+ """Returns an XML with the SAML 2.0 metadata for this SP as configured in the settings.py file."""
def get(self, request, *args, **kwargs):
conf = self.get_sp_config(request)
metadata = entity_descriptor(conf)
- return HttpResponse(content=str(metadata).encode('utf-8'), content_type="text/xml; charset=utf-8")
+ return HttpResponse(
+ content=str(metadata).encode("utf-8"),
+ content_type="text/xml; charset=utf-8",
+ )
def get_namespace_prefixes():
- from saml2 import md, saml, samlp, xmlenc, xmldsig
- return {'saml': saml.NAMESPACE,
- 'samlp': samlp.NAMESPACE,
- 'md': md.NAMESPACE,
- 'ds': xmldsig.NAMESPACE,
- 'xenc': xmlenc.NAMESPACE}
+ from saml2 import md, saml, samlp, xmldsig, xmlenc
+
+ return {
+ "saml": saml.NAMESPACE,
+ "samlp": samlp.NAMESPACE,
+ "md": md.NAMESPACE,
+ "ds": xmldsig.NAMESPACE,
+ "xenc": xmlenc.NAMESPACE,
+ }
diff --git a/docs/source/_static/custom.css b/docs/source/_static/custom.css
index 5584d1b7..9d0a45b4 100644
--- a/docs/source/_static/custom.css
+++ b/docs/source/_static/custom.css
@@ -1,7 +1,7 @@
body,
-h1, h2,
-.rst-content .toctree-wrapper p.caption,
-h3, h4, h5, h6,
+h1, h2,
+.rst-content .toctree-wrapper p.caption,
+h3, h4, h5, h6,
legend{
font-family: Lato,'Helvetica Neue',Arial,Helvetica,sans-serif;
}
@@ -10,13 +10,13 @@ legend{
background: #ffffff;
}
-.wy-side-nav-search>a,
+.wy-side-nav-search>a,
.wy-side-nav-search .wy-dropdown>a{
color: #9b9c9e;
font-weight: normal;
}
-.wy-menu-vertical header,
+.wy-menu-vertical header,
.wy-menu-vertical p.caption{
color: #fff;
font-size:85%;
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 00000000..5d906c1e
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,14 @@
+[tool.black]
+force-exclude = '''/(migrations)/'''
+target-version = ["py36"]
+
+[tool.isort]
+src_paths = ["djangosaml2", "tests"]
+profile = "black"
+known_django = ["django"]
+known_contrib = ["django.contrib"]
+known_saml2 = ["saml2"]
+known_first_party = ["djangosaml2"]
+known_tests = ["tests"]
+sections = ["FUTURE", "STDLIB", "DJANGO", "CONTRIB", "THIRDPARTY", "SAML2", "FIRSTPARTY", "TESTS", "LOCALFOLDER"]
+skip_glob = ["**/migrations/*.py"]
diff --git a/setup.cfg b/setup.cfg
index 2a9acf13..4ed9ab4e 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -1,2 +1,11 @@
[bdist_wheel]
universal = 1
+
+[flake8]
+# E203 ignore
+# https://github.com/PyCQA/pycodestyle/issues/373
+# https://github.com/PyCQA/pycodestyle/pull/914
+ignore = D106,E203,W503
+select = B0,B901,B902,B903,C,F,I,W
+max-line-length = 88
+exclude = .tox,.git,*/migrations/*,docs
diff --git a/setup.py b/setup.py
index 0b507233..e6765652 100644
--- a/setup.py
+++ b/setup.py
@@ -15,19 +15,22 @@
import codecs
import os
-from setuptools import setup, find_packages
+
+from setuptools import find_packages, setup
def read(*rnames):
- return codecs.open(os.path.join(os.path.dirname(__file__), *rnames), encoding='utf-8').read()
+ return codecs.open(
+ os.path.join(os.path.dirname(__file__), *rnames), encoding="utf-8"
+ ).read()
setup(
- name='djangosaml2',
- version='1.3.5',
- description='pysaml2 integration for Django',
- long_description=read('README.md'),
- long_description_content_type='text/markdown',
+ name="djangosaml2",
+ version="1.3.6",
+ description="pysaml2 integration for Django",
+ long_description=read("README.md"),
+ long_description_content_type="text/markdown",
classifiers=[
"Development Status :: 5 - Production/Stable",
"Environment :: Web Environment",
@@ -49,24 +52,20 @@ def read(*rnames):
"Topic :: Internet :: WWW/HTTP :: WSGI",
"Topic :: Security",
"Topic :: Software Development :: Libraries :: Application Frameworks",
- ],
+ ],
keywords="django,pysaml2,sso,saml2,federated authentication,authentication",
author="Yaco Sistemas and independent contributors",
author_email="lgs@yaco.es",
maintainer="Giuseppe De Marco",
url="https://github.com/IdentityPython/djangosaml2",
download_url="https://pypi.org/project/djangosaml2/",
- license='Apache 2.0',
+ license="Apache 2.0",
packages=find_packages(exclude=["tests", "tests.*"]),
include_package_data=True,
zip_safe=False,
install_requires=[
- 'defusedxml>=0.4.1',
- 'Django>=2.2,<5',
- 'pysaml2>=6.5.1',
- ],
- tests_require=[
- # Provides assert_called_once.
- 'mock',
- ]
- )
+ "defusedxml>=0.4.1",
+ "Django>=2.2,<5",
+ "pysaml2>=6.5.1",
+ ],
+)
diff --git a/tests/.coveragerc b/tests/.coveragerc
index d513e78f..f18e3f25 100644
--- a/tests/.coveragerc
+++ b/tests/.coveragerc
@@ -1,4 +1,3 @@
# .coveragerc to control coverage.py
[run]
source = djangosaml2
-
diff --git a/tests/run_tests.py b/tests/run_tests.py
index 7ef77c94..0ab2d7ee 100755
--- a/tests/run_tests.py
+++ b/tests/run_tests.py
@@ -17,8 +17,8 @@
import os
import sys
-from django.core.wsgi import get_wsgi_application
from django.core import management
+from django.core.wsgi import get_wsgi_application
PROJECT_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), os.path.pardir))
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "tests.settings")
@@ -26,4 +26,4 @@
# Load models
application = get_wsgi_application()
-management.call_command('test', 'djangosaml2.tests', 'testprofiles')
+management.call_command("test", "djangosaml2.tests", "testprofiles")
diff --git a/tests/settings.py b/tests/settings.py
index ba8a7d83..400b7d98 100644
--- a/tests/settings.py
+++ b/tests/settings.py
@@ -12,70 +12,66 @@
import os
-
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# Make this unique, and don't share it with anybody.
-SECRET_KEY = 'xvds$ppv5ha75qg1yx3aax7ugr_2*fmdrc(lrc%x7kdez-63xn'
+SECRET_KEY = "xvds$ppv5ha75qg1yx3aax7ugr_2*fmdrc(lrc%x7kdez-63xn"
DEBUG = True
ALLOWED_HOSTS = []
INSTALLED_APPS = (
- 'testprofiles',
-
- 'django.contrib.admin',
- 'django.contrib.auth',
- 'django.contrib.contenttypes',
- 'django.contrib.sessions',
- 'django.contrib.messages',
- 'django.contrib.staticfiles',
-
- 'djangosaml2',
+ "testprofiles",
+ "django.contrib.admin",
+ "django.contrib.auth",
+ "django.contrib.contenttypes",
+ "django.contrib.sessions",
+ "django.contrib.messages",
+ "django.contrib.staticfiles",
+ "djangosaml2",
)
MIDDLEWARE = (
- 'django.middleware.security.SecurityMiddleware',
- 'django.contrib.sessions.middleware.SessionMiddleware',
- 'django.middleware.common.CommonMiddleware',
- 'django.middleware.csrf.CsrfViewMiddleware',
- 'django.contrib.auth.middleware.AuthenticationMiddleware',
- 'django.contrib.messages.middleware.MessageMiddleware',
- 'django.middleware.clickjacking.XFrameOptionsMiddleware',
-
+ "django.middleware.security.SecurityMiddleware",
+ "django.contrib.sessions.middleware.SessionMiddleware",
+ "django.middleware.common.CommonMiddleware",
+ "django.middleware.csrf.CsrfViewMiddleware",
+ "django.contrib.auth.middleware.AuthenticationMiddleware",
+ "django.contrib.messages.middleware.MessageMiddleware",
+ "django.middleware.clickjacking.XFrameOptionsMiddleware",
# SameSite Cookie handler
- 'djangosaml2.middleware.SamlSessionMiddleware'
+ "djangosaml2.middleware.SamlSessionMiddleware",
)
-ROOT_URLCONF = 'testprofiles.urls'
+ROOT_URLCONF = "testprofiles.urls"
TEMPLATES = [
{
- 'BACKEND': 'django.template.backends.django.DjangoTemplates',
- 'DIRS': [],
- 'APP_DIRS': True,
- 'OPTIONS': {
- 'context_processors': [
- 'django.template.context_processors.debug',
- 'django.template.context_processors.request',
- 'django.contrib.auth.context_processors.auth',
- 'django.contrib.messages.context_processors.messages',
+ "BACKEND": "django.template.backends.django.DjangoTemplates",
+ "DIRS": [],
+ "APP_DIRS": True,
+ "OPTIONS": {
+ "context_processors": [
+ "django.template.context_processors.debug",
+ "django.template.context_processors.request",
+ "django.contrib.auth.context_processors.auth",
+ "django.contrib.messages.context_processors.messages",
],
},
},
]
-WSGI_APPLICATION = 'testprofiles.wsgi.application'
+WSGI_APPLICATION = "testprofiles.wsgi.application"
# Database
# https://docs.djangoproject.com/en/1.10/ref/settings/#databases
DATABASES = {
- 'default': {
- 'ENGINE': 'django.db.backends.sqlite3',
- 'NAME': os.path.join(BASE_DIR, 'tests/db.sqlite3'),
+ "default": {
+ "ENGINE": "django.db.backends.sqlite3",
+ "NAME": os.path.join(BASE_DIR, "tests/db.sqlite3"),
}
}
@@ -84,25 +80,25 @@
AUTH_PASSWORD_VALIDATORS = [
{
- 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
+ "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
},
{
- 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
+ "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
},
{
- 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
+ "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",
},
{
- 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
+ "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",
},
]
# Internationalization
# https://docs.djangoproject.com/en/1.10/topics/i18n/
-LANGUAGE_CODE = 'en-us'
+LANGUAGE_CODE = "en-us"
-TIME_ZONE = 'UTC'
+TIME_ZONE = "UTC"
SITE_ID = 1
@@ -116,10 +112,10 @@
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/1.10/howto/static-files/
-STATIC_URL = '/static/'
+STATIC_URL = "/static/"
-AUTH_USER_MODEL = 'testprofiles.TestUser'
+AUTH_USER_MODEL = "testprofiles.TestUser"
# A sample logging configuration. The only tangible logging
# performed by this configuration is to send an email to
@@ -127,38 +123,32 @@
# See http://docs.djangoproject.com/en/dev/topics/logging for
# more details on how to customize your logging configuration.
LOGGING = {
- 'version': 1,
- 'disable_existing_loggers': False,
- 'filters': {
- 'require_debug_false': {
- '()': 'django.utils.log.RequireDebugFalse'
- }
- },
- 'handlers': {
- 'mail_admins': {
- 'level': 'ERROR',
- 'filters': ['require_debug_false'],
- 'class': 'django.utils.log.AdminEmailHandler'
+ "version": 1,
+ "disable_existing_loggers": False,
+ "filters": {"require_debug_false": {"()": "django.utils.log.RequireDebugFalse"}},
+ "handlers": {
+ "mail_admins": {
+ "level": "ERROR",
+ "filters": ["require_debug_false"],
+ "class": "django.utils.log.AdminEmailHandler",
},
- 'console': {
- 'level': 'DEBUG',
- 'class': 'logging.StreamHandler',
+ "console": {
+ "level": "DEBUG",
+ "class": "logging.StreamHandler",
},
},
- 'loggers': {
- 'django.request': {
- 'handlers': ['mail_admins'],
- 'level': 'ERROR',
- 'propagate': True,
+ "loggers": {
+ "django.request": {
+ "handlers": ["mail_admins"],
+ "level": "ERROR",
+ "propagate": True,
},
- 'djangosaml2': {
- 'handlers': ['console'],
- 'level': 'DEBUG',
+ "djangosaml2": {
+ "handlers": ["console"],
+ "level": "DEBUG",
},
- }
+ },
}
-AUTHENTICATION_BACKENDS = (
- 'djangosaml2.backends.Saml2Backend',
-)
+AUTHENTICATION_BACKENDS = ("djangosaml2.backends.Saml2Backend",)
diff --git a/tests/testprofiles/app.py b/tests/testprofiles/app.py
index ce963e8d..045d32d0 100644
--- a/tests/testprofiles/app.py
+++ b/tests/testprofiles/app.py
@@ -2,6 +2,6 @@
class TestProfilesConfig(AppConfig):
- name = 'testprofiles'
- verbose_name = 'Test profiles'
- default_auto_field = 'django.db.models.AutoField'
+ name = "testprofiles"
+ verbose_name = "Test profiles"
+ default_auto_field = "django.db.models.AutoField"
diff --git a/tests/testprofiles/models.py b/tests/testprofiles/models.py
index 4062ddec..d7cb5b25 100644
--- a/tests/testprofiles/models.py
+++ b/tests/testprofiles/models.py
@@ -13,9 +13,10 @@
# See the License for the specific language governing permissions and
# limitations under the License.
-from django.contrib.auth.models import AbstractUser
from django.db import models
+from django.contrib.auth.models import AbstractUser
+
class TestUser(AbstractUser):
age = models.CharField(max_length=100, blank=True)
@@ -24,7 +25,7 @@ def process_first_name(self, first_name):
self.first_name = first_name[0]
class Meta:
- app_label = 'testprofiles'
+ app_label = "testprofiles"
class StandaloneUserModel(models.Model):
@@ -32,6 +33,7 @@ class StandaloneUserModel(models.Model):
Does not inherit from Django's base abstract user and does not define a
USERNAME_FIELD.
"""
+
username = models.CharField(max_length=30, unique=True)
@@ -39,7 +41,7 @@ class RequiredFieldUser(models.Model):
email = models.EmailField(unique=True)
email_verified = models.BooleanField()
- USERNAME_FIELD = 'email'
+ USERNAME_FIELD = "email"
def __repr__(self):
return self.email
diff --git a/tests/testprofiles/tests.py b/tests/testprofiles/tests.py
index 707a74fc..9bb39e72 100644
--- a/tests/testprofiles/tests.py
+++ b/tests/testprofiles/tests.py
@@ -15,173 +15,184 @@
# limitations under the License.
from django.conf import settings
-from django.contrib.auth import get_user_model
-from djangosaml2.backends import get_saml_user_model
-from django.contrib.auth.models import User as DjangoUserModel
from django.core.exceptions import ImproperlyConfigured
from django.test import TestCase, override_settings
-from djangosaml2.backends import Saml2Backend, set_attribute
+from django.contrib.auth import get_user_model
+from django.contrib.auth.models import User as DjangoUserModel
+
+from djangosaml2.backends import Saml2Backend, get_saml_user_model, set_attribute
from testprofiles.models import TestUser
class BackendUtilMethodsTests(TestCase):
-
def test_set_attribute(self):
u = TestUser()
- self.assertFalse(hasattr(u, 'custom_attribute'))
+ self.assertFalse(hasattr(u, "custom_attribute"))
# Set attribute initially
- changed = set_attribute(u, 'custom_attribute', 'value')
+ changed = set_attribute(u, "custom_attribute", "value")
self.assertTrue(changed)
- self.assertEqual(u.custom_attribute, 'value')
+ self.assertEqual(u.custom_attribute, "value")
# 'Update' to the same value again
- changed_same = set_attribute(u, 'custom_attribute', 'value')
+ changed_same = set_attribute(u, "custom_attribute", "value")
self.assertFalse(changed_same)
- self.assertEqual(u.custom_attribute, 'value')
+ self.assertEqual(u.custom_attribute, "value")
# Update to a different value
- changed_different = set_attribute(u, 'custom_attribute', 'new_value')
+ changed_different = set_attribute(u, "custom_attribute", "new_value")
self.assertTrue(changed_different)
- self.assertEqual(u.custom_attribute, 'new_value')
+ self.assertEqual(u.custom_attribute, "new_value")
class dummyNameId:
- text = 'dummyNameId'
+ text = "dummyNameId"
class Saml2BackendTests(TestCase):
- """ UnitTests on backend classes
- """
+ """UnitTests on backend classes"""
+
backend_cls = Saml2Backend
def setUp(self):
self.backend = self.backend_cls()
- self.user = TestUser.objects.create(username='john')
+ self.user = TestUser.objects.create(username="john")
def test_get_model_ok(self):
self.assertEqual(self.backend._user_model, TestUser)
def test_get_model_nonexisting(self):
- with override_settings(SAML_USER_MODEL='testprofiles.NonExisting'):
- with self.assertRaisesMessage(ImproperlyConfigured, "Model 'testprofiles.NonExisting' could not be loaded"):
+ with override_settings(SAML_USER_MODEL="testprofiles.NonExisting"):
+ with self.assertRaisesMessage(
+ ImproperlyConfigured,
+ "Model 'testprofiles.NonExisting' could not be loaded",
+ ):
self.assertEqual(self.backend._user_model, None)
def test_get_model_invalid_specifier(self):
- with override_settings(SAML_USER_MODEL='random_package.specifier.testprofiles.NonExisting'):
- with self.assertRaisesMessage(ImproperlyConfigured, "Model was specified as 'random_package.specifier.testprofiles.NonExisting', but it must be of the form 'app_label.model_name'"):
+ with override_settings(
+ SAML_USER_MODEL="random_package.specifier.testprofiles.NonExisting"
+ ):
+ with self.assertRaisesMessage(
+ ImproperlyConfigured,
+ "Model was specified as 'random_package.specifier.testprofiles.NonExisting', but it must be of the form 'app_label.model_name'",
+ ):
self.assertEqual(self.backend._user_model, None)
def test_user_model_specified(self):
- with override_settings(AUTH_USER_MODEL='auth.User'):
- with override_settings(SAML_USER_MODEL='testprofiles.TestUser'):
+ with override_settings(AUTH_USER_MODEL="auth.User"):
+ with override_settings(SAML_USER_MODEL="testprofiles.TestUser"):
self.assertEqual(self.backend._user_model, TestUser)
def test_user_model_default(self):
- with override_settings(AUTH_USER_MODEL='auth.User'):
+ with override_settings(AUTH_USER_MODEL="auth.User"):
self.assertEqual(self.backend._user_model, DjangoUserModel)
def test_user_lookup_attribute_specified(self):
- with override_settings(SAML_USER_MODEL='testprofiles.TestUser'):
- with override_settings(SAML_DJANGO_USER_MAIN_ATTRIBUTE='age'):
- self.assertEqual(self.backend._user_lookup_attribute, 'age')
+ with override_settings(SAML_USER_MODEL="testprofiles.TestUser"):
+ with override_settings(SAML_DJANGO_USER_MAIN_ATTRIBUTE="age"):
+ self.assertEqual(self.backend._user_lookup_attribute, "age")
def test_user_lookup_attribute_default(self):
- with override_settings(SAML_USER_MODEL='testprofiles.TestUser'):
- self.assertEqual(self.backend._user_lookup_attribute, 'username')
+ with override_settings(SAML_USER_MODEL="testprofiles.TestUser"):
+ self.assertEqual(self.backend._user_lookup_attribute, "username")
def test_extract_user_identifier_params_use_nameid_present(self):
- with override_settings(SAML_USER_MODEL='testprofiles.TestUser'):
+ with override_settings(SAML_USER_MODEL="testprofiles.TestUser"):
with override_settings(SAML_USE_NAME_ID_AS_USERNAME=True):
- _, lookup_value = self.backend._extract_user_identifier_params({'name_id': dummyNameId()}, {}, {})
- self.assertEqual(lookup_value, 'dummyNameId')
+ _, lookup_value = self.backend._extract_user_identifier_params(
+ {"name_id": dummyNameId()}, {}, {}
+ )
+ self.assertEqual(lookup_value, "dummyNameId")
def test_extract_user_identifier_params_use_nameid_missing(self):
- with override_settings(SAML_USER_MODEL='testprofiles.TestUser'):
+ with override_settings(SAML_USER_MODEL="testprofiles.TestUser"):
with override_settings(SAML_USE_NAME_ID_AS_USERNAME=True):
- _, lookup_value = self.backend._extract_user_identifier_params({}, {}, {})
+ _, lookup_value = self.backend._extract_user_identifier_params(
+ {}, {}, {}
+ )
self.assertEqual(lookup_value, None)
def test_is_authorized(self):
- self.assertTrue(self.backend.is_authorized({}, {}, '', {}))
+ self.assertTrue(self.backend.is_authorized({}, {}, "", {}))
def test_clean_attributes(self):
- attributes = {'random': 'dummy', 'value': 123}
- self.assertEqual(self.backend.clean_attributes(attributes, ''), attributes)
+ attributes = {"random": "dummy", "value": 123}
+ self.assertEqual(self.backend.clean_attributes(attributes, ""), attributes)
def test_clean_user_main_attribute(self):
- self.assertEqual(self.backend.clean_user_main_attribute('value'), 'value')
+ self.assertEqual(self.backend.clean_user_main_attribute("value"), "value")
def test_update_user_simple(self):
- u = TestUser(username='johny')
+ u = TestUser(username="johny")
self.assertIsNone(u.pk)
u = self.backend._update_user(u, {}, {})
self.assertIsNotNone(u.pk)
def test_update_user(self):
attribute_mapping = {
- 'uid': ('username', ),
- 'mail': ('email', ),
- 'cn': ('first_name', ),
- 'sn': ('last_name', ),
- }
+ "uid": ("username",),
+ "mail": ("email",),
+ "cn": ("first_name",),
+ "sn": ("last_name",),
+ }
attributes = {
- 'uid': ('john', ),
- 'mail': ('john@example.com', ),
- 'cn': ('John', ),
- 'sn': ('Doe', ),
- }
+ "uid": ("john",),
+ "mail": ("john@example.com",),
+ "cn": ("John",),
+ "sn": ("Doe",),
+ }
self.backend._update_user(self.user, attributes, attribute_mapping)
- self.assertEqual(self.user.email, 'john@example.com')
- self.assertEqual(self.user.first_name, 'John')
- self.assertEqual(self.user.last_name, 'Doe')
+ self.assertEqual(self.user.email, "john@example.com")
+ self.assertEqual(self.user.first_name, "John")
+ self.assertEqual(self.user.last_name, "Doe")
- attribute_mapping['saml_age'] = ('age', )
- attributes['saml_age'] = ('22', )
+ attribute_mapping["saml_age"] = ("age",)
+ attributes["saml_age"] = ("22",)
self.backend._update_user(self.user, attributes, attribute_mapping)
- self.assertEqual(self.user.age, '22')
+ self.assertEqual(self.user.age, "22")
def test_update_user_callable_attributes(self):
attribute_mapping = {
- 'uid': ('username', ),
- 'mail': ('email', ),
- 'cn': ('process_first_name', ),
- 'sn': ('last_name', ),
- }
+ "uid": ("username",),
+ "mail": ("email",),
+ "cn": ("process_first_name",),
+ "sn": ("last_name",),
+ }
attributes = {
- 'uid': ('john', ),
- 'mail': ('john@example.com', ),
- 'cn': ('John', ),
- 'sn': ('Doe', ),
- }
+ "uid": ("john",),
+ "mail": ("john@example.com",),
+ "cn": ("John",),
+ "sn": ("Doe",),
+ }
self.backend._update_user(self.user, attributes, attribute_mapping)
- self.assertEqual(self.user.email, 'john@example.com')
- self.assertEqual(self.user.first_name, 'John')
- self.assertEqual(self.user.last_name, 'Doe')
+ self.assertEqual(self.user.email, "john@example.com")
+ self.assertEqual(self.user.first_name, "John")
+ self.assertEqual(self.user.last_name, "Doe")
def test_update_user_empty_attribute(self):
- self.user.last_name = 'Smith'
+ self.user.last_name = "Smith"
self.user.save()
attribute_mapping = {
- 'uid': ('username', ),
- 'mail': ('email', ),
- 'cn': ('first_name', ),
- 'sn': ('last_name', ),
- }
+ "uid": ("username",),
+ "mail": ("email",),
+ "cn": ("first_name",),
+ "sn": ("last_name",),
+ }
attributes = {
- 'uid': ('john', ),
- 'mail': ('john@example.com', ),
- 'cn': ('John', ),
- 'sn': (),
- }
- with self.assertLogs('djangosaml2', level='DEBUG') as logs:
+ "uid": ("john",),
+ "mail": ("john@example.com",),
+ "cn": ("John",),
+ "sn": (),
+ }
+ with self.assertLogs("djangosaml2", level="DEBUG") as logs:
self.backend._update_user(self.user, attributes, attribute_mapping)
- self.assertEqual(self.user.email, 'john@example.com')
- self.assertEqual(self.user.first_name, 'John')
+ self.assertEqual(self.user.email, "john@example.com")
+ self.assertEqual(self.user.first_name, "John")
# empty attribute list: no update
- self.assertEqual(self.user.last_name, 'Smith')
+ self.assertEqual(self.user.last_name, "Smith")
self.assertIn(
'DEBUG:djangosaml2:Could not find value for "sn", not updating fields "(\'last_name\',)"',
logs.output,
@@ -189,16 +200,24 @@ def test_update_user_empty_attribute(self):
def test_invalid_model_attribute_log(self):
attribute_mapping = {
- 'uid': ['username'],
- 'cn': ['nonexistent'],
+ "uid": ["username"],
+ "cn": ["nonexistent"],
}
attributes = {
- 'uid': ['john'],
- 'cn': ['John'],
+ "uid": ["john"],
+ "cn": ["John"],
}
- with self.assertLogs('djangosaml2', level='DEBUG') as logs:
- user, _ = self.backend.get_or_create_user(self.backend._user_lookup_attribute, 'john', True, None, None, None, None)
+ with self.assertLogs("djangosaml2", level="DEBUG") as logs:
+ user, _ = self.backend.get_or_create_user(
+ self.backend._user_lookup_attribute,
+ "john",
+ True,
+ None,
+ None,
+ None,
+ None,
+ )
self.backend._update_user(user, attributes, attribute_mapping)
self.assertIn(
@@ -206,20 +225,25 @@ def test_invalid_model_attribute_log(self):
logs.output,
)
- @override_settings(SAML_USER_MODEL='testprofiles.RequiredFieldUser')
+ @override_settings(SAML_USER_MODEL="testprofiles.RequiredFieldUser")
def test_create_user_with_required_fields(self):
- attribute_mapping = {
- 'mail': ['email'],
- 'mail_verified': ['email_verified']
- }
+ attribute_mapping = {"mail": ["email"], "mail_verified": ["email_verified"]}
attributes = {
- 'mail': ['john@example.org'],
- 'mail_verified': [True],
+ "mail": ["john@example.org"],
+ "mail_verified": [True],
}
# User creation does not fail if several fields are required.
- user, created = self.backend.get_or_create_user(self.backend._user_lookup_attribute, 'john@example.org', True, None, None, None, None)
+ user, created = self.backend.get_or_create_user(
+ self.backend._user_lookup_attribute,
+ "john@example.org",
+ True,
+ None,
+ None,
+ None,
+ None,
+ )
- self.assertEqual(user.email, 'john@example.org')
+ self.assertEqual(user.email, "john@example.org")
self.assertIs(user.email_verified, None)
user = self.backend._update_user(user, attributes, attribute_mapping, created)
@@ -227,37 +251,44 @@ def test_create_user_with_required_fields(self):
def test_django_user_main_attribute(self):
old_username_field = get_user_model().USERNAME_FIELD
- get_user_model().USERNAME_FIELD = 'slug'
- self.assertEqual(self.backend._user_lookup_attribute, 'slug')
+ get_user_model().USERNAME_FIELD = "slug"
+ self.assertEqual(self.backend._user_lookup_attribute, "slug")
get_user_model().USERNAME_FIELD = old_username_field
- with override_settings(AUTH_USER_MODEL='auth.User'):
+ with override_settings(AUTH_USER_MODEL="auth.User"):
self.assertEqual(
- DjangoUserModel.USERNAME_FIELD,
- self.backend._user_lookup_attribute)
+ DjangoUserModel.USERNAME_FIELD, self.backend._user_lookup_attribute
+ )
- with override_settings(
- AUTH_USER_MODEL='testprofiles.StandaloneUserModel'):
- self.assertEqual(
- self.backend._user_lookup_attribute,
- 'username')
+ with override_settings(AUTH_USER_MODEL="testprofiles.StandaloneUserModel"):
+ self.assertEqual(self.backend._user_lookup_attribute, "username")
- with override_settings(SAML_DJANGO_USER_MAIN_ATTRIBUTE='foo'):
- self.assertEqual(self.backend._user_lookup_attribute, 'foo')
+ with override_settings(SAML_DJANGO_USER_MAIN_ATTRIBUTE="foo"):
+ self.assertEqual(self.backend._user_lookup_attribute, "foo")
def test_get_or_create_user_existing(self):
- with override_settings(SAML_USER_MODEL='testprofiles.TestUser'):
- user, created = self.backend.get_or_create_user(self.backend._user_lookup_attribute, 'john', False, None, None, None, None)
+ with override_settings(SAML_USER_MODEL="testprofiles.TestUser"):
+ user, created = self.backend.get_or_create_user(
+ self.backend._user_lookup_attribute,
+ "john",
+ False,
+ None,
+ None,
+ None,
+ None,
+ )
self.assertTrue(isinstance(user, TestUser))
self.assertFalse(created)
def test_get_or_create_user_duplicates(self):
- TestUser.objects.create(username='paul')
+ TestUser.objects.create(username="paul")
- with self.assertLogs('djangosaml2', level='DEBUG') as logs:
- with override_settings(SAML_USER_MODEL='testprofiles.TestUser'):
- user, created = self.backend.get_or_create_user('age', '', False, None, None, None, None)
+ with self.assertLogs("djangosaml2", level="DEBUG") as logs:
+ with override_settings(SAML_USER_MODEL="testprofiles.TestUser"):
+ user, created = self.backend.get_or_create_user(
+ "age", "", False, None, None, None, None
+ )
self.assertTrue(user is None)
self.assertFalse(created)
@@ -267,9 +298,17 @@ def test_get_or_create_user_duplicates(self):
)
def test_get_or_create_user_no_create(self):
- with self.assertLogs('djangosaml2', level='DEBUG') as logs:
- with override_settings(SAML_USER_MODEL='testprofiles.TestUser'):
- user, created = self.backend.get_or_create_user(self.backend._user_lookup_attribute, 'paul', False, None, None, None, None)
+ with self.assertLogs("djangosaml2", level="DEBUG") as logs:
+ with override_settings(SAML_USER_MODEL="testprofiles.TestUser"):
+ user, created = self.backend.get_or_create_user(
+ self.backend._user_lookup_attribute,
+ "paul",
+ False,
+ None,
+ None,
+ None,
+ None,
+ )
self.assertTrue(user is None)
self.assertFalse(created)
@@ -279,77 +318,95 @@ def test_get_or_create_user_no_create(self):
)
def test_get_or_create_user_create(self):
- with self.assertLogs('djangosaml2', level='DEBUG') as logs:
- with override_settings(SAML_USER_MODEL='testprofiles.TestUser'):
- user, created = self.backend.get_or_create_user(self.backend._user_lookup_attribute, 'paul', True, None, None, None, None)
+ with self.assertLogs("djangosaml2", level="DEBUG") as logs:
+ with override_settings(SAML_USER_MODEL="testprofiles.TestUser"):
+ user, created = self.backend.get_or_create_user(
+ self.backend._user_lookup_attribute,
+ "paul",
+ True,
+ None,
+ None,
+ None,
+ None,
+ )
self.assertTrue(isinstance(user, TestUser))
self.assertTrue(created)
self.assertIn(
- "DEBUG:djangosaml2:New user created: {}".format(user),
+ f"DEBUG:djangosaml2:New user created: {user}",
logs.output,
)
def test_deprecations(self):
- attribute_mapping = {
- 'mail': ['email'],
- 'mail_verified': ['email_verified']
- }
+ attribute_mapping = {"mail": ["email"], "mail_verified": ["email_verified"]}
attributes = {
- 'mail': ['john@example.org'],
- 'mail_verified': [True],
+ "mail": ["john@example.org"],
+ "mail_verified": [True],
}
- old = self.backend.get_attribute_value('email_verified', attributes, attribute_mapping)
+ old = self.backend.get_attribute_value(
+ "email_verified", attributes, attribute_mapping
+ )
self.assertEqual(old, True)
- self.assertEqual(self.backend.get_django_user_main_attribute(), self.backend._user_lookup_attribute)
+ self.assertEqual(
+ self.backend.get_django_user_main_attribute(),
+ self.backend._user_lookup_attribute,
+ )
- with override_settings(SAML_DJANGO_USER_MAIN_ATTRIBUTE_LOOKUP='user_name'):
- self.assertEqual(self.backend.get_django_user_main_attribute_lookup(), settings.SAML_DJANGO_USER_MAIN_ATTRIBUTE_LOOKUP)
+ with override_settings(SAML_DJANGO_USER_MAIN_ATTRIBUTE_LOOKUP="user_name"):
+ self.assertEqual(
+ self.backend.get_django_user_main_attribute_lookup(),
+ settings.SAML_DJANGO_USER_MAIN_ATTRIBUTE_LOOKUP,
+ )
- self.assertEqual(self.backend.get_user_query_args(''), {'username'})
+ self.assertEqual(self.backend.get_user_query_args(""), {"username"})
- u = TestUser(username='mathieu')
- self.assertEqual(u.email, '')
+ u = TestUser(username="mathieu")
+ self.assertEqual(u.email, "")
new_u = self.backend.configure_user(u, attributes, attribute_mapping)
self.assertIsNotNone(new_u.pk)
- self.assertEqual(new_u.email, 'john@example.org')
+ self.assertEqual(new_u.email, "john@example.org")
- u = TestUser(username='mathieu_2')
- self.assertEqual(u.email, '')
+ u = TestUser(username="mathieu_2")
+ self.assertEqual(u.email, "")
new_u = self.backend.update_user(u, attributes, attribute_mapping)
self.assertIsNotNone(new_u.pk)
- self.assertEqual(new_u.email, 'john@example.org')
+ self.assertEqual(new_u.email, "john@example.org")
u = TestUser()
- self.assertTrue(self.backend._set_attribute(u, 'new_attribute', True))
- self.assertFalse(self.backend._set_attribute(u, 'new_attribute', True))
- self.assertTrue(self.backend._set_attribute(u, 'new_attribute', False))
+ self.assertTrue(self.backend._set_attribute(u, "new_attribute", True))
+ self.assertFalse(self.backend._set_attribute(u, "new_attribute", True))
+ self.assertTrue(self.backend._set_attribute(u, "new_attribute", False))
self.assertEqual(get_saml_user_model(), TestUser)
class CustomizedBackend(Saml2Backend):
- """ Override the available methods with some customized implementation to test customization
- """
- def is_authorized(self, attributes, attribute_mapping, idp_entityid: str, assertion_info, **kwargs):
- ''' Allow only staff users from the IDP '''
- return attributes.get('is_staff', (None, ))[0] == True and assertion_info.get('assertion_id', None) != None
-
+ """Override the available methods with some customized implementation to test customization"""
+
+ def is_authorized(
+ self, attributes, attribute_mapping, idp_entityid: str, assertion_info, **kwargs
+ ):
+ """Allow only staff users from the IDP"""
+ return (
+ attributes.get("is_staff", (None,))[0] == True
+ and assertion_info.get("assertion_id", None) != None
+ )
+
def clean_attributes(self, attributes: dict, idp_entityid: str, **kwargs) -> dict:
- ''' Keep only certain attribute '''
+ """Keep only certain attribute"""
return {
- 'age': attributes.get('age', (None, )),
- 'mail': attributes.get('mail', (None, )),
- 'is_staff': attributes.get('is_staff', (None, )),
- 'uid': attributes.get('uid', (None, )),
+ "age": attributes.get("age", (None,)),
+ "mail": attributes.get("mail", (None,)),
+ "is_staff": attributes.get("is_staff", (None,)),
+ "uid": attributes.get("uid", (None,)),
}
def clean_user_main_attribute(self, main_attribute):
- ''' Partition string on @ and return the first part '''
+ """Partition string on @ and return the first part"""
if main_attribute:
- return main_attribute.partition('@')[0]
+ return main_attribute.partition("@")[0]
return main_attribute
@@ -358,70 +415,82 @@ class CustomizedSaml2BackendTests(Saml2BackendTests):
def test_is_authorized(self):
attribute_mapping = {
- 'uid': ('username', ),
- 'mail': ('email', ),
- 'cn': ('first_name', ),
- 'sn': ('last_name', ),
- }
+ "uid": ("username",),
+ "mail": ("email",),
+ "cn": ("first_name",),
+ "sn": ("last_name",),
+ }
attributes = {
- 'uid': ('john', ),
- 'mail': ('john@example.com', ),
- 'cn': ('John', ),
- 'sn': ('Doe', ),
- }
+ "uid": ("john",),
+ "mail": ("john@example.com",),
+ "cn": ("John",),
+ "sn": ("Doe",),
+ }
assertion_info = {
- 'assertion_id': None,
- 'not_on_or_after': None,
+ "assertion_id": None,
+ "not_on_or_after": None,
}
- self.assertFalse(self.backend.is_authorized(attributes, attribute_mapping, '', assertion_info))
- attributes['is_staff'] = (True, )
- self.assertFalse(self.backend.is_authorized(attributes, attribute_mapping, '', assertion_info))
- assertion_info['assertion_id'] = 'abcdefg12345'
- self.assertTrue(self.backend.is_authorized(attributes, attribute_mapping, '', assertion_info))
+ self.assertFalse(
+ self.backend.is_authorized(
+ attributes, attribute_mapping, "", assertion_info
+ )
+ )
+ attributes["is_staff"] = (True,)
+ self.assertFalse(
+ self.backend.is_authorized(
+ attributes, attribute_mapping, "", assertion_info
+ )
+ )
+ assertion_info["assertion_id"] = "abcdefg12345"
+ self.assertTrue(
+ self.backend.is_authorized(
+ attributes, attribute_mapping, "", assertion_info
+ )
+ )
def test_clean_attributes(self):
- attributes = {'random': 'dummy', 'value': 123, 'age': '28'}
+ attributes = {"random": "dummy", "value": 123, "age": "28"}
self.assertEqual(
- self.backend.clean_attributes(attributes, ''),
- {'age': '28', 'mail': (None,), 'is_staff': (None,), 'uid': (None,)}
+ self.backend.clean_attributes(attributes, ""),
+ {"age": "28", "mail": (None,), "is_staff": (None,), "uid": (None,)},
)
def test_clean_user_main_attribute(self):
- self.assertEqual(self.backend.clean_user_main_attribute('john@example.com'), 'john')
+ self.assertEqual(
+ self.backend.clean_user_main_attribute("john@example.com"), "john"
+ )
def test_authenticate(self):
attribute_mapping = {
- 'uid': ('username', ),
- 'mail': ('email', ),
- 'cn': ('first_name', ),
- 'sn': ('last_name', ),
- 'age': ('age', ),
- 'is_staff': ('is_staff', ),
- }
+ "uid": ("username",),
+ "mail": ("email",),
+ "cn": ("first_name",),
+ "sn": ("last_name",),
+ "age": ("age",),
+ "is_staff": ("is_staff",),
+ }
attributes = {
- 'uid': ('john', ),
- 'mail': ('john@example.com', ),
- 'cn': ('John', ),
- 'sn': ('Doe', ),
- 'age': ('28', ),
- 'is_staff': (True, ),
- }
+ "uid": ("john",),
+ "mail": ("john@example.com",),
+ "cn": ("John",),
+ "sn": ("Doe",),
+ "age": ("28",),
+ "is_staff": (True,),
+ }
assertion_info = {
- 'assertion_id': 'abcdefg12345',
- 'not_on_or_after': '',
+ "assertion_id": "abcdefg12345",
+ "not_on_or_after": "",
}
- self.assertEqual(self.user.age, '')
+ self.assertEqual(self.user.age, "")
self.assertEqual(self.user.is_staff, False)
- user = self.backend.authenticate(
- None
- )
+ user = self.backend.authenticate(None)
self.assertIsNone(user)
user = self.backend.authenticate(
None,
- session_info={'random': 'content'},
+ session_info={"random": "content"},
attribute_mapping=attribute_mapping,
assertion_info=assertion_info,
)
@@ -430,25 +499,25 @@ def test_authenticate(self):
with override_settings(SAML_USE_NAME_ID_AS_USERNAME=True):
user = self.backend.authenticate(
None,
- session_info={'ava': attributes, 'issuer': 'dummy_entity_id'},
+ session_info={"ava": attributes, "issuer": "dummy_entity_id"},
attribute_mapping=attribute_mapping,
assertion_info=assertion_info,
)
self.assertIsNone(user)
- attributes['is_staff'] = (False, )
+ attributes["is_staff"] = (False,)
user = self.backend.authenticate(
None,
- session_info={'ava': attributes, 'issuer': 'dummy_entity_id'},
+ session_info={"ava": attributes, "issuer": "dummy_entity_id"},
attribute_mapping=attribute_mapping,
assertion_info=assertion_info,
)
self.assertIsNone(user)
- attributes['is_staff'] = (True, )
+ attributes["is_staff"] = (True,)
user = self.backend.authenticate(
None,
- session_info={'ava': attributes, 'issuer': 'dummy_entity_id'},
+ session_info={"ava": attributes, "issuer": "dummy_entity_id"},
attribute_mapping=attribute_mapping,
assertion_info=assertion_info,
)
@@ -456,7 +525,7 @@ def test_authenticate(self):
self.assertEqual(user, self.user)
self.user.refresh_from_db()
- self.assertEqual(self.user.age, '28')
+ self.assertEqual(self.user.age, "28")
self.assertEqual(self.user.is_staff, True)
def test_user_cleaned_main_attribute(self):
@@ -466,27 +535,27 @@ def test_user_cleaned_main_attribute(self):
updating the user, the username remains the same.
"""
attribute_mapping = {
- 'mail': ('username',),
- 'cn': ('first_name',),
- 'sn': ('last_name',),
- 'is_staff': ('is_staff', ),
+ "mail": ("username",),
+ "cn": ("first_name",),
+ "sn": ("last_name",),
+ "is_staff": ("is_staff",),
}
attributes = {
- 'mail': ('john@example.com',),
- 'cn': ('John',),
- 'sn': ('Doe',),
- 'is_staff': (True, ),
+ "mail": ("john@example.com",),
+ "cn": ("John",),
+ "sn": ("Doe",),
+ "is_staff": (True,),
}
assertion_info = {
- 'assertion_id': 'abcdefg12345',
+ "assertion_id": "abcdefg12345",
}
user = self.backend.authenticate(
None,
- session_info={'ava': attributes, 'issuer': 'dummy_entity_id'},
+ session_info={"ava": attributes, "issuer": "dummy_entity_id"},
attribute_mapping=attribute_mapping,
assertion_info=assertion_info,
)
self.assertEqual(user, self.user)
self.user.refresh_from_db()
- self.assertEqual(user.username, 'john')
+ self.assertEqual(user.username, "john")
diff --git a/tests/testprofiles/urls.py b/tests/testprofiles/urls.py
index 3cc5838a..0d8ec524 100644
--- a/tests/testprofiles/urls.py
+++ b/tests/testprofiles/urls.py
@@ -1,16 +1,15 @@
+from django.http import HttpResponse
from django.urls import include, path
+
from django.contrib import admin
-from django.http import HttpResponse
testpatterns = (
- [
- path('dashboard/', lambda request: HttpResponse(''), name='dashboard')
- ],
- 'testprofiles' # app_name
+ [path("dashboard/", lambda request: HttpResponse(""), name="dashboard")],
+ "testprofiles", # app_name
)
urlpatterns = [
- path('saml2/', include('djangosaml2.urls')),
- path('admin/', admin.site.urls),
- path('', include(testpatterns))
+ path("saml2/", include("djangosaml2.urls")),
+ path("admin/", admin.site.urls),
+ path("", include(testpatterns)),
]
diff --git a/tox.ini b/tox.ini
index 1c9469a4..a04936ec 100644
--- a/tox.ini
+++ b/tox.ini
@@ -12,7 +12,7 @@ deps =
django3.2: django~=3.2
django4.0: django~=4.0
djangomaster: https://github.com/django/django/archive/master.tar.gz
- .[test]
+ .
ignore_outcome =
djangomaster: True