Skip to content

Commit 6746998

Browse files
author
Louis Rannou
committed
Support for OAuth2 sessions revocation
Add support for revocation in OAuth2 sessions. Make an example with GitLab. Documentation. Signed-off-by: Louis Rannou <louis.rannou@syslinbit.com>
1 parent eee74a2 commit 6746998

File tree

3 files changed

+165
-0
lines changed

3 files changed

+165
-0
lines changed

docs/examples/gitlab.rst

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
GitLab OAuth 2 Tutorial
2+
==========================
3+
4+
Add a new application on `GitLab`_ (redirect URI can be `https://example.com`
5+
and check the box `read_user`). When you have obtained a ``client_id`` and a
6+
``client_secret`` you can try out the command line interactive example below.
7+
8+
.. _`GitLab`:
9+
https://gitlab.com/-/user_settings/applications
10+
11+
.. code-block:: pycon
12+
13+
14+
>>> # Credentials you get from registering a new application
15+
>>> client_id = '<the id you get from github>'
16+
>>> client_secret = '<the secret you get from github>'
17+
>>> redirect_uri = '<the URI you gave>'
18+
>>> scope = '<the scope you checked>'
19+
20+
>>> # OAuth endpoints given in the GitLab API documentation
21+
>>> authorization_base_url = 'https://gitlab.com/oauth/authorize'
22+
>>> token_url = 'https://gitlab.com/oauth/token'
23+
24+
>>> from requests_oauthlib import OAuth2Session
25+
>>> gitlab = OAuth2Session(client_id, scope=scope, redirect_uri=redirect_uri)
26+
27+
>>> # Redirect user to GitLab for authorization
28+
>>> authorization_url, state = gitlab.authorization_url(authorization_base_url)
29+
>>> print('Please go here and authorize,', authorization_url)
30+
31+
>>> # Get the authorization verifier code from the callback url
32+
>>> redirect_response = input('Paste the full redirect URL here:')
33+
34+
>>> # Fetch the access token
35+
>>> gitlab.fetch_token(token_url, client_secret=client_secret,
36+
>>> authorization_response=redirect_response)
37+
38+
>>> # Fetch a protected resource, i.e. user profile
39+
>>> r = gitlab.get('https://gitlab.com/api/v4/users')
40+
>>> print(r.content)
41+
42+
>>> # Refresh the token
43+
>>> refresh_url = token_url # True for GitLab but not all providers.
44+
>>> gitlab.refresh_token(refresh_url,
45+
>>> client_id=client_id, client_secret=client_secret)
46+
47+
>>> # Revoke the token
48+
>>> revoke_url = 'https://gitlab.com/oauth/revoke'
49+
>>> gitlab.revoke_token(revoke_url,
50+
>>> client_id=client_id, client_secret=client_secret)

docs/oauth2_workflow.rst

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,36 @@ however that you still need to update ``expires_in`` to trigger the refresh.
288288
... auto_refresh_kwargs=extra, token_updater=token_saver)
289289
>>> r = oauth.get(protected_url)
290290
291+
292+
Revoking tokens
293+
---------------
294+
295+
Certain providers will provide a ``revoke`` API. It can be used to revoke the
296+
access token or the refresh token.
297+
298+
.. code-block:: pycon
299+
300+
>>> token = {
301+
... 'access_token': 'eswfld123kjhn1v5423',
302+
... 'refresh_token': 'asdfkljh23490sdf',
303+
... 'token_type': 'Bearer',
304+
... 'expires_in': '-30', # initially 3600, need to be updated by you
305+
... }
306+
>>> client_id = r'foo'
307+
>>> revoke_url = 'https://provider.com/revoke'
308+
309+
>>> # some providers will ask you for extra credentials to be passed along
310+
>>> # when refreshing tokens, usually for authentication purposes.
311+
>>> extra = {
312+
... 'client_id': client_id,
313+
... 'client_secret': r'potato',
314+
... }
315+
316+
>>> from requests_oauthlib import OAuth2Session
317+
>>> from oauthlib.oauth2 import TokenExpiredError
318+
>>> oauth = OAuth2Session(client_id, token=token)
319+
>>> oauth.revoke_token(revoke_url, **extra)
320+
291321
TLS Client Authentication
292322
-------------------------
293323

requests_oauthlib/oauth2_session.py

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
from oauthlib.oauth2 import WebApplicationClient, InsecureTransportError
55
from oauthlib.oauth2 import LegacyApplicationClient
66
from oauthlib.oauth2 import TokenExpiredError, is_secure_transport
7+
from oauthlib.oauth2 import UnsupportedTokenTypeError
8+
from oauthlib.oauth2 import TemporarilyUnavailableError, ServerError
79
import requests
810

