Esempio n. 1
0
class TestIdentityProviderCallbackController(FunctionalTest):
    @mock.patch.object(
        sso_api_controller.SSO_BACKEND,
        "verify_response",
        mock.MagicMock(side_effect=Exception("fooobar")),
    )
    def test_default_backend_unknown_exception(self):
        expected_error = {"faultstring": "Internal Server Error"}
        response = self.app.post_json(SSO_CALLBACK_V1_PATH, {"foo": "bar"},
                                      expect_errors=True)
        self.assertTrue(response.status_code,
                        http_client.INTERNAL_SERVER_ERROR)
        self.assertDictEqual(response.json, expected_error)

    def test_default_backend_not_implemented(self):
        expected_error = {"faultstring": noop.NOT_IMPLEMENTED_MESSAGE}
        response = self.app.post_json(SSO_CALLBACK_V1_PATH, {"foo": "bar"},
                                      expect_errors=True)
        self.assertTrue(response.status_code,
                        http_client.INTERNAL_SERVER_ERROR)
        self.assertDictEqual(response.json, expected_error)

    @mock.patch.object(
        sso_api_controller.SSO_BACKEND,
        "verify_response",
        mock.MagicMock(return_value={
            "referer": MOCK_REFERER,
            "username": MOCK_USER
        }),
    )
    def test_idp_callback(self):
        expected_body = sso_api_controller.CALLBACK_SUCCESS_RESPONSE_BODY % MOCK_REFERER
        response = self.app.post_json(SSO_CALLBACK_V1_PATH, {"foo": "bar"},
                                      expect_errors=False)
        self.assertTrue(response.status_code, http_client.OK)
        self.assertEqual(expected_body, response.body.decode("utf-8"))

        set_cookies_list = [
            h for h in response.headerlist if h[0] == "Set-Cookie"
        ]
        self.assertEqual(len(set_cookies_list), 1)
        self.assertIn("st2-auth-token", set_cookies_list[0][1])

        cookie = urllib.parse.unquote(set_cookies_list[0][1]).split("=")
        st2_auth_token = json.loads(cookie[1].split(";")[0])
        self.assertIn("token", st2_auth_token)
        self.assertEqual(st2_auth_token["user"], MOCK_USER)

    @mock.patch.object(
        sso_api_controller.SSO_BACKEND,
        "verify_response",
        mock.MagicMock(
            side_effect=auth_exc.SSOVerificationError("Verification Failed")),
    )
    def test_idp_callback_verification_failed(self):
        expected_error = {"faultstring": "Verification Failed"}
        response = self.app.post_json(SSO_CALLBACK_V1_PATH, {"foo": "bar"},
                                      expect_errors=True)
        self.assertTrue(response.status_code, http_client.UNAUTHORIZED)
        self.assertDictEqual(response.json, expected_error)
