Repository: box/box-python-sdk

Issue:
Add a way to revoke OAuth tokens
Using the OAuth2 class you can easily authenticate and refresh tokens but there is not an easy way to revoke the token.

Right now to revoke the token, we would have to do something like:
```python
data = {
  'client_id': CLIENT_ID,
  'client_secret': CLIENT_SECRET,
  'token': refresh_token
}
client.make_request(
  'POST',
  '{0}/revoke'.format(API.OAUTH2_API_URL),
  data=data
)
```

It would be nice if I could do something like `oauth.revoke()` to revoke and clear the access token and the refresh token.

Discussion:

Patch:
diff --git a/HISTORY.rst b/HISTORY.rst
index 5ca5148..ec26cb8 100644
--- a/HISTORY.rst
+++ b/HISTORY.rst
@@ -6,6 +6,10 @@ Release History
 Upcoming
 ++++++++
 
+1.5.1 (2016-03-23)
+++++++++++++++++++
+
+- Added a ``revoke()`` method to the ``OAuth2`` class. Calling it will revoke the current access/refresh token pair.
 
 
 1.5.0 (2016-03-17)
diff --git a/boxsdk/auth/developer_token_auth.py b/boxsdk/auth/developer_token_auth.py
index 33c7446..9fa9c4e 100644
--- a/boxsdk/auth/developer_token_auth.py
+++ b/boxsdk/auth/developer_token_auth.py
@@ -32,3 +32,9 @@ def _refresh(self, access_token):
         """
         self._access_token = self._refresh_developer_token()
         return self._access_token, None
+
+    def revoke(self):
+        """
+        Base class override.
+        Do nothing; developer tokens can't be revoked without client ID and secret.
+        """
diff --git a/boxsdk/auth/oauth2.py b/boxsdk/auth/oauth2.py
index 9daf9ad..a810e79 100644
--- a/boxsdk/auth/oauth2.py
+++ b/boxsdk/auth/oauth2.py
@@ -213,6 +213,8 @@ def _get_state_csrf_token():
         return 'box_csrf_token_' + ''.join(ascii_alphabet[int(system_random.random() * ascii_len)] for _ in range(16))
 
     def _store_tokens(self, access_token, refresh_token):
+        self._access_token = access_token
+        self._refresh_token = refresh_token
         if self._store_tokens_callback is not None:
             self._store_tokens_callback(access_token, refresh_token)
 
@@ -240,17 +242,41 @@ def send_token_request(self, data, access_token, expect_refresh_token=True):
             url,
             data=data,
             headers=headers,
-            access_token=access_token
+            access_token=access_token,
         )
         if not network_response.ok:
             raise BoxOAuthException(network_response.status_code, network_response.content, url, 'POST')
         try:
             response = network_response.json()
-            self._access_token = response['access_token']
-            self._refresh_token = response.get('refresh_token', None)
-            if self._refresh_token is None and expect_refresh_token:
+            access_token = response['access_token']
+            refresh_token = response.get('refresh_token', None)
+            if refresh_token is None and expect_refresh_token:
                 raise BoxOAuthException(network_response.status_code, network_response.content, url, 'POST')
         except (ValueError, KeyError):
             raise BoxOAuthException(network_response.status_code, network_response.content, url, 'POST')
-        self._store_tokens(self._access_token, self._refresh_token)
+        self._store_tokens(access_token, refresh_token)
         return self._access_token, self._refresh_token
+
+    def revoke(self):
+        """
+        Revoke the authorization for the current access/refresh token pair.
+        """
+        with self._refresh_lock:
+            access_token, refresh_token = self._get_tokens()
+            token_to_revoke = access_token or refresh_token
+            if token_to_revoke is None:
+                return
+            url = '{base_auth_url}/revoke'.format(base_auth_url=API.OAUTH2_API_URL)
+            network_response = self._network_layer.request(
+                'POST',
+                url,
+                data={
+                    'client_id': self._client_id,
+                    'client_secret': self._client_secret,
+                    'token': token_to_revoke,
+                },
+                access_token=access_token,
+            )
+            if not network_response.ok:
+                raise BoxOAuthException(network_response.status_code, network_response.content, url, 'POST')
+            self._store_tokens(None, None)
diff --git a/boxsdk/version.py b/boxsdk/version.py
index cfa1704..c64a173 100644
--- a/boxsdk/version.py
+++ b/boxsdk/version.py
@@ -3,4 +3,4 @@
 from __future__ import unicode_literals, absolute_import
 
 
-__version__ = '1.5.0'
+__version__ = '1.5.1'


Test patch:
diff --git a/test/unit/auth/test_oauth2.py b/test/unit/auth/test_oauth2.py
index 4e5b28b..af4d6ed 100644
--- a/test/unit/auth/test_oauth2.py
+++ b/test/unit/auth/test_oauth2.py
@@ -275,3 +275,42 @@ def test_token_request_allows_missing_refresh_token(mock_network_layer):
         network_layer=mock_network_layer,
     )
     oauth.send_token_request({}, access_token=None, expect_refresh_token=False)
+
+
+@pytest.mark.parametrize(
+    'access_token,refresh_token,expected_token_to_revoke',
+    (
+        ('fake_access_token', 'fake_refresh_token', 'fake_access_token'),
+        (None, 'fake_refresh_token', 'fake_refresh_token')
+    )
+)
+def test_revoke_sends_revoke_request(
+        client_id,
+        client_secret,
+        mock_network_layer,
+        access_token,
+        refresh_token,
+        expected_token_to_revoke,
+):
+    mock_network_response = Mock()
+    mock_network_response.ok = True
+    mock_network_layer.request.return_value = mock_network_response
+    oauth = OAuth2(
+        client_id=client_id,
+        client_secret=client_secret,
+        access_token=access_token,
+        refresh_token=refresh_token,
+        network_layer=mock_network_layer,
+    )
+    oauth.revoke()
+    mock_network_layer.request.assert_called_once_with(
+        'POST',
+        '{0}/revoke'.format(API.OAUTH2_API_URL),
+        data={
+            'client_id': client_id,
+            'client_secret': client_secret,
+            'token': expected_token_to_revoke,
+        },
+        access_token=access_token,
+    )
+    assert oauth.access_token is None