def setUp(self):
        super(TestBookingTokenAuthentication, self).setUp()

        with self.settings(TOKEN_AUTH=TOKEN_AUTH_SETTINGS):
            self.request = RequestFactory().get('/api/sso/redirect')

            # To keep things easy, let's just change the valid token to put some Xs
            # on it at the beginning of each of those lines.
            self.token = 'XbaTf5AVWkpkiACH6nNZZUVzZR0rye7rbiqrm3Qrgph5Sn3EwsFERytBwoj' \
                'XaqSdISPvvc7aefusFmHDXAJbwLvCJ3N73x4whT7XPiJz7kfrFKYal6WlD8' \
                'Xu5JZgVTmV5hdywGQkPMFT1Z7m4z1ga6Oud2KoQNhrf5cKzQ5CSdTojZmZ0' \
                'XT24jBuwm5YUqFbvwTBxg=='
            self.corrupt_token = self.token

            self.auth_backend = TokenAuthentication(self.request,
                                                    token=self.token)

            self.checked_token = CheckedTokenFactory.create()
            self.data = (
                'time=2013-12-23 17:51:15|username=johndoe|name=John Doe'
                '|[email protected]')

            # Get the new security keys to use it around in the tests.
            self.hmac_key = self.auth_backend.settings['hmac_key'].encode(
                'utf-8')
            self.aes_key = self.auth_backend.settings['aes_key'].encode(
                'utf-8')
 def test_authenticate_fail_no_token(self):
     """
     Tests that ``authenticate`` method raises an exception when no token
     is provided.
     """
     with self.settings(TOKEN_AUTH=TOKEN_AUTH_SETTINGS):
         auth_backend = TokenAuthentication(self.request)
         self.assertRaisesMessage(TokenAuthenticationError,
                                  'No token provided',
                                  auth_backend.authenticate)
 def test_authenticate_fail_corrupted_token(self):
     """
     Tests that ``authenticate`` method raises an exception when a corrupt
     token is received (HMAC-SHA1 checking).
     """
     with self.settings(TOKEN_AUTH=TOKEN_AUTH_SETTINGS):
         auth_backend = TokenAuthentication(self.request,
                                            token=self.corrupt_token)
         self.assertRaisesMessage(TokenAuthenticationError,
                                  'HMAC authentication failed',
                                  auth_backend.authenticate)
    def test_decrypts_message(self):
        """
        Tests the method to decrypt the AES encoded message.
        """
        with self.settings(TOKEN_AUTH=TOKEN_AUTH_SETTINGS):
            aes_message, hmac_digest = self._encode_message(self.data)
            token = base64.urlsafe_b64encode(
                aes_message + hmac_digest.digest()).decode('utf-8')
            auth_backend = TokenAuthentication(self.request, token=token)
            message = auth_backend.decrypt_message()

            self.assertEqual(
                message, {
                    'timestamp': '2013-12-23 17:51:15',
                    'first_name': 'John',
                    'last_name': 'Doe',
                    'email': '*****@*****.**',
                    'username': '******',
                    'remote_id': '*****@*****.**'
                })
    def test_authenticate_fail_token_used(self):
        """
        Tests that ``authenticate`` method raises an exception when a used
        token is provided.
        """
        with self.settings(TOKEN_AUTH=TOKEN_AUTH_SETTINGS):
            auth_backend = TokenAuthentication(self.request,
                                               token=self.checked_token.token)

            self.assertRaisesMessage(
                TokenAuthenticationError,
                'Token was already used and is not valid',
                auth_backend.authenticate)
    def test_authenticate_successful_login(self):
        """
        Tests ``authenticate`` method when it performs a successful login.
        """
        with self.settings(TOKEN_AUTH=TOKEN_AUTH_SETTINGS):
            timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
            message = 'time={0}|username=johndoe|name=John Doe|' \
                      '[email protected]'.format(timestamp)
            aes_message, hmac_digest = self._encode_message(message)
            token = base64.urlsafe_b64encode(
                aes_message + hmac_digest.digest()).decode('utf-8')

            auth_backend = TokenAuthentication(self.request, token=token)
            user, created = auth_backend.authenticate()

            # Check created user data.
            self.assertEqual(user.first_name, 'John')
            self.assertEqual(user.is_active, True)

            # Check `CheckedToken` related object.
            checked_token = CheckedToken.objects.latest('pk')
            self.assertEqual(checked_token.token, token)
            self.assertEqual(checked_token.user.username, user.username)
    def test_authenticate_fail_token_expired(self):
        """
        Tests that ``authenticate`` method raises an exception when the token
        expired.
        """
        with self.settings(TOKEN_AUTH=TOKEN_AUTH_SETTINGS):
            # Set up a token with an old date (year 2012).
            message = 'time=2012-12-18 11:51:15|username=johndoe|name=John Doe|' \
                      '[email protected]'
            aes_message, hmac_digest = self._encode_message(message)
            token = base64.urlsafe_b64encode(
                aes_message + hmac_digest.digest()).decode('utf-8')

            auth_backend = TokenAuthentication(self.request, token=token)

            self.assertRaisesMessage(TokenAuthenticationError,
                                     'Authentication token expired',
                                     auth_backend.authenticate)
    def test_authenticate_fail_invalid_login_data(self):
        """
        Tests that ``authenticate`` method raises an exception when a valid
        token was received but it didn't contained valid authentication data,
        so the message contained in the token was not as expected.
        """
        with self.settings(TOKEN_AUTH=TOKEN_AUTH_SETTINGS):
            message = 'xxxx=2013-12-18 11:51:15|xxxxxxxx=johndoe|xxxx=John Doe|' \
                      '[email protected]'
            aes_message, hmac_digest = self._encode_message(message)
            token = base64.urlsafe_b64encode(
                aes_message + hmac_digest.digest()).decode('utf-8')
            auth_backend = TokenAuthentication(self.request, token=token)

            self.assertRaisesMessage(
                TokenAuthenticationError,
                'Message does not contain valid login data',
                auth_backend.authenticate)
