diff --git a/.gitignore b/.gitignore
index 457da65c8..bf45b883b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -39,3 +39,5 @@ local_settings.py
sessions/
_build/
fabfile.py
+
+.DS_Store
\ No newline at end of file
diff --git a/README.rst b/README.rst
index 755c7a7a0..65fc4f137 100644
--- a/README.rst
+++ b/README.rst
@@ -43,6 +43,7 @@ or extend current one):
* Angel_ OAuth2
* Behance_ OAuth2
* Bitbucket_ OAuth1
+ * Box_ OAuth2
* Dailymotion_ OAuth2
* Disqus_ OAuth2
* Douban_ OAuth1 and OAuth2
@@ -179,6 +180,7 @@ check `django-social-auth LICENCE`_ for details:
.. _Angel: https://angel.co
.. _Behance: https://www.behance.net
.. _Bitbucket: https://bitbucket.org
+.. _Box: https://www.box.com
.. _Dailymotion: https://dailymotion.com
.. _Disqus: https://disqus.com
.. _Douban: http://www.douban.com
diff --git a/docs/backends/box.rst b/docs/backends/box.rst
new file mode 100644
index 000000000..d9de6580c
--- /dev/null
+++ b/docs/backends/box.rst
@@ -0,0 +1,24 @@
+Box.net
+======
+
+Box works similar to Facebook (OAuth2).
+
+- Register an application at `Manage Box Applications`_
+
+- Fill the **Consumer Key** and **Consumer Secret** values in your settings::
+
+ SOCIAL_AUTH_BOX_KEY = ''
+ SOCIAL_AUTH_BOX_SECRET = ''
+
+- By default the token is not permanent, it will last an hour::
+
+ To refresh the access token just do::
+
+ from social.apps.django_app.utils import load_strategy
+
+ strategy = load_strategy(backend='box')
+ user = User.objects.get(pk=foo)
+ social = user.social_auth.filter(provider='box')[0]
+ social.refresh_token(strategy=strategy)
+
+.. _Manage Box Applications: https://app.box.com/developers/services
diff --git a/docs/intro.rst b/docs/intro.rst
index d814b1dc9..81eb494c5 100644
--- a/docs/intro.rst
+++ b/docs/intro.rst
@@ -35,6 +35,7 @@ or extend current one):
* Angel_ OAuth2
* Behance_ OAuth2
* Bitbucket_ OAuth1
+ * Box_ OAuth2
* Dailymotion_ OAuth2
* Disqus_ OAuth2
* Douban_ OAuth1 and OAuth2
@@ -105,6 +106,7 @@ suits your project. Check `Authentication Pipeline`_ section.
.. _Angel: https://angel.co
.. _Behance: https://www.behance.net
.. _Bitbucket: https://bitbucket.org
+.. _Box: https://www.box.com
.. _Dailymotion: https://dailymotion.com
.. _Disqus: https://disqus.com
.. _Douban: http://www.douban.com
diff --git a/examples/django_example/dj/settings.py b/examples/django_example/dj/settings.py
index 47b9dc1e4..da78f74e0 100644
--- a/examples/django_example/dj/settings.py
+++ b/examples/django_example/dj/settings.py
@@ -131,6 +131,7 @@
'social.backends.angel.AngelOAuth2',
'social.backends.behance.BehanceOAuth2',
'social.backends.bitbucket.BitbucketOAuth',
+ 'social.backends.box.BoxOAuth2',
'social.backends.linkedin.LinkedinOAuth',
'social.backends.linkedin.LinkedinOAuth2',
'social.backends.github.GithubOAuth2',
diff --git a/examples/django_example/dj/templates/home.html b/examples/django_example/dj/templates/home.html
index 96333e60c..5e68b0f33 100644
--- a/examples/django_example/dj/templates/home.html
+++ b/examples/django_example/dj/templates/home.html
@@ -14,6 +14,7 @@
Angel OAuth2
Behance OAuth2
Bitbucket OAuth
+Box.net OAuth2
LinkedIn OAuth
Github OAuth2
Foursquare OAuth2
diff --git a/examples/flask_example/settings.py b/examples/flask_example/settings.py
index 54c118773..fc88e0278 100644
--- a/examples/flask_example/settings.py
+++ b/examples/flask_example/settings.py
@@ -28,6 +28,7 @@
'social.backends.angel.AngelOAuth2',
'social.backends.behance.BehanceOAuth2',
'social.backends.bitbucket.BitbucketOAuth',
+ 'social.backends.box.BoxOAuth2',
'social.backends.linkedin.LinkedinOAuth',
'social.backends.github.GithubOAuth2',
'social.backends.foursquare.FoursquareOAuth2',
diff --git a/examples/flask_example/templates/home.html b/examples/flask_example/templates/home.html
index e56afb3dd..9363653d9 100644
--- a/examples/flask_example/templates/home.html
+++ b/examples/flask_example/templates/home.html
@@ -13,6 +13,7 @@
Angel OAuth2
Behance OAuth2
Bitbucket OAuth
+Box OAuth2
LinkedIn OAuth
Github OAuth2
Foursquare OAuth2
diff --git a/examples/webpy_example/app.py b/examples/webpy_example/app.py
index 01c862d1e..dda2e1e7d 100644
--- a/examples/webpy_example/app.py
+++ b/examples/webpy_example/app.py
@@ -31,6 +31,7 @@
'social.backends.angel.AngelOAuth2',
'social.backends.behance.BehanceOAuth2',
'social.backends.bitbucket.BitbucketOAuth',
+ 'social.backends.box.BoxOAuth2',
'social.backends.linkedin.LinkedinOAuth',
'social.backends.github.GithubOAuth2',
'social.backends.foursquare.FoursquareOAuth2',
diff --git a/examples/webpy_example/templates/home.html b/examples/webpy_example/templates/home.html
index de5e470e9..feb31e3a2 100644
--- a/examples/webpy_example/templates/home.html
+++ b/examples/webpy_example/templates/home.html
@@ -13,6 +13,7 @@
Angel OAuth2
Behance OAuth2
Bitbucket OAuth
+Box OAuth2
LinkedIn OAuth
Github OAuth2
Foursquare OAuth2
diff --git a/social/backends/box.py b/social/backends/box.py
new file mode 100644
index 000000000..dde3b9975
--- /dev/null
+++ b/social/backends/box.py
@@ -0,0 +1,59 @@
+"""
+Box.net OAuth support.
+
+This contribution adds support for GitHub OAuth service. The settings
+SOCIAL_AUTH_BOX_KEY and SOCIAL_AUTH_BOX_SECRET must be defined with the values
+given by Box.net application registration process.
+
+Extended permissions are supported by defining BOX_EXTENDED_PERMISSIONS
+setting, it must be a list of values to request.
+
+By default account id and token expiration time are stored in extra_data
+field, check OAuthBackend class for details on how to extend it.
+"""
+from social.backends.oauth import BaseOAuth2
+
+
+class BoxOAuth2(BaseOAuth2):
+ """Box.net OAuth authentication backend"""
+ name = 'box'
+ AUTHORIZATION_URL = 'https://www.box.com/api/oauth2/authorize'
+ ACCESS_TOKEN_METHOD = 'POST'
+ ACCESS_TOKEN_URL = 'https://www.box.com/api/oauth2/token'
+ REVOKE_TOKEN_URL = 'https://www.box.com/api/oauth2/revoke'
+ SCOPE_SEPARATOR = ','
+ EXTRA_DATA = [
+ ('refresh_token', 'refresh_token', True),
+ ('id', 'id'),
+ ('expires', 'expires'),
+ ]
+
+ def do_auth(self, access_token, response=None, *args, **kwargs):
+ response = response or {}
+ data = self.user_data(access_token)
+
+ data['access_token'] = response.get('access_token')
+ data['refresh_token'] = response.get('refresh_token')
+ data['expires'] = response.get('expires_in')
+ kwargs.update({'backend': self, 'response': data})
+ return self.strategy.authenticate(*args, **kwargs)
+
+ def get_user_details(self, response):
+ """Return user details Box.net account"""
+ return {'username': response.get('login'),
+ 'email': response.get('login') or '',
+ 'first_name': response.get('name')}
+
+ def user_data(self, access_token, *args, **kwargs):
+ """Loads user data from service"""
+ params = self.setting('PROFILE_EXTRA_PARAMS', {})
+ params['access_token'] = access_token
+ return self.get_json('https://api.box.com/2.0/users/me',
+ params=params)
+
+ def refresh_token(self, token, *args, **kwargs):
+ params = self.refresh_token_params(token, *args, **kwargs)
+ request = self.request(self.REFRESH_TOKEN_URL or self.ACCESS_TOKEN_URL,
+ data=params, headers=self.auth_headers(),
+ method='POST')
+ return self.process_refresh_token_response(request, *args, **kwargs)
diff --git a/tests/backends/box_test.py b/tests/backends/box_test.py
new file mode 100644
index 000000000..9e898a77d
--- /dev/null
+++ b/tests/backends/box_test.py
@@ -0,0 +1,66 @@
+from sure import expect
+from tests.oauth import OAuth2Test
+import json
+
+
+class BoxOAuth2Test(OAuth2Test):
+ backend_path = 'social.backends.box.BoxOAuth2'
+ user_data_url = 'https://api.box.com/2.0/users/me'
+ expected_username = 'sean+awesome@box.com'
+ access_token_body = json.dumps({
+ "access_token": "T9cE5asGnuyYCCqIZFoWjFHvNbvVqHjl",
+ "expires_in": 3600,
+ "restricted_to": [],
+ "token_type": "bearer",
+ "refresh_token": "J7rxTiWOHMoSC1isKZKBZWizoRXjkQzig5C6jFgCVJ9bUnsUfGMinKBDLZWP9BgR"
+ })
+ user_data_body = json.dumps({
+ "type": "user",
+ "id": "181216415",
+ "name": "sean rose",
+ "login": "sean+awesome@box.com",
+ "created_at": "2012-05-03T21:39:11-07:00",
+ "modified_at": "2012-11-14T11:21:32-08:00",
+ "role": "admin",
+ "language": "en",
+ "space_amount": 11345156112,
+ "space_used": 1237009912,
+ "max_upload_size": 2147483648,
+ "tracking_codes": [],
+ "can_see_managed_users": True,
+ "is_sync_enabled": True,
+ "status": "active",
+ "job_title": "",
+ "phone": "6509241374",
+ "address": "",
+ "avatar_url": "https://www.box.com/api/avatar/large/181216415",
+ "is_exempt_from_device_limits": False,
+ "is_exempt_from_login_verification": False,
+ "enterprise": {
+ "type": "enterprise",
+ "id": "17077211",
+ "name": "seanrose enterprise"
+ }
+ })
+ refresh_token_body = json.dumps({
+ "access_token": "T9cE5asGnuyYCCqIZFoWjFHvNbvVqHjl",
+ "expires_in": 3600,
+ "restricted_to": [],
+ "token_type": "bearer",
+ "refresh_token": "J7rxTiWOHMoSC1isKZKBZWizoRXjkQzig5C6jFgCVJ9bUnsUfGMinKBDLZWP9BgR"
+ })
+
+ def test_login(self):
+ self.do_login()
+
+ def test_partial_pipeline(self):
+ self.do_partial_pipeline()
+
+ def refresh_token_arguments(self):
+ uri = self.strategy.build_absolute_uri('/complete/box/')
+ return {'redirect_uri': uri}
+
+ def test_refresh_token(self):
+ user, social = self.do_refresh_token()
+ expect(social.extra_data['access_token']).to.equal('T9cE5asGnuyYCCqIZFoWjFHvNbvVqHjl')
+