예제 #1
0
class TestExternalProviderOAuth2(OsfTestCase):
    # Test functionality of the ExternalProvider class, for OAuth 2.0

    def setUp(self):
        super(TestExternalProviderOAuth2, self).setUp()
        self.user = UserFactory()
        self.provider = MockOAuth2Provider()

    def tearDown(self):
        ExternalAccount._clear_caches()
        ExternalAccount.remove()
        self.user.remove()
        super(TestExternalProviderOAuth2, self).tearDown()

    def test_oauth_version_default(self):
        # OAuth 2.0 is the default version
        assert_is(self.provider._oauth_version, OAUTH2)

    def test_start_flow(self):
        # Generate the appropriate URL and state token

        with self.app.app.test_request_context("/oauth/connect/mock2/"):

            # make sure the user is logged in
            authenticate(user=self.user, access_token=None, response=None)

            # auth_url is a property method - it calls out to the external
            #   service to get a temporary key and secret before returning the
            #   auth url
            url = self.provider.auth_url

            # Temporary credentials are added to the session
            creds = session.data['oauth_states'][self.provider.short_name]
            assert_in('state', creds)

            # The URL to which the user would be redirected
            parsed = urlparse.urlparse(url)
            params = urlparse.parse_qs(parsed.query)

            # check parameters
            assert_equal(
                params, {
                    'state': [creds['state']],
                    'response_type': ['code'],
                    'client_id': [self.provider.client_id],
                    'redirect_uri': [
                        web_url_for('oauth_callback',
                                    service_name=self.provider.short_name,
                                    _absolute=True)
                    ]
                })

            # check base URL
            assert_equal(
                url.split("?")[0],
                "https://mock2.com/auth",
            )

    @httpretty.activate
    def test_callback(self):
        # Exchange temporary credentials for permanent credentials

        # Mock the exchange of the code for an access token
        _prepare_mock_oauth2_handshake_response()

        user = UserFactory()

        # Fake a request context for the callback
        with self.app.app.test_request_context(
                path="/oauth/callback/mock2/",
                query_string="code=mock_code&state=mock_state"):

            # make sure the user is logged in
            authenticate(user=self.user, access_token=None, response=None)

            session.data['oauth_states'] = {
                self.provider.short_name: {
                    'state': 'mock_state',
                },
            }
            session.save()

            # do the key exchange

            self.provider.auth_callback(user=user)

        account = ExternalAccount.find_one()
        assert_equal(account.oauth_key, 'mock_access_token')
        assert_equal(account.provider_id, 'mock_provider_id')

    @httpretty.activate
    def test_provider_down(self):

        # Create a 500 error
        _prepare_mock_500_error()

        user = UserFactory()
        # Fake a request context for the callback
        with self.app.app.test_request_context(
                path="/oauth/callback/mock2/",
                query_string="code=mock_code&state=mock_state"):
            # make sure the user is logged in
            authenticate(user=user, access_token=None, response=None)

            session.data['oauth_states'] = {
                self.provider.short_name: {
                    'state': 'mock_state',
                },
            }
            session.save()

            # do the key exchange

            with assert_raises(HTTPError) as error_raised:
                self.provider.auth_callback(user=user)

            assert_equal(
                error_raised.exception.code,
                503,
            )

    @httpretty.activate
    def test_user_denies_access(self):

        # Create a 401 error
        _prepare_mock_401_error()

        user = UserFactory()
        # Fake a request context for the callback
        with self.app.app.test_request_context(
                path="/oauth/callback/mock2/",
                query_string="error=mock_error&code=mock_code&state=mock_state"
        ):
            # make sure the user is logged in
            authenticate(user=user, access_token=None, response=None)

            session.data['oauth_states'] = {
                self.provider.short_name: {
                    'state': 'mock_state',
                },
            }
            session.save()

            assert_false(self.provider.auth_callback(user=user))

    @httpretty.activate
    def test_multiple_users_associated(self):
        # Create only one ExternalAccount for multiple OSF users
        #
        # For some providers (ex: GitHub), the act of completing the OAuth flow
        # revokes previously generated credentials. In addition, there is often no
        # way to know the user's id on the external service until after the flow
        # has completed.
        #
        # Having only one ExternalAccount instance per account on the external
        # service means that connecting subsequent OSF users to the same external
        # account will not invalidate the credentials used by the OSF for users
        # already associated.
        user_a = UserFactory()
        external_account = ExternalAccountFactory(
            provider='mock2',
            provider_id='mock_provider_id',
            provider_name='Mock Provider',
        )
        user_a.external_accounts.append(external_account)
        user_a.save()

        user_b = UserFactory()

        # Mock the exchange of the code for an access token
        _prepare_mock_oauth2_handshake_response()

        # Fake a request context for the callback
        with self.app.app.test_request_context(
                path="/oauth/callback/mock2/",
                query_string="code=mock_code&state=mock_state") as ctx:

            # make sure the user is logged in
            authenticate(user=user_b, access_token=None, response=None)

            session.data['oauth_states'] = {
                self.provider.short_name: {
                    'state': 'mock_state',
                },
            }
            session.save()

            # do the key exchange
            self.provider.auth_callback(user=user_b)

        user_a.reload()
        user_b.reload()
        external_account.reload()

        assert_equal(
            user_a.external_accounts,
            user_b.external_accounts,
        )

        assert_equal(ExternalAccount.find().count(), 1)

    @httpretty.activate
    def test_force_refresh_oauth_key(self):
        external_account = ExternalAccountFactory(
            provider='mock2',
            provider_id='mock_provider_id',
            provider_name='Mock Provider',
            oauth_key='old_key',
            oauth_secret='old_secret',
            expires_at=datetime.utcfromtimestamp(time.time() - 200))

        # mock a successful call to the provider to refresh tokens
        httpretty.register_uri(httpretty.POST,
                               self.provider.auto_refresh_url,
                               body=json.dumps({
                                   'access_token':
                                   'refreshed_access_token',
                                   'expires_in':
                                   3600,
                                   'refresh_token':
                                   'refreshed_refresh_token'
                               }))

        old_expiry = external_account.expires_at
        self.provider.account = external_account
        self.provider.refresh_oauth_key(force=True)
        external_account.reload()

        assert_equal(external_account.oauth_key, 'refreshed_access_token')
        assert_equal(external_account.refresh_token, 'refreshed_refresh_token')
        assert_not_equal(external_account.expires_at, old_expiry)
        assert_true(external_account.expires_at > old_expiry)

    @httpretty.activate
    def test_does_need_refresh(self):
        external_account = ExternalAccountFactory(
            provider='mock2',
            provider_id='mock_provider_id',
            provider_name='Mock Provider',
            oauth_key='old_key',
            oauth_secret='old_secret',
            expires_at=datetime.utcfromtimestamp(time.time() - 200),
        )

        # mock a successful call to the provider to refresh tokens
        httpretty.register_uri(httpretty.POST,
                               self.provider.auto_refresh_url,
                               body=json.dumps({
                                   'access_token':
                                   'refreshed_access_token',
                                   'expires_in':
                                   3600,
                                   'refresh_token':
                                   'refreshed_refresh_token'
                               }))

        old_expiry = external_account.expires_at
        self.provider.account = external_account
        self.provider.refresh_oauth_key(force=False)
        external_account.reload()

        assert_equal(external_account.oauth_key, 'refreshed_access_token')
        assert_equal(external_account.refresh_token, 'refreshed_refresh_token')
        assert_not_equal(external_account.expires_at, old_expiry)
        assert_true(external_account.expires_at > old_expiry)

    @httpretty.activate
    def test_does_not_need_refresh(self):
        self.provider.refresh_time = 1
        external_account = ExternalAccountFactory(
            provider='mock2',
            provider_id='mock_provider_id',
            provider_name='Mock Provider',
            oauth_key='old_key',
            oauth_secret='old_secret',
            refresh_token='old_refresh',
            expires_at=datetime.utcfromtimestamp(time.time() + 200),
        )

        # mock a successful call to the provider to refresh tokens
        httpretty.register_uri(httpretty.POST,
                               self.provider.auto_refresh_url,
                               body=json.dumps(
                                   {'err_msg': 'Should not be hit'}),
                               status=500)

        # .reload() has the side effect of rounding the microsends down to 3 significant figures
        # (e.g. DT(YMDHMS, 365420) becomes DT(YMDHMS, 365000)),
        # but must occur after possible refresh to reload tokens.
        # Doing so before allows the `old_expiry == EA.expires_at` comparison to work.
        external_account.reload()
        old_expiry = external_account.expires_at
        self.provider.account = external_account
        self.provider.refresh_oauth_key(force=False)
        external_account.reload()

        assert_equal(external_account.oauth_key, 'old_key')
        assert_equal(external_account.refresh_token, 'old_refresh')
        assert_equal(external_account.expires_at, old_expiry)

    @httpretty.activate
    def test_refresh_oauth_key_does_not_need_refresh(self):
        external_account = ExternalAccountFactory(
            provider='mock2',
            provider_id='mock_provider_id',
            provider_name='Mock Provider',
            oauth_key='old_key',
            oauth_secret='old_secret',
            expires_at=0  # causes `.needs_refresh()` to return False
        )

        # mock a successful call to the provider to refresh tokens
        httpretty.register_uri(httpretty.POST,
                               self.provider.auto_refresh_url,
                               body=json.dumps(
                                   {'err_msg': 'Should not be hit'}),
                               status=500)

        self.provider.account = external_account
        ret = self.provider.refresh_oauth_key(force=False)
        assert_false(ret)

    @httpretty.activate
    def test_refresh_with_broken_provider(self):
        external_account = ExternalAccountFactory(
            provider='mock2',
            provider_id='mock_provider_id',
            provider_name='Mock Provider',
            oauth_key='old_key',
            oauth_secret='old_secret',
            expires_at=datetime.utcfromtimestamp(time.time() - 200))
        self.provider.client_id = None
        self.provider.client_secret = None
        self.provider.account = external_account

        # mock a successful call to the provider to refresh tokens
        httpretty.register_uri(httpretty.POST,
                               self.provider.auto_refresh_url,
                               body=json.dumps(
                                   {'err_msg': 'Should not be hit'}),
                               status=500)

        ret = self.provider.refresh_oauth_key(force=False)
        assert_false(ret)

    @httpretty.activate
    def test_refresh_without_account_or_refresh_url(self):
        external_account = ExternalAccountFactory(
            provider='mock2',
            provider_id='mock_provider_id',
            provider_name='Mock Provider',
            oauth_key='old_key',
            oauth_secret='old_secret',
            expires_at=datetime.utcfromtimestamp(time.time() + 200))

        # mock a successful call to the provider to refresh tokens
        httpretty.register_uri(httpretty.POST,
                               self.provider.auto_refresh_url,
                               body=json.dumps(
                                   {'err_msg': 'Should not be hit'}),
                               status=500)

        ret = self.provider.refresh_oauth_key(force=False)
        assert_false(ret)

    @httpretty.activate
    def test_refresh_with_expired_credentials(self):
        external_account = ExternalAccountFactory(
            provider='mock2',
            provider_id='mock_provider_id',
            provider_name='Mock Provider',
            oauth_key='old_key',
            oauth_secret='old_secret',
            expires_at=datetime.utcfromtimestamp(
                time.time() -
                10000)  # Causes has_expired_credentials to be True
        )
        self.provider.account = external_account

        # mock a successful call to the provider to refresh tokens
        httpretty.register_uri(httpretty.POST,
                               self.provider.auto_refresh_url,
                               body=json.dumps({'err': 'Should not be hit'}),
                               status=500)

        ret = self.provider.refresh_oauth_key(force=False)
        assert_false(ret)

    @httpretty.activate
    def test_force_refresh_with_expired_credentials(self):
        external_account = ExternalAccountFactory(
            provider='mock2',
            provider_id='mock_provider_id',
            provider_name='Mock Provider',
            oauth_key='old_key',
            oauth_secret='old_secret',
            expires_at=datetime.utcfromtimestamp(
                time.time() -
                10000)  # Causes has_expired_credentials to be True
        )
        self.provider.account = external_account

        # mock a failing call to the provider to refresh tokens
        httpretty.register_uri(httpretty.POST,
                               self.provider.auto_refresh_url,
                               body=json.dumps({
                                   'error': 'invalid_grant',
                               }),
                               status=401)

        with assert_raises(OAuth2Error):
            ret = self.provider.refresh_oauth_key(force=True)
