diff --git a/.gitignore b/.gitignore
index f5552467..f1fadf08 100644
--- a/.gitignore
+++ b/.gitignore
@@ -46,4 +46,5 @@ idp.xml
# Custom IDE files
.vscode/
-
+.pre-commit-config.yaml
+.flake8
diff --git a/bin/setup-ckan.bash b/bin/setup-ckan.bash
index 4c45aec5..3ace4257 100755
--- a/bin/setup-ckan.bash
+++ b/bin/setup-ckan.bash
@@ -5,6 +5,7 @@ echo "This is setup-ckan.bash..."
echo "Installing the packages that CKAN requires..."
sudo apt-get update -qq
+sudo apt-get install xmlsec1 libxmlsec1-dev
echo "Installing CKAN and its Python dependencies..."
git clone https://github.com/ckan/ckan
diff --git a/ckanext/saml2auth/tests/extras/provider0/idp.xml b/ckanext/saml2auth/tests/extras/provider0/idp.xml
new file mode 100644
index 00000000..6faea392
--- /dev/null
+++ b/ckanext/saml2auth/tests/extras/provider0/idp.xml
@@ -0,0 +1,89 @@
+
+
+
+
+ example.com
+
+
+
+ Consortium Company Test IdP
+
+
+ Consortium Company Test IdP
+
+
+
+ This Identity Provider gives support for the Consortium Company Test's user community
+
+
+ Questo Identity Provider di test fornisce supporto alla comunita' utenti Company Test
+
+
+
+
+
+
+
+
+ MIIDZjCCAk6gAwIBAgIVAL9O+PA7SXtlwZZY8MVSE9On1cVWMA0GCSqGSIb3DQEB
+ BQUAMCkxJzAlBgNVBAMTHmlkZW0tcHVwYWdlbnQuZG16LWludC51bmltby5pdDAe
+ Fw0xMzA3MjQwMDQ0MTRaFw0zMzA3MjQwMDQ0MTRaMCkxJzAlBgNVBAMTHmlkZW0t
+ cHVwYWdlbnQuZG16LWludC51bmltby5pdDCCASIwDQYJKoZIhvcNAMIIDQADggEP
+ ADCCAQoCggEBAIAcp/VyzZGXUF99kwj4NvL/Rwv4YvBgLWzpCuoxqHZ/hmBwJtqS
+ v0y9METBPFbgsF3hCISnxbcmNVxf/D0MoeKtw1YPbsUmow/bFe+r72hZ+IVAcejN
+ iDJ7t5oTjsRN1t1SqvVVk6Ryk5AZhpFW+W9pE9N6c7kJ16Rp2/mbtax9OCzxpece
+ byi1eiLfIBmkcRawL/vCc2v6VLI18i6HsNVO3l2yGosKCbuSoGDx2fCdAOk/rgdz
+ cWOvFsIZSKuD+FVbSS/J9GVs7yotsS4PRl4iX9UMnfDnOMfO7bcBgbXtDl4SCU1v
+ dJrRw7IL/pLz34Rv9a8nYitrzrxtLOp3nYUCAwEAAaOBhDCBgTBgBgMIIDEEWTBX
+ gh5pZGVtLXB1cGFnZW50LmRtei1pbnQudW5pbW8uaXSGNWh0dHBzOi8vaWRlbS1w
+ dXBhZ2VudC5kbXotaW50LnVuaW1vLml0L2lkcC9zaGliYm9sZXRoMB0GA1UdDgQW
+ BBT8PANzz+adGnTRe8ldcyxAwe4VnzANBgkqhkiG9w0BAQUFAAOCAQEAOEnO8Clu
+ 9z/Lf/8XOOsTdxJbV29DIF3G8KoQsB3dBsLwPZVEAQIP6ceS32Xaxrl6FMTDDNkL
+ qUvvInUisw0+I5zZwYHybJQCletUWTnz58SC4C9G7FpuXHFZnOGtRcgGD1NOX4UU
+ duus/4nVcGSLhDjszZ70Xtj0gw2Sn46oQPHTJ81QZ3Y9ih+Aj1c9OtUSBwtWZFkU
+ yooAKoR8li68Yb21zN2N65AqV+ndL98M8xUYMKLONuAXStDeoVCipH6PJ09Z5U2p
+ V5p4IQRV6QBsNw9CISJFuHzkVYTH5ZxzN80Ru46vh4y2M0Nu8GQ9I085KoZkrf5e
+ Cq53OZt9ISjHEw==
+
+
+
+
+
+
+
+
+
+
+ Consortium Company Test
+
+
+ Consortium Company Test
+
+
+
+ Consortium Company Test
+
+
+ Consortium Company Test
+
+
+
+ http://www.company-test.com
+
+
+ https://www.company-test.com.ar
+
+
+
+
+ mailto:technical.contact@example.com
+
+
+
\ No newline at end of file
diff --git a/ckanext/saml2auth/tests/extras/provider1/idp_cert_template.xml b/ckanext/saml2auth/tests/extras/provider1/idp_cert_template.xml
new file mode 100644
index 00000000..83d14fd2
--- /dev/null
+++ b/ckanext/saml2auth/tests/extras/provider1/idp_cert_template.xml
@@ -0,0 +1,45 @@
+
+
+
+
+
+
+ {{ certificate }}
+
+
+
+ urn:oasis:names:tc:SAML:2.0:nameid-format:persistent
+ urn:oasis:names:tc:SAML:2.0:nameid-format:emailAddress
+
+
+
+
+
+
+
+ {{ certificate }}
+
+
+
+
+ {{ org_name }}
+ {{ org_name }}
+ {{ org_url }}
+
+
+
+ urn:oasis:names:tc:SAML:2.0:nameid-format:persistent
+ urn:oasis:names:tc:SAML:2.0:nameid-format:emailAddress
+
+
+ {{ org_name }}
+ {{ org_name }}
+ {{ org_url }}
+
+
+
diff --git a/ckanext/saml2auth/tests/responses/test-signed-encrypted-example.xml b/ckanext/saml2auth/tests/responses/test-signed-encrypted-example.xml
new file mode 100644
index 00000000..a96a2f3c
--- /dev/null
+++ b/ckanext/saml2auth/tests/responses/test-signed-encrypted-example.xml
@@ -0,0 +1,149 @@
+
+
+ urn:gov:gsa:SAML:2.0.profiles:sp:sso:test:entity
+
+
+
+
+
+
+
+
+
+
+ 4M6bvWvJ12UY+/g3eUq8ZoLk5LGJjcqWVFLfenx5/dY=
+
+
+ lb+Bt293tx82qQLCJA9Gxjn0VQmUeqHdQExyVeq/w/8JNE5myib2pkUqfGlv1WX7 YSqNb6C8Tgkh/ZG2c3tmOgvL026JKtYX8LOBD7CVzLzCF4lhMBUGvPHooq9pO5k8 A6Eiv2dpPvq+UG4Ah0dMNleLf2+JrEoqR+/MfK9isfk=
+
+
+ MIICQTCCAaoCAQEwDQYJKoZIhvcNAQELBQAwaTELMAkGA1UEBhMCc2UxCzAJBgNVBAgMAmFjMQ0wCwYDVQQHDAR1bWVhMRgwFgYDVQQKDA9UZXN0IFVuaXZlcnNpdHkxDTALBgNVBAsMBERlY2ExFTATBgNVBAMMDGxvY2FsaG9zdC5jYTAeFw0yMTAxMTUxOTIwMjNaFw0yMTAxMTUxOTIwMjRaMGkxCzAJBgNVBAYTAnNlMQswCQYDVQQIDAJhYzENMAsGA1UEBwwEdW1lYTEYMBYGA1UECgwPVGVzdCBVbml2ZXJzaXR5MQ0wCwYDVQQLDAREZWNhMRUwEwYDVQQDDAxsb2NhbGhvc3QuY2EwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAKvHgRbUwLtmgkwUtGit4NXPMpXtoCLQJaK8uDKhsGg9UC8ShSK67ci5tzOP/DbQ3W6S4vLpOG7zl1cadhsGHdGYCHkl/Z1L6a1rLhIJHoGNyYZpPlASfugZ5Jbl3BSc9d/Ht1zeO/SBBraAiXNwR36B9fdH5L4vJ7Yd/+ZsQPPxAgMBAAEwDQYJKoZIhvcNAQELBQADgYEAn8dJxfsRbtFk6LXcn6i5OKZwfKVQHeyMUsUTwTjevqyCO7kK7xV3geJSCb5hWnHYOls4bj+NKpEEsay+cekJL7dgDe8HMB1Pk22/X8Vg8tRhgZAJKG+3yySiCSGDqgq/0PkmAWVZghjjUGHlan16roUue2T9Y7UbyzIE1bo9TVM=
+
+
+
+
+
+
+
+
+
+
+
+
+
+ my-rsa-key
+
+
+ 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
+Q7r6v8WvuEjpBYdtVzzMZBKVmJj8zEIm
+
+
+
+
diff --git a/ckanext/saml2auth/tests/responses/test-signed-example.xml b/ckanext/saml2auth/tests/responses/test-signed-example.xml
new file mode 100644
index 00000000..59b3b1eb
--- /dev/null
+++ b/ckanext/saml2auth/tests/responses/test-signed-example.xml
@@ -0,0 +1,88 @@
+
+
+ urn:gov:gsa:SAML:2.0.profiles:sp:sso:test:entity
+
+
+
+
+
+
+
+
+
+
+ 61ljft1X0eYnJmhlF2zFE7Guwxfn56mZQkKz6wRMLxg=
+
+
+ FeIuwIrarxsQI9g4ipHmbDcBG8RgzsTkZUzPA5zIarfi269z0AwJnrEROElsd2Z6 JoZRsh8jGWPHMkWNvmZl/QdQ0ha2l8ibCYPDOs/l5lMorXUi/SA7fwB7r4IF0w83 7NBXRF8RYK/5pRq8nr0k8kjc3pRLpVNxsdCLtFyE3WU=
+
+
+ MIICQTCCAaoCAQEwDQYJKoZIhvcNAQELBQAwaTELMAkGA1UEBhMCc2UxCzAJBgNVBAgMAmFjMQ0wCwYDVQQHDAR1bWVhMRgwFgYDVQQKDA9UZXN0IFVuaXZlcnNpdHkxDTALBgNVBAsMBERlY2ExFTATBgNVBAMMDGxvY2FsaG9zdC5jYTAeFw0yMTAxMTUxOTI0MDlaFw0yMTAxMTUxOTI0MTBaMGkxCzAJBgNVBAYTAnNlMQswCQYDVQQIDAJhYzENMAsGA1UEBwwEdW1lYTEYMBYGA1UECgwPVGVzdCBVbml2ZXJzaXR5MQ0wCwYDVQQLDAREZWNhMRUwEwYDVQQDDAxsb2NhbGhvc3QuY2EwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAJhQJRLBZHIqdd410PIpZ6FR52+Qwp6HPN+PTGw2z0+QAAA4prKgO36DiJLdnooBxYKGjGU3SMQ+KmWoTW+5ryd31Nv0q1H0QhtIig7FEGJEHsm+Qwvy879deF8JbmwapO7Fso9xHQPdmb/MY9QSsm6qAQKsB28HXH1OEsdLgwi3AgMBAAEwDQYJKoZIhvcNAQELBQADgYEABPb1Lnn5/B7EvXX1svM+MukIA37h0zGFUvKBugnNeWla1YE2ktlX0ZHo72rHefjzCD7QxCOTVur6+DhkcmHf8BEWbdaWjqQOviO41PKR8t1fqbg6LbpxxCsGqHtzV6wllj8lEKoU5I9n0DTG8YPQi7I+e1EFdpi9WuRgbk28KFc=
+
+
+
+
+
+
+
+
diff --git a/ckanext/saml2auth/tests/responses/test-simple-example.xml b/ckanext/saml2auth/tests/responses/test-simple-example.xml
new file mode 100644
index 00000000..9799c073
--- /dev/null
+++ b/ckanext/saml2auth/tests/responses/test-simple-example.xml
@@ -0,0 +1,31 @@
+
+ urn:gov:gsa:SAML:2.0.profiles:sp:sso:test:entity
+
+
+
+
+ https://organization.com/saml
+
+ 44444444-4444-4444-4444-444444444444
+
+
+
+
+
+
+ urn:gov:gsa:SAML:2.0.profiles:sp:sso:test:entity
+
+
+
+
+ urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport
+
+
+
+
+ example@email.com
+
+
+
+
\ No newline at end of file
diff --git a/ckanext/saml2auth/tests/responses/unsigned0.xml b/ckanext/saml2auth/tests/responses/unsigned0.xml
new file mode 100644
index 00000000..4192560f
--- /dev/null
+++ b/ckanext/saml2auth/tests/responses/unsigned0.xml
@@ -0,0 +1,48 @@
+
+
+ http://idp.example.com/metadata.php
+
+
+
+
+ http://idp.example.com/metadata.php
+
+ _ce3d2948b4cf20146dee0a0b3dd6f69b6cf86f62d7
+
+
+
+
+
+
+ {{ entity_id }}
+
+
+
+
+ urn:oasis:names:tc:SAML:2.0:ac:classes:Password
+
+
+
+
+ test
+
+
+ test@example.com
+
+
+ users
+ examplerole1
+
+
+
+
\ No newline at end of file
diff --git a/ckanext/saml2auth/tests/test_blueprint_get_request.py b/ckanext/saml2auth/tests/test_blueprint_get_request.py
new file mode 100644
index 00000000..d7987ac8
--- /dev/null
+++ b/ckanext/saml2auth/tests/test_blueprint_get_request.py
@@ -0,0 +1,340 @@
+# encoding: utf-8
+import base64
+from datetime import datetime
+from jinja2 import Template
+from nose.tools import assert_equal, assert_in
+import os
+import pytest
+
+from saml2.xmldsig import SIG_RSA_SHA256
+from saml2.xmldsig import DIGEST_SHA256
+from saml2.saml import NAMEID_FORMAT_ENTITY
+from saml2.saml import Issuer
+from saml2.server import Server
+from saml2.authn_context import INTERNETPROTOCOLPASSWORD
+
+
+here = os.path.dirname(os.path.abspath(__file__))
+extras_folder = os.path.join(here, 'extras')
+responses_folder = os.path.join(here, 'responses')
+
+
+@pytest.mark.usefixtures(u'clean_db', u'clean_index')
+@pytest.mark.ckan_config(u'ckan.plugins', u'saml2auth')
+class TestGetRequest:
+ """ test getting request from external source """
+
+ @pytest.mark.ckan_config(u'ckanext.saml2auth.idp_metadata.location', u'local')
+ @pytest.mark.ckan_config(u'ckanext.saml2auth.idp_metadata.local_path', os.path.join(extras_folder, 'provider0', 'idp.xml'))
+ @pytest.mark.ckan_config(u'ckanext.saml2auth.want_response_signed', u'False')
+ @pytest.mark.ckan_config(u'ckanext.saml2auth.want_assertions_signed', u'False')
+ @pytest.mark.ckan_config(u'ckanext.saml2auth.want_assertions_or_response_signed', u'False')
+ def test_empty_request(self, app):
+
+ url = '/acs'
+ data = {
+ 'SAMLResponse': ''
+ }
+ response = app.post(url=url, status=400, params=data)
+ assert_equal(400, response.status_code)
+ assert_in(u'Empty login request', response)
+
+ @pytest.mark.ckan_config(u'ckanext.saml2auth.idp_metadata.location', u'local')
+ @pytest.mark.ckan_config(u'ckanext.saml2auth.idp_metadata.local_path', os.path.join(extras_folder, 'provider0', 'idp.xml'))
+ @pytest.mark.ckan_config(u'ckanext.saml2auth.want_response_signed', u'False')
+ @pytest.mark.ckan_config(u'ckanext.saml2auth.want_assertions_signed', u'False')
+ @pytest.mark.ckan_config(u'ckanext.saml2auth.want_assertions_or_response_signed', u'False')
+ def test_bad_request(self, app):
+
+ url = '/acs'
+ data = {
+ 'SAMLResponse': ''
+ }
+ response = app.post(url=url, status=400, params=data)
+ assert_equal(400, response.status_code)
+ assert_in(u'Bad login request', response)
+
+ def _b4_encode_string(self, message):
+ message_bytes = message.encode('ascii')
+ base64_bytes = base64.b64encode(message_bytes)
+ return base64_bytes.decode('ascii')
+
+ @pytest.mark.ckan_config(u'ckanext.saml2auth.entity_id', u'urn:gov:gsa:SAML:2.0.profiles:sp:sso:test:entity')
+ @pytest.mark.ckan_config(u'ckanext.saml2auth.idp_metadata.location', u'local')
+ @pytest.mark.ckan_config(u'ckanext.saml2auth.idp_metadata.local_path', os.path.join(extras_folder, 'provider0', 'idp.xml'))
+ @pytest.mark.ckan_config(u'ckanext.saml2auth.want_response_signed', u'False')
+ @pytest.mark.ckan_config(u'ckanext.saml2auth.want_assertions_signed', u'False')
+ @pytest.mark.ckan_config(u'ckanext.saml2auth.want_assertions_or_response_signed', u'False')
+ def test_unsigned_request(self, app):
+
+ # read about saml2 responses: https://www.samltool.com/generic_sso_res.php
+ unsigned_response_file = os.path.join(responses_folder, 'unsigned0.xml')
+ unsigned_response = open(unsigned_response_file).read()
+ # parse values
+ context = {
+ 'entity_id': 'urn:gov:gsa:SAML:2.0.profiles:sp:sso:test:entity',
+ 'destination': 'http://test.ckan.net/acs',
+ 'recipient': 'http://test.ckan.net/acs',
+ 'issue_instant': datetime.now().isoformat()
+ }
+ t = Template(unsigned_response)
+ final_response = t.render(**context)
+
+ encoded_response = self._b4_encode_string(final_response)
+ url = '/acs'
+
+ data = {
+ 'SAMLResponse': encoded_response
+ }
+ response = app.post(url=url, params=data)
+ assert_equal(200, response.status_code)
+
+ def render_file(self, path, context, save_as=None):
+ """ open file and render contect values """
+ txt = open(path).read()
+ t = Template(txt)
+ response = t.render(**context)
+
+ if save_as is not None:
+ f = open(save_as, 'w')
+ f.write(response)
+ f.close()
+
+ return response
+
+ def _load_base(
+ self,
+ destination='http://test.ckan.net/acs',
+ issuer_url='https://organization.com/saml/',
+ entity_id='urn:gov:gsa:SAML:2.0.profiles:sp:sso:test:entity'
+ ):
+
+ cert_file = os.path.join(extras_folder, 'provider1', 'mycert.pem')
+ f = open(cert_file)
+ cert = f.read()
+ f.close()
+
+ x509_cert = cert.replace('\x0D', '')
+ x509_cert = x509_cert.replace('\r', '')
+ x509_cert = x509_cert.replace('\n', '')
+ x509_cert = x509_cert.replace('-----BEGIN CERTIFICATE-----', '')
+ x509_cert = x509_cert.replace('-----END CERTIFICATE-----', '')
+ x509_cert = x509_cert.replace(' ', '')
+
+ self.context = {
+ 'entity_id': entity_id,
+ 'entity_session_id': '_session_ID_44444',
+ 'issuer_url': issuer_url,
+ 'destination': destination,
+ 'org_name': 'IDP Organization',
+ 'org_url': 'https://idp.organization.com',
+ 'redirect_login_url': 'https://idp.organization.com/auth',
+ 'attributes_url': 'https://idp.organization.com/attributes',
+ 'certificate': x509_cert
+ }
+
+ self.render_file(
+ path=os.path.join(extras_folder, 'provider1', 'idp_cert_template.xml'),
+ context=self.context,
+ save_as=os.path.join(extras_folder, 'provider1', 'idp.xml')
+ )
+
+ key_file = os.path.join(extras_folder, 'provider1', 'mykey.pem')
+ cert_file = os.path.join(extras_folder, 'provider1', 'mycert.pem')
+ self.config = {
+ 'description': 'CKAN saml2 Service Provider',
+ 'service': {
+ 'sp': {
+ 'name_id_format': [
+ 'urn:oasis:names:tc:SAML:2.0:nameid-format:persistent',
+ 'urn:oasis:names:tc:SAML:2.0:nameid-format:transient',
+ 'urn:oasis:names:tc:SAML:2.0:nameid-format:emailAddress'
+ ],
+ 'want_response_signed': False,
+ 'name': 'CKAN SP',
+ 'want_assertions_signed': True,
+ 'allow_unsolicited': True,
+ 'endpoints': {
+ 'assertion_consumer_service': ['http://ckan:5000/acs', 'http://test.ckan.net/acs']
+ },
+ 'want_assertions_or_response_signed': True,
+ 'name_id_policy_format': [
+ 'urn:oasis:names:tc:SAML:2.0:nameid-format:persistent',
+ 'urn:oasis:names:tc:SAML:2.0:nameid-format:transient',
+ 'urn:oasis:names:tc:SAML:2.0:nameid-format:emailAddress'
+ ]
+ }
+ },
+ 'name_form': 'urn:oasis:names:tc:SAML:2.0:attrname-format:uri',
+ 'debug': 0,
+ 'entityid': entity_id,
+ 'allow_unknown_attributes': 'true',
+ 'metadata': {
+ 'local': [os.path.join(extras_folder, 'provider1', 'idp.xml')]
+ },
+ 'key_file': key_file,
+ 'cert_file': cert_file,
+ 'encryption_keypairs': [
+ {'key_file': key_file, 'cert_file': cert_file}
+ ]
+ }
+
+ def _generate_cert(self):
+ from saml2.cert import OpenSSLWrapper
+
+ cert_info_ca = {
+ "cn": "localhost.ca",
+ "country_code": "se",
+ "state": "ac",
+ "city": "umea",
+ "organization": "Test University",
+ "organization_unit": "Deca"
+ }
+
+ osw = OpenSSLWrapper()
+ ca_cert, ca_key = osw.create_certificate(
+ cert_info_ca,
+ request=False,
+ write_to_file=False
+ )
+
+ cert_str, key_str = osw.create_certificate(cert_info_ca, request=True)
+ re_cert_str = osw.create_cert_signed_certificate(
+ ca_cert,
+ ca_key,
+ cert_str,
+ valid_from=0,
+ valid_to=1
+ )
+
+ f = open(os.path.join(extras_folder, 'provider1', 'mycert.pem'), 'w')
+ f.write(re_cert_str)
+ f.close()
+
+ f = open(os.path.join(extras_folder, 'provider1', 'mykey.pem'), 'wb')
+ f.write(key_str)
+ f.close()
+
+ self.key_str = key_str
+ self.cert_str = re_cert_str
+
+ @pytest.mark.ckan_config(u'ckanext.saml2auth.entity_id', u'urn:gov:gsa:SAML:2.0.profiles:sp:sso:test:entity')
+ @pytest.mark.ckan_config(u'ckanext.saml2auth.idp_metadata.location', u'local')
+ @pytest.mark.ckan_config(u'ckanext.saml2auth.idp_metadata.local_path', os.path.join(extras_folder, 'provider1', 'idp.xml'))
+ @pytest.mark.ckan_config(u'ckanext.saml2auth.want_response_signed', u'False')
+ @pytest.mark.ckan_config(u'ckanext.saml2auth.want_assertions_signed', u'False')
+ @pytest.mark.ckan_config(u'ckanext.saml2auth.want_assertions_or_response_signed', u'True')
+ @pytest.mark.ckan_config(u'ckanext.saml2auth.key_file_path', os.path.join(extras_folder, 'provider1', 'mykey.pem'))
+ @pytest.mark.ckan_config(u'ckanext.saml2auth.cert_file_path', os.path.join(extras_folder, 'provider1', 'mycert.pem'))
+ def test_encrypted_assertion(self, app):
+
+ self._generate_cert()
+ self._load_base()
+
+ # define the user identity
+ IDENTITY = {
+ "eduPersonAffiliation": ["staff", "member"],
+ "surName": ["Jeter"], "givenName": ["Derek"],
+ "email": ["foo@gmail.com"],
+ "title": ["shortstop"]
+ }
+
+ # start a server to generate the expected response
+ server = Server(self.config)
+ name_id = server.ident.transient_nameid(self.context['entity_id'], "id12")
+ issuer = Issuer(text=self.context['entity_id'], format=NAMEID_FORMAT_ENTITY)
+ authn = {
+ "class_ref": INTERNETPROTOCOLPASSWORD,
+ "authn_auth": "http://www.example.com/login"
+ }
+ response = server.create_authn_response(
+ identity=IDENTITY,
+ in_response_to="id12",
+ destination=self.context['destination'],
+ sp_entity_id=self.context['entity_id'],
+ name_id=name_id,
+ sign_assertion=True,
+ sign_response=True,
+ issuer=issuer,
+ sign_alg=SIG_RSA_SHA256,
+ digest_alg=DIGEST_SHA256,
+ encrypt_assertion=True,
+ encrypt_cert_assertion=self.cert_str,
+ encrypt_assertion_self_contained=True,
+ authn=authn
+ )
+
+ # finishe the response and b64 encode to send to our /acs endpoint
+ final_signed_response = response # .to_string()
+
+ # To check the response
+ f = open(os.path.join(extras_folder, 'provider1', 'test-signed-encrypted.xml'), 'w')
+ f.write(final_signed_response)
+ f.close()
+ encoded_response = self._b4_encode_string(final_signed_response)
+ url = '/acs'
+
+ data = {
+ 'SAMLResponse': encoded_response
+ }
+ response = app.post(url=url, params=data)
+ assert_equal(200, response.status_code)
+
+ @pytest.mark.ckan_config(u'ckanext.saml2auth.entity_id', u'urn:gov:gsa:SAML:2.0.profiles:sp:sso:test:entity')
+ @pytest.mark.ckan_config(u'ckanext.saml2auth.idp_metadata.location', u'local')
+ @pytest.mark.ckan_config(u'ckanext.saml2auth.idp_metadata.local_path', os.path.join(extras_folder, 'provider1', 'idp.xml'))
+ @pytest.mark.ckan_config(u'ckanext.saml2auth.want_response_signed', u'False')
+ @pytest.mark.ckan_config(u'ckanext.saml2auth.want_assertions_signed', u'False')
+ @pytest.mark.ckan_config(u'ckanext.saml2auth.want_assertions_or_response_signed', u'True')
+ @pytest.mark.ckan_config(u'ckanext.saml2auth.key_file_path', os.path.join(extras_folder, 'provider1', 'mykey.pem'))
+ @pytest.mark.ckan_config(u'ckanext.saml2auth.cert_file_path', os.path.join(extras_folder, 'provider1', 'mycert.pem'))
+ def test_signed_not_encrypted_assertion(self, app):
+
+ self._generate_cert()
+ self._load_base()
+
+ # define the user identity
+ IDENTITY = {
+ "eduPersonAffiliation": ["staff", "member"],
+ "surName": ["Jeter"], "givenName": ["Derek"],
+ "email": ["foo@gmail.com"],
+ "title": ["shortstop"]
+ }
+
+ # start a server to generate the expected response
+ server = Server(self.config)
+ name_id = server.ident.transient_nameid(self.context['entity_id'], "id12")
+ issuer = Issuer(text=self.context['entity_id'], format=NAMEID_FORMAT_ENTITY)
+ authn = {
+ "class_ref": INTERNETPROTOCOLPASSWORD,
+ "authn_auth": "http://www.example.com/login"
+ }
+ response = server.create_authn_response(
+ identity=IDENTITY,
+ in_response_to="id12",
+ destination=self.context['destination'],
+ sp_entity_id=self.context['entity_id'],
+ name_id=name_id,
+ sign_assertion=True,
+ sign_response=True,
+ issuer=issuer,
+ sign_alg=SIG_RSA_SHA256,
+ digest_alg=DIGEST_SHA256,
+ authn=authn
+ )
+
+ # finishe the response and b64 encode to send to our /acs endpoint
+ final_signed_response = response # .to_string()
+
+ # To check the response
+ f = open(os.path.join(extras_folder, 'provider1', 'test-signed.xml'), 'w')
+ f.write(final_signed_response)
+ f.close()
+ encoded_response = self._b4_encode_string(final_signed_response)
+ url = '/acs'
+
+ data = {
+ 'SAMLResponse': encoded_response
+ }
+ response = app.post(url=url, params=data)
+ assert_equal(200, response.status_code)
diff --git a/ckanext/saml2auth/views/saml2auth.py b/ckanext/saml2auth/views/saml2auth.py
index 1ea3a2eb..def90d05 100644
--- a/ckanext/saml2auth/views/saml2auth.py
+++ b/ckanext/saml2auth/views/saml2auth.py
@@ -1,4 +1,5 @@
# encoding: utf-8
+import logging
from flask import Blueprint
from saml2 import entity
@@ -15,15 +16,12 @@
from ckanext.saml2auth import helpers as h
+log = logging.getLogger(__name__)
saml2auth = Blueprint(u'saml2auth', __name__)
-def acs():
- u'''The location where the SAML assertion is sent with a HTTP POST.
- This is often referred to as the SAML Assertion Consumer Service (ACS) URL.
- '''
- g.user = None
- g.userobj = None
+def process_user(email, saml_id, firstname, lastname):
+ """ Check if CKAN-SAML user exists for the current SAML login """
context = {
u'ignore_auth': True,
@@ -31,76 +29,12 @@ def acs():
u'model': model
}
- saml_user_firstname = \
- config.get(u'ckanext.saml2auth.user_firstname')
- saml_user_lastname = \
- config.get(u'ckanext.saml2auth.user_lastname')
- saml_user_email = \
- config.get(u'ckanext.saml2auth.user_email')
-
- client = h.saml_client(sp_config())
- auth_response = client.parse_authn_request_response(
- request.form.get(u'SAMLResponse', None),
- entity.BINDING_HTTP_POST)
- auth_response.get_identity()
- user_info = auth_response.get_subject()
-
- # SAML username - unique
- saml_id = user_info.text
- # Required user attributes for user creation
- email = auth_response.ava[saml_user_email][0]
- firstname = auth_response.ava[saml_user_firstname][0]
- lastname = auth_response.ava[saml_user_lastname][0]
-
- # Check if CKAN-SAML user exists for the current SAML login
saml_user = model.Session.query(model.User) \
.filter(model.User.plugin_extras[(u'saml2auth', u'saml_id')].astext == saml_id) \
.first()
# First we check if there is a SAML-CKAN user
- if not saml_user:
- # If there is no SAML user but there is a regular CKAN
- # user with the same email as the current login,
- # make that user a SAML-CKAN user and change
- # it's pass so the user can use only SSO
- ckan_user = model.User.by_email(email)
- if ckan_user:
- # If account exists and is deleted, reactivate it.
- h.activate_user_if_deleted(ckan_user[0])
-
- ckan_user_dict = model_dictize.user_dictize(ckan_user[0], context)
- try:
- ckan_user_dict[u'password'] = h.generate_password()
- ckan_user_dict[u'plugin_extras'] = {
- u'saml2auth': {
- # Store the saml username
- # in the corresponding CKAN user
- u'saml_id': saml_id
- }
- }
- g.user = logic.get_action(u'user_update')(context, ckan_user_dict)[u'name']
- except logic.ValidationError as e:
- error_message = (e.error_summary or e.message or e.error_dict)
- base.abort(400, error_message)
- else:
- data_dict = {u'name': _get_random_username_from_email(email),
- u'fullname': u'{0} {1}'.format(firstname, lastname),
- u'email': email,
- u'password': h.generate_password(),
- u'plugin_extras': {
- u'saml2auth': {
- # Store the saml username
- # in the corresponding CKAN user
- u'saml_id': saml_id
- }
- }}
- try:
- g.user = logic.get_action(u'user_create')(context, data_dict)[u'name']
- except logic.ValidationError as e:
- error_message = (e.error_summary or e.message or e.error_dict)
- base.abort(400, error_message)
-
- else:
+ if saml_user:
# If account exists and is deleted, reactivate it.
h.activate_user_if_deleted(saml_user)
@@ -117,7 +51,97 @@ def acs():
except logic.ValidationError as e:
error_message = (e.error_summary or e.message or e.error_dict)
base.abort(400, error_message)
- g.user = user_dict['name']
+ return user_dict['name']
+
+ # If there is no SAML user but there is a regular CKAN
+ # user with the same email as the current login,
+ # make that user a SAML-CKAN user and change
+ # it's pass so the user can use only SSO
+ ckan_user = model.User.by_email(email)
+ if ckan_user:
+ # If account exists and is deleted, reactivate it.
+ h.activate_user_if_deleted(ckan_user[0])
+
+ ckan_user_dict = model_dictize.user_dictize(ckan_user[0], context)
+ try:
+ ckan_user_dict[u'password'] = h.generate_password()
+ ckan_user_dict[u'plugin_extras'] = {
+ u'saml2auth': {
+ # Store the saml username
+ # in the corresponding CKAN user
+ u'saml_id': saml_id
+ }
+ }
+ return logic.get_action(u'user_update')(context, ckan_user_dict)[u'name']
+ except logic.ValidationError as e:
+ error_message = (e.error_summary or e.message or e.error_dict)
+ base.abort(400, error_message)
+
+ data_dict = {
+ u'name': _get_random_username_from_email(email),
+ u'fullname': u'{0} {1}'.format(firstname, lastname),
+ u'email': email,
+ u'password': h.generate_password(),
+ u'plugin_extras': {
+ u'saml2auth': {
+ # Store the saml username
+ # in the corresponding CKAN user
+ u'saml_id': saml_id
+ }
+ }
+ }
+ try:
+ return logic.get_action(u'user_create')(context, data_dict)[u'name']
+ except logic.ValidationError as e:
+ error_message = (e.error_summary or e.message or e.error_dict)
+ base.abort(400, error_message)
+
+
+def acs():
+ u'''The location where the SAML assertion is sent with a HTTP POST.
+ This is often referred to as the SAML Assertion Consumer Service (ACS) URL.
+ '''
+ g.user = None
+ g.userobj = None
+
+ saml_user_firstname = \
+ config.get(u'ckanext.saml2auth.user_firstname')
+ saml_user_lastname = \
+ config.get(u'ckanext.saml2auth.user_lastname')
+ saml_user_email = \
+ config.get(u'ckanext.saml2auth.user_email')
+
+ client = h.saml_client(sp_config())
+ saml_response = request.form.get(u'SAMLResponse', None)
+
+ error = None
+ try:
+ auth_response = client.parse_authn_request_response(
+ saml_response,
+ entity.BINDING_HTTP_POST)
+ except Exception as e:
+ error = 'Bad login request: {}'.format(e)
+ else:
+ if auth_response is None:
+ error = 'Empty login request'
+
+ if error is not None:
+ log.error(error)
+ extra_vars = {u'code': [400], u'content': error}
+ return base.render(u'error_document_template.html', extra_vars), 400
+
+ auth_response.get_identity()
+ user_info = auth_response.get_subject()
+
+ # SAML username - unique
+ saml_id = user_info.text
+ # Required user attributes for user creation
+ email = auth_response.ava[saml_user_email][0]
+
+ firstname = auth_response.ava.get(saml_user_firstname, [email.split('@')[0]])[0]
+ lastname = auth_response.ava.get(saml_user_lastname, [email.split('@')[1]])[0]
+
+ g.user = process_user(email, saml_id, firstname, lastname)
# Check if the authenticated user email is in given list of emails
# and make that user sysadmin and opposite