Skip to content

Commit

Permalink
Added a OAuth2 backend for Bitbucket
Browse files Browse the repository at this point in the history
  • Loading branch information
mark-adams committed Jun 21, 2015
1 parent 0a4b744 commit 84f001a
Show file tree
Hide file tree
Showing 4 changed files with 184 additions and 56 deletions.
37 changes: 24 additions & 13 deletions docs/backends/bitbucket.rst
Original file line number Diff line number Diff line change
@@ -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 = '<your-consumer-key>'
SOCIAL_AUTH_BITBUCKET_OAUTH2_SECRET = '<your-consumer-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 = '<your-consumer-key>'
SOCIAL_AUTH_BITBUCKET_SECRET = '<your-consumer-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
74 changes: 60 additions & 14 deletions social/backends/bitbucket.py
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -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

Expand All @@ -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))
8 changes: 8 additions & 0 deletions social/backends/oauth.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'}
Expand All @@ -371,19 +374,24 @@ 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)

@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 {})
Expand Down
121 changes: 92 additions & 29 deletions social/tests/backends/test_bitbucket.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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()

Expand Down Expand Up @@ -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()

0 comments on commit 84f001a

Please sign in to comment.