예제 #2
0
class TestExternalProviderOAuth1(OsfTestCase):
    # Test functionality of the ExternalProvider class, for OAuth 1.0a

    def setUp(self):
        super(TestExternalProviderOAuth1, self).setUp()
        self.user = UserFactory()
        self.provider = MockOAuth1Provider()

    def tearDown(self):
        ExternalAccount.remove()
        self.user.remove()
        super(TestExternalProviderOAuth1, self).tearDown()

    @httpretty.activate
    def test_start_flow(self):
        # Request temporary credentials from provider, provide auth redirect
        httpretty.register_uri(httpretty.POST,
                               'http://mock1a.com/request',
                               body='{"oauth_token_secret": "temp_secret", '
                               '"oauth_token": "temp_token", '
                               '"oauth_callback_confirmed": "true"}',
                               status=200,
                               content_type='application/json')

        with self.app.app.test_request_context('/oauth/connect/mock1a/'):

            # make sure the user is logged in
            authenticate(user=self.user, access_token=None, response=None)

            # auth_url is a property method - it calls out to the external
            #   service to get a temporary key and secret before returning the
            #   auth url
            url = self.provider.auth_url

            # The URL to which the user would be redirected
            assert_equal(url, "http://mock1a.com/auth?oauth_token=temp_token")

            # Temporary credentials are added to the session
            creds = session.data['oauth_states'][self.provider.short_name]
            assert_equal(creds['token'], 'temp_token')
            assert_equal(creds['secret'], 'temp_secret')

    @httpretty.activate
    def test_callback(self):
        # Exchange temporary credentials for permanent credentials

        # mock a successful call to the provider to exchange temp keys for
        #   permanent keys
        httpretty.register_uri(
            httpretty.POST,
            'http://mock1a.com/callback',
            body=('oauth_token=perm_token'
                  '&oauth_token_secret=perm_secret'
                  '&oauth_callback_confirmed=true'),
        )

        user = UserFactory()

        # Fake a request context for the callback
        ctx = self.app.app.test_request_context(
            path='/oauth/callback/mock1a/',
            query_string='oauth_token=temp_key&oauth_verifier=mock_verifier',
        )
        with ctx:

            # make sure the user is logged in
            authenticate(user=user, access_token=None, response=None)

            session.data['oauth_states'] = {
                self.provider.short_name: {
                    'token': 'temp_key',
                    'secret': 'temp_secret',
                },
            }
            session.save()

            # do the key exchange
            self.provider.auth_callback(user=user)

        account = ExternalAccount.find_one()
        assert_equal(account.oauth_key, 'perm_token')
        assert_equal(account.oauth_secret, 'perm_secret')
        assert_equal(account.provider_id, 'mock_provider_id')
        assert_equal(account.provider_name, 'Mock OAuth 1.0a Provider')

    @httpretty.activate
    def test_callback_wrong_user(self):
        # Reject temporary credentials not assigned to the user
        #
        # This prohibits users from associating their external account with
        # another user's OSF account by using XSS or similar attack vector to
        # complete the OAuth flow using the logged-in user but their own account
        # on the external service.
        #
        # If the OSF were to allow login via OAuth with the provider in question,
        # this would allow attackers to hijack OSF accounts with a simple script
        # injection.

        # mock a successful call to the provider to exchange temp keys for
        #   permanent keys
        httpretty.register_uri(
            httpretty.POST,
            'http://mock1a.com/callback',
            body='oauth_token=perm_token'
            '&oauth_token_secret=perm_secret'
            '&oauth_callback_confirmed=true',
        )

        user = UserFactory()
        account = ExternalAccountFactory(provider="mock1a",
                                         provider_name='Mock 1A',
                                         oauth_key="temp_key",
                                         oauth_secret="temp_secret",
                                         temporary=True)
        account.save()
        # associate this ExternalAccount instance with the user
        user.external_accounts.append(account)
        user.save()

        malicious_user = UserFactory()

        # Fake a request context for the callback
        with self.app.app.test_request_context(
                path="/oauth/callback/mock1a/",
                query_string="oauth_token=temp_key&oauth_verifier=mock_verifier"
        ):
            # make sure the user is logged in
            authenticate(user=malicious_user, access_token=None, response=None)

            with assert_raises(PermissionsError):
                # do the key exchange
                self.provider.auth_callback(user=malicious_user)