911
log = logging.getLogger(__name__)
@@ -98,8 +100,10 @@ def __init__(
98100
self.compliance_hook = {
99101
"access_token_response": set(),
100102
"refresh_token_response": set(),
103+
"revoke_token_response": set(),
101104
"protected_request": set(),
102105
"refresh_token_request": set(),
106+
"revoke_token_request": set(),
103107
"access_token_request": set(),
104108
}
105109

@@ -499,6 +503,85 @@ def refresh_token(
499503
self.token["refresh_token"] = refresh_token
500504
return self.token
501505

506+
def revoke_token(
507+
self,
508+
token_url,
509+
token=None,
510+
token_type=None,
511+
body="",
512+
auth=None,
513+
timeout=None,
514+
headers=None,
515+
verify=None,
516+
proxies=None,
517+
**kwargs
518+
):
519+
"""Revoke a token pair using a token.
520+
521+
:param token_url: The token endpoint, must be HTTPS.
522+
:param token: The token to revoke.
523+
:param token_type: The type of token to revoke.
524+
:param body: Optional application/x-www-form-urlencoded body to add the
525+
include in the token request. Prefer kwargs over body.
526+
:param auth: An auth tuple or method as accepted by `requests`.
527+
:param timeout: Timeout of the request in seconds.
528+
:param headers: A dict of headers to be used by `requests`.
529+
:param verify: Verify SSL certificate.
530+
:param proxies: The `proxies` argument will be passed to `requests`.
531+
:param kwargs: Extra parameters to include in the token request.
532+
:return: A token dict
533+
"""
534+
if not token_url:
535+
raise ValueError("No token endpoint set for revoke.")
536+
537+
if not is_secure_transport(token_url):
538+
raise InsecureTransportError()
539+
540+
token = token or self.token.get("access_token")
541+
token_type = token_type or self.token.get("token_type")
542+
543+
_request_headers = headers or {}
544+
545+
if token_type:
546+
(url, _headers, body) = self._client.prepare_token_revocation_request(
547+
token_url, token, token_type, body=body, scope=self.scope, **kwargs)
548+
else:
549+
(url, _headers, body) = self._client.prepare_revocation_request(
550+
token_url, token, body=body, scope=self.scope, **kwargs)
551+
_request_headers.update(_headers)
552+
log.debug("Prepared revocation request %s", body)
553+
554+
for hook in self.compliance_hook["revoke_token_request"]:
555+
log.debug("Invoking revoke_token_request hook %s.", hook)
556+
url, _request_headers, body = hook(url, _headers, body)
557+
558+
r = self.post(
559+
url,
560+
data=dict(urldecode(body)),
561+
auth=auth,
562+
timeout=timeout,
563+
headers=_request_headers,
564+
verify=verify,
565+
withhold_token=True,
566+
proxies=proxies,
567+
)
568+
log.debug("Request to revoke token completed with status %s.", r.status_code)
569+
log.debug("Response headers were %s and content %s.", r.headers, r.text)
570+
log.debug(
571+
"Invoking %d token response hooks.",
572+
len(self.compliance_hook["revoke_token_response"]),
573+
)
574+
for hook in self.compliance_hook["revoke_token_response"]:
575+
log.debug("Invoking hook %s.", hook)
576+
r = hook(r)
577+
578+
if not r.ok and r.status_code == 400:
579+
if 'unsupported_token_type' in r.text:
580+
raise UnsupportedTokenTypeError("Revocation not supported by server")
581+
raise ServerError('Server error')
582+
elif not r.ok and r.code == 503:
583+
raise TemporarilyUnavailableError("Service unavailable")
584+
502585
def request(
503586
self,
504587
method,
@@ -573,9 +656,11 @@ def register_compliance_hook(self, hook_type, hook):
573656
Available hooks are:
574657
access_token_response invoked before token parsing.
575658
refresh_token_response invoked before refresh token parsing.
659+
revoke_token_response invoked after token revocation.
576660
protected_request invoked before making a request.
577661
access_token_request invoked before making a token fetch request.
578662
refresh_token_request invoked before making a refresh request.
663+
revoke_token_request invoked before making a revoke request.
579664
580665
If you find a new hook is needed please send a GitHub PR request
581666
or open an issue.

0 commit comments

Comments
 (0)