diff --git a/docs/backends/bitbucket.rst b/docs/backends/bitbucket.rst index a66478b26..00809a3c4 100644 --- a/docs/backends/bitbucket.rst +++ b/docs/backends/bitbucket.rst @@ -1,26 +1,37 @@ Bitbucket ========= -Bitbucket works similar to Twitter OAuth. +Bitbucket supports both OAuth2 and OAuth1 logins. -- Register a new application by emailing ``support@bitbucket.org`` with an - application name and a bit of a description, +1. Register a new OAuth Consumer by following the instructions in the + Bitbucket documentation: `OAuth on Bitbucket`_ + + Note: For OAuth2, your consumer MUST have the "account" scope otherwise + the user profile information (username, name, etc.) won't be accessible. + +2. Configure the appropriate settings for OAuth2 or OAuth1 (see below). + +OAuth2 +------ - Fill ``Consumer Key`` and ``Consumer Secret`` values in the settings:: - SOCIAL_AUTH_BITBUCKET_KEY = '' - SOCIAL_AUTH_BITBUCKET_SECRET = '' + SOCIAL_AUTH_BITBUCKET_OAUTH2_KEY = '' + SOCIAL_AUTH_BITBUCKET_OAUTH2_SECRET = '' +- If you would like to restrict access to only users with verified e-mail + addresses, set ``SOCIAL_AUTH_BITBUCKET_OAUTH2_VERIFIED_EMAILS_ONLY = True`` +OAuth1 +------ -Settings --------- +- OAuth1 works similarly to OAuth2, but you must fill in the following settings + instead:: -Sometimes Bitbucket users don't have a verified email address, making it -impossible to get the basic user information to continue the auth process. -It's possible to avoid these users with this setting:: + SOCIAL_AUTH_BITBUCKET_KEY = '' + SOCIAL_AUTH_BITBUCKET_SECRET = '' - SOCIAL_AUTH_BITBUCKET_VERIFIED_EMAILS_ONLY = True +- If you would like to restrict access to only users with verified e-mail + addresses, set ``SOCIAL_AUTH_BITBUCKET_VERIFIED_EMAILS_ONLY = True`` -By default the setting is set to ``False`` since it's possible for a project to -gather this information by other methods. +.. _OAuth on Bitbucket: https://confluence.atlassian.com/display/BITBUCKET/OAuth+on+Bitbucket diff --git a/social/backends/bitbucket.py b/social/backends/bitbucket.py index c0574659a..fd638803d 100644 --- a/social/backends/bitbucket.py +++ b/social/backends/bitbucket.py @@ -1,18 +1,12 @@ """ -Bitbucket OAuth1 backend, docs at: +Bitbucket OAuth2 and OAuth1 backends, docs at: http://psa.matiasaguirre.net/docs/backends/bitbucket.html """ from social.exceptions import AuthForbidden -from social.backends.oauth import BaseOAuth1 +from social.backends.oauth import BaseOAuth1, BaseOAuth2 -class BitbucketOAuth(BaseOAuth1): - """Bitbucket OAuth authentication backend""" - name = 'bitbucket' - AUTHORIZATION_URL = 'https://bitbucket.org/api/1.0/oauth/authenticate' - REQUEST_TOKEN_URL = 'https://bitbucket.org/api/1.0/oauth/request_token' - ACCESS_TOKEN_URL = 'https://bitbucket.org/api/1.0/oauth/access_token' - +class BitbucketOAuthBase(object): # Bitbucket usernames can change. The account ID should always be the UUID # See: https://confluence.atlassian.com/display/BITBUCKET/Use+the+Bitbucket+REST+APIs ID_KEY = 'uuid' @@ -27,10 +21,9 @@ def get_user_details(self, response): 'first_name': first_name, 'last_name': last_name} - def user_data(self, access_token): + def user_data(self, access_token, *args, **kwargs): """Return user data provided""" - emails = self.get_json('https://api.bitbucket.org/2.0/user/emails', - auth=self.oauth_auth(access_token)) + emails = self._get_emails(access_token) email = None @@ -44,10 +37,63 @@ def user_data(self, access_token): self, 'Bitbucket account has no verified email' ) - user = self.get_json('https://api.bitbucket.org/2.0/user', - auth=self.oauth_auth(access_token)) + user = self._get_user(access_token) if email: user['email'] = email return user + + def _get_user(self, access_token=None): + raise NotImplementedError + + def _get_emails(self, access_token=None): + raise NotImplementedError + + +class BitbucketOAuth2(BitbucketOAuthBase, BaseOAuth2): + name = 'bitbucket-oauth2' + SCOPE_SEPARATOR = ' ' + AUTHORIZATION_URL = 'https://bitbucket.org/site/oauth2/authorize' + ACCESS_TOKEN_URL = 'https://bitbucket.org/site/oauth2/access_token' + ACCESS_TOKEN_METHOD = 'POST' + REDIRECT_STATE = False + EXTRA_DATA = [ + ('scopes', 'scopes'), + ('expires_in', 'expires'), + ('token_type', 'token_type'), + ('refresh_token', 'refresh_token') + ] + + def auth_complete_credentials(self): + return self.get_key_and_secret() + + def _get_user(self, access_token=None): + return self.get_json('https://api.bitbucket.org/2.0/user', + params={'access_token': access_token}) + + def _get_emails(self, access_token=None): + return self.get_json('https://api.bitbucket.org/2.0/user/emails', + params={'access_token': access_token}) + + def refresh_token(self, *args, **kwargs): + raise NotImplementedError('Refresh tokens for Bitbucket have not been implemented') + + +class BitbucketOAuth(BitbucketOAuthBase, BaseOAuth1): + """Bitbucket OAuth authentication backend""" + name = 'bitbucket' + AUTHORIZATION_URL = 'https://bitbucket.org/api/1.0/oauth/authenticate' + REQUEST_TOKEN_URL = 'https://bitbucket.org/api/1.0/oauth/request_token' + ACCESS_TOKEN_URL = 'https://bitbucket.org/api/1.0/oauth/access_token' + + def oauth_auth(self, *args, **kwargs): + return super(BitbucketOAuth, self).oauth_auth(*args, **kwargs) + + def _get_user(self, access_token=None): + return self.get_json('https://api.bitbucket.org/2.0/user', + auth=self.oauth_auth(access_token)) + + def _get_emails(self, access_token=None): + return self.get_json('https://api.bitbucket.org/2.0/user/emails', + auth=self.oauth_auth(access_token)) diff --git a/social/backends/oauth.py b/social/backends/oauth.py index d15460669..0396495c4 100644 --- a/social/backends/oauth.py +++ b/social/backends/oauth.py @@ -350,6 +350,9 @@ def auth_complete_params(self, state=None): 'redirect_uri': self.get_redirect_uri(state) } + def auth_complete_credentials(self): + return None + def auth_headers(self): return {'Content-Type': 'application/x-www-form-urlencoded', 'Accept': 'application/json'} @@ -371,12 +374,15 @@ def auth_complete(self, *args, **kwargs): """Completes loging process, must return user instance""" state = self.validate_state() self.process_error(self.data) + response = self.request_access_token( self.access_token_url(), data=self.auth_complete_params(state), headers=self.auth_headers(), + auth=self.auth_complete_credentials(), method=self.ACCESS_TOKEN_METHOD ) + print(dict(response)) self.process_error(response) return self.do_auth(response['access_token'], response=response, *args, **kwargs) @@ -384,6 +390,8 @@ def auth_complete(self, *args, **kwargs): @handle_http_errors def do_auth(self, access_token, *args, **kwargs): """Finish the auth process once the access_token was retrieved""" + print(args) + print(kwargs) data = self.user_data(access_token, *args, **kwargs) response = kwargs.get('response') or {} response.update(data or {}) diff --git a/social/tests/backends/test_bitbucket.py b/social/tests/backends/test_bitbucket.py index 0c0f91202..400ea5f66 100644 --- a/social/tests/backends/test_bitbucket.py +++ b/social/tests/backends/test_bitbucket.py @@ -4,22 +4,32 @@ from social.p3 import urlencode from social.exceptions import AuthForbidden -from social.tests.backends.oauth import OAuth1Test +from social.tests.backends.oauth import OAuth1Test, OAuth2Test -class BitbucketOAuth1Test(OAuth1Test): - backend_path = 'social.backends.bitbucket.BitbucketOAuth' +class BitbucketOAuthMixin(object): user_data_url = 'https://api.bitbucket.org/2.0/user' expected_username = 'foobar' - access_token_body = json.dumps({ - 'access_token': 'foobar', - 'token_type': 'bearer' - }) - request_token_body = urlencode({ - 'oauth_token_secret': 'foobar-secret', - 'oauth_token': 'foobar', - 'oauth_callback_confirmed': 'true' + bb_api_user_emails = 'https://api.bitbucket.org/2.0/user/emails' + + user_data_body = json.dumps({ + u'created_on': u'2012-03-29T18:07:38+00:00', + u'display_name': u'Foo Bar', + u'links': { + u'avatar': {u'href': u'https://bitbucket.org/account/foobar/avatar/32/'}, + u'followers': {u'href': u'https://api.bitbucket.org/2.0/users/foobar/followers'}, + u'following': {u'href': u'https://api.bitbucket.org/2.0/users/foobar/following'}, + u'hooks': {u'href': u'https://api.bitbucket.org/2.0/users/foobar/hooks'}, + u'html': {u'href': u'https://bitbucket.org/foobar'}, + u'repositories': {u'href': u'https://api.bitbucket.org/2.0/repositories/foobar'}, + u'self': {u'href': u'https://api.bitbucket.org/2.0/users/foobar'}}, + u'location': u'Fooville, Bar', + u'type': u'user', + u'username': u'foobar', + u'uuid': u'{397621dc-0f78-329f-8d6d-727396248e3f}', + u'website': u'http://foobar.com' }) + emails_body = json.dumps({ u'page': 1, u'pagelen': 10, @@ -41,33 +51,31 @@ class BitbucketOAuth1Test(OAuth1Test): } ] }) - user_data_body = json.dumps({ - u'created_on': u'2012-03-29T18:07:38+00:00', - u'display_name': u'Foo Bar', - u'links': { - u'avatar': {u'href': u'https://bitbucket.org/account/foobar/avatar/32/'}, - u'followers': {u'href': u'https://api.bitbucket.org/2.0/users/foobar/followers'}, - u'following': {u'href': u'https://api.bitbucket.org/2.0/users/foobar/following'}, - u'hooks': {u'href': u'https://api.bitbucket.org/2.0/users/foobar/hooks'}, - u'html': {u'href': u'https://bitbucket.org/foobar'}, - u'repositories': {u'href': u'https://api.bitbucket.org/2.0/repositories/foobar'}, - u'self': {u'href': u'https://api.bitbucket.org/2.0/users/foobar'}}, - u'location': u'Fooville, Bar', - u'type': u'user', - u'username': u'foobar', - u'uuid': u'{397621dc-0f78-329f-8d6d-727396248e3f}', - u'website': u'http://foobar.com' + + +class BitbucketOAuth1Test(BitbucketOAuthMixin, OAuth1Test): + backend_path = 'social.backends.bitbucket.BitbucketOAuth' + + request_token_body = urlencode({ + 'oauth_token_secret': 'foobar-secret', + 'oauth_token': 'foobar', + 'oauth_callback_confirmed': 'true' + }) + + access_token_body = json.dumps({ + 'access_token': 'foobar', + 'token_type': 'bearer' }) def test_login(self): HTTPretty.register_uri(HTTPretty.GET, - 'https://api.bitbucket.org/2.0/user/emails', + self.bb_api_user_emails, status=200, body=self.emails_body) self.do_login() def test_partial_pipeline(self): HTTPretty.register_uri(HTTPretty.GET, - 'https://api.bitbucket.org/2.0/user/emails', + self.bb_api_user_emails, status=200, body=self.emails_body) self.do_partial_pipeline() @@ -101,3 +109,58 @@ def test_partial_pipeline(self): }) with self.assertRaises(AuthForbidden): super(BitbucketOAuth1FailTest, self).test_partial_pipeline() + + +class BitbucketOAuth2Test(BitbucketOAuthMixin, OAuth2Test): + backend_path = 'social.backends.bitbucket.BitbucketOAuth2' + + access_token_body = json.dumps({ + 'access_token': 'foobar_access', + 'scopes': 'foo_scope', + 'expires_in': 3600, + 'refresh_token': 'foobar_refresh', + 'token_type': 'bearer' + }) + + def test_login(self): + HTTPretty.register_uri(HTTPretty.GET, + self.bb_api_user_emails, + status=200, body=self.emails_body) + self.do_login() + + def test_partial_pipeline(self): + HTTPretty.register_uri(HTTPretty.GET, + self.bb_api_user_emails, + status=200, body=self.emails_body) + self.do_partial_pipeline() + + +class BitbucketOAuth2FailTest(BitbucketOAuth2Test): + emails_body = json.dumps({ + u'page': 1, + u'pagelen': 10, + u'size': 1, + u'values': [ + { + u'email': u'foo@bar.com', + u'is_confirmed': False, + u'is_primary': True, + u'links': { u'self': {u'href': u'https://api.bitbucket.org/2.0/user/emails/foo@bar.com'}}, + u'type': u'email' + } + ] + }) + + def test_login(self): + self.strategy.set_settings({ + 'SOCIAL_AUTH_BITBUCKET_OAUTH2_VERIFIED_EMAILS_ONLY': True + }) + with self.assertRaises(AuthForbidden): + super(BitbucketOAuth2FailTest, self).test_login() + + def test_partial_pipeline(self): + self.strategy.set_settings({ + 'SOCIAL_AUTH_BITBUCKET_OAUTH2_VERIFIED_EMAILS_ONLY': True + }) + with self.assertRaises(AuthForbidden): + super(BitbucketOAuth2FailTest, self).test_partial_pipeline()