예제 #3
0
class TestExternalProviderOAuth2(OsfTestCase):
    # Test functionality of the ExternalProvider class, for OAuth 2.0

    def setUp(self):
        super(TestExternalProviderOAuth2, self).setUp()
        self.user = UserFactory()
        self.provider = MockOAuth2Provider()

    def tearDown(self):
        ExternalAccount._clear_caches()
        ExternalAccount.remove()
        self.user.remove()
        super(TestExternalProviderOAuth2, self).tearDown()

    def test_oauth_version_default(self):
        # OAuth 2.0 is the default version
        assert_is(self.provider._oauth_version, OAUTH2)

    def test_start_flow(self):
        # Generate the appropriate URL and state token

        with self.app.app.test_request_context("/oauth/connect/mock2/"):

            # make sure the user is logged in
            authenticate(user=self.user, access_token=None, response=None)

            # auth_url is a property method - it calls out to the external
            #   service to get a temporary key and secret before returning the
            #   auth url
            url = self.provider.auth_url

            session = get_session()

            # Temporary credentials are added to the session
            creds = session.data['oauth_states'][self.provider.short_name]
            assert_in('state', creds)

            # The URL to which the user would be redirected
            parsed = urlparse.urlparse(url)
            params = urlparse.parse_qs(parsed.query)

            # check parameters
            assert_equal(
                params,
                {
                    'state': [creds['state']],
                    'response_type': ['code'],
                    'client_id': [self.provider.client_id],
                    'redirect_uri': [
                        web_url_for('oauth_callback',
                                    service_name=self.provider.short_name,
                                    _absolute=True)
                    ]
                }
            )

            # check base URL
            assert_equal(
                url.split("?")[0],
                "https://mock2.com/auth",
            )

    @httpretty.activate
    def test_callback(self):
        # Exchange temporary credentials for permanent credentials

        # Mock the exchange of the code for an access token
        _prepare_mock_oauth2_handshake_response()

        user = UserFactory()

        # Fake a request context for the callback
        with self.app.app.test_request_context(
                path="/oauth/callback/mock2/",
                query_string="code=mock_code&state=mock_state"
        ):

            # make sure the user is logged in
            authenticate(user=self.user, access_token=None, response=None)

            session = get_session()
            session.data['oauth_states'] = {
                self.provider.short_name: {
                    'state': 'mock_state',
                },
            }
            session.save()

            # do the key exchange

            self.provider.auth_callback(user=user)

        account = ExternalAccount.find_one()
        assert_equal(account.oauth_key, 'mock_access_token')
        assert_equal(account.provider_id, 'mock_provider_id')

    @httpretty.activate
    def test_provider_down(self):

        # Create a 500 error
        _prepare_mock_500_error()

        user = UserFactory()
        # Fake a request context for the callback
        with self.app.app.test_request_context(
                path="/oauth/callback/mock2/",
                query_string="code=mock_code&state=mock_state"
        ):
            # make sure the user is logged in
            authenticate(user=user, access_token=None, response=None)

            session = get_session()
            session.data['oauth_states'] = {
                self.provider.short_name: {
                    'state': 'mock_state',
                },
            }
            session.save()

            # do the key exchange

            with assert_raises(HTTPError) as error_raised:
                self.provider.auth_callback(user=user)

            assert_equal(
                error_raised.exception.code,
                503,
            )

    @httpretty.activate
    def test_multiple_users_associated(self):
        # Create only one ExternalAccount for multiple OSF users
        #
        # For some providers (ex: GitHub), the act of completing the OAuth flow
        # revokes previously generated credentials. In addition, there is often no
        # way to know the user's id on the external service until after the flow
        # has completed.
        #
        # Having only one ExternalAccount instance per account on the external
        # service means that connecting subsequent OSF users to the same external
        # account will not invalidate the credentials used by the OSF for users
        # already associated.
        user_a = UserFactory()
        external_account = ExternalAccountFactory(
            provider='mock2',
            provider_id='mock_provider_id',
            provider_name='Mock Provider',
        )
        user_a.external_accounts.append(external_account)
        user_a.save()

        user_b = UserFactory()

        # Mock the exchange of the code for an access token
        _prepare_mock_oauth2_handshake_response()

        # Fake a request context for the callback
        with self.app.app.test_request_context(
                path="/oauth/callback/mock2/",
                query_string="code=mock_code&state=mock_state"
        ) as ctx:

            # make sure the user is logged in
            authenticate(user=user_b, access_token=None, response=None)

            session = get_session()
            session.data['oauth_states'] = {
                self.provider.short_name: {
                    'state': 'mock_state',
                },
            }
            session.save()

            # do the key exchange
            self.provider.auth_callback(user=user_b)

        user_a.reload()
        user_b.reload()
        external_account.reload()

        assert_equal(
            user_a.external_accounts,
            user_b.external_accounts,
        )

        assert_equal(
            ExternalAccount.find().count(),
            1
        )
