mirror of https://github.com/zulip/zulip.git
saml: Support IdP-initiated SSO.
This commit is contained in:
parent
f2d052bff8
commit
b66dc9de50
|
@ -135,6 +135,24 @@ found at `https://yourzulipdomain.example.com/saml/metadata.xml`. You
|
||||||
can use this for verifying your configuration or provide it to your
|
can use this for verifying your configuration or provide it to your
|
||||||
IdP.
|
IdP.
|
||||||
|
|
||||||
|
### IdP-initiated SSO
|
||||||
|
|
||||||
|
The above configuration is sufficient for Service Provider initialized
|
||||||
|
SSO, i.e. you can visit the Zulip webapp and click "Sign in with
|
||||||
|
{IdP}" and it'll correctly start the authentication flow. If you are
|
||||||
|
not hosting multiple organizations, with Zulip 2.2+, the above
|
||||||
|
configuration is also sufficient for Identity Provider initiated SSO,
|
||||||
|
i.e. clicking a "Sign in to Zulip" button on the IdP's website can
|
||||||
|
correctly authenticate the user to Zulip.
|
||||||
|
|
||||||
|
If you're hosting multiple organizations and thus using the
|
||||||
|
`SOCIAL_AUTH_SUBDOMAIN` setting, you'll need to configure a custom
|
||||||
|
`RelayState` in your IdP of the form `{"subdomain":
|
||||||
|
"yourzuliporganization"}` to let Zulip know which organization to
|
||||||
|
authenticate the user to when they visit your SSO URL from the IdP.
|
||||||
|
(If the organization is on the root domain, use the empty string:
|
||||||
|
`{"subdomain": ""}`.).
|
||||||
|
|
||||||
```eval_rst
|
```eval_rst
|
||||||
.. _ldap:
|
.. _ldap:
|
||||||
```
|
```
|
||||||
|
|
|
@ -1424,7 +1424,7 @@ class SAMLAuthBackendTest(SocialAuthBase):
|
||||||
parsed_url = urllib.parse.urlparse(result.url)
|
parsed_url = urllib.parse.urlparse(result.url)
|
||||||
relay_state = urllib.parse.parse_qs(parsed_url.query)['RelayState'][0]
|
relay_state = urllib.parse.parse_qs(parsed_url.query)['RelayState'][0]
|
||||||
# Make sure params are getting encoded into RelayState:
|
# Make sure params are getting encoded into RelayState:
|
||||||
data = SAMLAuthBackend.get_data_from_redis(relay_state)
|
data = SAMLAuthBackend.get_data_from_redis(ujson.loads(relay_state)['state_token'])
|
||||||
if next:
|
if next:
|
||||||
self.assertEqual(data['next'], next)
|
self.assertEqual(data['next'], next)
|
||||||
if is_signup:
|
if is_signup:
|
||||||
|
@ -1512,7 +1512,9 @@ class SAMLAuthBackendTest(SocialAuthBase):
|
||||||
mock.patch('zproject.backends.logging.info') as m:
|
mock.patch('zproject.backends.logging.info') as m:
|
||||||
# This mock causes AuthFailed to be raised.
|
# This mock causes AuthFailed to be raised.
|
||||||
saml_response = self.generate_saml_response(self.email, self.name)
|
saml_response = self.generate_saml_response(self.email, self.name)
|
||||||
relay_state = SAMLAuthBackend.put_data_in_redis({"subdomain": "zulip"})
|
relay_state = ujson.dumps(dict(
|
||||||
|
state_token=SAMLAuthBackend.put_data_in_redis({"subdomain": "zulip"})
|
||||||
|
))
|
||||||
post_params = {"SAMLResponse": saml_response, "RelayState": relay_state}
|
post_params = {"SAMLResponse": saml_response, "RelayState": relay_state}
|
||||||
result = self.client_post('/complete/saml/', post_params)
|
result = self.client_post('/complete/saml/', post_params)
|
||||||
self.assertEqual(result.status_code, 302)
|
self.assertEqual(result.status_code, 302)
|
||||||
|
@ -1525,7 +1527,9 @@ class SAMLAuthBackendTest(SocialAuthBase):
|
||||||
side_effect=AuthStateForbidden('State forbidden')), \
|
side_effect=AuthStateForbidden('State forbidden')), \
|
||||||
mock.patch('zproject.backends.logging.warning') as m:
|
mock.patch('zproject.backends.logging.warning') as m:
|
||||||
saml_response = self.generate_saml_response(self.email, self.name)
|
saml_response = self.generate_saml_response(self.email, self.name)
|
||||||
relay_state = SAMLAuthBackend.put_data_in_redis({"subdomain": "zulip"})
|
relay_state = ujson.dumps(dict(
|
||||||
|
state_token=SAMLAuthBackend.put_data_in_redis({"subdomain": "zulip"})
|
||||||
|
))
|
||||||
post_params = {"SAMLResponse": saml_response, "RelayState": relay_state}
|
post_params = {"SAMLResponse": saml_response, "RelayState": relay_state}
|
||||||
result = self.client_post('/complete/saml/', post_params)
|
result = self.client_post('/complete/saml/', post_params)
|
||||||
self.assertEqual(result.status_code, 302)
|
self.assertEqual(result.status_code, 302)
|
||||||
|
@ -1540,12 +1544,14 @@ class SAMLAuthBackendTest(SocialAuthBase):
|
||||||
result = self.client_get('/complete/saml/')
|
result = self.client_get('/complete/saml/')
|
||||||
self.assertEqual(result.status_code, 302)
|
self.assertEqual(result.status_code, 302)
|
||||||
self.assertIn('login', result.url)
|
self.assertIn('login', result.url)
|
||||||
m.assert_called_with("SAML authentication failed: missing RelayState.")
|
m.assert_called_with("/complete/saml/: No SAMLResponse in request.")
|
||||||
|
|
||||||
# Check that POSTing the RelayState, but with missing SAMLResponse,
|
# Check that POSTing the RelayState, but with missing SAMLResponse,
|
||||||
# doesn't cause errors either:
|
# doesn't cause errors either:
|
||||||
with mock.patch('zproject.backends.logging.info') as m:
|
with mock.patch('zproject.backends.logging.info') as m:
|
||||||
relay_state = SAMLAuthBackend.put_data_in_redis({"subdomain": "zulip"})
|
relay_state = ujson.dumps(dict(
|
||||||
|
state_token=SAMLAuthBackend.put_data_in_redis({"subdomain": "zulip"})
|
||||||
|
))
|
||||||
post_params = {"RelayState": relay_state}
|
post_params = {"RelayState": relay_state}
|
||||||
result = self.client_post('/complete/saml/', post_params)
|
result = self.client_post('/complete/saml/', post_params)
|
||||||
self.assertEqual(result.status_code, 302)
|
self.assertEqual(result.status_code, 302)
|
||||||
|
@ -1554,7 +1560,9 @@ class SAMLAuthBackendTest(SocialAuthBase):
|
||||||
|
|
||||||
# Now test bad SAMLResponses.
|
# Now test bad SAMLResponses.
|
||||||
with mock.patch('zproject.backends.logging.info') as m:
|
with mock.patch('zproject.backends.logging.info') as m:
|
||||||
relay_state = SAMLAuthBackend.put_data_in_redis({"subdomain": "zulip"})
|
relay_state = ujson.dumps(dict(
|
||||||
|
state_token=SAMLAuthBackend.put_data_in_redis({"subdomain": "zulip"})
|
||||||
|
))
|
||||||
post_params = {"RelayState": relay_state, 'SAMLResponse': ''}
|
post_params = {"RelayState": relay_state, 'SAMLResponse': ''}
|
||||||
result = self.client_post('/complete/saml/', post_params)
|
result = self.client_post('/complete/saml/', post_params)
|
||||||
self.assertEqual(result.status_code, 302)
|
self.assertEqual(result.status_code, 302)
|
||||||
|
@ -1562,7 +1570,9 @@ class SAMLAuthBackendTest(SocialAuthBase):
|
||||||
m.assert_called()
|
m.assert_called()
|
||||||
|
|
||||||
with mock.patch('zproject.backends.logging.info') as m:
|
with mock.patch('zproject.backends.logging.info') as m:
|
||||||
relay_state = SAMLAuthBackend.put_data_in_redis({"subdomain": "zulip"})
|
relay_state = ujson.dumps(dict(
|
||||||
|
state_token=SAMLAuthBackend.put_data_in_redis({"subdomain": "zulip"})
|
||||||
|
))
|
||||||
post_params = {"RelayState": relay_state, 'SAMLResponse': 'b'}
|
post_params = {"RelayState": relay_state, 'SAMLResponse': 'b'}
|
||||||
result = self.client_post('/complete/saml/', post_params)
|
result = self.client_post('/complete/saml/', post_params)
|
||||||
self.assertEqual(result.status_code, 302)
|
self.assertEqual(result.status_code, 302)
|
||||||
|
@ -1570,36 +1580,31 @@ class SAMLAuthBackendTest(SocialAuthBase):
|
||||||
m.assert_called()
|
m.assert_called()
|
||||||
|
|
||||||
with mock.patch('zproject.backends.logging.info') as m:
|
with mock.patch('zproject.backends.logging.info') as m:
|
||||||
relay_state = SAMLAuthBackend.put_data_in_redis({"subdomain": "zulip"})
|
relay_state = ujson.dumps(dict(
|
||||||
|
state_token=SAMLAuthBackend.put_data_in_redis({"subdomain": "zulip"})
|
||||||
|
))
|
||||||
post_params = {"RelayState": relay_state, 'SAMLResponse': 'dGVzdA=='} # base64 encoded 'test'
|
post_params = {"RelayState": relay_state, 'SAMLResponse': 'dGVzdA=='} # base64 encoded 'test'
|
||||||
result = self.client_post('/complete/saml/', post_params)
|
result = self.client_post('/complete/saml/', post_params)
|
||||||
self.assertEqual(result.status_code, 302)
|
self.assertEqual(result.status_code, 302)
|
||||||
self.assertIn('login', result.url)
|
self.assertIn('login', result.url)
|
||||||
m.assert_called()
|
m.assert_called()
|
||||||
|
|
||||||
with mock.patch('zproject.backends.logging.info') as m:
|
|
||||||
relay_state = SAMLAuthBackend.put_data_in_redis({"subdomain": "zulip"})
|
|
||||||
relay_state = relay_state[:-1] # Break the token by removing the last character
|
|
||||||
post_params = {"RelayState": relay_state}
|
|
||||||
result = self.client_post('/complete/saml/', post_params)
|
|
||||||
self.assertEqual(result.status_code, 302)
|
|
||||||
self.assertIn('login', result.url)
|
|
||||||
m.assert_called_with("SAML authentication failed: bad RelayState token.")
|
|
||||||
|
|
||||||
def test_social_auth_complete_no_subdomain(self) -> None:
|
def test_social_auth_complete_no_subdomain(self) -> None:
|
||||||
with mock.patch('zproject.backends.logging.info') as m:
|
with mock.patch('zproject.backends.logging.info') as m:
|
||||||
relay_state = SAMLAuthBackend.put_data_in_redis({})
|
post_params = {"RelayState": '',
|
||||||
post_params = {"RelayState": relay_state,
|
|
||||||
'SAMLResponse': self.generate_saml_response(email=self.example_email("hamlet"),
|
'SAMLResponse': self.generate_saml_response(email=self.example_email("hamlet"),
|
||||||
name="King Hamlet")}
|
name="King Hamlet")}
|
||||||
|
with mock.patch.object(SAMLAuthBackend, 'choose_subdomain', return_value=None):
|
||||||
result = self.client_post('/complete/saml/', post_params)
|
result = self.client_post('/complete/saml/', post_params)
|
||||||
self.assertEqual(result.status_code, 302)
|
self.assertEqual(result.status_code, 302)
|
||||||
self.assertEqual('/login/', result.url)
|
self.assertEqual('/login/', result.url)
|
||||||
self.assertIn("/complete/saml/: Missing subdomain value in relayed_params.",
|
self.assertIn("/complete/saml/: Can't figure out subdomain for this authentication request",
|
||||||
m.call_args.args[0])
|
m.call_args.args[0])
|
||||||
|
|
||||||
def test_social_auth_complete_wrong_issuing_idp(self) -> None:
|
def test_social_auth_complete_wrong_issuing_idp(self) -> None:
|
||||||
relay_state = SAMLAuthBackend.put_data_in_redis({"subdomain": "zulip"})
|
relay_state = ujson.dumps(dict(
|
||||||
|
state_token=SAMLAuthBackend.put_data_in_redis({"subdomain": "zulip"})
|
||||||
|
))
|
||||||
saml_response = self.generate_saml_response(email=self.example_email("hamlet"),
|
saml_response = self.generate_saml_response(email=self.example_email("hamlet"),
|
||||||
name="King Hamlet")
|
name="King Hamlet")
|
||||||
|
|
||||||
|
@ -1626,7 +1631,9 @@ class SAMLAuthBackendTest(SocialAuthBase):
|
||||||
|
|
||||||
with mock.patch('zproject.backends.logging.info') as m, \
|
with mock.patch('zproject.backends.logging.info') as m, \
|
||||||
mock.patch.object(SAMLAuthBackend, 'get_issuing_idp', return_value='test_idp'):
|
mock.patch.object(SAMLAuthBackend, 'get_issuing_idp', return_value='test_idp'):
|
||||||
relay_state = SAMLAuthBackend.put_data_in_redis({"subdomain": "zulip"})
|
relay_state = ujson.dumps(dict(
|
||||||
|
state_token=SAMLAuthBackend.put_data_in_redis({"subdomain": "zulip"})
|
||||||
|
))
|
||||||
post_params = {"RelayState": relay_state, 'SAMLResponse': 'dGVzdA=='}
|
post_params = {"RelayState": relay_state, 'SAMLResponse': 'dGVzdA=='}
|
||||||
result = self.client_post('/complete/saml/', post_params)
|
result = self.client_post('/complete/saml/', post_params)
|
||||||
self.assertEqual(result.status_code, 302)
|
self.assertEqual(result.status_code, 302)
|
||||||
|
@ -1720,7 +1727,8 @@ class SAMLAuthBackendTest(SocialAuthBase):
|
||||||
self.assertEqual(result.status_code, 302)
|
self.assertEqual(result.status_code, 302)
|
||||||
self.assertEqual('/login/', result.url)
|
self.assertEqual('/login/', result.url)
|
||||||
m.assert_called_with(
|
m.assert_called_with(
|
||||||
"User authenticated with IdP %s but this provider is not enabled for this realm %s.",
|
'/complete/saml/: Authentication request with IdP %s but this provider is not enabled ' +
|
||||||
|
'for this subdomain %s.',
|
||||||
"test_idp", "zephyr",
|
"test_idp", "zephyr",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -1761,6 +1769,104 @@ class SAMLAuthBackendTest(SocialAuthBase):
|
||||||
"IdPs don't have limit_to_subdomains specified and will be ignored: " +
|
"IdPs don't have limit_to_subdomains specified and will be ignored: " +
|
||||||
"['test_idp']")
|
"['test_idp']")
|
||||||
|
|
||||||
|
def test_idp_initiated_signin_subdomain_specified(self) -> None:
|
||||||
|
post_params = {
|
||||||
|
"RelayState": '{"subdomain": "zulip"}',
|
||||||
|
"SAMLResponse": self.generate_saml_response(email=self.email, name=self.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
with mock.patch.object(OneLogin_Saml2_Response, 'is_valid', return_value=True):
|
||||||
|
# We're not able to generate valid signatures in tests, so we need the mock.
|
||||||
|
result = self.client_post('/complete/saml/', post_params)
|
||||||
|
|
||||||
|
data = load_subdomain_token(result)
|
||||||
|
self.assertEqual(data['email'], self.example_email("hamlet"))
|
||||||
|
self.assertEqual(data['full_name'], self.name)
|
||||||
|
self.assertEqual(data['subdomain'], 'zulip')
|
||||||
|
self.assertEqual(result.status_code, 302)
|
||||||
|
parsed_url = urllib.parse.urlparse(result.url)
|
||||||
|
uri = "{}://{}{}".format(parsed_url.scheme, parsed_url.netloc,
|
||||||
|
parsed_url.path)
|
||||||
|
self.assertTrue(uri.startswith('http://zulip.testserver/accounts/login/subdomain/'))
|
||||||
|
|
||||||
|
self.client_get(uri)
|
||||||
|
self.assert_logged_in_user_id(self.example_user("hamlet").id)
|
||||||
|
|
||||||
|
def test_choose_subdomain_invalid_subdomain_specified(self) -> None:
|
||||||
|
post_params = {
|
||||||
|
"RelayState": '{"subdomain": "invalid"}',
|
||||||
|
"SAMLResponse": self.generate_saml_response(email=self.email, name=self.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
with mock.patch.object(OneLogin_Saml2_Response, 'is_valid', return_value=True):
|
||||||
|
# We're not able to generate valid signatures in tests, so we need the mock.
|
||||||
|
result = self.client_post('/complete/saml/', post_params)
|
||||||
|
|
||||||
|
self.assertEqual(result.status_code, 302)
|
||||||
|
self.assertEqual(result.url, "/accounts/find/")
|
||||||
|
|
||||||
|
def test_idp_initiated_signin_subdomain_implicit(self) -> None:
|
||||||
|
post_params = {
|
||||||
|
"RelayState": '',
|
||||||
|
"SAMLResponse": self.generate_saml_response(email=self.email, name=self.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
with mock.patch.object(OneLogin_Saml2_Response, 'is_valid', return_value=True):
|
||||||
|
# We're not able to generate valid signatures in tests, so we need the mock.
|
||||||
|
result = self.client_post('http://zulip.testserver/complete/saml/', post_params)
|
||||||
|
|
||||||
|
data = load_subdomain_token(result)
|
||||||
|
self.assertEqual(data['email'], self.example_email("hamlet"))
|
||||||
|
self.assertEqual(data['full_name'], self.name)
|
||||||
|
self.assertEqual(data['subdomain'], 'zulip')
|
||||||
|
self.assertEqual(result.status_code, 302)
|
||||||
|
parsed_url = urllib.parse.urlparse(result.url)
|
||||||
|
uri = "{}://{}{}".format(parsed_url.scheme, parsed_url.netloc,
|
||||||
|
parsed_url.path)
|
||||||
|
self.assertTrue(uri.startswith('http://zulip.testserver/accounts/login/subdomain/'))
|
||||||
|
|
||||||
|
self.client_get(uri)
|
||||||
|
self.assert_logged_in_user_id(self.example_user("hamlet").id)
|
||||||
|
|
||||||
|
def test_idp_initiated_signin_subdomain_implicit_no_relaystate_param(self) -> None:
|
||||||
|
post_params = {
|
||||||
|
"SAMLResponse": self.generate_saml_response(email=self.email, name=self.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
with mock.patch.object(OneLogin_Saml2_Response, 'is_valid', return_value=True):
|
||||||
|
# We're not able to generate valid signatures in tests, so we need the mock.
|
||||||
|
result = self.client_post('http://zulip.testserver/complete/saml/', post_params)
|
||||||
|
|
||||||
|
data = load_subdomain_token(result)
|
||||||
|
self.assertEqual(data['email'], self.example_email("hamlet"))
|
||||||
|
self.assertEqual(data['full_name'], self.name)
|
||||||
|
self.assertEqual(data['subdomain'], 'zulip')
|
||||||
|
self.assertEqual(result.status_code, 302)
|
||||||
|
parsed_url = urllib.parse.urlparse(result.url)
|
||||||
|
uri = "{}://{}{}".format(parsed_url.scheme, parsed_url.netloc,
|
||||||
|
parsed_url.path)
|
||||||
|
self.assertTrue(uri.startswith('http://zulip.testserver/accounts/login/subdomain/'))
|
||||||
|
|
||||||
|
self.client_get(uri)
|
||||||
|
self.assert_logged_in_user_id(self.example_user("hamlet").id)
|
||||||
|
|
||||||
|
def test_idp_initiated_signin_subdomain_implicit_invalid(self) -> None:
|
||||||
|
post_params = {
|
||||||
|
"RelayState": '',
|
||||||
|
"SAMLResponse": self.generate_saml_response(email=self.email, name=self.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
with mock.patch("logging.info") as m:
|
||||||
|
with mock.patch('zproject.backends.get_subdomain', return_value='invalid'):
|
||||||
|
# Due to the quirks of our test setup, get_subdomain on all these `some_subdomain.testserver`
|
||||||
|
# requests returns 'zulip', so we need to mock it here.
|
||||||
|
result = self.client_post('http://invalid.testserver/complete/saml/', post_params)
|
||||||
|
|
||||||
|
self.assertEqual(result.status_code, 302)
|
||||||
|
self.assertEqual('/login/', result.url)
|
||||||
|
self.assertIn("/complete/saml/: Can't figure out subdomain for this authentication request",
|
||||||
|
m.call_args.args[0])
|
||||||
|
|
||||||
class GitHubAuthBackendTest(SocialAuthBase):
|
class GitHubAuthBackendTest(SocialAuthBase):
|
||||||
__unittest_skip__ = False
|
__unittest_skip__ = False
|
||||||
|
|
||||||
|
|
|
@ -16,6 +16,7 @@ import binascii
|
||||||
import copy
|
import copy
|
||||||
import logging
|
import logging
|
||||||
import magic
|
import magic
|
||||||
|
import ujson
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
from typing import Any, Callable, Dict, List, Optional, Set, Tuple, Type, TypeVar, Union, \
|
from typing import Any, Callable, Dict, List, Optional, Set, Tuple, Type, TypeVar, Union, \
|
||||||
cast
|
cast
|
||||||
|
@ -64,6 +65,7 @@ from zerver.lib.rate_limiter import RateLimitedObject
|
||||||
from zerver.lib.request import JsonableError
|
from zerver.lib.request import JsonableError
|
||||||
from zerver.lib.users import check_full_name, validate_user_custom_profile_field
|
from zerver.lib.users import check_full_name, validate_user_custom_profile_field
|
||||||
from zerver.lib.redis_utils import get_redis_client, get_dict_from_redis, put_dict_in_redis
|
from zerver.lib.redis_utils import get_redis_client, get_dict_from_redis, put_dict_in_redis
|
||||||
|
from zerver.lib.subdomains import get_subdomain
|
||||||
from zerver.models import CustomProfileField, DisposableEmailError, DomainNotAllowedForRealmError, \
|
from zerver.models import CustomProfileField, DisposableEmailError, DomainNotAllowedForRealmError, \
|
||||||
EmailContainsPlusError, PreregistrationUser, UserProfile, Realm, custom_profile_fields_for_realm, \
|
EmailContainsPlusError, PreregistrationUser, UserProfile, Realm, custom_profile_fields_for_realm, \
|
||||||
get_user_profile_by_id, remote_user_to_email, \
|
get_user_profile_by_id, remote_user_to_email, \
|
||||||
|
@ -1549,7 +1551,7 @@ class SAMLAuthBackend(SocialAuthMixin, SAMLAuth):
|
||||||
data_to_relay = {
|
data_to_relay = {
|
||||||
key: request_data[key] for key in params_to_relay if key in request_data
|
key: request_data[key] for key in params_to_relay if key in request_data
|
||||||
}
|
}
|
||||||
relay_state = self.put_data_in_redis(data_to_relay)
|
relay_state = ujson.dumps({"state_token": self.put_data_in_redis(data_to_relay)})
|
||||||
|
|
||||||
return auth.login(return_to=relay_state)
|
return auth.login(return_to=relay_state)
|
||||||
|
|
||||||
|
@ -1565,10 +1567,6 @@ class SAMLAuthBackend(SocialAuthMixin, SAMLAuth):
|
||||||
if key.startswith('saml_token_'):
|
if key.startswith('saml_token_'):
|
||||||
# Safety if statement, to not allow someone to poke around arbitrary redis keys here.
|
# Safety if statement, to not allow someone to poke around arbitrary redis keys here.
|
||||||
data = get_dict_from_redis(redis_client, "saml_token_{token}", key)
|
data = get_dict_from_redis(redis_client, "saml_token_{token}", key)
|
||||||
if data is None:
|
|
||||||
# TODO: We will need some sort of user-facing message
|
|
||||||
# about the authentication session having expired here.
|
|
||||||
logging.info("SAML authentication failed: bad RelayState token.")
|
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
@ -1596,6 +1594,46 @@ class SAMLAuthBackend(SocialAuthMixin, SAMLAuth):
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def get_relayed_params(self) -> Dict[str, Any]:
|
||||||
|
request_data = self.strategy.request_data()
|
||||||
|
if 'RelayState' not in request_data:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
relay_state = request_data['RelayState']
|
||||||
|
try:
|
||||||
|
data = ujson.loads(relay_state)
|
||||||
|
if 'state_token' in data:
|
||||||
|
# SP-initiated sign in. We stored relevant information in the first
|
||||||
|
# step of the flow
|
||||||
|
return self.get_data_from_redis(data['state_token']) or {}
|
||||||
|
else:
|
||||||
|
# IdP-initiated sign in. Right now we only support transporting subdomain through json in
|
||||||
|
# RelayState, but this format is nice in that it allows easy extensibility here.
|
||||||
|
return {'subdomain': data.get('subdomain')}
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
return {}
|
||||||
|
|
||||||
|
def choose_subdomain(self, relayed_params: Dict[str, Any]) -> Optional[str]:
|
||||||
|
subdomain = relayed_params.get("subdomain")
|
||||||
|
if subdomain is not None:
|
||||||
|
return subdomain
|
||||||
|
|
||||||
|
# If not specified otherwise, the intended subdomain for this
|
||||||
|
# authentication attempt is the subdomain of the request.
|
||||||
|
request_subdomain = get_subdomain(self.strategy.request)
|
||||||
|
try:
|
||||||
|
# We only want to do a basic sanity-check here for whether
|
||||||
|
# this subdomain has a realm one could try to authenticate
|
||||||
|
# to. True validation of whether the realm is active, the
|
||||||
|
# IdP is appropriate for the subdomain, etc. happens
|
||||||
|
# elsewhere in the flow and we shouldn't duplicate such
|
||||||
|
# logic here.
|
||||||
|
get_realm(request_subdomain)
|
||||||
|
except Realm.DoesNotExist:
|
||||||
|
return None
|
||||||
|
else:
|
||||||
|
return request_subdomain
|
||||||
|
|
||||||
def auth_complete(self, *args: Any, **kwargs: Any) -> Optional[HttpResponse]:
|
def auth_complete(self, *args: Any, **kwargs: Any) -> Optional[HttpResponse]:
|
||||||
"""
|
"""
|
||||||
Additional ugly wrapping on top of auth_complete in SocialAuthMixin.
|
Additional ugly wrapping on top of auth_complete in SocialAuthMixin.
|
||||||
|
@ -1610,41 +1648,37 @@ class SAMLAuthBackend(SocialAuthMixin, SAMLAuth):
|
||||||
and then change the RelayState param to the idp_name, because that's what
|
and then change the RelayState param to the idp_name, because that's what
|
||||||
SAMLAuth.auth_complete() expects.
|
SAMLAuth.auth_complete() expects.
|
||||||
"""
|
"""
|
||||||
if 'RelayState' not in self.strategy.request_data():
|
|
||||||
logging.info("SAML authentication failed: missing RelayState.")
|
|
||||||
return None
|
|
||||||
|
|
||||||
# Set the relevant params that we transported in the RelayState:
|
|
||||||
redis_key = self.strategy.request_data()['RelayState']
|
|
||||||
relayed_params = self.get_data_from_redis(redis_key)
|
|
||||||
if relayed_params is None:
|
|
||||||
return None
|
|
||||||
|
|
||||||
SAMLResponse = self.strategy.request_data().get('SAMLResponse')
|
SAMLResponse = self.strategy.request_data().get('SAMLResponse')
|
||||||
if SAMLResponse is None:
|
if SAMLResponse is None:
|
||||||
logging.info("/complete/saml/: No SAMLResponse in request.")
|
logging.info("/complete/saml/: No SAMLResponse in request.")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
relayed_params = self.get_relayed_params()
|
||||||
|
|
||||||
|
subdomain = self.choose_subdomain(relayed_params)
|
||||||
|
if subdomain is None:
|
||||||
|
error_msg = "/complete/saml/: Can't figure out subdomain for this authentication request. " + \
|
||||||
|
"relayed_params: %s"
|
||||||
|
logging.info(error_msg, relayed_params)
|
||||||
|
return None
|
||||||
|
|
||||||
idp_name = self.get_issuing_idp(SAMLResponse)
|
idp_name = self.get_issuing_idp(SAMLResponse)
|
||||||
if idp_name is None:
|
if idp_name is None:
|
||||||
logging.info("/complete/saml/: No valid IdP as issuer of the SAMLResponse.")
|
logging.info("/complete/saml/: No valid IdP as issuer of the SAMLResponse.")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
subdomain = relayed_params.get("subdomain")
|
|
||||||
if subdomain is None:
|
|
||||||
logging.info("/complete/saml/: Missing subdomain value in relayed_params.")
|
|
||||||
return None
|
|
||||||
|
|
||||||
idp_valid = self.validate_idp_for_subdomain(idp_name, subdomain)
|
idp_valid = self.validate_idp_for_subdomain(idp_name, subdomain)
|
||||||
if not idp_valid:
|
if not idp_valid:
|
||||||
error_msg = "User authenticated with IdP %s but this provider is not " + \
|
error_msg = "/complete/saml/: Authentication request with IdP %s but this provider is not " + \
|
||||||
"enabled for this realm %s."
|
"enabled for this subdomain %s."
|
||||||
logging.info(error_msg, idp_name, subdomain)
|
logging.info(error_msg, idp_name, subdomain)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
result = None
|
result = None
|
||||||
try:
|
try:
|
||||||
for param, value in relayed_params.items():
|
params = relayed_params.copy()
|
||||||
|
params['subdomain'] = subdomain
|
||||||
|
for param, value in params.items():
|
||||||
if param in self.standard_relay_params:
|
if param in self.standard_relay_params:
|
||||||
self.strategy.session_set(param, value)
|
self.strategy.session_set(param, value)
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue