saml_idp: cleanup, fix XML signing

This commit is contained in:
Jens Langhammer 2018-12-26 21:56:08 +01:00
parent aa7e3c2a15
commit ebda84bcaf
No known key found for this signature in database
GPG Key ID: BEBC05297D92821B
11 changed files with 36 additions and 63 deletions

View File

@ -51,7 +51,7 @@ class Processor:
_saml_response = None
_session_index = None
_subject = None
_subject_format = 'urn:oasis:names:tc:SAML:2.0:nameid-format:email'
_subject_format = 'urn:oasis:names:tc:SAML:2.0:nameid-format:persistent'
_system_params = {
'ISSUER': CONFIG.y('saml_idp.issuer'),
}
@ -84,8 +84,7 @@ class Processor:
'AUTH_INSTANT': get_time_string(),
'ISSUE_INSTANT': get_time_string(),
'NOT_BEFORE': get_time_string(-1 * HOURS), # TODO: Make these settings.
'NOT_ON_OR_AFTER': get_time_string(int(CONFIG.y('saml_idp.assertion_valid_for'))
* MINUTES),
'NOT_ON_OR_AFTER': get_time_string(86400 * MINUTES),
'SESSION_INDEX': self._session_index,
'SESSION_NOT_ON_OR_AFTER': get_time_string(8 * HOURS),
'SP_NAME_QUALIFIER': self._audience,
@ -226,7 +225,7 @@ class Processor:
self._saml_response = sp_config
self._session_index = sp_config
self._subject = sp_config
self._subject_format = 'urn:oasis:names:tc:SAML:2.0:nameid-format:email'
self._subject_format = 'urn:oasis:names:tc:SAML:2.0:nameid-format:persistent'
self._system_params = {
'ISSUER': CONFIG.y('saml_idp.issuer'),
}

View File

@ -40,8 +40,12 @@ class SAMLProvider(Provider):
def link_download_metadata(self):
"""Get link to download XML metadata for admin interface"""
# pylint: disable=no-member
if self.application:
# pylint: disable=no-member
return reverse('passbook_saml_idp:metadata_xml',
kwargs={'provider_id': self.pk})
kwargs={'application': self.application.slug})
return None
class Meta:

View File

@ -5,14 +5,14 @@
<saml:Issuer xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">{{ ISSUER }}</saml:Issuer>
{% include 'saml/xml/signature.xml' %}
{{ SUBJECT_STATEMENT }}
<saml:Conditions NotBefore="2014-07-17T01:01:18Z" NotOnOrAfter="2024-01-18T06:21:48Z">
<saml:Conditions NotBefore="{{ NOT_BEFORE }}" NotOnOrAfter="{{ NOT_ON_OR_AFTER }}">
<saml:AudienceRestriction>
<saml:Audience>{{ AUDIENCE }}</saml:Audience>
</saml:AudienceRestriction>
</saml:Conditions>
<saml:AuthnStatement AuthnInstant="2014-07-17T01:01:48Z" SessionNotOnOrAfter="2024-07-17T09:01:48Z" SessionIndex="_be9967abd904ddcae3c0eb4189adbe3f71e327cf93">
<saml:AuthnStatement AuthnInstant="{{ NOT_BEFORE }}" SessionIndex="{{ ASSERTION_ID }}">
<saml:AuthnContext>
<saml:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:Password</saml:AuthnContextClassRef>
<saml:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport</saml:AuthnContextClassRef>
</saml:AuthnContext>
</saml:AuthnStatement>
{{ ATTRIBUTE_STATEMENT }}

View File

@ -1,7 +1,7 @@
<saml:AttributeStatement>
{% for attr in attributes %}
<saml:Attribute FriendlyName="{{ attr.FriendlyName }}" Name="{{ attr.Name }}" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri">
<saml:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="xs:string">{{ attr.Value }}</saml:AttributeValue>
<saml:Attribute {% if attr.FriendlyName %}FriendlyName="{{ attr.FriendlyName }}" {% endif %}Name="{{ attr.Name }}">
<saml:AttributeValue>{{ attr.Value }}</saml:AttributeValue>
</saml:Attribute>
{% endfor %}
</saml:AttributeStatement>

View File

@ -16,7 +16,7 @@
</ds:KeyInfo>
</md:KeyDescriptor>
<md:SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="{{ slo_url }}"/>
<md:NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:email</md:NameIDFormat>
<md:NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:persistent</md:NameIDFormat>
<md:SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="{{ sso_url }}"/>
</md:IDPSSODescriptor>
{% comment %}

View File

@ -1,4 +1,5 @@
<samlp:Response xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"
xmlns:ds="http://www.w3.org/2000/09/xmldsig#"
Destination="{{ ACS_URL }}"
ID="{{ RESPONSE_ID }}"
{{ IN_RESPONSE_TO|safe }}

View File

@ -1,5 +1,5 @@
<saml:Subject>
<saml:NameID Format="{{ SUBJECT_FORMAT }}" SPNameQualifier="{{ SP_NAME_QUALIFIER }}">
<saml:NameID Format="{{ SUBJECT_FORMAT }}">
{{ SUBJECT }}
</saml:NameID>
<saml:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer">

View File