예제 #4
0
class TestExternalProviderOAuth1(OsfTestCase):
    # Test functionality of the ExternalProvider class, for OAuth 1.0a

    def setUp(self):
        super(TestExternalProviderOAuth1, self).setUp()
        self.user = UserFactory()
        self.provider = MockOAuth1Provider()

    def tearDown(self):
        ExternalAccount.remove()
        self.user.remove()
        super(TestExternalProviderOAuth1, self).tearDown()

    @httpretty.activate
    def test_start_flow(self):
        # Request temporary credentials from provider, provide auth redirect
        httpretty.register_uri(httpretty.POST, 'http://mock1a.com/request',
                  body='{"oauth_token_secret": "temp_secret", '
                       '"oauth_token": "temp_token", '
                       '"oauth_callback_confirmed": "true"}',
                  status=200,
                  content_type='application/json')

        with self.app.app.test_request_context('/oauth/connect/mock1a/'):

            # make sure the user is logged in
            authenticate(user=self.user, access_token=None, response=None)

            # auth_url is a property method - it calls out to the external
            #   service to get a temporary key and secret before returning the
            #   auth url
            url = self.provider.auth_url

            # The URL to which the user would be redirected
            assert_equal(url, "http://mock1a.com/auth?oauth_token=temp_token")

            session = get_session()

            # Temporary credentials are added to the session
            creds = session.data['oauth_states'][self.provider.short_name]
            assert_equal(creds['token'], 'temp_token')
            assert_equal(creds['secret'], 'temp_secret')

    @httpretty.activate
    def test_callback(self):
        # Exchange temporary credentials for permanent credentials

        # mock a successful call to the provider to exchange temp keys for
        #   permanent keys
        httpretty.register_uri(
            httpretty.POST,
            'http://mock1a.com/callback',
            body=(
                'oauth_token=perm_token'
                '&oauth_token_secret=perm_secret'
                '&oauth_callback_confirmed=true'
            ),
        )

        user = UserFactory()

        # Fake a request context for the callback
        ctx = self.app.app.test_request_context(
            path='/oauth/callback/mock1a/',
            query_string='oauth_token=temp_key&oauth_verifier=mock_verifier',
        )
        with ctx:

            # make sure the user is logged in
            authenticate(user=user, access_token=None, response=None)

            session = get_session()
            session.data['oauth_states'] = {
                self.provider.short_name: {
                    'token': 'temp_key',
                    'secret': 'temp_secret',
                },
            }
            session.save()

            # do the key exchange
            self.provider.auth_callback(user=user)

        account = ExternalAccount.find_one()
        assert_equal(account.oauth_key, 'perm_token')
        assert_equal(account.oauth_secret, 'perm_secret')
        assert_equal(account.provider_id, 'mock_provider_id')
        assert_equal(account.provider_name, 'Mock OAuth 1.0a Provider')

    @httpretty.activate
    def test_callback_wrong_user(self):
        # Reject temporary credentials not assigned to the user
        #
        # This prohibits users from associating their external account with
        # another user's OSF account by using XSS or similar attack vector to
        # complete the OAuth flow using the logged-in user but their own account
        # on the external service.
        #
        # If the OSF were to allow login via OAuth with the provider in question,
        # this would allow attackers to hijack OSF accounts with a simple script
        # injection.

        # mock a successful call to the provider to exchange temp keys for
        #   permanent keys
        httpretty.register_uri(
            httpretty.POST,
            'http://mock1a.com/callback',
             body='oauth_token=perm_token'
                  '&oauth_token_secret=perm_secret'
                  '&oauth_callback_confirmed=true',
        )

        user = UserFactory()
        account = ExternalAccountFactory(
            provider="mock1a",
            provider_name='Mock 1A',
            oauth_key="temp_key",
            oauth_secret="temp_secret",
            temporary=True
        )
        account.save()
        # associate this ExternalAccount instance with the user
        user.external_accounts.append(account)
        user.save()

        malicious_user = UserFactory()

        # Fake a request context for the callback
        with self.app.app.test_request_context(
                path="/oauth/callback/mock1a/",
                query_string="oauth_token=temp_key&oauth_verifier=mock_verifier"
        ):
            # make sure the user is logged in
            authenticate(user=malicious_user, access_token=None, response=None)

            with assert_raises(PermissionsError):
                # do the key exchange
                self.provider.auth_callback(user=malicious_user)