class TestSingleSignOnControllerWithSAML2(BaseSAML2Controller):

    def test_cls_init(self):
        # Delay import here otherwise setupClass will not have run.
        from st2auth.controllers.v1 import sso as sso_api_controller
        instance = sso_api_controller.SSO_BACKEND

        self.assertEqual(instance.entity_id, MOCK_ENTITY_ID)
        self.assertIsNotNone(instance.relay_state_id)
        self.assertEqual(instance.https_acs_url, MOCK_ACS_URL)
        self.assertEqual(instance.saml_metadata_url, MOCK_METADATA_URL)

        expected_saml_client_settings = {
            'entityid': MOCK_ENTITY_ID,
            'metadata': {'inline': [MockSamlMetadata().text]},
            'service': {
                'sp': {
                    'endpoints': {
                        'assertion_consumer_service': [
                            (MOCK_ACS_URL, saml2.BINDING_HTTP_REDIRECT),
                            (MOCK_ACS_URL, saml2.BINDING_HTTP_POST)
                        ]
                    },
                    'allow_unsolicited': True,
                    'authn_requests_signed': False,
                    'logout_requests_signed': True,
                    'want_assertions_signed': True,
                    'want_response_signed': True
                }
            }
        }

        self.assertDictEqual(instance.saml_client_settings, expected_saml_client_settings)

    @mock.patch.object(
        saml.SAML2SingleSignOnBackend,
        '_handle_verification_error',
        mock.MagicMock(side_effect=auth_exc.SSOVerificationError('See unit test.')))
    def test_idp_redirect_bad_referer(self):
        headers = {'referer': 'https://hahahaha.fooled.ya'}
        expected_error = {'faultstring': 'Internal Server Error'}
        expected_msg = 'Invalid referer.'
        response = self.app.get(SSO_REQUEST_V1_PATH, headers=headers, expect_errors=True)
        self.assertTrue(response.status_code, http_client.INTERNAL_SERVER_ERROR)
        self.assertDictEqual(response.json, expected_error)
        self.assertTrue(saml.SAML2SingleSignOnBackend._handle_verification_error.called)
        saml.SAML2SingleSignOnBackend._handle_verification_error.assert_called_with(expected_msg)

    @mock.patch.object(
        saml2.client.Saml2Client,
        'prepare_for_authenticate',
        mock.MagicMock(return_value=(None, MOCK_REDIRECT_INFO)))
    def test_idp_redirect(self):
        headers = {'referer': MOCK_ENTITY_ID}
        response = self.app.get(SSO_REQUEST_V1_PATH, headers=headers, expect_errors=False)
        self.assertTrue(response.status_code, http_client.TEMPORARY_REDIRECT)
        self.assertEqual(response.location, MOCK_REDIRECT_URL)
