New SAML/Shibboleth tests - PR 8518
This commit is contained in:
@@ -368,7 +368,7 @@ def signin_user(request):
|
||||
for msg in messages.get_messages(request):
|
||||
if msg.extra_tags.split()[0] == "social-auth":
|
||||
# msg may or may not be translated. Try translating [again] in case we are able to:
|
||||
third_party_auth_error = _(msg) # pylint: disable=translation-of-non-string
|
||||
third_party_auth_error = _(unicode(msg)) # pylint: disable=translation-of-non-string
|
||||
break
|
||||
|
||||
context = {
|
||||
|
||||
@@ -60,7 +60,7 @@ class Command(BaseCommand):
|
||||
self.stdout.write("\n→ Fetching {}\n".format(url))
|
||||
if not url.lower().startswith('https'):
|
||||
self.stdout.write("→ WARNING: This URL is not secure! It should use HTTPS.\n")
|
||||
response = requests.get(url, verify=True) # May raise HTTPError or SSLError
|
||||
response = requests.get(url, verify=True) # May raise HTTPError or SSLError or ConnectionError
|
||||
response.raise_for_status() # May raise an HTTPError
|
||||
|
||||
try:
|
||||
@@ -75,7 +75,7 @@ class Command(BaseCommand):
|
||||
public_key, sso_url, expires_at = self._parse_metadata_xml(xml, entity_id)
|
||||
self._update_data(entity_id, public_key, sso_url, expires_at)
|
||||
except Exception as err: # pylint: disable=broad-except
|
||||
self.stderr.write("→ ERROR: {}\n\n".format(err.message))
|
||||
self.stderr.write(u"→ ERROR: {}\n\n".format(err.message))
|
||||
|
||||
@classmethod
|
||||
def _parse_metadata_xml(cls, xml, entity_id):
|
||||
|
||||
@@ -8,6 +8,7 @@ from django.conf import settings
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import models
|
||||
from django.utils import timezone
|
||||
from django.utils.translation import ugettext as _
|
||||
import json
|
||||
import logging
|
||||
from social.backends.base import BaseAuth
|
||||
@@ -53,7 +54,7 @@ class AuthNotConfigured(SocialAuthBaseException):
|
||||
self.provider_name = provider_name
|
||||
|
||||
def __str__(self):
|
||||
return 'Authentication with {} is currently unavailable.'.format(
|
||||
return _('Authentication with {} is currently unavailable.').format(
|
||||
self.provider_name
|
||||
)
|
||||
|
||||
@@ -313,10 +314,20 @@ class SAMLConfiguration(ConfigurationModel):
|
||||
self.org_info_str = clean_json(self.org_info_str, dict)
|
||||
self.other_config_str = clean_json(self.other_config_str, dict)
|
||||
|
||||
self.private_key = self.private_key.replace("-----BEGIN PRIVATE KEY-----", "").strip()
|
||||
self.private_key = self.private_key.replace("-----END PRIVATE KEY-----", "").strip()
|
||||
self.public_key = self.public_key.replace("-----BEGIN CERTIFICATE-----", "").strip()
|
||||
self.public_key = self.public_key.replace("-----END CERTIFICATE-----", "").strip()
|
||||
self.private_key = (
|
||||
self.private_key
|
||||
.replace("-----BEGIN RSA PRIVATE KEY-----", "")
|
||||
.replace("-----BEGIN PRIVATE KEY-----", "")
|
||||
.replace("-----END RSA PRIVATE KEY-----", "")
|
||||
.replace("-----END PRIVATE KEY-----", "")
|
||||
.strip()
|
||||
)
|
||||
self.public_key = (
|
||||
self.public_key
|
||||
.replace("-----BEGIN CERTIFICATE-----", "")
|
||||
.replace("-----END CERTIFICATE-----", "")
|
||||
.strip()
|
||||
)
|
||||
|
||||
def get_setting(self, name):
|
||||
""" Get the value of a setting, or raise KeyError """
|
||||
|
||||
15
common/djangoapps/third_party_auth/tests/data/saml_key.key
Normal file
15
common/djangoapps/third_party_auth/tests/data/saml_key.key
Normal file
@@ -0,0 +1,15 @@
|
||||
-----BEGIN RSA PRIVATE KEY-----
|
||||
MIICWwIBAAKBgQDM+Nf7IeRdIIgYUke6sR3n7osHVYXwH6pb+Ovq8j3hUoy8kzT9
|
||||
kJF0RB3h3Q2VJ3ZWiQtT94fZX2YYorVdoGVK2NWzjLwgpHUsgfeJq5pCjP0d2OQu
|
||||
9Qvjg6YOtYP6PN3j7eK7pUcxQvIcaY9APDF57ua/zPsm3UzbjhRlJZQUewIDAQAB
|
||||
AoGADWBsD/qdQaqe1x9/iOKINhuuPRNKw2n9nzT2iIW4nhzaDHB689VceL79SEE5
|
||||
4rMJmQomkBtGZVxBeHgd5/dQxNy3bC9lPN1uoMuzjQs7UMk+lvy0MoHfiJcuIxPX
|
||||
RdyZTV9LKN8vq+ZpVykVu6pBdDlne4psPZeQ76ynxke/24ECQQD3NX7JeluZ64la
|
||||
tC6b3VHzA4Hd1qTXDWtEekh2WaR2xuKzcLyOWhqPIWprylBqVc1m+FA/LRRWQ9y6
|
||||
vJMiXMk7AkEA1ELWj9DtZzk9BV1JxsDUUP0/IMAiYliVac3YrvQfys8APCY1xr9q
|
||||
BAGurH4VWXuEnbx1yNXK89HqFI7kDrMtwQJAVTXtVAmHFZEosUk2X6d0He3xj8Py
|
||||
4eXQObRk0daoaAC6F9weQnsweHGuOyVrfpvAx2OEVaJ2Rh3yMbPai5esDQJAS9Yh
|
||||
gLqdx26M3bjJ3igQ82q3vkTHRCnwICA6la+FGFnC9LqWJg9HmmzbcqeNiy31YMHv
|
||||
tzSjUV+jaXrwAkyEQQJAK/SCIVsWRhFe/ssr8hS//V+hZC4kvCv4b3NqzZK1x+Xm
|
||||
7GaGMV0xEWN7shqVSRBU4O2vn/RWD6/6x3sHkU57qg==
|
||||
-----END RSA PRIVATE KEY-----
|
||||
17
common/djangoapps/third_party_auth/tests/data/saml_key.pub
Normal file
17
common/djangoapps/third_party_auth/tests/data/saml_key.pub
Normal file
@@ -0,0 +1,17 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIICsDCCAhmgAwIBAgIJAJrENr8EPgpcMA0GCSqGSIb3DQEBBQUAMEUxCzAJBgNV
|
||||
BAYTAkFVMRMwEQYDVQQIEwpTb21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBX
|
||||
aWRnaXRzIFB0eSBMdGQwHhcNMTUwNjEzMDEwNTE0WhcNMjUwNjEyMDEwNTE0WjBF
|
||||
MQswCQYDVQQGEwJBVTETMBEGA1UECBMKU29tZS1TdGF0ZTEhMB8GA1UEChMYSW50
|
||||
ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKB
|
||||
gQDM+Nf7IeRdIIgYUke6sR3n7osHVYXwH6pb+Ovq8j3hUoy8kzT9kJF0RB3h3Q2V
|
||||
J3ZWiQtT94fZX2YYorVdoGVK2NWzjLwgpHUsgfeJq5pCjP0d2OQu9Qvjg6YOtYP6
|
||||
PN3j7eK7pUcxQvIcaY9APDF57ua/zPsm3UzbjhRlJZQUewIDAQABo4GnMIGkMB0G
|
||||
A1UdDgQWBBTjOyPvAuej5q4C80jlFrQmOlszmzB1BgNVHSMEbjBsgBTjOyPvAuej
|
||||
5q4C80jlFrQmOlszm6FJpEcwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgTClNvbWUt
|
||||
U3RhdGUxITAfBgNVBAoTGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZIIJAJrENr8E
|
||||
PgpcMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADgYEAV5w0SxjUTFWfL3ZG
|
||||
6sgA0gKf8aV8w3AlihLt9tKCRgrK4sBK9xmfwp/fnbdxkHU58iozI894HqmrRzCi
|
||||
aRLWmy3W8640E/XCa6P+i8ET7RksgNJ5cD9WtISHkGc2dnW76+2nv8d24JKeIx2w
|
||||
oJAtspMywzr0SoxDIJr42N6Kvjk=
|
||||
-----END CERTIFICATE-----
|
||||
@@ -0,0 +1,16 @@
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAMoR8CP+HlvsPRwi
|
||||
VCCuWxZOdNjYa4Qre3JEWPkqlUwpci1XGTBqH7DK9b2hmBXMjYoDKOnF5pL7Y453
|
||||
3JSJ2+AG7D4AJGSotA3boKF18EDgeMzAWjAhDVhTprGz+/1G+W0R4SSyY5QGyBhL
|
||||
Z36xF2w5HyeiqN/Iiq3QKGl2CFORAgMBAAECgYEAwH2CAudqSCqstAZHmbI99uva
|
||||
B09ybD93owxUrVbRTfIVX/eeeS4+7g0JNxGebPWkxxnneXoaAV4UIn0v1RfWKMs3
|
||||
QGiBsOSup1DWWwkBfvQ1hNlJfVCqgZH1QVbhPpw9M9gxhLZQaSZoI/qY/8n/54L0
|
||||
zU4S6VYBH6hnkgZZmiECQQDpYUS8HgnkMUX/qcDNBJT23qHewHsZOe6uqC+7+YxQ
|
||||
xKT8iCxybDbZU7hmZ1Av8Ns4iF7EvZ0faFM8Ls76wFX1AkEA3afLUMLHfTx40XwO
|
||||
oU7GWrYFyLNCc3/7JeWi6ZKzwzQqiGvFderRf/QGQsCtpLQ8VoLz/knF9TkQdSh6
|
||||
yuIprQJATfcmxUmruEYVwnFtbZBoS4jYvtfCyAyohkS9naiijaEEFTFQ1/D66eOk
|
||||
KOG+0iU+t0YnksZdpU5u8B4bG34BuQJAXv6FhTQk+MhM40KupnUzTzcJXY1t4kAs
|
||||
K36yBjZoMjWOMO83LiUX6iVz9XHMOXVBEraGySlm3IS7R+q0TXUF9QJAQ69wautf
|
||||
8q1OQiLcg5WTFmSFBEXqAvVwX6FcDSxor9UnI0iHwyKBss3a2IXY9LoTPTjR5SHh
|
||||
GDq2lXmP+kmbnQ==
|
||||
-----END PRIVATE KEY-----
|
||||
@@ -0,0 +1,15 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIICWDCCAcGgAwIBAgIJAMlM2wrOvplkMA0GCSqGSIb3DQEBBQUAMEUxCzAJBgNV
|
||||
BAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBX
|
||||
aWRnaXRzIFB0eSBMdGQwHhcNMTUwNjEzMDEyMTAwWhcNMjUwNjEyMDEyMTAwWjBF
|
||||
MQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50
|
||||
ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKB
|
||||
gQDKEfAj/h5b7D0cIlQgrlsWTnTY2GuEK3tyRFj5KpVMKXItVxkwah+wyvW9oZgV
|
||||
zI2KAyjpxeaS+2OOd9yUidvgBuw+ACRkqLQN26ChdfBA4HjMwFowIQ1YU6axs/v9
|
||||
RvltEeEksmOUBsgYS2d+sRdsOR8noqjfyIqt0ChpdghTkQIDAQABo1AwTjAdBgNV
|
||||
HQ4EFgQUU0TNPc1yGas/W4HJl/Hgtrmdu6MwHwYDVR0jBBgwFoAUU0TNPc1yGas/
|
||||
W4HJl/Hgtrmdu6MwDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQUFAAOBgQCE4BqJ
|
||||
v2s99DS16NbZtR7tpqXDxiDaCg59VtgcHQwxN4qXcixZi5N4yRvzjYschAQN5tQ6
|
||||
bofXdIK3tJY9Ynm0KPO+5l0RCv7CkhNgftTww0bWC91xaHJ/y66AqONuLpaP6s43
|
||||
SZYG2D6ric57ZY4kQ6ZlUv854TPzmvapnGG7Hw==
|
||||
-----END CERTIFICATE-----
|
||||
@@ -0,0 +1,155 @@
|
||||
<!-- Cached and simplified copy of https://www.testshib.org/metadata/testshib-providers.xml -->
|
||||
<EntitiesDescriptor Name="urn:mace:shibboleth:testshib:two"
|
||||
xmlns="urn:oasis:names:tc:SAML:2.0:metadata" xmlns:ds="http://www.w3.org/2000/09/xmldsig#"
|
||||
xmlns:mdalg="urn:oasis:names:tc:SAML:metadata:algsupport" xmlns:mdui="urn:oasis:names:tc:SAML:metadata:ui"
|
||||
xmlns:shibmd="urn:mace:shibboleth:metadata:1.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
|
||||
|
||||
<EntityDescriptor entityID="https://idp.testshib.org/idp/shibboleth">
|
||||
|
||||
<Extensions>
|
||||
<mdalg:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha512" />
|
||||
<mdalg:DigestMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#sha384" />
|
||||
<mdalg:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256" />
|
||||
<mdalg:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1" />
|
||||
<mdalg:SigningMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha512" />
|
||||
<mdalg:SigningMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha384" />
|
||||
<mdalg:SigningMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256" />
|
||||
<mdalg:SigningMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1" />
|
||||
</Extensions>
|
||||
|
||||
<IDPSSODescriptor
|
||||
protocolSupportEnumeration="urn:oasis:names:tc:SAML:1.1:protocol urn:mace:shibboleth:1.0 urn:oasis:names:tc:SAML:2.0:protocol">
|
||||
<Extensions>
|
||||
<shibmd:Scope regexp="false">testshib.org</shibmd:Scope>
|
||||
<mdui:UIInfo>
|
||||
<mdui:DisplayName xml:lang="en">TestShib Test IdP</mdui:DisplayName>
|
||||
<mdui:Description xml:lang="en">TestShib IdP. Use this as a source of attributes
|
||||
for your test SP.</mdui:Description>
|
||||
<mdui:Logo height="88" width="253"
|
||||
>https://www.testshib.org/testshibtwo.jpg</mdui:Logo>
|
||||
</mdui:UIInfo>
|
||||
|
||||
</Extensions>
|
||||
<KeyDescriptor>
|
||||
<ds:KeyInfo>
|
||||
<ds:X509Data>
|
||||
<ds:X509Certificate>
|
||||
MIIEDjCCAvagAwIBAgIBADANBgkqhkiG9w0BAQUFADBnMQswCQYDVQQGEwJVUzEV
|
||||
MBMGA1UECBMMUGVubnN5bHZhbmlhMRMwEQYDVQQHEwpQaXR0c2J1cmdoMREwDwYD
|
||||
VQQKEwhUZXN0U2hpYjEZMBcGA1UEAxMQaWRwLnRlc3RzaGliLm9yZzAeFw0wNjA4
|
||||
MzAyMTEyMjVaFw0xNjA4MjcyMTEyMjVaMGcxCzAJBgNVBAYTAlVTMRUwEwYDVQQI
|
||||
EwxQZW5uc3lsdmFuaWExEzARBgNVBAcTClBpdHRzYnVyZ2gxETAPBgNVBAoTCFRl
|
||||
c3RTaGliMRkwFwYDVQQDExBpZHAudGVzdHNoaWIub3JnMIIBIjANBgkqhkiG9w0B
|
||||
AQEFAAOCAQ8AMIIBCgKCAQEArYkCGuTmJp9eAOSGHwRJo1SNatB5ZOKqDM9ysg7C
|
||||
yVTDClcpu93gSP10nH4gkCZOlnESNgttg0r+MqL8tfJC6ybddEFB3YBo8PZajKSe
|
||||
3OQ01Ow3yT4I+Wdg1tsTpSge9gEz7SrC07EkYmHuPtd71CHiUaCWDv+xVfUQX0aT
|
||||
NPFmDixzUjoYzbGDrtAyCqA8f9CN2txIfJnpHE6q6CmKcoLADS4UrNPlhHSzd614
|
||||
kR/JYiks0K4kbRqCQF0Dv0P5Di+rEfefC6glV8ysC8dB5/9nb0yh/ojRuJGmgMWH
|
||||
gWk6h0ihjihqiu4jACovUZ7vVOCgSE5Ipn7OIwqd93zp2wIDAQABo4HEMIHBMB0G
|
||||
A1UdDgQWBBSsBQ869nh83KqZr5jArr4/7b+QazCBkQYDVR0jBIGJMIGGgBSsBQ86
|
||||
9nh83KqZr5jArr4/7b+Qa6FrpGkwZzELMAkGA1UEBhMCVVMxFTATBgNVBAgTDFBl
|
||||
bm5zeWx2YW5pYTETMBEGA1UEBxMKUGl0dHNidXJnaDERMA8GA1UEChMIVGVzdFNo
|
||||
aWIxGTAXBgNVBAMTEGlkcC50ZXN0c2hpYi5vcmeCAQAwDAYDVR0TBAUwAwEB/zAN
|
||||
BgkqhkiG9w0BAQUFAAOCAQEAjR29PhrCbk8qLN5MFfSVk98t3CT9jHZoYxd8QMRL
|
||||
I4j7iYQxXiGJTT1FXs1nd4Rha9un+LqTfeMMYqISdDDI6tv8iNpkOAvZZUosVkUo
|
||||
93pv1T0RPz35hcHHYq2yee59HJOco2bFlcsH8JBXRSRrJ3Q7Eut+z9uo80JdGNJ4
|
||||
/SJy5UorZ8KazGj16lfJhOBXldgrhppQBb0Nq6HKHguqmwRfJ+WkxemZXzhediAj
|
||||
Geka8nz8JjwxpUjAiSWYKLtJhGEaTqCYxCCX2Dw+dOTqUzHOZ7WKv4JXPK5G/Uhr
|
||||
8K/qhmFT2nIQi538n6rVYLeWj8Bbnl+ev0peYzxFyF5sQA==
|
||||
</ds:X509Certificate>
|
||||
</ds:X509Data>
|
||||
</ds:KeyInfo>
|
||||
<EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#aes256-cbc"/>
|
||||
<EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#aes192-cbc" />
|
||||
<EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#aes128-cbc"/>
|
||||
<EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#tripledes-cbc"/>
|
||||
<EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p"/>
|
||||
<EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#rsa-1_5"/>
|
||||
</KeyDescriptor>
|
||||
|
||||
<ArtifactResolutionService Binding="urn:oasis:names:tc:SAML:1.0:bindings:SOAP-binding"
|
||||
Location="https://idp.testshib.org:8443/idp/profile/SAML1/SOAP/ArtifactResolution"
|
||||
index="1"/>
|
||||
<ArtifactResolutionService Binding="urn:oasis:names:tc:SAML:2.0:bindings:SOAP"
|
||||
Location="https://idp.testshib.org:8443/idp/profile/SAML2/SOAP/ArtifactResolution"
|
||||
index="2"/>
|
||||
|
||||
<NameIDFormat>urn:mace:shibboleth:1.0:nameIdentifier</NameIDFormat>
|
||||
<NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:transient</NameIDFormat>
|
||||
|
||||
<SingleSignOnService Binding="urn:mace:shibboleth:1.0:profiles:AuthnRequest"
|
||||
Location="https://idp.testshib.org/idp/profile/Shibboleth/SSO"/>
|
||||
<SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
|
||||
Location="https://idp.testshib.org/idp/profile/SAML2/POST/SSO"/>
|
||||
<SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
|
||||
Location="https://idp.testshib.org/idp/profile/SAML2/Redirect/SSO"/>
|
||||
<SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:SOAP"
|
||||
Location="https://idp.testshib.org/idp/profile/SAML2/SOAP/ECP"/>
|
||||
|
||||
</IDPSSODescriptor>
|
||||
|
||||
|
||||
<AttributeAuthorityDescriptor
|
||||
protocolSupportEnumeration="urn:oasis:names:tc:SAML:1.1:protocol urn:oasis:names:tc:SAML:2.0:protocol">
|
||||
|
||||
<KeyDescriptor>
|
||||
<ds:KeyInfo>
|
||||
<ds:X509Data>
|
||||
<ds:X509Certificate>
|
||||
MIIEDjCCAvagAwIBAgIBADANBgkqhkiG9w0BAQUFADBnMQswCQYDVQQGEwJVUzEV
|
||||
MBMGA1UECBMMUGVubnN5bHZhbmlhMRMwEQYDVQQHEwpQaXR0c2J1cmdoMREwDwYD
|
||||
VQQKEwhUZXN0U2hpYjEZMBcGA1UEAxMQaWRwLnRlc3RzaGliLm9yZzAeFw0wNjA4
|
||||
MzAyMTEyMjVaFw0xNjA4MjcyMTEyMjVaMGcxCzAJBgNVBAYTAlVTMRUwEwYDVQQI
|
||||
EwxQZW5uc3lsdmFuaWExEzARBgNVBAcTClBpdHRzYnVyZ2gxETAPBgNVBAoTCFRl
|
||||
c3RTaGliMRkwFwYDVQQDExBpZHAudGVzdHNoaWIub3JnMIIBIjANBgkqhkiG9w0B
|
||||
AQEFAAOCAQ8AMIIBCgKCAQEArYkCGuTmJp9eAOSGHwRJo1SNatB5ZOKqDM9ysg7C
|
||||
yVTDClcpu93gSP10nH4gkCZOlnESNgttg0r+MqL8tfJC6ybddEFB3YBo8PZajKSe
|
||||
3OQ01Ow3yT4I+Wdg1tsTpSge9gEz7SrC07EkYmHuPtd71CHiUaCWDv+xVfUQX0aT
|
||||
NPFmDixzUjoYzbGDrtAyCqA8f9CN2txIfJnpHE6q6CmKcoLADS4UrNPlhHSzd614
|
||||
kR/JYiks0K4kbRqCQF0Dv0P5Di+rEfefC6glV8ysC8dB5/9nb0yh/ojRuJGmgMWH
|
||||
gWk6h0ihjihqiu4jACovUZ7vVOCgSE5Ipn7OIwqd93zp2wIDAQABo4HEMIHBMB0G
|
||||
A1UdDgQWBBSsBQ869nh83KqZr5jArr4/7b+QazCBkQYDVR0jBIGJMIGGgBSsBQ86
|
||||
9nh83KqZr5jArr4/7b+Qa6FrpGkwZzELMAkGA1UEBhMCVVMxFTATBgNVBAgTDFBl
|
||||
bm5zeWx2YW5pYTETMBEGA1UEBxMKUGl0dHNidXJnaDERMA8GA1UEChMIVGVzdFNo
|
||||
aWIxGTAXBgNVBAMTEGlkcC50ZXN0c2hpYi5vcmeCAQAwDAYDVR0TBAUwAwEB/zAN
|
||||
BgkqhkiG9w0BAQUFAAOCAQEAjR29PhrCbk8qLN5MFfSVk98t3CT9jHZoYxd8QMRL
|
||||
I4j7iYQxXiGJTT1FXs1nd4Rha9un+LqTfeMMYqISdDDI6tv8iNpkOAvZZUosVkUo
|
||||
93pv1T0RPz35hcHHYq2yee59HJOco2bFlcsH8JBXRSRrJ3Q7Eut+z9uo80JdGNJ4
|
||||
/SJy5UorZ8KazGj16lfJhOBXldgrhppQBb0Nq6HKHguqmwRfJ+WkxemZXzhediAj
|
||||
Geka8nz8JjwxpUjAiSWYKLtJhGEaTqCYxCCX2Dw+dOTqUzHOZ7WKv4JXPK5G/Uhr
|
||||
8K/qhmFT2nIQi538n6rVYLeWj8Bbnl+ev0peYzxFyF5sQA==
|
||||
</ds:X509Certificate>
|
||||
</ds:X509Data>
|
||||
</ds:KeyInfo>
|
||||
<EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#aes256-cbc"/>
|
||||
<EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#aes192-cbc" />
|
||||
<EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#aes128-cbc"/>
|
||||
<EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#tripledes-cbc"/>
|
||||
<EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p"/>
|
||||
<EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#rsa-1_5"/>
|
||||
</KeyDescriptor>
|
||||
|
||||
|
||||
<AttributeService Binding="urn:oasis:names:tc:SAML:1.0:bindings:SOAP-binding"
|
||||
Location="https://idp.testshib.org:8443/idp/profile/SAML1/SOAP/AttributeQuery"/>
|
||||
<AttributeService Binding="urn:oasis:names:tc:SAML:2.0:bindings:SOAP"
|
||||
Location="https://idp.testshib.org:8443/idp/profile/SAML2/SOAP/AttributeQuery"/>
|
||||
|
||||
<NameIDFormat>urn:mace:shibboleth:1.0:nameIdentifier</NameIDFormat>
|
||||
<NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:transient</NameIDFormat>
|
||||
|
||||
</AttributeAuthorityDescriptor>
|
||||
|
||||
<Organization>
|
||||
<OrganizationName xml:lang="en">TestShib Two Identity Provider</OrganizationName>
|
||||
<OrganizationDisplayName xml:lang="en">TestShib Two</OrganizationDisplayName>
|
||||
<OrganizationURL xml:lang="en">http://www.testshib.org/testshib-two/</OrganizationURL>
|
||||
</Organization>
|
||||
<ContactPerson contactType="technical">
|
||||
<GivenName>Nate</GivenName>
|
||||
<SurName>Klingenstein</SurName>
|
||||
<EmailAddress>ndk@internet2.edu</EmailAddress>
|
||||
</ContactPerson>
|
||||
</EntityDescriptor>
|
||||
|
||||
</EntitiesDescriptor>
|
||||
File diff suppressed because one or more lines are too long
229
common/djangoapps/third_party_auth/tests/specs/test_testshib.py
Normal file
229
common/djangoapps/third_party_auth/tests/specs/test_testshib.py
Normal file
@@ -0,0 +1,229 @@
|
||||
"""
|
||||
Third_party_auth integration tests using a mock version of the TestShib provider
|
||||
"""
|
||||
from django.core.management import call_command
|
||||
from django.core.urlresolvers import reverse
|
||||
import httpretty
|
||||
from mock import patch
|
||||
import StringIO
|
||||
from student.tests.factories import UserFactory
|
||||
from third_party_auth.tests import testutil
|
||||
import unittest
|
||||
|
||||
TESTSHIB_ENTITY_ID = 'https://idp.testshib.org/idp/shibboleth'
|
||||
TESTSHIB_METADATA_URL = 'https://mock.testshib.org/metadata/testshib-providers.xml'
|
||||
TESTSHIB_SSO_URL = 'https://idp.testshib.org/idp/profile/SAML2/Redirect/SSO'
|
||||
|
||||
TPA_TESTSHIB_LOGIN_URL = '/auth/login/tpa-saml/?auth_entry=login&next=%2Fdashboard&idp=testshib'
|
||||
TPA_TESTSHIB_REGISTER_URL = '/auth/login/tpa-saml/?auth_entry=register&next=%2Fdashboard&idp=testshib'
|
||||
TPA_TESTSHIB_COMPLETE_URL = '/auth/complete/tpa-saml/'
|
||||
|
||||
|
||||
@unittest.skipUnless(testutil.AUTH_FEATURE_ENABLED, 'third_party_auth not enabled')
|
||||
class TestShibIntegrationTest(testutil.SAMLTestCase):
|
||||
"""
|
||||
TestShib provider Integration Test, to test SAML functionality
|
||||
"""
|
||||
def setUp(self):
|
||||
super(TestShibIntegrationTest, self).setUp()
|
||||
self.login_page_url = reverse('signin_user')
|
||||
self.register_page_url = reverse('register_user')
|
||||
self.enable_saml(
|
||||
private_key=self._get_private_key(),
|
||||
public_key=self._get_public_key(),
|
||||
entity_id="https://saml.example.none",
|
||||
)
|
||||
# Mock out HTTP requests that may be made to TestShib:
|
||||
httpretty.enable()
|
||||
|
||||
def metadata_callback(_request, _uri, headers):
|
||||
""" Return a cached copy of TestShib's metadata by reading it from disk """
|
||||
return (200, headers, self._read_data_file('testshib_metadata.xml'))
|
||||
httpretty.register_uri(httpretty.GET, TESTSHIB_METADATA_URL, content_type='text/xml', body=metadata_callback)
|
||||
self.addCleanup(httpretty.disable)
|
||||
self.addCleanup(httpretty.reset)
|
||||
|
||||
# Configure the SAML library to use the same request ID for every request.
|
||||
# Doing this and freezing the time allows us to play back recorded request/response pairs
|
||||
uid_patch = patch('onelogin.saml2.utils.OneLogin_Saml2_Utils.generate_unique_id', return_value='TESTID')
|
||||
uid_patch.start()
|
||||
self.addCleanup(uid_patch.stop)
|
||||
|
||||
def test_login_before_metadata_fetched(self):
|
||||
self._configure_testshib_provider(fetch_metadata=False)
|
||||
# The user goes to the login page, and sees a button to login with TestShib:
|
||||
self._check_login_page()
|
||||
# The user clicks on the TestShib button:
|
||||
try_login_response = self.client.get(TPA_TESTSHIB_LOGIN_URL)
|
||||
# The user should be redirected to back to the login page:
|
||||
self.assertEqual(try_login_response.status_code, 302)
|
||||
self.assertEqual(try_login_response['Location'], self.url_prefix + self.login_page_url)
|
||||
# When loading the login page, the user will see an error message:
|
||||
response = self.client.get(self.login_page_url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertIn('Authentication with TestShib is currently unavailable.', response.content)
|
||||
|
||||
# Note: the following patch is only needed until https://github.com/edx/edx-platform/pull/8262 is merged
|
||||
@patch.dict("django.conf.settings.FEATURES", {"AUTOMATIC_AUTH_FOR_TESTING": True})
|
||||
def test_register(self):
|
||||
self._configure_testshib_provider()
|
||||
self._freeze_time(timestamp=1434326820) # This is the time when the saved request/response was recorded.
|
||||
# The user goes to the register page, and sees a button to register with TestShib:
|
||||
self._check_register_page()
|
||||
# The user clicks on the TestShib button:
|
||||
try_login_response = self.client.get(TPA_TESTSHIB_REGISTER_URL)
|
||||
# The user should be redirected to TestShib:
|
||||
self.assertEqual(try_login_response.status_code, 302)
|
||||
self.assertTrue(try_login_response['Location'].startswith(TESTSHIB_SSO_URL))
|
||||
# Now the user will authenticate with the SAML provider
|
||||
testshib_response = self._fake_testshib_login_and_return()
|
||||
# We should be redirected to the register screen since this account is not linked to an edX account:
|
||||
self.assertEqual(testshib_response.status_code, 302)
|
||||
self.assertEqual(testshib_response['Location'], self.url_prefix + self.register_page_url)
|
||||
register_response = self.client.get(self.register_page_url)
|
||||
# We'd now like to see if the "You've successfully signed into TestShib" message is
|
||||
# shown, but it's managed by a JavaScript runtime template, and we can't run JS in this
|
||||
# type of test, so we just check for the variable that triggers that message.
|
||||
self.assertIn('"currentProvider": "TestShib"', register_response.content)
|
||||
self.assertIn('"errorMessage": null', register_response.content)
|
||||
# Now do a crude check that the data (e.g. email) from the provider is displayed in the form:
|
||||
self.assertIn('"defaultValue": "myself@testshib.org"', register_response.content)
|
||||
self.assertIn('"defaultValue": "Me Myself And I"', register_response.content)
|
||||
# Now complete the form:
|
||||
ajax_register_response = self.client.post(
|
||||
reverse('user_api_registration'),
|
||||
{
|
||||
'email': 'myself@testshib.org',
|
||||
'name': 'Myself',
|
||||
'username': 'myself',
|
||||
'honor_code': True,
|
||||
}
|
||||
)
|
||||
self.assertEqual(ajax_register_response.status_code, 200)
|
||||
# Then the AJAX will finish the third party auth:
|
||||
continue_response = self.client.get(TPA_TESTSHIB_COMPLETE_URL)
|
||||
# And we should be redirected to the dashboard:
|
||||
self.assertEqual(continue_response.status_code, 302)
|
||||
self.assertEqual(continue_response['Location'], self.url_prefix + reverse('dashboard'))
|
||||
|
||||
# Now check that we can login again:
|
||||
self.client.logout()
|
||||
self._test_return_login()
|
||||
|
||||
def test_login(self):
|
||||
self._configure_testshib_provider()
|
||||
self._freeze_time(timestamp=1434326820) # This is the time when the saved request/response was recorded.
|
||||
user = UserFactory.create()
|
||||
# The user goes to the login page, and sees a button to login with TestShib:
|
||||
self._check_login_page()
|
||||
# The user clicks on the TestShib button:
|
||||
try_login_response = self.client.get(TPA_TESTSHIB_LOGIN_URL)
|
||||
# The user should be redirected to TestShib:
|
||||
self.assertEqual(try_login_response.status_code, 302)
|
||||
self.assertTrue(try_login_response['Location'].startswith(TESTSHIB_SSO_URL))
|
||||
# Now the user will authenticate with the SAML provider
|
||||
testshib_response = self._fake_testshib_login_and_return()
|
||||
# We should be redirected to the login screen since this account is not linked to an edX account:
|
||||
self.assertEqual(testshib_response.status_code, 302)
|
||||
self.assertEqual(testshib_response['Location'], self.url_prefix + self.login_page_url)
|
||||
login_response = self.client.get(self.login_page_url)
|
||||
# We'd now like to see if the "You've successfully signed into TestShib" message is
|
||||
# shown, but it's managed by a JavaScript runtime template, and we can't run JS in this
|
||||
# type of test, so we just check for the variable that triggers that message.
|
||||
self.assertIn('"currentProvider": "TestShib"', login_response.content)
|
||||
self.assertIn('"errorMessage": null', login_response.content)
|
||||
# Now the user enters their username and password.
|
||||
# The AJAX on the page will log them in:
|
||||
ajax_login_response = self.client.post(
|
||||
reverse('user_api_login_session'),
|
||||
{'email': user.email, 'password': 'test'}
|
||||
)
|
||||
self.assertEqual(ajax_login_response.status_code, 200)
|
||||
# Then the AJAX will finish the third party auth:
|
||||
continue_response = self.client.get(TPA_TESTSHIB_COMPLETE_URL)
|
||||
# And we should be redirected to the dashboard:
|
||||
self.assertEqual(continue_response.status_code, 302)
|
||||
self.assertEqual(continue_response['Location'], self.url_prefix + reverse('dashboard'))
|
||||
|
||||
# Now check that we can login again:
|
||||
self.client.logout()
|
||||
self._test_return_login()
|
||||
|
||||
def _test_return_login(self):
|
||||
""" Test logging in to an account that is already linked. """
|
||||
# Make sure we're not logged in:
|
||||
dashboard_response = self.client.get(reverse('dashboard'))
|
||||
self.assertEqual(dashboard_response.status_code, 302)
|
||||
# The user goes to the login page, and sees a button to login with TestShib:
|
||||
self._check_login_page()
|
||||
# The user clicks on the TestShib button:
|
||||
try_login_response = self.client.get(TPA_TESTSHIB_LOGIN_URL)
|
||||
# The user should be redirected to TestShib:
|
||||
self.assertEqual(try_login_response.status_code, 302)
|
||||
self.assertTrue(try_login_response['Location'].startswith(TESTSHIB_SSO_URL))
|
||||
# Now the user will authenticate with the SAML provider
|
||||
login_response = self._fake_testshib_login_and_return()
|
||||
# There will be one weird redirect required to set the login cookie:
|
||||
self.assertEqual(login_response.status_code, 302)
|
||||
self.assertEqual(login_response['Location'], self.url_prefix + TPA_TESTSHIB_COMPLETE_URL)
|
||||
# And then we should be redirected to the dashboard:
|
||||
login_response = self.client.get(TPA_TESTSHIB_COMPLETE_URL)
|
||||
self.assertEqual(login_response.status_code, 302)
|
||||
self.assertEqual(login_response['Location'], self.url_prefix + reverse('dashboard'))
|
||||
# Now we are logged in:
|
||||
dashboard_response = self.client.get(reverse('dashboard'))
|
||||
self.assertEqual(dashboard_response.status_code, 200)
|
||||
|
||||
def _freeze_time(self, timestamp):
|
||||
""" Mock the current time for SAML, so we can replay canned requests/responses """
|
||||
now_patch = patch('onelogin.saml2.utils.OneLogin_Saml2_Utils.now', return_value=timestamp)
|
||||
now_patch.start()
|
||||
self.addCleanup(now_patch.stop)
|
||||
|
||||
def _check_login_page(self):
|
||||
""" Load the login form and check that it contains a TestShib button """
|
||||
response = self.client.get(self.login_page_url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertIn("TestShib", response.content)
|
||||
self.assertIn(TPA_TESTSHIB_LOGIN_URL.replace('&', '&'), response.content)
|
||||
return response
|
||||
|
||||
def _check_register_page(self):
|
||||
""" Load the login form and check that it contains a TestShib button """
|
||||
response = self.client.get(self.register_page_url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertIn("TestShib", response.content)
|
||||
self.assertIn(TPA_TESTSHIB_REGISTER_URL.replace('&', '&'), response.content)
|
||||
return response
|
||||
|
||||
def _configure_testshib_provider(self, **kwargs):
|
||||
""" Enable and configure the TestShib SAML IdP as a third_party_auth provider """
|
||||
fetch_metadata = kwargs.pop('fetch_metadata', True)
|
||||
kwargs.setdefault('name', 'TestShib')
|
||||
kwargs.setdefault('enabled', True)
|
||||
kwargs.setdefault('idp_slug', 'testshib')
|
||||
kwargs.setdefault('entity_id', TESTSHIB_ENTITY_ID)
|
||||
kwargs.setdefault('metadata_source', TESTSHIB_METADATA_URL)
|
||||
kwargs.setdefault('icon_class', 'fa-university')
|
||||
kwargs.setdefault('attr_email', 'urn:oid:1.3.6.1.4.1.5923.1.1.1.6') # eduPersonPrincipalName
|
||||
self.configure_saml_provider(**kwargs)
|
||||
|
||||
if fetch_metadata:
|
||||
stdout = StringIO.StringIO()
|
||||
stderr = StringIO.StringIO()
|
||||
self.assertTrue(httpretty.is_enabled())
|
||||
call_command('saml', 'pull', stdout=stdout, stderr=stderr)
|
||||
stdout = stdout.getvalue().decode('utf-8')
|
||||
stderr = stderr.getvalue().decode('utf-8')
|
||||
self.assertEqual(stderr, '')
|
||||
self.assertIn(u'Fetching {}'.format(TESTSHIB_METADATA_URL), stdout)
|
||||
self.assertIn(u'Created new record for SAMLProviderData', stdout)
|
||||
|
||||
def _fake_testshib_login_and_return(self):
|
||||
""" Mocked: the user logs in to TestShib and then gets redirected back """
|
||||
# The SAML provider (TestShib) will authenticate the user, then get the browser to POST a response:
|
||||
return self.client.post(
|
||||
TPA_TESTSHIB_COMPLETE_URL,
|
||||
content_type='application/x-www-form-urlencoded',
|
||||
data=self._read_data_file('testshib_response.txt'),
|
||||
)
|
||||
64
common/djangoapps/third_party_auth/tests/test_views.py
Normal file
64
common/djangoapps/third_party_auth/tests/test_views.py
Normal file
@@ -0,0 +1,64 @@
|
||||
"""
|
||||
Test the views served by third_party_auth.
|
||||
"""
|
||||
# pylint: disable=no-member
|
||||
import ddt
|
||||
from lxml import etree
|
||||
import unittest
|
||||
from .testutil import AUTH_FEATURE_ENABLED, SAMLTestCase
|
||||
|
||||
# Define some XML namespaces:
|
||||
SAML_XML_NS = 'urn:oasis:names:tc:SAML:2.0:metadata'
|
||||
XMLDSIG_XML_NS = 'http://www.w3.org/2000/09/xmldsig#'
|
||||
|
||||
|
||||
@unittest.skipUnless(AUTH_FEATURE_ENABLED, 'third_party_auth not enabled')
|
||||
@ddt.ddt
|
||||
class SAMLMetadataTest(SAMLTestCase):
|
||||
"""
|
||||
Test the SAML metadata view
|
||||
"""
|
||||
METADATA_URL = '/auth/saml/metadata.xml'
|
||||
|
||||
def test_saml_disabled(self):
|
||||
""" When SAML is not enabled, the metadata view should return 404 """
|
||||
self.enable_saml(enabled=False)
|
||||
response = self.client.get(self.METADATA_URL)
|
||||
self.assertEqual(response.status_code, 404)
|
||||
|
||||
@ddt.data('saml_key', 'saml_key_alt') # Test two slightly different key pair export formats
|
||||
def test_metadata(self, key_name):
|
||||
self.enable_saml(
|
||||
private_key=self._get_private_key(key_name),
|
||||
public_key=self._get_public_key(key_name),
|
||||
entity_id="https://saml.example.none",
|
||||
)
|
||||
doc = self._fetch_metadata()
|
||||
# Check the ACS URL:
|
||||
acs_node = doc.find(".//{}".format(etree.QName(SAML_XML_NS, 'AssertionConsumerService')))
|
||||
self.assertIsNotNone(acs_node)
|
||||
self.assertEqual(acs_node.attrib['Location'], 'http://example.none/auth/complete/tpa-saml/')
|
||||
|
||||
def test_signed_metadata(self):
|
||||
self.enable_saml(
|
||||
private_key=self._get_private_key(),
|
||||
public_key=self._get_public_key(),
|
||||
entity_id="https://saml.example.none",
|
||||
other_config_str='{"SECURITY_CONFIG": {"signMetadata": true} }',
|
||||
)
|
||||
doc = self._fetch_metadata()
|
||||
sig_node = doc.find(".//{}".format(etree.QName(XMLDSIG_XML_NS, 'SignatureValue')))
|
||||
self.assertIsNotNone(sig_node)
|
||||
|
||||
def _fetch_metadata(self):
|
||||
""" Fetch and parse the metadata XML at self.METADATA_URL """
|
||||
response = self.client.get(self.METADATA_URL)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response['Content-Type'], 'text/xml')
|
||||
# The result should be valid XML:
|
||||
try:
|
||||
metadata_doc = etree.fromstring(response.content)
|
||||
except etree.LxmlError:
|
||||
self.fail('SAML metadata must be valid XML')
|
||||
self.assertEqual(metadata_doc.tag, etree.QName(SAML_XML_NS, 'EntityDescriptor'))
|
||||
return metadata_doc
|
||||
@@ -8,6 +8,7 @@ from contextlib import contextmanager
|
||||
from django.conf import settings
|
||||
import django.test
|
||||
import mock
|
||||
import os.path
|
||||
|
||||
from third_party_auth.models import OAuth2ProviderConfig, SAMLProviderConfig, SAMLConfiguration, cache as config_cache
|
||||
|
||||
@@ -87,6 +88,33 @@ class TestCase(ThirdPartyAuthTestMixin, django.test.TestCase):
|
||||
pass
|
||||
|
||||
|
||||
class SAMLTestCase(TestCase):
|
||||
"""
|
||||
Base class for SAML-related third_party_auth tests
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
super(SAMLTestCase, self).setUp()
|
||||
self.client.defaults['SERVER_NAME'] = 'example.none' # The SAML lib we use doesn't like testserver' as a domain
|
||||
self.url_prefix = 'http://example.none'
|
||||
|
||||
@classmethod
|
||||
def _get_public_key(cls, key_name='saml_key'):
|
||||
""" Get a public key for use in the test. """
|
||||
return cls._read_data_file('{}.pub'.format(key_name))
|
||||
|
||||
@classmethod
|
||||
def _get_private_key(cls, key_name='saml_key'):
|
||||
""" Get a private key for use in the test. """
|
||||
return cls._read_data_file('{}.key'.format(key_name))
|
||||
|
||||
@staticmethod
|
||||
def _read_data_file(filename):
|
||||
""" Read the contents of a file in the data folder """
|
||||
with open(os.path.join(os.path.dirname(__file__), 'data', filename)) as f:
|
||||
return f.read()
|
||||
|
||||
|
||||
@contextmanager
|
||||
def simulate_running_pipeline(pipeline_target, backend, email=None, fullname=None, username=None):
|
||||
"""Simulate that a pipeline is currently running.
|
||||
|
||||
@@ -198,7 +198,7 @@ def _third_party_auth_context(request, redirect_to):
|
||||
for msg in messages.get_messages(request):
|
||||
if msg.extra_tags.split()[0] == "social-auth":
|
||||
# msg may or may not be translated. Try translating [again] in case we are able to:
|
||||
context['errorMessage'] = _(msg) # pylint: disable=translation-of-non-string
|
||||
context['errorMessage'] = _(unicode(msg)) # pylint: disable=translation-of-non-string
|
||||
break
|
||||
|
||||
return context
|
||||
|
||||
Reference in New Issue
Block a user