예제 #5
0
class TestExternalProviderOAuth2(OsfTestCase):
    # Test functionality of the ExternalProvider class, for OAuth 2.0

    def setUp(self):
        super(TestExternalProviderOAuth2, self).setUp()
        self.user = UserFactory()
        self.provider = MockOAuth2Provider()

    def tearDown(self):
        ExternalAccount._clear_caches()
        ExternalAccount.remove()
        self.user.remove()
        super(TestExternalProviderOAuth2, self).tearDown()

    def test_oauth_version_default(self):
        # OAuth 2.0 is the default version
        assert_is(self.provider._oauth_version, OAUTH2)

    def test_start_flow(self):
        # Generate the appropriate URL and state token

        with self.app.app.test_request_context("/oauth/connect/mock2/") as ctx:

            # make sure the user is logged in
            authenticate(user=self.user, response=None)

            # auth_url is a property method - it calls out to the external
            #   service to get a temporary key and secret before returning the
            #   auth url
            url = self.provider.auth_url

            session = get_session()

            # Temporary credentials are added to the session
            creds = session.data['oauth_states'][self.provider.short_name]
            assert_in('state', creds)

            # The URL to which the user would be redirected
            parsed = urlparse.urlparse(url)
            params = urlparse.parse_qs(parsed.query)

            # check parameters
            assert_equal(
                params, {
                    'state': [creds['state']],
                    'response_type': ['code'],
                    'client_id': [self.provider.client_id],
                    'redirect_uri': [
                        web_url_for('oauth_callback',
                                    service_name=self.provider.short_name,
                                    _absolute=True)
                    ]
                })

            # check base URL
            assert_equal(
                url.split("?")[0],
                "https://mock2.com/auth",
            )

    @responses.activate
    def test_callback(self):
        # Exchange temporary credentials for permanent credentials

        # Mock the exchange of the code for an access token
        _prepare_mock_oauth2_handshake_response()

        user = UserFactory()

        # Fake a request context for the callback
        with self.app.app.test_request_context(
                path="/oauth/callback/mock2/",
                query_string="code=mock_code&state=mock_state") as ctx:

            # make sure the user is logged in
            authenticate(user=user, response=None)

            session = get_session()
            session.data['oauth_states'] = {
                self.provider.short_name: {
                    'state': 'mock_state',
                },
            }
            session.save()

            # do the key exchange

            self.provider.auth_callback(user=user)

        account = ExternalAccount.find_one()
        assert_equal(account.oauth_key, 'mock_access_token')
        assert_equal(account.provider_id, 'mock_provider_id')

    @responses.activate
    def test_provider_down(self):

        # Create a 500 error
        _prepare_mock_500_error()

        user = UserFactory()
        # Fake a request context for the callback
        with self.app.app.test_request_context(
                path="/oauth/callback/mock2/",
                query_string="code=mock_code&state=mock_state"):
            # make sure the user is logged in
            authenticate(user=user, response=None)

            session = get_session()
            session.data['oauth_states'] = {
                self.provider.short_name: {
                    'state': 'mock_state',
                },
            }
            session.save()

            # do the key exchange

            with assert_raises(HTTPError) as error_raised:
                self.provider.auth_callback(user=user)

            assert_equal(
                error_raised.exception.code,
                503,
            )

    @responses.activate
    def test_multiple_users_associated(self):
        # Create only one ExternalAccount for multiple OSF users
        #
        # For some providers (ex: GitHub), the act of completing the OAuth flow
        # revokes previously generated credentials. In addition, there is often no
        # way to know the user's id on the external service until after the flow
        # has completed.
        #
        # Having only one ExternalAccount instance per account on the external
        # service means that connecting subsequent OSF users to the same external
        # account will not invalidate the credentials used by the OSF for users
        # already associated.
        user_a = UserFactory()
        external_account = ExternalAccountFactory(
            provider='mock2',
            provider_id='mock_provider_id',
            provider_name='Mock Provider',
        )
        user_a.external_accounts.append(external_account)
        user_a.save()

        user_b = UserFactory()

        # Mock the exchange of the code for an access token
        _prepare_mock_oauth2_handshake_response()

        # Fake a request context for the callback
        with self.app.app.test_request_context(
                path="/oauth/callback/mock2/",
                query_string="code=mock_code&state=mock_state") as ctx:

            # make sure the user is logged in
            authenticate(user=user_b, response=None)

            session = get_session()
            session.data['oauth_states'] = {
                self.provider.short_name: {
                    'state': 'mock_state',
                },
            }
            session.save()

            # do the key exchange
            self.provider.auth_callback(user=user_b)

        user_a.reload()
        user_b.reload()
        external_account.reload()

        assert_equal(
            user_a.external_accounts,
            user_b.external_accounts,
        )

        assert_equal(ExternalAccount.find().count(), 1)
