diff options
author | Ib Lundgren <ib.lundgren@gmail.com> | 2013-05-30 17:05:27 +0100 |
---|---|---|
committer | Ib Lundgren <ib.lundgren@gmail.com> | 2013-05-30 17:05:27 +0100 |
commit | ba39888bf4d0224bc0bb9281f037402afbc46e12 (patch) | |
tree | 93e7ea76388c1b0f2c830959fa18061bb0a88935 | |
parent | 99681d2b80619ba0cd17cccd08a6c63b9024421f (diff) | |
download | oauthlib-ba39888bf4d0224bc0bb9281f037402afbc46e12.tar.gz |
Remove framework specific decorators. Fix #167.
-rw-r--r-- | docs/decorators.rst | 21 | ||||
-rw-r--r-- | docs/server2.rst | 98 | ||||
-rw-r--r-- | oauthlib/oauth2/ext/__init__.py | 0 | ||||
-rw-r--r-- | oauthlib/oauth2/ext/auditor.py | 108 | ||||
-rw-r--r-- | oauthlib/oauth2/ext/django.py | 118 | ||||
-rw-r--r-- | oauthlib/oauth2/ext/flask.py | 136 |
6 files changed, 25 insertions, 456 deletions
diff --git a/docs/decorators.rst b/docs/decorators.rst deleted file mode 100644 index a005c85..0000000 --- a/docs/decorators.rst +++ /dev/null @@ -1,21 +0,0 @@ -========== -Decorators -========== - -Hopefully, it should be quite straightforward to port the django decorator to other web frameworks as the decorator mainly provide a means for translating the framework specific request object into uri, http_method, headers and body. - -Django OAuth 2 Decorator ------------------------- - -TODO: hows and important notes about csrf, xss, etc - -Implementing a new OAuth 2 Provider Decorator ---------------------------------------------- - -TODO: what needs to be done, what to keep an eye out for - -Extensions per web framework ----------------------------- - -If you are currently developing an extension, please let us know and we will -list it here. Or better yet, send a PR! diff --git a/docs/server2.rst b/docs/server2.rst index 3b648e6..5c50d1a 100644 --- a/docs/server2.rst +++ b/docs/server2.rst @@ -2,35 +2,23 @@ OAuth 2: Creating a Provider ============================ -Note that OAuth 2 provider is still very much a work in progress, consider it a -preview of a near future =) +OAuthLib is a dependency free library that may be used with any web +framework. That said, there are framework specific helper libraries +to make your life easier. -**1. Which framework are you using?** +- For Django there is `django-oauth-toolkit`_. +- For Flask there is `flask-oauthlib`_. - OAuthLib is a dependency free library that may be used with any web - framework. That said, there are framework specific helper decorator classes - to make your life easier. The one we will be using in this example is for - Django. For others, and information on how to create one, check out - :doc:`decorators`. +If there is no support for your favourite framework and you are interested +in providing it then you have come to the right place. OAuthLib can handle +the OAuth logic and leave you to support a few framework and setup specific +tasks such as marshalling request objects into URI, headers and body arguments +as well as provide an interface for a backend to store tokens, clients, etc. - The main purpose of these decoraters is to help marshall between the - framework specific request object and framework agnostic url, headers, body - and http method parameters. They may also be useful for making sure common - best security practices are followed. +.. _`django-oauth-toolkit`: https://github.com/evonove/django-oauth-toolkit +.. _`flask-oauthlib`: https://github.com/lepture/flask-oauthlib - Their purpose is not to be a full solution to all your needs as a provider, - for that you will want to seek out framework specific extensions building - upon OAuthLib. See the section on :doc:`decorators` for a list of - extensions. - - Relevant sections include: - - .. toctree:: - :maxdepth: 1 - - decorators - -**2. Create your datastore models** +**1. Create your datastore models** These models will represent various OAuth specific concepts. There are a few important links between them that the security of OAuth is based on. Below @@ -191,7 +179,7 @@ preview of a near future =) expires_at = django.db.models.DateTimeField() -**3. Implement a validator** +**2. Implement a validator** The majority of the work involved in implementing an OAuth 2 provider relates to mapping various validation and persistence methods to a storage @@ -232,7 +220,7 @@ preview of a near future =) security -**4. Create your composite endpoint** +**3. Create your composite endpoint** Each of the endpoints can function independently from each other, however for this example it is easier to consider them as one unit. An example of a @@ -246,7 +234,6 @@ preview of a near future =) validator = MyRequestValidator() server = WebApplicationServer(validator) - provider = OAuth2ProviderDecorator('/error', server) # See next section Relevant sections include: @@ -256,55 +243,20 @@ preview of a near future =) preconfigured_servers -**5. Decorate your endpoint views** +**4. Create your endpoint views** We are implementing support for the Authorization Code Grant and will therefore need two views for the authorization, pre- and post-authorization together with the token view. We also include an error page to redirect users to if the client supplied invalid credentials in their redirection, - for example an invalid redirect URI:: - - @login_required - @provider.pre_authorization_view - def authorize(request, client_id=None, scopes=None, state=None, - redirect_uri=None, response_type=None): - # This is the traditional authorization page - # Scopes will be the list of scopes client requested access too - # You will want to present them in a nice form where the user can - # select which scopes they allow the client to access. - return render(request, 'authorize.html', {'scopes': scopes}) - - - @login_required - @provider.post_authorization_view - def authorization_response(request): - # This is where the form submitted from authorize should end up - # Which scopes user authorized access to + extra credentials you want - # appended to the request object passed into the validator methods. - # In almost every case, you will want to include the current - # user in these extra credentials in order to associate the user with - # the authorization code or bearer token. - return request.POST.getlist['scopes'], {'user': request.user} - - - @provider.access_token_view - def token_response(request): - # Not much too do here for you, return a dict with extra credentials - # you want appended to request.credentials passed to the save_bearer_token - # method of the validator. - return {'extra': 'creds'} - - def error(request): - # The /error page users will be redirected to if there was something - # wrong with the credentials the client included when redirecting the - # user to the authorization form. Mainly if the client was invalid or - # included a malformed / invalid redirect url. - # Error and description can be found in - # GET['error'] and GET['error_description'] - return HttpResponse('Bad client! Warn user!') - - -**6. Protect your APIs using scopes** + for example an invalid redirect URI. + + The rest of this section will come soon. + +**5. Protect your APIs using scopes** + + The definition of ``protected_resource_view`` will come soon. Please see + the framework specific extensions outlined in the beginning for now. At this point you are ready to protect your API views with OAuth. Take some time to come up with a good set of scopes as they can be very powerful in @@ -330,7 +282,7 @@ preview of a near future =) # A view that has its views functionally set. return HttpResponse('pictures of cats') -**7. Let us know how it went!** +**6. Let us know how it went!** Drop a line in our `G+ community`_ or open a `GitHub issue`_ =) diff --git a/oauthlib/oauth2/ext/__init__.py b/oauthlib/oauth2/ext/__init__.py deleted file mode 100644 index e69de29..0000000 --- a/oauthlib/oauth2/ext/__init__.py +++ /dev/null diff --git a/oauthlib/oauth2/ext/auditor.py b/oauthlib/oauth2/ext/auditor.py deleted file mode 100644 index 8057d69..0000000 --- a/oauthlib/oauth2/ext/auditor.py +++ /dev/null @@ -1,108 +0,0 @@ -# A functionality/security auditor that can be used to verify the correctness -# of a live OAuth 2 provider implementation given certain credentials. - -# Implemented as a unittest class that you can subclass, configure and -# run against any spec compliant provider. - -# Uses requests for HTTP requests, pip install requests -# Uses selenium to drive authorizations, pip install selenium - -from __future__ import absolute_import - -import requests -import selenium -import unittest - - -class OAuth1Auditor(unittest.TestCase): - # Aiming for initial release in oauthlib 0.5 - pass - - -class OAuth2Auditor(unittest.TestCase): - """Verify your OAuth 2 provider implementation live.""" - - def setUp(self): - self.grant_types = ['supported', 'grant types'] - self.response_types = ['supported', 'response types'] - self.clients = [('client id', ['grant types'], ['redirect uris'], ['scope', 'list'])] - self.authorization_url = 'https://your.site/authorize' - self.token_url = 'https://your.site/token' - self.protected_resources = [('/url', ['scope', 'list'])] - self.configure() - - def configure(self): - """Configure the credentials that will be exercised by the auditor.""" - raise NotImplementedError('Subclasses must implement this method.') - - def authenticate_client(self, request): - """Add authentication credentials to the request object.""" - raise NotImplementedError('Subclasses must implement this method.') - - def select_scopes(self, webdriver, scopes): - """Select scopes in your authorization view using webdriver.""" - raise NotImplementedError('Subclasses must implement this method.') - - def test_authorization_code_grant_flow(self): - """Auth + Token + Refresh flow.""" - pass - - def test_implicit_grant_flow(self): - """Auth + Token flow. Ensure no refresh token.""" - pass - - def test_password_grant_flow(self): - """Token + Refresh flow.""" - pass - - def test_client_credentials_grant_flow(self): - """Token flow. Ensure no refresh token.""" - pass - - def test_authorization_code_reuse(self): - """Ensure authorization codes are only valid once.""" - pass - - def test_unauthorized_scope_access(self): - """Ensure protected views account for scope restrictions.""" - pass - - def test_unauthorized_scope_request(self): - """Ensure clients can't request scope access outside their reach.""" - pass - - def test_scope_change_during_authorization(self): - """Ensure users can opt-out of scopes.""" - pass - - def test_mismatching_redirect_uri(self): - """Ensure only exact matches of redirect URIs are allowed.""" - pass - - def test_no_default_redirect_uri(self): - """Ensure no random redirect uri is set when no default exists.""" - pass - - def test_state_preservation(self): - """Ensure state remains unchanged throughout auth flow.""" - pass - - def test_token_request_redirect_uri_mismatch(self): - """Ensure redirect_uri is equal in auth and token request.""" - pass - - def test_https(self): - """OAuth 2 **requires** HTTPS everywhere.""" - pass - - def test_client_grant_types(self): - """Ensure client can only use one grant type.""" - pass - - def test_client_response_types(self): - """Ensure client can only use the response type matching its grant type.""" - pass - - def test_wrong_client_valid_code(self): - """Ensure one client can't use the auth code of another.""" - pass diff --git a/oauthlib/oauth2/ext/django.py b/oauthlib/oauth2/ext/django.py deleted file mode 100644 index 2a604b1..0000000 --- a/oauthlib/oauth2/ext/django.py +++ /dev/null @@ -1,118 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals, absolute_import -from django.http import HttpResponseRedirect, HttpResponse, HttpResponseForbidden -from django.views.decorators.csrf import csrf_exempt -import functools -import logging -from oauthlib.common import urlencode -from oauthlib.oauth2.draft25 import errors - -log = logging.getLogger('oauthlib') - - -class OAuth2ProviderDecorator(object): - - def __init__(self, error_uri, server=None, authorization_endpoint=None, - token_endpoint=None, resource_endpoint=None): - self._authorization_endpoint = authorization_endpoint or server - self._token_endpoint = token_endpoint or server - self._resource_endpoint = resource_endpoint or server - self._error_uri = error_uri - - def _extract_params(self, request): - log.debug('Extracting parameters from request.') - uri = request.build_absolute_uri() - http_method = request.method - headers = request.META - if 'wsgi.input' in headers: - del headers['wsgi.input'] - if 'wsgi.errors' in headers: - del headers['wsgi.errors'] - if 'HTTP_AUTHORIZATION' in headers: - headers['Authorization'] = headers['HTTP_AUTHORIZATION'] - body = urlencode(request.POST.items()) - return uri, http_method, body, headers - - def pre_authorization_view(self, f): - @functools.wraps(f) - def wrapper(request, *args, **kwargs): - uri, http_method, body, headers = self._extract_params(request) - redirect_uri = request.GET.get('redirect_uri', None) - log.debug('Found redirect uri %s.', redirect_uri) - try: - scopes, credentials = self._authorization_endpoint.validate_authorization_request( - uri, http_method, body, headers) - log.debug('Saving credentials to session, %r.', credentials) - request.session['oauth2_credentials'] = credentials - kwargs['scopes'] = scopes - kwargs.update(credentials) - log.debug('Invoking view method, %r.', f) - return f(request, *args, **kwargs) - - except errors.FatalClientError as e: - log.debug('Fatal client error, redirecting to error page.') - return HttpResponseRedirect(e.in_uri(self._error_uri)) - return wrapper - - def post_authorization_view(self, f): - @functools.wraps(f) - def wrapper(request, *args, **kwargs): - uri, http_method, body, headers = self._extract_params(request) - scopes, credentials = f(request, *args, **kwargs) - log.debug('Fetched credentials view, %r.', credentials) - credentials.update(request.session.get('oauth2_credentials', {})) - log.debug('Fetched credentials from session, %r.', credentials) - redirect_uri = credentials.get('redirect_uri') - log.debug('Found redirect uri %s.', redirect_uri) - try: - url, headers, body, status = self._authorization_endpoint.create_authorization_response( - uri, http_method, body, headers, scopes, credentials) - log.debug('Authorization successful, redirecting to client.') - return HttpResponseRedirect(url) - except errors.FatalClientError as e: - log.debug('Fatal client error, redirecting to error page.') - return HttpResponseRedirect(e.in_uri(self._error_uri)) - except errors.OAuth2Error as e: - log.debug('Client error, redirecting back to client.') - return HttpResponseRedirect(e.in_uri(redirect_uri)) - - return wrapper - - def access_token_view(self, f): - @csrf_exempt - @functools.wraps(f) - def wrapper(request, *args, **kwargs): - uri, http_method, body, headers = self._extract_params(request) - credentials = f(request, *args, **kwargs) - log.debug('Fetched credentials view, %r.', credentials) - url, headers, body, status = self._token_endpoint.create_token_response( - uri, http_method, body, headers, credentials) - response = HttpResponse(content=body, status=status) - for k, v in headers.items(): - response[k] = v - return response - return wrapper - - def protected_resource_view(self, scopes=None): - def decorator(f): - @csrf_exempt - @functools.wraps(f) - def wrapper(request, *args, **kwargs): - try: - scopes_list = scopes(request) - except TypeError: - scopes_list = scopes - uri, http_method, body, headers = self._extract_params(request) - valid, r = self._resource_endpoint.verify_request( - uri, http_method, body, headers, scopes_list) - kwargs.update({ - 'client': r.client, - 'user': r.user, - 'scopes': r.scopes - }) - if valid: - return f(request, *args, **kwargs) - else: - return HttpResponseForbidden() - return wrapper - return decorator diff --git a/oauthlib/oauth2/ext/flask.py b/oauthlib/oauth2/ext/flask.py deleted file mode 100644 index d6437a2..0000000 --- a/oauthlib/oauth2/ext/flask.py +++ /dev/null @@ -1,136 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals, absolute_import -from flask import abort, make_response, redirect, request, session -try: - from flask import _app_ctx_stack as stack -except ImportError: - from flask import _request_ctx_stack as stack -import functools -import logging -from oauthlib.common import urlencode -from oauthlib.oauth2.draft25 import errors - -log = logging.getLogger('oauthlib') - - -class OAuth2ProviderDecorator(object): - - def __init__(self, error_uri, server=None, authorization_endpoint=None, - token_endpoint=None, resource_endpoint=None): - self._authorization_endpoint = authorization_endpoint or server - self._token_endpoint = token_endpoint or server - self._resource_endpoint = resource_endpoint or server - self._error_uri = error_uri - - def __getattr__(self, name): - ctx = stack.top - if ctx is not None: - oauth_name = 'oauth_' + name - try: - return getattr(ctx, oauth_name) - except AttributeError: - return None - return None - - def _set_ctx(self, name, value): - ctx = stack.top - if ctx is not None: - oauth_name = 'oauth_' + name - setattr(ctx, oauth_name, value) - - def _extract_params(self): - log.debug('Extracting parameters from request.') - uri = request.url - http_method = request.method - headers = dict(request.headers) - if 'wsgi.input' in headers: - del headers['wsgi.input'] - if 'wsgi.errors' in headers: - del headers['wsgi.errors'] - if 'HTTP_AUTHORIZATION' in headers: - headers['Authorization'] = headers['HTTP_AUTHORIZATION'] - body = urlencode(request.form.items()) - return uri, http_method, body, headers - - def pre_authorization_view(self, f): - @functools.wraps(f) - def wrapper(*args, **kwargs): - uri, http_method, body, headers = self._extract_params() - redirect_uri = request.args.get('redirect_uri', None) - log.debug('Found redirect uri %s.', redirect_uri) - try: - scopes, credentials = self._authorization_endpoint.validate_authorization_request( - uri, http_method, body, headers) - log.debug('Saving credentials to session, %r.', credentials) - session['oauth2_credentials'] = credentials - kwargs['scopes'] = scopes - kwargs.update(credentials) - log.debug('Invoking view method, %r.', f) - return f(*args, **kwargs) - - except errors.FatalClientError as e: - log.debug('Fatal client error, redirecting to error page.') - return redirect(e.in_uri(self._error_uri)) - return wrapper - - def post_authorization_view(self, f): - @functools.wraps(f) - def wrapper(*args, **kwargs): - uri, http_method, body, headers = self._extract_params() - scopes, credentials = f(*args, **kwargs) - log.debug('Fetched credentials view, %r.', credentials) - credentials.update(session.get('oauth2_credentials', {})) - log.debug('Fetched credentials from session, %r.', credentials) - redirect_uri = credentials.get('redirect_uri') - log.debug('Found redirect uri %s.', redirect_uri) - try: - url, headers, body, status = self._authorization_endpoint.create_authorization_response( - uri, http_method, body, headers, scopes, credentials) - log.debug('Authorization successful, redirecting to client.') - return redirect(url) - except errors.FatalClientError as e: - log.debug('Fatal client error, redirecting to error page.') - return redirect(e.in_uri(self._error_uri)) - except errors.OAuth2Error as e: - log.debug('Client error, redirecting back to client.') - return redirect(e.in_uri(redirect_uri)) - - return wrapper - - def access_token_view(self, f): - @functools.wraps(f) - def wrapper(*args, **kwargs): - uri, http_method, body, headers = self._extract_params() - credentials = f(*args, **kwargs) - log.debug('Fetched credentials view, %r.', credentials) - url, headers, body, status = self._token_endpoint.create_token_response( - uri, http_method, body, headers, credentials) - response = make_response(body, status) - for k, v in headers.items(): - response.headers[k] = v - return response - return wrapper - - def protected_resource_view(self, scopes=None, forbidden=None): - def decorator(f): - @functools.wraps(f) - def wrapper(*args, **kwargs): - try: - scopes_list = scopes() - except TypeError: - scopes_list = scopes - uri, http_method, body, headers = self._extract_params() - valid, r = self._resource_endpoint.verify_request( - uri, http_method, body, headers, scopes_list) - self._set_ctx('client', r.client) - self._set_ctx('user', r.user) - self._set_ctx('scopes', r.scopes) - self._set_ctx('token_scopes', r.token_scopes) - if valid: - return f(*args, **kwargs) - elif forbidden is not None: - return forbidden() - else: - abort(401) - return wrapper - return decorator |