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)
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)
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 )
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)
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)
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)