예제 #6
0
class TestExternalProviderOAuth2(OsfTestCase):
    # Test functionality of the ExternalProvider class, for OAuth 2.0

    def setUp(self):
        super(TestExternalProviderOAuth2, self).setUp()
        self.user = UserFactory()
        self.provider = MockOAuth2Provider()

    def tearDown(self):
        ExternalAccount._clear_caches()
        ExternalAccount.remove()
        self.user.remove()
        super(TestExternalProviderOAuth2, self).tearDown()

    def test_oauth_version_default(self):
        # OAuth 2.0 is the default version
        assert_is(self.provider._oauth_version, OAUTH2)

    def test_start_flow(self):
        # Generate the appropriate URL and state token

        with self.app.app.test_request_context("/oauth/connect/mock2/"):

            # make sure the user is logged in
            authenticate(user=self.user, access_token=None, response=None)

            # auth_url is a property method - it calls out to the external
            #   service to get a temporary key and secret before returning the
            #   auth url
            url = self.provider.auth_url

            # Temporary credentials are added to the session
            creds = session.data['oauth_states'][self.provider.short_name]
            assert_in('state', creds)

            # The URL to which the user would be redirected
            parsed = urlparse.urlparse(url)
            params = urlparse.parse_qs(parsed.query)

            # check parameters
            assert_equal(
                params,
                {
                    'state': [creds['state']],
                    'response_type': ['code'],
                    'client_id': [self.provider.client_id],
                    'redirect_uri': [
                        web_url_for('oauth_callback',
                                    service_name=self.provider.short_name,
                                    _absolute=True)
                    ]
                }
            )

            # check base URL
            assert_equal(
                url.split("?")[0],
                "https://mock2.com/auth",
            )

    @httpretty.activate
    def test_callback(self):
        # Exchange temporary credentials for permanent credentials

        # Mock the exchange of the code for an access token
        _prepare_mock_oauth2_handshake_response()

        user = UserFactory()

        # Fake a request context for the callback
        with self.app.app.test_request_context(
                path="/oauth/callback/mock2/",
                query_string="code=mock_code&state=mock_state"
        ):

            # make sure the user is logged in
            authenticate(user=self.user, access_token=None, response=None)

            session.data['oauth_states'] = {
                self.provider.short_name: {
                    'state': 'mock_state',
                },
            }
            session.save()

            # do the key exchange

            self.provider.auth_callback(user=user)

        account = ExternalAccount.find_one()
        assert_equal(account.oauth_key, 'mock_access_token')
        assert_equal(account.provider_id, 'mock_provider_id')

    @httpretty.activate
    def test_provider_down(self):

        # Create a 500 error
        _prepare_mock_500_error()

        user = UserFactory()
        # Fake a request context for the callback
        with self.app.app.test_request_context(
                path="/oauth/callback/mock2/",
                query_string="code=mock_code&state=mock_state"
        ):
            # make sure the user is logged in
            authenticate(user=user, access_token=None, response=None)

            session.data['oauth_states'] = {
                self.provider.short_name: {
                    'state': 'mock_state',
                },
            }
            session.save()

            # do the key exchange

            with assert_raises(HTTPError) as error_raised:
                self.provider.auth_callback(user=user)

            assert_equal(
                error_raised.exception.code,
                503,
            )

    @httpretty.activate
    def test_user_denies_access(self):

        # Create a 401 error
        _prepare_mock_401_error()

        user = UserFactory()
        # Fake a request context for the callback
        with self.app.app.test_request_context(
                path="/oauth/callback/mock2/",
                query_string="error=mock_error&code=mock_code&state=mock_state"
        ):
            # make sure the user is logged in
            authenticate(user=user, access_token=None, response=None)

            session.data['oauth_states'] = {
                self.provider.short_name: {
                    'state': 'mock_state',
                },
            }
            session.save()

            assert_false(self.provider.auth_callback(user=user))

    @httpretty.activate
    def test_multiple_users_associated(self):
        # Create only one ExternalAccount for multiple OSF users
        #
        # For some providers (ex: GitHub), the act of completing the OAuth flow
        # revokes previously generated credentials. In addition, there is often no
        # way to know the user's id on the external service until after the flow
        # has completed.
        #
        # Having only one ExternalAccount instance per account on the external
        # service means that connecting subsequent OSF users to the same external
        # account will not invalidate the credentials used by the OSF for users
        # already associated.
        user_a = UserFactory()
        external_account = ExternalAccountFactory(
            provider='mock2',
            provider_id='mock_provider_id',
            provider_name='Mock Provider',
        )
        user_a.external_accounts.append(external_account)
        user_a.save()

        user_b = UserFactory()

        # Mock the exchange of the code for an access token
        _prepare_mock_oauth2_handshake_response()

        # Fake a request context for the callback
        with self.app.app.test_request_context(
                path="/oauth/callback/mock2/",
                query_string="code=mock_code&state=mock_state"
        ) as ctx:

            # make sure the user is logged in
            authenticate(user=user_b, access_token=None, response=None)

            session.data['oauth_states'] = {
                self.provider.short_name: {
                    'state': 'mock_state',
                },
            }
            session.save()

            # do the key exchange
            self.provider.auth_callback(user=user_b)

        user_a.reload()
        user_b.reload()
        external_account.reload()

        assert_equal(
            user_a.external_accounts,
            user_b.external_accounts,
        )

        assert_equal(
            ExternalAccount.find().count(),
            1
        )

    @httpretty.activate
    def test_force_refresh_oauth_key(self):
        external_account = ExternalAccountFactory(
            provider='mock2',
            provider_id='mock_provider_id',
            provider_name='Mock Provider',
            oauth_key='old_key',
            oauth_secret='old_secret',
            expires_at=datetime.utcfromtimestamp(time.time() - 200)
        )

        # mock a successful call to the provider to refresh tokens
        httpretty.register_uri(
            httpretty.POST,
            self.provider.auto_refresh_url,
             body=json.dumps({
                'access_token': 'refreshed_access_token',
                'expires_in': 3600,
                'refresh_token': 'refreshed_refresh_token'
            })
        )
        
        old_expiry = external_account.expires_at
        self.provider.account = external_account
        self.provider.refresh_oauth_key(force=True)
        external_account.reload()

        assert_equal(external_account.oauth_key, 'refreshed_access_token')
        assert_equal(external_account.refresh_token, 'refreshed_refresh_token')
        assert_not_equal(external_account.expires_at, old_expiry)
        assert_true(external_account.expires_at > old_expiry)

    @httpretty.activate
    def test_does_need_refresh(self):
        external_account = ExternalAccountFactory(
            provider='mock2',
            provider_id='mock_provider_id',
            provider_name='Mock Provider',
            oauth_key='old_key',
            oauth_secret='old_secret',
            expires_at=datetime.utcfromtimestamp(time.time() - 200),
        )

        # mock a successful call to the provider to refresh tokens
        httpretty.register_uri(
            httpretty.POST,
            self.provider.auto_refresh_url,
             body=json.dumps({
                'access_token': 'refreshed_access_token',
                'expires_in': 3600,
                'refresh_token': 'refreshed_refresh_token'
            })
        )
        
        old_expiry = external_account.expires_at
        self.provider.account = external_account
        self.provider.refresh_oauth_key(force=False)
        external_account.reload()

        assert_equal(external_account.oauth_key, 'refreshed_access_token')
        assert_equal(external_account.refresh_token, 'refreshed_refresh_token')
        assert_not_equal(external_account.expires_at, old_expiry)
        assert_true(external_account.expires_at > old_expiry)

    @httpretty.activate
    def test_does_not_need_refresh(self):
        self.provider.refresh_time = 1
        external_account = ExternalAccountFactory(
            provider='mock2',
            provider_id='mock_provider_id',
            provider_name='Mock Provider',
            oauth_key='old_key',
            oauth_secret='old_secret',
            refresh_token='old_refresh',
            expires_at=datetime.utcfromtimestamp(time.time() + 200),
        )

        # mock a successful call to the provider to refresh tokens
        httpretty.register_uri(
            httpretty.POST,
            self.provider.auto_refresh_url,
             body=json.dumps({
                'err_msg': 'Should not be hit'
            }),
            status=500
        )

        # .reload() has the side effect of rounding the microsends down to 3 significant figures 
        # (e.g. DT(YMDHMS, 365420) becomes DT(YMDHMS, 365000)), 
        # but must occur after possible refresh to reload tokens.
        # Doing so before allows the `old_expiry == EA.expires_at` comparison to work.
        external_account.reload()  
        old_expiry = external_account.expires_at
        self.provider.account = external_account
        self.provider.refresh_oauth_key(force=False)
        external_account.reload()

        assert_equal(external_account.oauth_key, 'old_key')
        assert_equal(external_account.refresh_token, 'old_refresh')
        assert_equal(external_account.expires_at, old_expiry)

    @httpretty.activate
    def test_refresh_oauth_key_does_not_need_refresh(self):
        external_account = ExternalAccountFactory(
            provider='mock2',
            provider_id='mock_provider_id',
            provider_name='Mock Provider',
            oauth_key='old_key',
            oauth_secret='old_secret',
            expires_at=0  # causes `.needs_refresh()` to return False
        )

        # mock a successful call to the provider to refresh tokens
        httpretty.register_uri(
            httpretty.POST,
            self.provider.auto_refresh_url,
             body=json.dumps({
                'err_msg': 'Should not be hit'
            }),
            status=500
        )  
        
        self.provider.account = external_account
        ret = self.provider.refresh_oauth_key(force=False)
        assert_false(ret)

    @httpretty.activate
    def test_refresh_with_broken_provider(self):
        external_account = ExternalAccountFactory(
            provider='mock2',
            provider_id='mock_provider_id',
            provider_name='Mock Provider',
            oauth_key='old_key',
            oauth_secret='old_secret',
            expires_at=datetime.utcfromtimestamp(time.time() - 200)
        )
        self.provider.client_id = None
        self.provider.client_secret = None

        # mock a successful call to the provider to refresh tokens
        httpretty.register_uri(
            httpretty.POST,
            self.provider.auto_refresh_url,
             body=json.dumps({
                'err_msg': 'Should not be hit'
            }),
            status=500
        )  
        
        ret = self.provider.refresh_oauth_key(force=False)
        assert_false(ret)

    @httpretty.activate
    def test_refresh_without_account_or_refresh_url(self):
        external_account = ExternalAccountFactory(
            provider='mock2',
            provider_id='mock_provider_id',
            provider_name='Mock Provider',
            oauth_key='old_key',
            oauth_secret='old_secret',
            expires_at=datetime.utcfromtimestamp(time.time() + 200)
        )


        # mock a successful call to the provider to refresh tokens
        httpretty.register_uri(
            httpretty.POST,
            self.provider.auto_refresh_url,
             body=json.dumps({
                'err_msg': 'Should not be hit'
            }),
            status=500
        )  
        
        ret = self.provider.refresh_oauth_key(force=False)
        assert_false(ret)