class TestBookingTokenAuthentication(BluebottleTestCase):
    """
    Tests the Token Authentication backend.
    """
    def setUp(self):
        super(TestBookingTokenAuthentication, self).setUp()

        with self.settings(TOKEN_AUTH=TOKEN_AUTH_SETTINGS):
            self.request = RequestFactory().get('/api/sso/redirect')

            # To keep things easy, let's just change the valid token to put some Xs
            # on it at the beginning of each of those lines.
            self.token = 'XbaTf5AVWkpkiACH6nNZZUVzZR0rye7rbiqrm3Qrgph5Sn3EwsFERytBwoj' \
                'XaqSdISPvvc7aefusFmHDXAJbwLvCJ3N73x4whT7XPiJz7kfrFKYal6WlD8' \
                'Xu5JZgVTmV5hdywGQkPMFT1Z7m4z1ga6Oud2KoQNhrf5cKzQ5CSdTojZmZ0' \
                'XT24jBuwm5YUqFbvwTBxg=='
            self.corrupt_token = self.token

            self.auth_backend = TokenAuthentication(self.request,
                                                    token=self.token)

            self.checked_token = CheckedTokenFactory.create()
            self.data = (
                'time=2013-12-23 17:51:15|username=johndoe|name=John Doe'
                '|[email protected]')

            # Get the new security keys to use it around in the tests.
            self.hmac_key = self.auth_backend.settings['hmac_key'].encode(
                'utf-8')
            self.aes_key = self.auth_backend.settings['aes_key'].encode(
                'utf-8')

    def _encode_message(self, message):
        """
        Helper method for unit tests which returns an encoded version of the
        message passed as an argument.

        It returns a tuple containing a string formed by two elements:

        1. A string formed by the initialization vector and the AES-128
        encrypted message.
        2. The HMAC-SHA1 hash of that string.
        """
        pad = lambda s: s + (AES.block_size - len(s) % AES.block_size) * chr(
            AES.block_size - len(s) % AES.block_size)
        init_vector = Random.new().read(AES.block_size)
        cipher = AES.new(self.aes_key, AES.MODE_CBC, init_vector)
        padded_message = pad(message).encode('utf-8')
        aes_message = init_vector + cipher.encrypt(padded_message)
        hmac_digest = hmac.new(self.hmac_key, aes_message, hashlib.sha1)

        return aes_message, hmac_digest

    def test_sso_url(self):
        with self.settings(TOKEN_AUTH=TOKEN_AUTH_SETTINGS):
            self.assertEqual(self.auth_backend.sso_url(),
                             TOKEN_AUTH_SETTINGS['sso_url'])

    def test_sso_url_custom_target(self):
        with self.settings(TOKEN_AUTH=TOKEN_AUTH_SETTINGS):
            self.assertEqual(
                self.auth_backend.sso_url(target_url='/test/'),
                TOKEN_AUTH_SETTINGS['sso_url'] + '?url=%2Ftest%2F')

    def test_sso_url_custom_target_unicode(self):
        with self.settings(TOKEN_AUTH=TOKEN_AUTH_SETTINGS):
            self.assertEqual(
                self.auth_backend.sso_url(target_url=u'/test/\u2026/bla'),
                TOKEN_AUTH_SETTINGS['sso_url'] +
                '?url=%2Ftest%2F%E2%80%A6%2Fbla')

    def test_check_hmac_signature_ok(self):
        """
        Tests that the method to check up HMAC signature of the token message
        returns True when it is a valid signature.
        """
        message = base64.urlsafe_b64decode(self.checked_token.token)
        self.assertTrue(self.auth_backend.check_hmac_signature(message))

    def test_check_hmac_signature_wrong(self):
        """
        Tests the method to check up HMAC signature when the token is corrupted
        and the signatures is not valid.
        """
        message = base64.b64decode(self.corrupt_token)

        self.assertFalse(self.auth_backend.check_hmac_signature(message))

    def test_decrypts_message(self):
        """
        Tests the method to decrypt the AES encoded message.
        """
        with self.settings(TOKEN_AUTH=TOKEN_AUTH_SETTINGS):
            aes_message, hmac_digest = self._encode_message(self.data)
            token = base64.urlsafe_b64encode(
                aes_message + hmac_digest.digest()).decode('utf-8')
            auth_backend = TokenAuthentication(self.request, token=token)
            message = auth_backend.decrypt_message()

            self.assertEqual(
                message, {
                    'timestamp': '2013-12-23 17:51:15',
                    'first_name': 'John',
                    'last_name': 'Doe',
                    'email': '*****@*****.**',
                    'username': '******',
                    'remote_id': '*****@*****.**'
                })

    def test_get_login_data(self):
        """
        Tests the method to split the login message data into a 4-field tuple.
        """
        login_data = self.auth_backend.get_login_data(self.data)

        self.assertTupleEqual(login_data, ('2013-12-23 17:51:15', 'johndoe',
                                           'John Doe', '*****@*****.**'))

    def test_check_timestamp_valid_token(self):
        """
        Tests the method to check the login message timestamp when a good
        token is received.
        """
        login_time = (datetime.now() -
                      timedelta(seconds=10)).strftime('%Y-%m-%d %H:%M:%S')
        self.auth_backend.check_timestamp({'timestamp': login_time})

    def test_check_timestamp_timedout_token(self):
        """
        Tests the method to check the login message timestamp when a wrong
        timestamp is given.
        """
        with self.settings(TOKEN_AUTH=TOKEN_AUTH_SETTINGS):
            login_time = (
                datetime.now() -
                timedelta(days=self.auth_backend.settings['token_expiration'] +
                          1)).strftime('%Y-%m-%d %H:%M:%S')

            self.assertRaises(TokenAuthenticationError,
                              self.auth_backend.check_timestamp,
                              {'timestamp': login_time})

    def test_authenticate_fail_no_token(self):
        """
        Tests that ``authenticate`` method raises an exception when no token
        is provided.
        """
        with self.settings(TOKEN_AUTH=TOKEN_AUTH_SETTINGS):
            auth_backend = TokenAuthentication(self.request)
            self.assertRaisesMessage(TokenAuthenticationError,
                                     'No token provided',
                                     auth_backend.authenticate)

    def test_authenticate_fail_token_used(self):
        """
        Tests that ``authenticate`` method raises an exception when a used
        token is provided.
        """
        with self.settings(TOKEN_AUTH=TOKEN_AUTH_SETTINGS):
            auth_backend = TokenAuthentication(self.request,
                                               token=self.checked_token.token)

            self.assertRaisesMessage(
                TokenAuthenticationError,
                'Token was already used and is not valid',
                auth_backend.authenticate)

    def test_authenticate_fail_corrupted_token(self):
        """
        Tests that ``authenticate`` method raises an exception when a corrupt
        token is received (HMAC-SHA1 checking).
        """
        with self.settings(TOKEN_AUTH=TOKEN_AUTH_SETTINGS):
            auth_backend = TokenAuthentication(self.request,
                                               token=self.corrupt_token)
            self.assertRaisesMessage(TokenAuthenticationError,
                                     'HMAC authentication failed',
                                     auth_backend.authenticate)

    def test_authenticate_fail_invalid_login_data(self):
        """
        Tests that ``authenticate`` method raises an exception when a valid
        token was received but it didn't contained valid authentication data,
        so the message contained in the token was not as expected.
        """
        with self.settings(TOKEN_AUTH=TOKEN_AUTH_SETTINGS):
            message = 'xxxx=2013-12-18 11:51:15|xxxxxxxx=johndoe|xxxx=John Doe|' \
                      '[email protected]'
            aes_message, hmac_digest = self._encode_message(message)
            token = base64.urlsafe_b64encode(
                aes_message + hmac_digest.digest()).decode('utf-8')
            auth_backend = TokenAuthentication(self.request, token=token)

            self.assertRaisesMessage(
                TokenAuthenticationError,
                'Message does not contain valid login data',
                auth_backend.authenticate)

    def test_authenticate_fail_token_expired(self):
        """
        Tests that ``authenticate`` method raises an exception when the token
        expired.
        """
        with self.settings(TOKEN_AUTH=TOKEN_AUTH_SETTINGS):
            # Set up a token with an old date (year 2012).
            message = 'time=2012-12-18 11:51:15|username=johndoe|name=John Doe|' \
                      '[email protected]'
            aes_message, hmac_digest = self._encode_message(message)
            token = base64.urlsafe_b64encode(
                aes_message + hmac_digest.digest()).decode('utf-8')

            auth_backend = TokenAuthentication(self.request, token=token)

            self.assertRaisesMessage(TokenAuthenticationError,
                                     'Authentication token expired',
                                     auth_backend.authenticate)

    def test_authenticate_successful_login(self):
        """
        Tests ``authenticate`` method when it performs a successful login.
        """
        with self.settings(TOKEN_AUTH=TOKEN_AUTH_SETTINGS):
            timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
            message = 'time={0}|username=johndoe|name=John Doe|' \
                      '[email protected]'.format(timestamp)
            aes_message, hmac_digest = self._encode_message(message)
            token = base64.urlsafe_b64encode(
                aes_message + hmac_digest.digest()).decode('utf-8')

            auth_backend = TokenAuthentication(self.request, token=token)
            user, created = auth_backend.authenticate()

            # Check created user data.
            self.assertEqual(user.first_name, 'John')
            self.assertEqual(user.is_active, True)

            # Check `CheckedToken` related object.
            checked_token = CheckedToken.objects.latest('pk')
            self.assertEqual(checked_token.token, token)
            self.assertEqual(checked_token.user.username, user.username)

    @mock.patch.object(get_user_model(),
                       'get_jwt_token',
                       create=True,
                       return_value='test-token')
    def test_login_view(self, get_jwt_token):
        """
        Test the login view for booking
        """
        with self.settings(TOKEN_AUTH=TOKEN_AUTH_SETTINGS):
            timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
            message = 'time={0}|username=johndoe|name=John Doe|' \
                      '[email protected]'.format(timestamp)
            aes_message, hmac_digest = self._encode_message(message)
            token = base64.urlsafe_b64encode(
                aes_message + hmac_digest.digest()).decode('utf-8')

            login_url = reverse('token-login', kwargs={'token': token})
            response = self.client.get(login_url)
            self.assertEqual(response.status_code, 200)
            self.assertContains(response, 'var token = "test-token";')
            self.assertContains(response, 'storeToken')
            user = Member.objects.get(email='*****@*****.**')
            self.assertEqual(user.remote_id, '*****@*****.**')

    @mock.patch.object(get_user_model(),
                       'get_jwt_token',
                       create=True,
                       return_value='test-token')
    def test_link_view(self, get_jwt_token):
        """
        Test the link view for booking
        """
        with self.settings(TOKEN_AUTH=TOKEN_AUTH_SETTINGS):
            timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
            message = 'time={0}|username=johndoe|name=John Doe|' \
                      '[email protected]'.format(timestamp)
            aes_message, hmac_digest = self._encode_message(message)
            token = base64.urlsafe_b64encode(
                aes_message + hmac_digest.digest()).decode('utf-8')

            login_url = reverse('token-login-link',
                                kwargs={
                                    'token': token,
                                    'link': '/projects/my-project'
                                })
            response = self.client.get(login_url)
            self.assertEqual(response.status_code, 200)
            self.assertContains(response, 'var token = "test-token";')
            self.assertContains(response, 'storeToken')
            user = Member.objects.get(email='*****@*****.**')
            self.assertEqual(user.remote_id, '*****@*****.**')

    def test_redirect_view(self):
        """
        Test the redirect view for booking
        """
        with self.settings(TOKEN_AUTH=TOKEN_AUTH_SETTINGS):
            redirect_url = reverse('token-redirect')
            response = self.client.get(redirect_url,
                                       {'url': '/projects/my-project'})
            self.assertEqual(response.status_code, 302)
            self.assertEqual(
                response['Location'],
                "https://example.org?url=%2Fprojects%2Fmy-project")