Skip to content

Commit f993270

Browse files
test(saml): Add SAML2 provider integration tests
1 parent e65a71b commit f993270

File tree

4 files changed

+270
-0
lines changed

4 files changed

+270
-0
lines changed
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
<?xml version="1.0"?>
2+
<samlp:Response xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" ID="pfx01a16805-19b6-c6b8-6206-715754b4da77" Version="2.0" IssueInstant="2017-10-04T00:21:43Z" Destination="http://testserver.com/auth/sso/" InResponseTo="mock_response">
3+
<saml:Issuer>https://example.com/saml/metadata/1234</saml:Issuer>
4+
<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
5+
<ds:SignedInfo>
6+
<ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
7+
<ds:SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/>
8+
<ds:Reference URI="#pfx01a16805-19b6-c6b8-6206-715754b4da77">
9+
<ds:Transforms>
10+
<ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/>
11+
<ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
12+
</ds:Transforms>
13+
<ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
14+
<ds:DigestValue>none</ds:DigestValue>
15+
</ds:Reference>
16+
</ds:SignedInfo>
17+
<ds:SignatureValue>none</ds:SignatureValue>
18+
<ds:KeyInfo>
19+
<ds:X509Data>
20+
<ds:X509Certificate>none</ds:X509Certificate>
21+
</ds:X509Data>
22+
</ds:KeyInfo>
23+
</ds:Signature>
24+
<samlp:Status>
25+
<samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success"/>
26+
</samlp:Status>
27+
<saml:Assertion xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" Version="2.0" ID="A1337f0b68ad3cc03d1a782f9a9b049ac663fbc54" IssueInstant="2017-10-04T00:21:43Z">
28+
<saml:Issuer>https://example.com/saml/metadata/1234</saml:Issuer>
29+
<saml:Subject>
30+
<saml:NameID Format="urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress">[email protected]</saml:NameID>
31+
<saml:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer">
32+
<saml:SubjectConfirmationData NotOnOrAfter="2099-01-01T00:00:00Z" Recipient="http://testserver.com/auth/sso/" InResponseTo="mock_response"/>
33+
</saml:SubjectConfirmation>
34+
</saml:Subject>
35+
<saml:Conditions NotBefore="2017-01-01T00:00:00Z" NotOnOrAfter="2099-01-01T00:00:00Z">
36+
<saml:AudienceRestriction>
37+
<saml:Audience>http://testserver.com/saml/metadata/saml2-org/</saml:Audience>
38+
</saml:AudienceRestriction>
39+
</saml:Conditions>
40+
<saml:AuthnStatement AuthnInstant="2017-01-01T00:00:00Z" SessionNotOnOrAfter="2099-01-01T00:00:00Z" SessionIndex="_cded8ad0-8a91-0135-96f1-0687370acf48">
41+
<saml:AuthnContext>
42+
<saml:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport</saml:AuthnContextClassRef>
43+
</saml:AuthnContext>
44+
</saml:AuthnStatement>
45+
<saml:AttributeStatement>
46+
<saml:Attribute NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic" Name="user_id">
47+
<saml:AttributeValue xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="xs:string">1234</saml:AttributeValue>
48+
</saml:Attribute>
49+
<saml:Attribute NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic" Name="first_name">
50+
<saml:AttributeValue xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="xs:string">Rick</saml:AttributeValue>
51+
</saml:Attribute>
52+
<saml:Attribute NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic" Name="email">
53+
<saml:AttributeValue xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="xs:string">[email protected]</saml:AttributeValue>
54+
</saml:Attribute>
55+
<saml:Attribute NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic" Name="last_name">
56+
<saml:AttributeValue xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="xs:string">Sanchez</saml:AttributeValue>
57+
</saml:Attribute>
58+
</saml:AttributeStatement>
59+
</saml:Assertion>
60+
</samlp:Response>
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<?xml version="1.0"?>
2+
<samlp:LogoutRequest xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" Version="2.0" IssueInstant="2017-10-04T21:53:53" ID="_6f99ca80-8b7c-0135-3652-02d733713b04">
3+
<saml:Issuer xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">https://example.com/saml/metadata/1234</saml:Issuer>
4+
<saml:NameID xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">[email protected]</saml:NameID>
5+
</samlp:LogoutRequest>
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
from __future__ import absolute_import, print_function
2+
3+
import pytest
4+
import mock
5+
6+
from sentry.auth.providers.saml2 import SAML2Provider, Attributes
7+
8+
from sentry.auth.exceptions import IdentityNotValid
9+
from sentry.models import AuthProvider
10+
from sentry.testutils import TestCase
11+
12+
dummy_provider_config = {
13+
'attribute_mapping': {
14+
Attributes.IDENTIFIER: 'id',
15+
Attributes.USER_EMAIL: 'email',
16+
Attributes.FIRST_NAME: 'first',
17+
Attributes.LAST_NAME: 'last',
18+
}
19+
}
20+
21+
22+
class SAML2ProviderTest(TestCase):
23+
def setUp(self):
24+
self.org = self.create_organization()
25+
self.auth_provider = AuthProvider.objects.create(
26+
provider='saml2',
27+
organization=self.org,
28+
)
29+
self.provider = SAML2Provider(key=self.auth_provider.provider)
30+
super(SAML2ProviderTest, self).setUp()
31+
32+
def test_build_config_adds_attributes(self):
33+
config = self.provider.build_config({})
34+
35+
assert 'attribute_mapping' in config
36+
37+
def test_buld_config_with_provider_attributes(self):
38+
with mock.patch.object(self.provider, 'attribute_mapping') as attribute_mapping:
39+
config = self.provider.build_config({})
40+
41+
assert 'attribute_mapping' in config
42+
assert config['attribute_mapping'] == attribute_mapping.return_value
43+
44+
def test_build_identity_invalid(self):
45+
self.provider.config = dummy_provider_config
46+
state = {'auth_attributes': {}}
47+
48+
with pytest.raises(IdentityNotValid):
49+
self.provider.build_identity(state)
50+
51+
state = {'auth_attributes': {'id': [''], 'email': ['[email protected]']}}
52+
53+
with pytest.raises(IdentityNotValid):
54+
self.provider.build_identity(state)
55+
56+
state = {'auth_attributes': {'id': ['1234'], 'email': ['']}}
57+
58+
with pytest.raises(IdentityNotValid):
59+
self.provider.build_identity(state)
60+
61+
def test_build_identity(self):
62+
self.provider.config = dummy_provider_config
63+
attrs = {
64+
'id': ['123'],
65+
'email': ['[email protected]'],
66+
'first': ['Morty'],
67+
'last': ['Smith'],
68+
}
69+
70+
state = {'auth_attributes': attrs}
71+
identity = self.provider.build_identity(state)
72+
73+
assert identity['id'] == '123'
74+
assert identity['email'] == '[email protected]'
75+
assert identity['name'] == 'Morty Smith'
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
from __future__ import absolute_import
2+
3+
import pytest
4+
import base64
5+
import mock
6+
from exam import fixture
7+
from six.moves.urllib.parse import urlencode, urlparse, parse_qs
8+
9+
from django.conf import settings
10+
from django.core.urlresolvers import reverse
11+
12+
from sentry.auth.providers.saml2 import SAML2Provider, Attributes, HAS_SAML2
13+
from sentry.models import AuthProvider
14+
from sentry.testutils import AuthProviderTestCase
15+
16+
17+
dummy_provider_config = {
18+
'idp': {
19+
'entity_id': 'https://example.com/saml/metadata/1234',
20+
'x509cert': 'foo_x509_cert',
21+
'sso_url': 'http://example.com/sso_url',
22+
'slo_url': 'http://example.com/slo_url',
23+
},
24+
'attribute_mapping': {
25+
Attributes.IDENTIFIER: 'user_id',
26+
Attributes.USER_EMAIL: 'email',
27+
Attributes.FIRST_NAME: 'first_name',
28+
Attributes.LAST_NAME: 'last_name',
29+
},
30+
}
31+
32+
33+
class DummySAML2Provider(SAML2Provider):
34+
strict_mode = False
35+
36+
def get_saml_setup_pipeline(self):
37+
return []
38+
39+
40+
@pytest.mark.skipif(not HAS_SAML2, reason='SAML2 library is not installed')
41+
class AuthSAML2Test(AuthProviderTestCase):
42+
provider = DummySAML2Provider
43+
provider_name = 'saml2_dummy'
44+
45+
def setUp(self):
46+
self.user = self.create_user('[email protected]')
47+
self.org = self.create_organization(owner=self.user, name='saml2-org')
48+
self.auth_provider = AuthProvider.objects.create(
49+
provider=self.provider_name,
50+
config=dummy_provider_config,
51+
organization=self.org,
52+
)
53+
54+
# The system.url-prefix, which is used to generate absolute URLs, must
55+
# have a TLD for the SAML2 library to consider the URL generated for
56+
# the ACS endpoint valid.
57+
self.url_prefix = settings.SENTRY_OPTIONS.get('system.url-prefix')
58+
59+
settings.SENTRY_OPTIONS.update({
60+
'system.url-prefix': 'http://testserver.com',
61+
})
62+
63+
super(AuthSAML2Test, self).setUp()
64+
65+
def tearDown(self):
66+
# restore url-prefix config
67+
settings.SENTRY_OPTIONS.update({
68+
'system.url-prefix': self.url_prefix,
69+
})
70+
71+
super(AuthSAML2Test, self).tearDown()
72+
73+
@fixture
74+
def login_path(self):
75+
return reverse('sentry-auth-organization', args=['saml2-org'])
76+
77+
@fixture
78+
def sso_path(self):
79+
return reverse('sentry-auth-sso')
80+
81+
def test_redirects_to_idp(self):
82+
resp = self.client.post(self.login_path, {'init': True})
83+
84+
assert resp.status_code == 302
85+
redirect = urlparse(resp.get('Location', ''))
86+
query = parse_qs(redirect.query)
87+
88+
assert redirect.path == '/sso_url'
89+
assert 'SAMLRequest' in query
90+
91+
def test_auth_from_idp(self):
92+
# Start auth process
93+
self.client.post(self.login_path, {'init': True})
94+
95+
saml_response = self.load_fixture('saml2_auth_response.xml')
96+
saml_response = base64.b64encode(saml_response)
97+
98+
# Disable validation of the SAML2 mock response
99+
is_valid = 'onelogin.saml2.response.OneLogin_Saml2_Response.is_valid'
100+
101+
with mock.patch(is_valid, return_value=True):
102+
resp = self.client.post(self.sso_path, {'SAMLResponse': saml_response})
103+
104+
assert resp.status_code == 200
105+
assert resp.context['existing_user'] == self.user
106+
107+
def test_saml_metadata(self):
108+
path = reverse('sentry-auth-organization-saml-metadata', args=['saml2-org'])
109+
resp = self.client.get(path)
110+
111+
assert resp.status_code == 200
112+
assert resp.get('content-type') == 'text/xml'
113+
114+
def test_logout_request(self):
115+
saml_request = self.load_fixture('saml2_slo_request.xml')
116+
saml_request = base64.b64encode(saml_request)
117+
118+
self.login_as(self.user)
119+
120+
path = reverse('sentry-auth-organization-saml-sls', args=['saml2-org'])
121+
path = path + '?' + urlencode({'SAMLRequest': saml_request})
122+
resp = self.client.get(path)
123+
124+
assert resp.status_code == 302
125+
126+
redirect = urlparse(resp.get('Location', ''))
127+
query = parse_qs(redirect.query)
128+
129+
assert redirect.path == '/slo_url'
130+
assert 'SAMLResponse' in query

0 commit comments

Comments
 (0)