class TestIdentityProviderCallbackController(BaseSAML2Controller):
    @mock.patch.object(
        saml.SAML2SingleSignOnBackend, '_handle_verification_error',
        mock.MagicMock(
            side_effect=auth_exc.SSOVerificationError('See unit test.')))
    def test_idp_callback_missing_response(self):
        expected_error = {
            'faultstring':
            'Error encountered while verifying the SAML2 response.'
        }
        expected_msg = 'The SAMLResponse attribute is missing.'
        response = self.app.post_json(SSO_CALLBACK_V1_PATH, {},
                                      expect_errors=True)
        self.assertTrue(response.status_code, http_client.UNAUTHORIZED)
        self.assertDictEqual(response.json, expected_error)
        self.assertTrue(
            saml.SAML2SingleSignOnBackend._handle_verification_error.called)
        saml.SAML2SingleSignOnBackend._handle_verification_error.assert_called_with(
            expected_msg)

    @mock.patch.object(
        saml.SAML2SingleSignOnBackend, '_handle_verification_error',
        mock.MagicMock(
            side_effect=auth_exc.SSOVerificationError('See unit test.')))
    def test_idp_callback_null_response(self):
        expected_error = {
            'faultstring':
            'Error encountered while verifying the SAML2 response.'
        }
        expected_msg = 'The SAMLResponse attribute is null.'
        saml_response = {'SAMLResponse': None}
        response = self.app.post_json(SSO_CALLBACK_V1_PATH,
                                      saml_response,
                                      expect_errors=True)
        self.assertTrue(response.status_code, http_client.UNAUTHORIZED)
        self.assertDictEqual(response.json, expected_error)
        self.assertTrue(
            saml.SAML2SingleSignOnBackend._handle_verification_error.called)
        saml.SAML2SingleSignOnBackend._handle_verification_error.assert_called_with(
            expected_msg)

    @mock.patch.object(
        saml.SAML2SingleSignOnBackend, '_handle_verification_error',
        mock.MagicMock(
            side_effect=auth_exc.SSOVerificationError('See unit test.')))
    def test_idp_callback_empty_response(self):
        expected_error = {
            'faultstring':
            'Error encountered while verifying the SAML2 response.'
        }
        expected_msg = 'The SAMLResponse attribute is empty.'
        saml_response = {'SAMLResponse': []}
        response = self.app.post_json(SSO_CALLBACK_V1_PATH,
                                      saml_response,
                                      expect_errors=True)
        self.assertTrue(response.status_code, http_client.UNAUTHORIZED)
        self.assertDictEqual(response.json, expected_error)
        self.assertTrue(
            saml.SAML2SingleSignOnBackend._handle_verification_error.called)
        saml.SAML2SingleSignOnBackend._handle_verification_error.assert_called_with(
            expected_msg)

    @mock.patch.object(
        saml.SAML2SingleSignOnBackend, '_handle_verification_error',
        mock.MagicMock(
            side_effect=auth_exc.SSOVerificationError('See unit test.')))
    def test_idp_callback_null_relay_state(self):
        expected_error = {
            'faultstring':
            'Error encountered while verifying the SAML2 response.'
        }
        expected_msg = 'The RelayState attribute is null.'
        saml_response = {
            'SAMLResponse': ['1234567890ABCDEFG'],
            'RelayState': None
        }
        response = self.app.post_json(SSO_CALLBACK_V1_PATH,
                                      saml_response,
                                      expect_errors=True)
        self.assertTrue(response.status_code, http_client.UNAUTHORIZED)
        self.assertDictEqual(response.json, expected_error)
        self.assertTrue(
            saml.SAML2SingleSignOnBackend._handle_verification_error.called)
        saml.SAML2SingleSignOnBackend._handle_verification_error.assert_called_with(
            expected_msg)

    @mock.patch.object(
        saml.SAML2SingleSignOnBackend, '_handle_verification_error',
        mock.MagicMock(
            side_effect=auth_exc.SSOVerificationError('See unit test.')))
    def test_idp_callback_empty_relay_state(self):
        expected_error = {
            'faultstring':
            'Error encountered while verifying the SAML2 response.'
        }
        expected_msg = 'The RelayState attribute is empty.'
        saml_response = {
            'SAMLResponse': ['1234567890ABCDEFG'],
            'RelayState': []
        }
        response = self.app.post_json(SSO_CALLBACK_V1_PATH,
                                      saml_response,
                                      expect_errors=True)
        self.assertTrue(response.status_code, http_client.UNAUTHORIZED)
        self.assertDictEqual(response.json, expected_error)
        self.assertTrue(
            saml.SAML2SingleSignOnBackend._handle_verification_error.called)
        saml.SAML2SingleSignOnBackend._handle_verification_error.assert_called_with(
            expected_msg)

    @mock.patch.object(
        saml.SAML2SingleSignOnBackend, '_handle_verification_error',
        mock.MagicMock(
            side_effect=auth_exc.SSOVerificationError('See unit test.')))
    def test_idp_callback_relay_state_missing_id(self):
        expected_error = {
            'faultstring':
            'Error encountered while verifying the SAML2 response.'
        }
        expected_msg = 'The value of the RelayState in the response does not match.'
        relay_state = json.dumps({'referer': MOCK_REFERER})
        saml_response = {
            'SAMLResponse': ['1234567890ABCDEFG'],
            'RelayState': [relay_state]
        }
        response = self.app.post_json(SSO_CALLBACK_V1_PATH,
                                      saml_response,
                                      expect_errors=True)
        self.assertTrue(response.status_code, http_client.UNAUTHORIZED)
        self.assertDictEqual(response.json, expected_error)
        self.assertTrue(
            saml.SAML2SingleSignOnBackend._handle_verification_error.called)
        saml.SAML2SingleSignOnBackend._handle_verification_error.assert_called_with(
            expected_msg)

    @mock.patch.object(
        saml.SAML2SingleSignOnBackend, '_handle_verification_error',
        mock.MagicMock(
            side_effect=auth_exc.SSOVerificationError('See unit test.')))
    def test_idp_callback_relay_state_bad_id(self):
        expected_error = {
            'faultstring':
            'Error encountered while verifying the SAML2 response.'
        }
        expected_msg = 'The value of the RelayState in the response does not match.'
        relay_state = json.dumps({'id': 'foobar', 'referer': MOCK_REFERER})
        saml_response = {
            'SAMLResponse': ['1234567890ABCDEFG'],
            'RelayState': [relay_state]
        }
        response = self.app.post_json(SSO_CALLBACK_V1_PATH,
                                      saml_response,
                                      expect_errors=True)
        self.assertTrue(response.status_code, http_client.UNAUTHORIZED)
        self.assertDictEqual(response.json, expected_error)
        self.assertTrue(
            saml.SAML2SingleSignOnBackend._handle_verification_error.called)
        saml.SAML2SingleSignOnBackend._handle_verification_error.assert_called_with(
            expected_msg)

    @mock.patch.object(saml.SAML2SingleSignOnBackend, '_get_relay_state_id',
                       mock.MagicMock(return_value='12345'))
    @mock.patch.object(
        saml.SAML2SingleSignOnBackend, '_handle_verification_error',
        mock.MagicMock(
            side_effect=auth_exc.SSOVerificationError('See unit test.')))
    def test_idp_callback_relay_state_missing_referer(self):
        expected_error = {
            'faultstring':
            'Error encountered while verifying the SAML2 response.'
        }
        expected_msg = 'The value of the RelayState in the response does not match.'
        relay_state = json.dumps({'id': '12345'})
        saml_response = {
            'SAMLResponse': ['1234567890ABCDEFG'],
            'RelayState': [relay_state]
        }
        response = self.app.post_json(SSO_CALLBACK_V1_PATH,
                                      saml_response,
                                      expect_errors=True)
        self.assertTrue(response.status_code, http_client.UNAUTHORIZED)
        self.assertDictEqual(response.json, expected_error)
        self.assertTrue(
            saml.SAML2SingleSignOnBackend._handle_verification_error.called)
        saml.SAML2SingleSignOnBackend._handle_verification_error.assert_called_with(
            expected_msg)

    @mock.patch.object(saml.SAML2SingleSignOnBackend, '_get_relay_state_id',
                       mock.MagicMock(return_value='12345'))
    @mock.patch.object(
        saml.SAML2SingleSignOnBackend, '_handle_verification_error',
        mock.MagicMock(
            side_effect=auth_exc.SSOVerificationError('See unit test.')))
    def test_idp_callback_relay_state_bad_referer(self):
        expected_error = {
            'faultstring':
            'Error encountered while verifying the SAML2 response.'
        }
        expected_msg = 'The value of the RelayState in the response does not match.'
        relay_state = json.dumps({'id': '12345', 'referer': 'https://foobar'})
        saml_response = {
            'SAMLResponse': ['1234567890ABCDEFG'],
            'RelayState': [relay_state]
        }
        response = self.app.post_json(SSO_CALLBACK_V1_PATH,
                                      saml_response,
                                      expect_errors=True)
        self.assertTrue(response.status_code, http_client.UNAUTHORIZED)
        self.assertDictEqual(response.json, expected_error)
        self.assertTrue(
            saml.SAML2SingleSignOnBackend._handle_verification_error.called)
        saml.SAML2SingleSignOnBackend._handle_verification_error.assert_called_with(
            expected_msg)

    @mock.patch.object(saml2.client.Saml2Client,
                       'parse_authn_request_response',
                       mock.MagicMock(return_value=MockAuthnResponse()))
    def test_idp_callback(self):
        saml_response = {'SAMLResponse': ['1234567890ABCDEFG']}
        expected_body = st2auth.controllers.v1.sso.CALLBACK_SUCCESS_RESPONSE_BODY % MOCK_REFERER
        response = self.app.post_json(SSO_CALLBACK_V1_PATH,
                                      saml_response,
                                      expect_errors=False)
        self.assertTrue(response.status_code, http_client.OK)
        self.assertEqual(expected_body, response.body.decode('utf-8'))

    @mock.patch.object(saml.SAML2SingleSignOnBackend, '_get_relay_state_id',
                       mock.MagicMock(return_value='12345'))
    @mock.patch.object(saml2.client.Saml2Client,
                       'parse_authn_request_response',
                       mock.MagicMock(return_value=MockAuthnResponse()))
    def test_idp_callback_with_relay_state(self):
        relay_state = json.dumps({'id': '12345', 'referer': MOCK_REFERER})
        saml_response = {
            'SAMLResponse': ['1234567890ABCDEFG'],
            'RelayState': [relay_state]
        }
        expected_body = st2auth.controllers.v1.sso.CALLBACK_SUCCESS_RESPONSE_BODY % MOCK_REFERER
        response = self.app.post_json(SSO_CALLBACK_V1_PATH,
                                      saml_response,
                                      expect_errors=False)
        self.assertTrue(response.status_code, http_client.OK)
        self.assertEqual(expected_body, response.body.decode('utf-8'))