@ -6,8 +6,8 @@ from passbook.saml_idp import views
urlpatterns = [
path('login/<slug:application>/',
views.LoginBeginView.as_view(), name="saml_login_begin"),
path('login/<slug:application>/idp_init/',
views.LoginInitView.as_view(), name="saml_login_init"),
path('login/<slug:application>/initiate/',
views.InitiateLoginView.as_view(), name="saml_login_init"),
path('login/<slug:application>/process/',
views.LoginProcessView.as_view(), name='saml_login_process'),
path('logout/', views.LogoutView.as_view(), name="saml_logout"),

View File

@ -1,7 +1,6 @@
"""passbook SAML IDP Views"""
from logging import getLogger
from django.conf import settings
from django.contrib.auth import logout
from django.contrib.auth.mixins import LoginRequiredMixin
from django.core.exceptions import ValidationError
@ -10,14 +9,6 @@ from django.http import HttpResponse, HttpResponseBadRequest
from django.shortcuts import get_object_or_404, redirect, render, reverse
from django.utils.datastructures import MultiValueDictKeyError
from django.views import View
from saml2 import BINDING_HTTP_POST
from saml2.authn_context import PASSWORD, AuthnBroker, authn_context_class_ref
from saml2.config import IdPConfig
from saml2.ident import NameID
from saml2.metadata import entity_descriptor
from saml2.s_utils import UnknownPrincipal, UnsupportedBinding
from saml2.saml import NAMEID_FORMAT_EMAILADDRESS, NAMEID_FORMAT_UNSPECIFIED
from saml2.server import Server
from signxml.util import strip_pem_header
from passbook.core.models import Application
@ -50,11 +41,13 @@ def render_xml(request, template, ctx):
class ProviderMixin:
"""Mixin class for Views using a provider instance"""
_provider = None
@property
def provider(self):
"""Get provider instance"""
if not self._provider:
application = get_object_or_404(Application, slug=self.kwargs['application'])
self._provider = get_object_or_404(SAMLProvider, pk=application.provider_id)
@ -123,7 +116,7 @@ class LoginProcessView(ProviderMixin, View):
saml_response=request.POST.get('SAMLResponse'),
relay_state=request.POST.get('RelayState'))
try:
full_res = _generate_response(request, provider)
full_res = _generate_response(request, self.provider)
return full_res
except exceptions.CannotHandleAssertion as exc:
LOGGER.debug(exc)
@ -166,33 +159,17 @@ class SLOLogout(CSRFExemptMixin, LoginRequiredMixin, View):
logout(request)
return render(request, 'saml/idp/logged_out.html')
class IdPMixin(ProviderMixin):
provider = None
def dispatch(self, request, application):
def get_identity(self, provider, user):
""" Create Identity dict (using SP-specific mapping)
"""
sp_mapping = {'username': 'username'}
# return provider.processor.create_identity(user, sp_mapping)
return {
out_attr: getattr(user, user_attr)
for user_attr, out_attr in sp_mapping.items()
if hasattr(user, user_attr)
}
class DescriptorDownloadView(ProviderMixin, View):
"""Replies with the XML Metadata IDSSODescriptor."""
def get(self, request, application):
"""Replies with the XML Metadata IDSSODescriptor."""
super().dispatch(request, application)
entity_id = CONFIG.y('saml_idp.issuer')
slo_url = request.build_absolute_uri(reverse('passbook_saml_idp:saml_logout'))
sso_url = request.build_absolute_uri(reverse('passbook_saml_idp:saml_login_begin'))
sso_url = request.build_absolute_uri(reverse('passbook_saml_idp:saml_login_begin', kwargs={
'application': application
}))
pubkey = strip_pem_header(self.provider.signing_cert.replace('\r', '')).replace('\n', '')
ctx = {
'entity_id': entity_id,
@ -207,19 +184,11 @@ class DescriptorDownloadView(ProviderMixin, View):
return response
class LoginInitView(IdPMixin, LoginRequiredMixin, View):
class InitiateLoginView(ProviderMixin, LoginRequiredMixin, View):
"""IdP-initiated Login"""
def dispatch(self, request, application):
"""Initiates an IdP-initiated link to a simple SP resource/target URL."""
super().dispatch(request, application)
# # linkdict = dict(metadata.get_links(sp_config))
# # pattern = linkdict[resource]
# # is_simple_link = ('/' not in resource)
# # if is_simple_link:
# # simple_target = kwargs['target']
# # url = pattern % simple_target
# # else:
# # url = pattern % kwargs
# provider.processor.init_deep_link(request, 'deep url')
# return _generate_response(request, provider)
self.provider.processor.init_deep_link(request, '')
return _generate_response(request, self.provider)

View File

@ -83,5 +83,5 @@ def get_response_xml(parameters, saml_provider: 'SAMLProvider', assertion_id='')
signed = sign_with_signxml(
saml_provider.signing_key, raw_response, saml_provider.signing_cert,
reference_uri=assertion_id).decode("utf-8")
reference_uri=assertion_id)
return signed

View File

@ -3,9 +3,8 @@ from logging import getLogger
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import serialization
from defusedxml import ElementTree
from lxml import etree # nosec
from signxml import XMLSigner
from signxml import XMLSigner, XMLVerifier
from passbook.lib.utils.template import render_to_string
@ -17,12 +16,13 @@ def sign_with_signxml(private_key, data, cert, reference_uri=None):
key = serialization.load_pem_private_key(
str.encode('\n'.join([x.strip() for x in private_key.split('\n')])),
password=None, backend=default_backend())
# LXML is used here because defusedxml causes issues with serialization
# data is trusted so no issues
# defused XML is not used here because it messes up XML namespaces
# Data is trusted, so lxml is ok
root = etree.fromstring(data) # nosec
signer = XMLSigner(c14n_algorithm='http://www.w3.org/2001/10/xml-exc-c14n#')
signed = signer.sign(root, key=key, cert=cert, reference_uri=reference_uri)
return ElementTree.tostring(signed)
signed = signer.sign(root, key=key, cert=[cert], reference_uri=reference_uri)
XMLVerifier().verify(signed, x509_cert=cert)
return etree.tostring(signed).decode('utf-8') # nosec
def get_signature_xml():