Esempio n. 4
0
 def _handle_verification_error(self, error_message):
     raise auth_exc.SSOVerificationError(error_message)
Esempio n. 5
0
    def verify_response(self, response):
        try:
            if not hasattr(response, 'SAMLResponse'):
                self._handle_verification_error(
                    'The SAMLResponse attribute is missing.')

            if getattr(response, 'SAMLResponse', None) is None:
                self._handle_verification_error(
                    'The SAMLResponse attribute is null.')

            # The SAMLResponse is an array and it cannot be empty.
            if len(getattr(response, 'SAMLResponse')) <= 0:
                self._handle_verification_error(
                    'The SAMLResponse attribute is empty.')

            # The relay state is set by the Sp -> Idp -> Sp flow. If the flow is started by the Idp,
            # the relay state is not set. Verify that the unique value passed as relay state during
            # the request step is the same given back here. The referer address should also be
            # restricted to starts with the address of the Sp (or entity id).
            has_relay_state = hasattr(response, 'RelayState')

            if has_relay_state and getattr(response, 'RelayState',
                                           None) is None:
                self._handle_verification_error(
                    'The RelayState attribute is null.')

            # The RelayState is an array and it cannot be empty.
            if has_relay_state and len(getattr(response, 'RelayState')) <= 0:
                self._handle_verification_error(
                    'The RelayState attribute is empty.')

            relay_state = json.loads(getattr(
                response, 'RelayState')[0]) if has_relay_state else {}

            if (has_relay_state and
                ('id' not in relay_state or 'referer' not in relay_state
                 or self._get_relay_state_id() != relay_state['id']
                 or not relay_state['referer'].startswith(self.entity_id))):
                error_message = 'The value of the RelayState in the response does not match.'
                self._handle_verification_error(error_message)

            # Parse the response and verify signature.
            saml_response = getattr(response, 'SAMLResponse')[0]
            saml_client = self._get_saml_client()

            authn_response = saml_client.parse_authn_request_response(
                saml_response, saml2.BINDING_HTTP_POST)

            if not authn_response:
                self._handle_verification_error(
                    'Unable to parse the data in SAMLResponse.')

            verified_user = {
                'referer': relay_state.get('referer') or self.entity_id,
                'username': str(authn_response.ava['Username'][0]),
                'email': str(authn_response.ava['Email'][0]),
                'last_name': str(authn_response.ava['LastName'][0]),
                'first_name': str(authn_response.ava['FirstName'][0])
            }
        except Exception:
            message = 'Error encountered while verifying the SAML2 response.'
            LOG.exception(message)
            raise auth_exc.SSOVerificationError(message)

        return verified_user